feature plus ux rearrangements

This commit is contained in:
JCEEE
2026-08-22 19:49:46 +01:00
parent f14f4e2ac1
commit 42b9a27ce6
44 changed files with 2020 additions and 538 deletions
+61 -7
View File
@@ -5,6 +5,9 @@ import { createPbClient } from '$lib/server/pocketbase';
import { setSessionCookie } from '$lib/server/session';
import { issueAccess } from '$lib/server/member-otp';
import { slugify, handle, famUsername, handleOf } from '@shared/slugify';
import { applyAccessCode } from '$lib/server/access';
import { createEmbeddedCheckoutSession, PLAN_IDS } from '$lib/server/stripe';
import type { PlanId } from '$lib/server/stripe';
class SignupError extends Error {}
@@ -32,11 +35,14 @@ export const actions = {
const username = famUsername(slug, handleName);
try {
const fam = await pbAdmin.create('fams', {
const famData: Record<string, unknown> = {
name: famName,
slug,
timezone: 'auto'
});
timezone: 'auto',
paymentMode: 'none',
active: false
};
const fam = await pbAdmin.create('fams', famData);
const user = await pbAdmin.create('users', {
username,
name: parentName,
@@ -61,7 +67,6 @@ export const actions = {
.catch(() => null);
if (authResult?.token) setSessionCookie(event.cookies, authResult.token);
// `username` here is the handle (URL segment), not the composite.
return { success: true, famSlug: slug, username: handleName };
},
@@ -71,11 +76,14 @@ export const actions = {
const fd = await event.request.formData();
const name = ((fd.get('member') as string) || '').trim();
// No [fam] URL param here (signup isn't fam-scoped) — resolve the slug
// from DB. Everywhere else, the slug comes from event.params.fam /
// page.data.famSlug ([fam] layout load) — never copy it into state.
const fam = await pbAdmin.getOne('fams', user.famId);
const famSlug = fam?.slug || user.famId;
if (!name) {
return { success: true, famSlug, username: handleOf(user.username) };
return { success: true, famSlug, username: handleOf(user.username || '') };
}
const { otp, joinUrl } = await issueAccess({
@@ -84,11 +92,57 @@ export const actions = {
name
});
return { success: true, code: otp, joinUrl, famSlug, username: handleOf(user.username) };
return { success: true, code: otp, joinUrl, famSlug, username: handleOf(user.username || '') };
},
// Step 3 — apply an access code (or skip via client-side navigation).
access: async (event: RequestEvent) => {
const user = requireUser(event);
const fd = await event.request.formData();
const code = ((fd.get('code') as string) || '').trim();
if (!code) {
return { ok: true, skipped: true };
}
const result = await applyAccessCode(user.famId, code);
if (result.error) return fail(400, { error: result.error });
return { ok: true, ...result };
},
// Step 4 — choose a plan, start embedded checkout.
choose: async (event: RequestEvent) => {
const user = requireUser(event);
const fd = await event.request.formData();
const plan = fd.get('plan') as PlanId;
if (!['monthly', 'yearly'].includes(plan)) {
return fail(400, { error: 'Unknown plan' });
}
const fam = await pbAdmin.getOne('fams', user.famId);
const parents = await pbAdmin.getList('users', `famId = '${user.famId}' && role = 'parent'`);
const email = (parents[0] as { email?: string } | undefined)?.email || null;
try {
const { clientSecret, sessionId } = await createEmbeddedCheckoutSession({
plan,
famId: user.famId,
famSlug: fam.slug,
email,
customerId: fam.stripeCustomerId || null,
origin: event.url.origin
});
return { success: true, clientSecret, sessionId, plan };
} catch (e) {
if (e instanceof redirect) throw e;
return fail(500, { error: e instanceof Error ? e.message : 'Failed to start checkout' });
}
}
};
function requireUser(event: RequestEvent) {
if (!event.locals.user) throw redirect(303, '/signup');
return event.locals.user;
}
}