Skip to content

Require Full Name During Sign-Up #240

Description

@b-at-neu

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

  • 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 !successreturn { 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.
  • Error: action { error }toast.error(message); unexpected throw → generic error toast.
  • 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    claudeWill be worked on by Claudepr openedPull request has been opened

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions