PURPOSE

One-time post-claim modal shown after a guest account is upgraded via magic link. Confirms the account is linked, prompts the player to set a callsign (display name), persists the chosen name to Supabase, and dismisses itself. Skipping leaves the default name (“Pilot”) intact. First POC of the medical-UI design system (tick 74) — uses the white-panel MedPanelModal + MedPanel doors variant aesthetic instead of the prior V32 dark/gold styling.

OWNS

  • Local form state: name (string), saving (boolean), error (string | null).
  • The handleSubmit flow: trims input, validates 1–24 chars, calls updateDisplayName, dismisses on success, surfaces error string on failure.
  • Markup for the linked-account header, callsign input, confirm button, and skip button.
  • Visual presentation via MedPanelModal overlay and MedPanel variant="doors" panel chrome with a fixed min(380px, 100%) width.

READS FROM

  • usePlayerStore selectors:
    • s.profile — for profile.email and profile.displayName display strings.
    • s.dismissWelcome — callback used to close the modal.
  • profileService.updateDisplayName — async setter for the persisted display name.
  • MedPanel, MedPanelModal from sibling ./MedPanel for chrome and overlay.
  • CSS utility classes from the medical-UI design system: med-h1, med-h2, med-text, med-text-muted, med-text-critical, med-divider, med-input, med-btn, med-btn-primary, med-btn-ghost, plus the --med-accent CSS variable.

PUSHES TO

  • updateDisplayName(trimmed) — pushes the chosen callsign to Supabase via profileService.
  • dismissWelcome() — clears playerStore.showWelcome so the modal unmounts on next render.

DOES NOT

  • Does not decide when to mount; the App layer renders it conditionally when playerStore.showWelcome is true.
  • Does not perform the magic-link claim itself or any auth flow — assumes the account is already linked.
  • Does not handle profile loading, retries, or queueing — a failed updateDisplayName is surfaced as an inline error string only.
  • Does not persist the skip choice anywhere beyond calling dismissWelcome; the default displayName (“Pilot”) is left untouched.
  • Does not enforce uniqueness, profanity filtering, or server-side validation of the callsign — only the local 1–24 character range.
  • Does not animate or sequence beyond what MedPanelModal / MedPanel variant="doors" provide.

Signals

  • onSubmit on the form → handleSubmit, which validates length, sets saving, calls updateDisplayName, then dismissWelcome or sets error.
  • onChange on the callsign input → setName.
  • onClick on the ghost button → dismissWelcome (skip path).
  • disabled is driven by saving (input + confirm) and by name.trim().length === 0 (confirm only).
  • Validation signal: trimmed length outside [1, 24] sets error to '1-24 characters' and aborts submit.
  • Error signal: a thrown Error from updateDisplayName is stringified into error; non-Error throws fall back to 'Failed to save'.

Entry points

  • Default named export WelcomeModal — a React function component with no props.
  • Imported and rendered at the App level, gated on playerStore.showWelcome.

Pattern notes

  • Selector-style Zustand reads: usePlayerStore(s => s.profile) and usePlayerStore(s => s.dismissWelcome) rather than destructuring the full store, minimizing re-renders.
  • Local UI state (name, saving, error) stays in useState; nothing about the form is hoisted into the global store.
  • Submit guard pattern: setSaving(true) before the await, setSaving(false) in finally so the spinner state clears on both success and failure.
  • Error handling narrows via err instanceof Error before reading err.message, with a string fallback for non-Error throws.
  • Validation is purely client-side and string-length based; copy “1-24 characters” mirrors the maxLength={24} attribute on the input.
  • Styling mixes the medical-UI CSS class system (med-*) with inline style objects for layout-specific tweaks (widths, margins, letter-spacing, font sizes). Accent color comes from the --med-accent CSS variable.
  • MedPanel variant="doors" provides the pneumatic door-reveal mount animation; the component itself does not orchestrate it.
  • autoFocus on the input ensures the callsign field is ready on mount; letterSpacing: '0.04em' and textAlign: 'center' give the sci-fi callsign feel.
  • Skip button uses the existing displayName (or 'Pilot' fallback) in its label so the player sees what they will keep.