You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
New users should be prompted to enter their full name as part of the sign-up flow, in addition to their email. Currently, OTP sign-up collects only an email and the name field is left null until the user fills in their profile.
Acceptance Criteria
After a new user completes OTP verification for the first time, they are prompted to enter their full name before proceeding
The name is saved to both the Neon Auth account and the app User row
Existing users (already have a name) skip this step
The prompt fits the existing post-registration redirect to /profile — consider whether the name collection belongs on a dedicated step or as the first field on the profile page
Name is required (cannot be blank) before the user can proceed into the app
Implementation notes
The current post-registration flow redirects to /profile when the user's profile is incomplete (app/(main)/(auth)/layout.tsx). The name collection could be added as a required first step there, or as an interstitial before /profile. Either way, once the name is collected, update both the Neon Auth user record (via authClient) and the Prisma User row.
Check whether Neon Auth's AuthView has a built-in name field for the sign-up step before building a custom interstitial.
Implementation Plan
Overview
The OTP AuthView (credentials={false} emailOTP) collects only an email — it has no name field, confirming the ticket's suspicion. So we collect the name in-app, on /profile, which is already the destination the auth gate redirects an incomplete user to and already sits outside the (auth) route group (so gating there can't loop). We add a name gate to the (auth) layout, render a required Name field at the top of /profile whenever user.name is missing, and a server action that writes the app User.name and syncs the Neon Auth account via authClient.updateUser({ name }) from the client. Existing users with a name see nothing new.
Changes
app/(main)/(auth)/layout.tsx — also redirect to /profile when user.name is blank (before the existing profile-completeness redirect); admins/managers gated too (name is universal).
prisma/actions/profile.ts — add setUserName(input: unknown) server action: zod-validate, scope the write to user.id, set User.name, revalidatePath('/profile') + revalidatePath('/', 'layout') (sidebar/nav shows the name).
components/features/name-field.tsx — new client leaf: react-hook-form + zod name input that calls setUserName then authClient.updateUser({ name }), toasts, and router.refresh() so the gate clears.
app/(main)/profile/page.tsx — render NameField above ProfileForm only when !user.name; pass defaultName={user.name ?? ''}.
app/(main)/profile/loading.tsx — add a name-field skeleton row at top (avoid layout shift for the gated case).
lib/constants.ts — add NAME_MAX_LENGTH (e.g. 100) if not present; reuse in the zod schema.
Implementation
Add setUserName to prisma/actions/profile.ts: getCurrentUser(), zod schema { name: z.string().trim().min(1).max(NAME_MAX_LENGTH) }, prisma.user.update({ where: { id: user.id }, data: { name } }), revalidatePath('/profile') + revalidatePath('/', 'layout'); return void on success.
Create components/features/name-field.tsx ('use client'): controlled input, submit handler calls setUserName, and on success await authClient.updateUser({ name }) to sync the auth account; isSubmitting disables the button (spinner via Loader2); preserve input on failure.
In app/(main)/profile/page.tsx, conditionally render <NameField defaultName={user.name ?? ''} /> above ProfileForm when !user.name?.trim().
In app/(main)/(auth)/layout.tsx, after resolving user, add if (!user.name?.trim()) redirect('/profile'); ahead of the admin/manager early returns so every authed area enforces the name (mirrors the existing completeness gate, but applies to all roles).
Add the name skeleton block to app/(main)/profile/loading.tsx.
Verify the sidebar/user-menu name display updates after submit (covered by the layout revalidate + router.refresh()).
Data & contracts
No Prisma schema change — User.name String? already exists. The app User.name is the source the app reads; the Neon Auth account name is synced for consistency but is not the read path.
setUserName(input: unknown) in prisma/actions/profile.ts:
Auth: getCurrentUser() (redirects if unauthenticated — no manual unauth branch needed since it never returns null).
zod: z.object({ name: z.string().trim().min(1, 'Enter your full name.').max(NAME_MAX_LENGTH) }); on !success → return { error: 'Enter your full name.' } (user-facing, actionable → toast).
Write scoped to caller: prisma.user.update({ where: { id: user.id }, data: { name: parsed.data.name } }) — no client-supplied ID, no IDOR.
Success → return void. A DB failure throws → generic toast (not user-actionable).
authClient.updateUser({ name }) runs client-side after the action succeeds (the auth client is a 'use client' singleton); if it returns an error, toast a non-blocking warning but treat the step as complete (the app User.name write is the gate-clearing source of truth).
UX states
Gated (new user):/profile shows a Name card at top — heading "What's your name?", helper "Enter your full name so reviewers know who you are.", a single labeled text input (FormLabel "Full name"), and a "Continue" button. Below it the normal profile questions remain visible/editable.
Loading:profile/loading.tsx shows a name-card skeleton above the question skeletons.
Pending: submit button disabled with spinner; input disabled during submit; input preserved on failure.
Empty/blank input: inline zod error "Enter your full name." under the field; button stays enabled to allow correction.
Success:toast.success('Name saved'), router.refresh() clears the gate and reveals the full app (or the original destination on next navigation).
a11y:<label htmlFor>/FormLabel tied to the input, visible focus ring, error text via aria-describedby, button is a real <button type="submit">.
Testing
As a brand-new email (never signed up), complete OTP sign-in → land on /profile with the Name card shown; no other authed route is reachable until name is set (e.g. visiting / redirects back to /profile).
Submit a blank/whitespace name → inline error, no write.
Submit a valid name → success toast, gate clears, sidebar shows the name; reload /profile → Name card no longer shown.
Confirm the Neon Auth account name updated (e.g. via the account/session reflecting the new name).
As an existing user who already has a name → /profile shows no Name card and no redirect loop.
As an admin and as a manager with no name → still gated to /profile until name is set.
Dev bypass user without a name → gated consistently.
Risks / notes
authClient.updateUser must run client-side (it's the browser auth singleton); the server action cannot call it. The app User.name write is authoritative for the gate, so an auth-side sync failure degrades gracefully.
Keep the gate check (!user.name?.trim()) identical in the (auth) layout and the /profile page so they never disagree and loop.
Goal
New users should be prompted to enter their full name as part of the sign-up flow, in addition to their email. Currently, OTP sign-up collects only an email and the name field is left null until the user fills in their profile.
Acceptance Criteria
Userrow/profile— consider whether the name collection belongs on a dedicated step or as the first field on the profile pageImplementation notes
The current post-registration flow redirects to
/profilewhen the user's profile is incomplete (app/(main)/(auth)/layout.tsx). The name collection could be added as a required first step there, or as an interstitial before/profile. Either way, once the name is collected, update both the Neon Auth user record (viaauthClient) and the PrismaUserrow.Check whether Neon Auth's
AuthViewhas a built-in name field for the sign-up step before building a custom interstitial.Implementation Plan
Overview
The OTP
AuthView(credentials={false} emailOTP) collects only an email — it has no name field, confirming the ticket's suspicion. So we collect the name in-app, on/profile, which is already the destination the auth gate redirects an incomplete user to and already sits outside the(auth)route group (so gating there can't loop). We add a name gate to the(auth)layout, render a required Name field at the top of/profilewheneveruser.nameis missing, and a server action that writes the appUser.nameand syncs the Neon Auth account viaauthClient.updateUser({ name })from the client. Existing users with a name see nothing new.Changes
app/(main)/(auth)/layout.tsx— also redirect to/profilewhenuser.nameis blank (before the existing profile-completeness redirect); admins/managers gated too (name is universal).prisma/actions/profile.ts— addsetUserName(input: unknown)server action: zod-validate, scope the write touser.id, setUser.name,revalidatePath('/profile')+revalidatePath('/', 'layout')(sidebar/nav shows the name).components/features/name-field.tsx— new client leaf: react-hook-form + zod name input that callssetUserNamethenauthClient.updateUser({ name }), toasts, androuter.refresh()so the gate clears.app/(main)/profile/page.tsx— renderNameFieldaboveProfileFormonly when!user.name; passdefaultName={user.name ?? ''}.app/(main)/profile/loading.tsx— add a name-field skeleton row at top (avoid layout shift for the gated case).lib/constants.ts— addNAME_MAX_LENGTH(e.g. 100) if not present; reuse in the zod schema.Implementation
setUserNametoprisma/actions/profile.ts:getCurrentUser(), zod schema{ name: z.string().trim().min(1).max(NAME_MAX_LENGTH) },prisma.user.update({ where: { id: user.id }, data: { name } }),revalidatePath('/profile')+revalidatePath('/', 'layout'); returnvoidon success.components/features/name-field.tsx('use client'): controlled input, submit handler callssetUserName, and on successawait authClient.updateUser({ name })to sync the auth account;isSubmittingdisables the button (spinner viaLoader2); preserve input on failure.app/(main)/profile/page.tsx, conditionally render<NameField defaultName={user.name ?? ''} />aboveProfileFormwhen!user.name?.trim().app/(main)/(auth)/layout.tsx, after resolvinguser, addif (!user.name?.trim()) redirect('/profile');ahead of the admin/manager early returns so every authed area enforces the name (mirrors the existing completeness gate, but applies to all roles).app/(main)/profile/loading.tsx.user-menuname display updates after submit (covered by the layout revalidate +router.refresh()).Data & contracts
No Prisma schema change —
User.name String?already exists. The appUser.nameis the source the app reads; the Neon Auth account name is synced for consistency but is not the read path.setUserName(input: unknown)inprisma/actions/profile.ts:getCurrentUser()(redirects if unauthenticated — no manual unauth branch needed since it never returns null).z.object({ name: z.string().trim().min(1, 'Enter your full name.').max(NAME_MAX_LENGTH) }); on!success→return { error: 'Enter your full name.' }(user-facing, actionable → toast).prisma.user.update({ where: { id: user.id }, data: { name: parsed.data.name } })— no client-supplied ID, no IDOR.void. A DB failure throws → generic toast (not user-actionable).authClient.updateUser({ name })runs client-side after the action succeeds (the auth client is a'use client'singleton); if it returns an error, toast a non-blocking warning but treat the step as complete (the appUser.namewrite is the gate-clearing source of truth).UX states
/profileshows a Name card at top — heading "What's your name?", helper "Enter your full name so reviewers know who you are.", a single labeled text input (FormLabel"Full name"), and a "Continue" button. Below it the normal profile questions remain visible/editable.profile/loading.tsxshows a name-card skeleton above the question skeletons.{ error }→toast.error(message); unexpected throw → generic error toast.toast.success('Name saved'),router.refresh()clears the gate and reveals the full app (or the original destination on next navigation).<label htmlFor>/FormLabeltied to the input, visible focus ring, error text viaaria-describedby, button is a real<button type="submit">.Testing
/profilewith the Name card shown; no other authed route is reachable until name is set (e.g. visiting/redirects back to/profile)./profile→ Name card no longer shown./profileshows no Name card and no redirect loop./profileuntil name is set.Risks / notes
authClient.updateUsermust run client-side (it's the browser auth singleton); the server action cannot call it. The appUser.namewrite is authoritative for the gate, so an auth-side sync failure degrades gracefully.!user.name?.trim()) identical in the(auth)layout and the/profilepage so they never disagree and loop.