Skip to content

feat(app): first-run signup wizard (skippable, WordPress-backed)#65

Merged
huyplb merged 2 commits into
mainfrom
feat/signup-wizard
Jul 12, 2026
Merged

feat(app): first-run signup wizard (skippable, WordPress-backed)#65
huyplb merged 2 commits into
mainfrom
feat/signup-wizard

Conversation

@huyplb

@huyplb huyplb commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

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.

  • 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 — 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.tsx matches SetupScreen'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.
  • Wired into main.tsx's boot(): 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.example documents SIGNUP_WEBHOOK_URL / SIGNUP_WEBHOOK_SECRET.

Test plan

  • cd apps/web && npx tsc --noEmit — clean
  • npx vitest run from repo root — 249 passed, 2 skipped (was 242; +7 new SignupModule tests: 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)
  • Manually verified live in a browser against the real dev server (frontend + API):
    • Fresh state → wizard renders, matches the dark theme
    • Submit (no webhook configured) → resolves immediately, app renders, state persists across reload
    • Skip → resolves immediately, no network call, app renders
    • Submit against an unreachable webhook → inline error shown, form stays open, both "Notify me" (retry) and "Skip for now" still available, wizard stays unresolved

🤖 Generated with Claude Code

huyplb added 2 commits July 12, 2026 11:01
…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.
@huyplb huyplb merged commit 697a42d into main Jul 12, 2026
4 of 6 checks passed
@huyplb huyplb deleted the feat/signup-wizard branch July 12, 2026 20:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant