From 09d73199eab1449fffa29a35561c3a5fa2a6db70 Mon Sep 17 00:00:00 2001 From: JCEEE <0xjceee@proton.me> Date: Mon, 24 Aug 2026 18:09:44 +0100 Subject: [PATCH] update from user testing payment platform --- MEMORY.md | 2 + .../src/lib/components/PricingPlans.svelte | 31 +- frontend/src/lib/forms.ts | 38 ++ frontend/src/lib/server/stripe.ts | 48 ++- frontend/src/routes/[fam]/+layout.svelte | 7 +- .../[fam]/[username]/settings/+page.server.ts | 51 ++- .../[fam]/[username]/settings/+page.svelte | 331 ++++++++++++------ .../src/routes/api/webhooks/stripe/+server.ts | 10 +- frontend/src/routes/pricing/+page.svelte | 89 +++-- frontend/src/routes/signup/+page.svelte | 268 ++++++++------ 10 files changed, 615 insertions(+), 260 deletions(-) create mode 100644 frontend/src/lib/forms.ts diff --git a/MEMORY.md b/MEMORY.md index ade0441..fbfdc40 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -336,4 +336,6 @@ - Note: `svelte-kit sync` needed after changing load return shapes or `$types` staleness doubles the error count. - **/admin login pattern:** action sets `platform_session` cookie + returns `{success:true}`; the form's `use:enhance` callback flips a local `authed` view state — no redirect, no reliance on inline invalidation or fetch-time Set-Cookie behavior (which proved flaky in-browser despite curl proving both response paths carried it). Cookie still covers subsequent loads; load errors surface as `data.loadError` on the login card instead of silently masquerading as logged-out. - **Platform-admin auth via hooks:** `hooks.server.ts` resolves `locals.platformAdmin` from the `platform_session` cookie (=== 'authenticated') on every request; `/admin` load/actions consume `event.locals.platformAdmin` (`requirePlatform(event)`) instead of raw cookie reads. Same central pattern as `pb_token` → `locals.user`. +- **`isDummyStripe` removed:** real test keys made every dummy branch dead code — and the webhook's `|| !signature` fallback accepted UNSIGNED events (forgeable access grants). Signature is now mandatory (400 without it); settings' simulated endSubscription/portal branches deleted. Dev verification stays via stripe-cli signed events (`pnpm stripe:listen`). - **Platform-admin auth hardened (supersedes the constant-cookie version):** `/admin` login now does a real `_superusers.authWithPassword` via PB; the minted superuser JWT goes in `platform_session` (`setPlatformSession` in session.ts). `hooks.server.ts` deviates on `/admin`: verifies the token with `_superusers.authRefresh` → `locals.platformAdmin` (forged values fail authRefresh and get cleared); fam-user pb_token flow skipped on that route. FINAL login shape (user-amended, working): action returns `{success:true}` (no redirect); form's enhance callback does `goto('/admin', { invalidateAll: true })` on success — forcing the load re-run with the fresh cookie; view branches on `data.authenticated`. Verified: real token renders dashboard, forged cookie gets login. +- **use:enhance redirect gotcha (session-wide lesson):** action-thrown redirects surface as `result.type === 'redirect'` in the RESOLVE callback — handlers checking only `'success'` silently drop them (bit /admin login, pricing choose, and settings billingPortal). Shared fix: `lib/forms.ts` → `handleResult(handler?)` factory follows redirects via `goto`, delegates the rest to the handler or default `update()`. Use it for any form whose action can throw a redirect. Also: enhance is two-stage — `{result}` only exists in the resolve fn, not the submit params (was mis-handled in pricing/signup handlers). diff --git a/frontend/src/lib/components/PricingPlans.svelte b/frontend/src/lib/components/PricingPlans.svelte index 4e888fa..cea8561 100644 --- a/frontend/src/lib/components/PricingPlans.svelte +++ b/frontend/src/lib/components/PricingPlans.svelte @@ -15,12 +15,12 @@ const tiers: Tier[] = [ { id: 'trial', - name: 'Trial', + name: 'Have a code?', price: 'Free', - period: 'for the trial period', - blurb: 'Kick the tires on us.', - features: ['Full family access', 'No card required', 'Code from us or a partner'], - cta: 'Start free trial' + period: 'with a valid code', + blurb: 'Been given an access or trial code?', + features: ['Full family access', 'Redeemed during signup', 'No card required'], + cta: 'Enter your code' }, { id: 'monthly', @@ -70,13 +70,17 @@
{tier.blurb}
- + {#if tier.id === 'trial'} + + {tier.cta} + {:else} + + {/if}Already have a family? Log in
- {:else if step === 'child'}We'll create a shareable join code so they can jump in on any device.
- (step = 'code')}>Skip for now → +
- {:else if step === 'code'}If you have a code (e.g. from your employer or a gift), enter it here. Otherwise skip to choose a plan.
++ If you have a code (e.g. from your employer or a gift), enter it here. Otherwise skip to + choose a plan. +
{#if accessMsg}{accessMsg}
{/if}- +
- {:else if step === 'plan'} -Pick the plan that works for your family. Your trial or subscription starts immediately.
-- You can close and go to your dashboard any time — access unlocks once payment completes. -
- Go to dashboard -+ {urlPlan === 'monthly' ? '£3/month, cancel anytime.' : '£30/year — two months free.'} + Payment opens below. +
+ + {:else if !activeSecret} ++ Pick the plan that works for your family. Your subscription starts immediately. +
++ You can close and go to your dashboard any time — access unlocks once payment completes. +
+ Go to dashboard {/if} - {:else if step === 'done'}You can add kids and share join codes any time from Family Settings.
@@ -261,8 +309,13 @@ import { page } from '$app/state'; font-weight: 600; cursor: pointer; } - button:hover { background: #3730a3; } - button:disabled { opacity: 0.6; cursor: default; } + button:hover { + background: #3730a3; + } + button:disabled { + opacity: 0.6; + cursor: default; + } .step-title { margin: 0 0 0.25rem; font-size: 1.1rem; @@ -292,7 +345,8 @@ import { page } from '$app/state'; color: #9ca3af; font-weight: 400; } - .preview .inline-code, .preview-hint .inline-code { + .preview .inline-code, + .preview-hint .inline-code { font-family: ui-monospace, monospace; background: #f3f4f6; border-radius: 4px; @@ -305,7 +359,11 @@ import { page } from '$app/state'; color: #6b7280; text-align: center; } - .alt a { color: #4338ca; text-decoration: none; font-weight: 500; } + .alt a { + color: #4338ca; + text-decoration: none; + font-weight: 500; + } .skip-link { background: none; border: none; @@ -315,7 +373,9 @@ import { page } from '$app/state'; cursor: pointer; padding: 0; } - .skip-link:hover { text-decoration: underline; } + .skip-link:hover { + text-decoration: underline; + } .access-msg { margin-top: 0.5rem; font-size: 0.85rem; @@ -335,5 +395,17 @@ import { page } from '$app/state'; font-weight: 600; text-decoration: none; } - .btn-primary:hover { background: #3730a3; } - \ No newline at end of file + .btn-primary:hover { + background: #3730a3; + } + .checkout-actions { + margin-bottom: 0.75rem; + } + .checkout-host { + width: 100%; + min-height: 480px; + } + .checkout-host :global(iframe) { + width: 100% !important; + } +