Skip to content

PR4 — My Account SPA (source/account-dashboard-app) #19

Description

@pedromvgomes

Summary

Build the My Account SPA for real in source/account-dashboard-app, composing the published @wardnet/ui components, wired to the wardnet-cloud account API. Built MSW-first so it is fully developable with zero backend, then pointed at the real endpoints via a Vite dev proxy.

This is PR4 of the My Account initiative. It can be developed and merged in parallel with the backend PRs because it builds against the contract below via MSW (the contract is the shared interface). Refines #1.

Design reference (prototype)

High-fidelity prototype + spec, committed to the repo:

Recreate it with real framework/routing/data — compose @wardnet/ui, do not re-implement components. Where the prototype diverges from the backend, follow the reconciliation below.

Stack

  • source/account-dashboard-app, yarn 4.17, Vite + React 19 + TypeScript + react-router.
  • @wardnet/ui (+ @wardnet/styles, @wardnet/brand) from GitHub Packages (@wardnet:registry=https://npm.pkg.github.com). Import @wardnet/ui/styles.css + @wardnet/styles theme/typography. Components are exported from @wardnet/ui (Card/CardHeader/Title/Subtitle/Action/Content, Button, Text, Heading, Field, Input, Banner, Pill, StatTile, Modal*, AlertModal*, DropdownMenu*, Logo, …) — see the DS src/index.ts.
  • Theming: data-theme="dark" on <html> (so Radix portals inherit); sun/moon ghost toggle. Light default.

@wardnet/ui access (dev + CI)

  • Local install: run gh auth refresh -s read:packages once, then the .npmrc/.yarnrc.yml reads the token from env (never commit a token).
  • CI: authenticate with the built-in GITHUB_TOKEN + permissions: { contents: read, packages: read }no PAT.
  • Live DS co-dev (no release cycles): opt-in yarn link to the sibling wardnet-design-system checkout (built with --watch); not committed → CI unaffected.

Routing (prototype state machine → routes)

  • Auth (unauthenticated): /signin, /register, /forgot, /confirm (6-box code; flow=signup|reset), /set-password.
  • Account (authenticated, app shell with top bar): /overview, /subscription, /security. Account menu + (dev-only) Demo switcher + theme toggle in the top bar.
  • The Demo dropdown is a reviewer/dev affordance only — drives the MSW state in dev; removed in production builds.

Auth wiring

  • Session is an httpOnly cookie the SPA cannot read. On load/resume call POST /v1/auth/token (silent exchange) → 5-min USER JWT held in memory; attach as Authorization: Bearer to USER calls. 401 → treat as logged-out → /signin.
  • OIDC: buttons navigate to GET /v1/auth/oidc/{google|github}/start; callback returns to the app.
  • Vite dev proxy /v1 → backend so the SPA and API are same-origin (httpOnly cookie works; mirrors nginx in prod).

Endpoint contract (what the SPA calls)

Screen / action Method + path Notes
Silent JWT POST /v1/auth/token session cookie → 5-min JWT
Sign in POST /v1/auth/password/login {email,password} sets session cookie
Sign up POST /v1/verification-codes {email,purpose:"signup"}/confirmPOST /v1/auth/password/signup {email,code,password} PR3
Forgot/reset POST /v1/verification-codes {email,purpose:"password_reset"}/confirmPOST /v1/auth/password/reset {code,new_password} reset revokes all sessions → back to sign-in w/ banner
OIDC GET /v1/auth/oidc/{provider}/start redirect
Logout POST /v1/auth/logout current session
Identity bootstrap GET /v1/me {tenant_id,email,subscription}
Networks GET /v1/tenants/{id}/networks list
Devices/daemons GET /v1/tenants/{id}/daemons (+ GET /v1/networks/{id}/daemons) per-network device count = daemons grouped by network_id
Add payment / renew / reactivate POST /v1/tenants/{id}/billing/checkout-session {price_id} → redirect to url hosted Checkout
Update payment / manage POST /v1/tenants/{id}/billing/portal → redirect to url hosted Portal
Payment-method card GET /v1/tenants/{id}/billing/payment-method PR2; `{brand,last4,exp_*}
Invoice table GET /v1/tenants/{id}/billing/invoices PR2; download = hosted_url
Cancel subscription PATCH /v1/tenants/{id} {subscription_status:"canceled"} AlertModal → cancel at period end
Deregister account DELETE /v1/tenants/{id} (if surfaced)
Connected methods GET /v1/me/identities; link via GET /v1/auth/oidc/{provider}/start?mode=link; DELETE /v1/me/identities/{provider} PR3; ≥1 remains
Change password code-exchange (see Forgot/reset) PR3
Sign out of all DELETE /v1/me/sessions PR3

Reconciliation vs the prototype (important)

  • No in-app card Modal. Replace the prototype's Stripe-fields modal with a redirect to hosted Checkout (add/renew/reactivate) / Portal (update). PCI SAQ-A.
  • Status mapping: trialing→Trial, active→Active, past_due→Grace, canceled→Cancelled. Derive every pill/CTA/banner/blocked-action from subscription status (the prototype's "subscription state is the spine").
  • Entitlement: subscription.entitlement.{max_networks,max_daemons}; usage = networks count + daemons count. Warn tone near a limit.
  • Network status: provisioning_stateactive=Online (ok), provisioning=Provisioning (info), deprovisioning=Deprovisioning (down). No "Degraded" health signal exists — drop it.
  • Reactivate = a fresh Checkout (no dedicated endpoint).

MSW (local mock layer)

  • Implement handlers for every endpoint above, returning the contract shapes.
  • Driven by the prototype's Demo switcher: subscription state (trial|active|grace|cancelled) and data state (ready|loading|error|empty). Sample data from the spec (Pedro, Pro $8/mo, networks 2/3, devices 24/25, Visa •••• 4242). Demo code 424242 for the confirm screen.
  • MSW is the inner dev loop (no backend) and is reused in component/integration tests. A build/env flag switches between MSW and the real /v1 dev proxy.

Component map (prototype → @wardnet/ui)

  • AuthShell: centered Logo + Card(padding 26), theme FAB, OIDC ghost Buttons + "or" divider, Field+Input, one emerald primary Button/view.
  • Confirm: segmented 6-box code input (digits, auto-advance, backspace-prev, arrows, paste-fills-all, autofocus) — custom inputs styled with tokens; Banner for notes; inline danger Text.
  • Overview: status Card (accent stripe + Pill + state CTA), two StatTiles (usage bars, warn near limit), networks list Card.
  • Subscription: Plan Card + Entitlements Card (equal height), grace Banner, lifecycle Card (state-specific), payment-method Card, billing-history table, AlertModal for cancel.
  • Security: connected-methods rows, change-password Fields, sessions buttons.
  • Shell: sticky top bar (Logo, tab nav w/ emerald underline, Demo DropdownMenu, theme toggle, account DropdownMenu). Loading=shimmer skeletons, error=card+Retry, empty states.

Acceptance criteria

  • source/account-dashboard-app scaffolded (yarn 4.17, Vite, React 19, TS, react-router); yarn build + yarn type-check green.
  • @wardnet/ui consumed from GitHub Packages; .npmrc/.yarnrc.yml token via env; CI installs with GITHUB_TOKEN (packages: read).
  • All screens implemented composing @wardnet/ui; light/dark; matches the prototype's tokens/copy/interactions (no invented colors/type).
  • MSW handlers cover the full contract; all Demo states render (trial/active/grace/cancelled × ready/loading/error/empty).
  • Silent JWT exchange + 401→sign-in; OIDC redirects; Checkout/Portal redirects; cancel via AlertModal; payment-method + invoices render from PR2 shapes.
  • Dev proxy /v1→backend documented; the SPA runs against a locally-run backend with cookies working.
  • Demo switcher stripped from production builds.

References

  • wardnet-design-system packages/ui (src/index.ts), packages/styles.
  • Backend contract: PR1 (refactor) / PR2 (billing reads) / PR3 (account+security) of this initiative.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions