Skip to content

feat(onboarding): forced three-phase signup wizard#142

Open
Rasbandit wants to merge 35 commits into
mainfrom
feat/signup-wizard
Open

feat(onboarding): forced three-phase signup wizard#142
Rasbandit wants to merge 35 commits into
mainfrom
feat/signup-wizard

Conversation

@Rasbandit
Copy link
Copy Markdown
Member

Summary

  • New RequireOnboarding plug gates /api/notes, /api/search, /api/folders, etc. behind TOS acceptance + active subscription
  • New user_agreements table (versioned, RLS-isolated) records TOS acceptances
  • Frontend OnboardingGate redirects new users through /onboard/agreement/onboard/billing → dashboard
  • Self-host bypass: when PADDLE_API_KEY is unset, :engram, :billing_enabled is false and the wizard short-circuits entirely
  • Frontend test infrastructure (vitest + happy-dom + @testing-library/react) was bootstrapped as part of this PR

Spec: docs/superpowers/specs/2026-05-15-signup-wizard-design.md
Plan: docs/superpowers/plans/2026-05-15-signup-wizard.md

Test plan

  • mix test — all backend tests pass including new plug, context, controller, and integration tests
  • bun run vitest run — frontend OnboardingGate and AgreementPage component tests pass
  • pytest e2e/tests/test_onboarding.py — E2E confirms 403 on protected route, status endpoint, accept-terms advance
  • Manual: sign up → see /onboard/agreement → accept → see /onboard/billing → complete Paddle sandbox checkout → land on dashboard
  • Manual self-host: start Phoenix without PADDLE_API_KEY → sign up → no redirect; reach dashboard immediately
  • Manual TOS bump: bump CURRENT_TOS_VERSION env, restart → existing user re-prompted on next request

🤖 Generated with Claude Code

Rasbandit and others added 30 commits May 14, 2026 19:11
Replaces the stale Stripe-style frontend with Paddle.js overlay
checkout. Adds `@paddle/paddle-js` and a `useBillingConfig` hook
that fetches `/api/billing/config` (token, environment, price IDs,
customer email, custom_data). `BillingPage` initializes Paddle
once on config load and `Paddle.Checkout.open` runs on plan-card
click with `customer`, `customData`, and a `successUrl` back to
`/billing?status=success`. Buttons stay disabled until the Paddle
instance resolves. Portal button still hits `/api/billing/portal`
(server returns the hosted Paddle URL); "Manage subscription in
Stripe" copy stripped.

Backend cutover landed in #132. Plan: docs/superpowers/plans/
2026-05-14-paddle-frontend-overlay.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Registers `paddle-docs` HTTP MCP server (hosted by Kapa.ai at
paddlehq.mcp.kapa.ai) in repo `.mcp.json` so Claude can query
live Paddle documentation during integration work. Checks in
the handoff plan that drove the frontend rewrite shipped in the
prior commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single-repo port of the workspace Makefile. Drops `cd backend/`
prefixes (backend is the repo root here), strips `plugin-*`
targets (plugin lives in a separate repo), and swaps frontend
`npm` for `bun` to match the repo's lockfile choice (#35 / 6ac17fa).
Targets that referenced missing scripts (test_plan.sh, deploy.sh,
e2e/unit_tests) are omitted rather than ported broken.

Top targets for local dev:
- `make dev` / `make dev-selfhost` — Phoenix at :4000 / :4001 with
  the matching .env.local file.
- `make frontend-dev` — Vite hot-reload at :5173. Phoenix watchers
  are disabled in config/dev.exs to avoid orphan node processes,
  so this is opt-in.
- `make backend-up/down` — full Docker stack (docker-compose.elixir).
- `make help` — auto-generated target listing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@paddle/paddle-js dynamically loads paddle.js from cdn.paddle.com,
opens the checkout iframe on (sandbox-)buy.paddle.com, and talks
to (sandbox-)api.paddle.com. The strict CSP installed by the SPA
pipeline blocked all three, so the overlay never rendered.

Wildcard `https://*.paddle.com` covers cdn / buy / sandbox-buy /
api / sandbox-api in one entry. Same change applies to script-src,
connect-src, and frame-src.

img-src already permits `https:`, style-src already has
'unsafe-inline'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures state post PR #141 so brainstorming can resume after
/compact: PR status, current Paddle sandbox resources, codebase
facts already gathered (router, AuthGuard, no TOS tracking), and
the clarifying questions queued for the user.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three-phase forced flow (account creation, TOS agreement, payment)
gated by RequireOnboarding plug + frontend OnboardingGate. Wizard is
fully disabled in self-host mode (PADDLE_API_KEY unset). Versioned
user_agreements table allows re-prompt when TOS version bumps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
18 TDD tasks covering migration, RLS wiring, context, plug, controller,
router, runtime config, frontend hooks, gate component, layout, agreement
page, billing wrapper, router wiring, E2E, and final PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the user_agreements table with tenant-isolation RLS policy,
two indexes for fast per-user lookups, and grants to engram_app.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Also migrates ip_address column from inet to text — pure audit storage,
no subnet queries needed, avoids Postgrex.INET struct encoding.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests fail with 404 (expected) — router wired in Task 8.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add GET /onboarding/status + POST /onboarding/accept-terms to
  user-scoped pipeline (exempt from RequireOnboarding)
- Insert RequireOnboarding into vault-scoped pipeline before VaultPlug
- Whitelist /onboard and /onboard/* in SPA scope
- Add integration test confirming 403→200 gate and self-host bypass

Note: integration test uses /api/folders (index, no required params)
rather than /api/folders/list (requires ?folder= param).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Installs vitest + @testing-library/react + happy-dom, wires up test
infrastructure (vite.config test block, test-setup.ts), and adds the
OnboardingGate layout route that redirects to the correct wizard step
based on useOnboardingStatus(). 5/5 unit tests pass; tsc clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 tests cover the full new-user onboarding journey:
- 403 with both missing=["terms","subscription"] before any action
- /api/onboarding/status reports next_step=agreement
- POST /api/onboarding/accept-terms advances to next_step=billing
- 403 with missing=["subscription"] after terms accepted

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rasbandit and others added 5 commits May 15, 2026 03:08
Also includes fix on top: AgreementPage navigates to /onboard after
acceptance so OnboardRedirect can forward to billing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…_terms

Previously accept_terms inserted a fresh row every call, so a user
clicking Continue repeatedly produced N duplicate agreement rows. The
gate read the latest by accepted_at anyway, but storing duplicates was
noise. Now:

- New unique index user_agreements_user_document_version_unique with a
  one-shot dedupe DELETE in the same migration for any pre-existing dupes
- accept_terms uses on_conflict {:replace, [:accepted_at, :ip_address,
  :user_agent]} so re-accepting the same version refreshes the audit
  metadata instead of erroring
- New test verifies re-acceptance produces exactly one row and refreshes
  ip_address

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The plugin repo was renamed dropping the -sync suffix. CI's fingerprint
job mints a GitHub App token scoped to the repo by name, so the rename
broke token minting and every downstream check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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