feat(app): first-run signup wizard (skippable, WordPress-backed)#65
Merged
Conversation
…s webhook
On first open (web and desktop, after any required setup resolves) the app
offers a one-time, skippable "stay in the loop" email prompt. Submitting or
skipping both dismiss it for good (tracked in app_settings).
- new SignupModule (backend): validates the email, POSTs {email, source} to
SIGNUP_WEBHOOK_URL with an X-Foxschema-Signup-Secret header when configured;
only marks the wizard resolved on a confirmed webhook accept, so a transient
failure leaves it showing next launch instead of silently losing the signup.
No webhook configured (e.g. local dev) resolves immediately — never blocks
on infra that isn't there.
- GET /api/signup/state, POST /api/signup, POST /api/signup/skip.
- SignupWizard.tsx matches SetupScreen's existing chrome; inline error state
keeps the form open with both retry and skip available on failure.
- Wired into main.tsx's boot() after the desktop security-setup gate (or
immediately on web) — fails open on a network hiccup so it never blocks boot.
The WordPress side (webhook endpoint + wp_mail notification to
contact@foxschema.com) is tracked separately — this lands with the endpoint
unconfigured (no-op) until that's deployed.
Security follow-up on the signup wizard, closing the abuse vectors on a public/self-hosted web instance where the endpoint can be unauthenticated: - Short-circuit: submit() no-ops once the wizard is already resolved (submitted or skipped), capping the external side effects (WordPress post + notification email) to ~one per install — a script can't drive an already-resolved install to POST/email again. - Per-IP rate limit (10 / 15 min) on POST /signup and /signup/skip via a small dependency-free in-memory limiter — bounds a flood during the pre-resolution window (and any webhook outage), returning 429 + Retry-After. - 5s AbortSignal.timeout on the outbound webhook fetch — a hung/slow WordPress can no longer tie up the request indefinitely. Verified: short-circuit + 429 via live curl; timeout signal + limiter windows via unit tests. 254 unit tests pass; web tsc clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
First-run "stay in the loop" email wizard, per the requirements: shows on first app open (web + desktop), skippable, emails get stored durably in WordPress so nothing's lost and it's exportable, and a notification goes to contact@foxschema.com per signup.
This PR is the app side only — see the follow-up for the WordPress mu-plugin (custom REST endpoint + wp_mail notification) that this webhook targets.
SignupModule(backend): validates the email, POSTs{email, source}toSIGNUP_WEBHOOK_URLwith anX-Foxschema-Signup-Secretheader when configured. Only marks the wizard resolved on a confirmed webhook accept — a transient failure leaves it showing next launch instead of silently dropping the signup (matches "nothing is ever lost"). No webhook configured (e.g. local dev, or before the WP side ships) resolves immediately so it never blocks on infra that isn't there yet.GET /api/signup/state,POST /api/signup,POST /api/signup/skip.SignupWizard.tsxmatchesSetupScreen's existing chrome (same Brand header, card, button styles). Inline error state keeps the form open with both retry and skip available on failure — never traps the user.main.tsx'sboot(): on desktop, after the existing (required, silent) encryption-key setup step resolves; on web, immediately. Fails open on a network hiccup (treats a failed state-check as "already shown") so it can never block boot..env.exampledocumentsSIGNUP_WEBHOOK_URL/SIGNUP_WEBHOOK_SECRET.Test plan
cd apps/web && npx tsc --noEmit— cleannpx vitest runfrom repo root — 249 passed, 2 skipped (was 242; +7 newSignupModuletests: unshown-by-default, skip, invalid email rejected without a network call, submit with no webhook configured, submit success posts the right body/header, submit failure leaves it unshown on both a non-2xx response and an unreachable host)🤖 Generated with Claude Code