feat(forms): verified-email retro-link of anonymous submissions (Phase 5)

- claimMyAnonymousSubmissions(): exported but safe — no trusted params,
  resolves member + email from the caller's session, gated on
  email_confirmed_at, idempotent. Links the caller's own prior
  anonymous submissions to their member row
- hooked best-effort from joinSpace (member insert now returns id;
  links prior anon submissions for the verified email) and
  finishOnboarding
- addMember/importMembers deliberately NOT auto-linked: those emails
  are admin-asserted, not verified; the locked decision forbids
  auto-link there, so the forms.manage linkSubmissionsForMember stays
  the admin manual-link path
- also repairs a build break committed in 295d220: finishOnboarding
  declared `const admin` twice (Phase 4 added a second). The probe and
  the completion update now share one admin client
- docs: API_REFERENCE / ARCHITECTURE / HANDOFF

287/287 tests, build compiles clean (verified Compiled successfully). No deploy.
This commit is contained in:
Moheeb Zara
2026-05-16 16:29:32 -07:00
parent da25950988
commit 40d4cdad98
6 changed files with 83 additions and 14 deletions
+12 -2
View File
@@ -9,6 +9,7 @@ import {
updateOnboardingStepSchema,
uuidSchema,
} from '@/lib/validations'
import { claimMyAnonymousSubmissions } from './forms'
const ONBOARDING_ADMIN_ROLES = ['admin', 'board'] as const
@@ -207,8 +208,7 @@ export async function finishOnboarding() {
// onboarding_completed_at is blocked by the self-change trigger (migration
// 024) so a member cannot skip required steps via a raw PostgREST call.
// Set it via the service client AFTER the required-steps check above, scoped
// to this member's own id.
const admin = createAdminClient()
// to this member's own id. Reuses the `admin` client created above.
const { error } = await admin
.from('space_members')
.update({ onboarding_completed_at: new Date().toISOString() })
@@ -216,6 +216,16 @@ export async function finishOnboarding() {
.eq('space_id', member.space_id)
if (error) return { error: error.message }
// Best-effort: pull in any prior anonymous submissions for this now-verified
// member (e.g., a waiver signed before they had an account). Never blocks
// finishing.
try {
await claimMyAnonymousSubmissions()
} catch {
/* advisory */
}
revalidatePath('/dashboard')
return { success: true as const }
}