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
handleSubmitflow: trims input, validates 1–24 chars, callsupdateDisplayName, dismisses on success, surfaces error string on failure. - Markup for the linked-account header, callsign input, confirm button, and skip button.
- Visual presentation via
MedPanelModaloverlay andMedPanel variant="doors"panel chrome with a fixedmin(380px, 100%)width.
READS FROM
usePlayerStoreselectors:s.profile— forprofile.emailandprofile.displayNamedisplay strings.s.dismissWelcome— callback used to close the modal.
profileService.updateDisplayName— async setter for the persisted display name.MedPanel,MedPanelModalfrom sibling./MedPanelfor 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-accentCSS variable.
PUSHES TO
updateDisplayName(trimmed)— pushes the chosen callsign to Supabase viaprofileService.dismissWelcome()— clearsplayerStore.showWelcomeso the modal unmounts on next render.
DOES NOT
- Does not decide when to mount; the App layer renders it conditionally when
playerStore.showWelcomeis 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
updateDisplayNameis surfaced as an inline error string only. - Does not persist the skip choice anywhere beyond calling
dismissWelcome; the defaultdisplayName(“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
onSubmiton the form →handleSubmit, which validates length, setssaving, callsupdateDisplayName, thendismissWelcomeor setserror.onChangeon the callsign input →setName.onClickon the ghost button →dismissWelcome(skip path).disabledis driven bysaving(input + confirm) and byname.trim().length === 0(confirm only).- Validation signal: trimmed length outside
[1, 24]setserrorto'1-24 characters'and aborts submit. - Error signal: a thrown
ErrorfromupdateDisplayNameis stringified intoerror; 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)andusePlayerStore(s => s.dismissWelcome)rather than destructuring the full store, minimizing re-renders. - Local UI state (
name,saving,error) stays inuseState; nothing about the form is hoisted into the global store. - Submit guard pattern:
setSaving(true)before the await,setSaving(false)infinallyso the spinner state clears on both success and failure. - Error handling narrows via
err instanceof Errorbefore readingerr.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 inlinestyleobjects for layout-specific tweaks (widths, margins, letter-spacing, font sizes). Accent color comes from the--med-accentCSS variable. MedPanel variant="doors"provides the pneumatic door-reveal mount animation; the component itself does not orchestrate it.autoFocuson the input ensures the callsign field is ready on mount;letterSpacing: '0.04em'andtextAlign: '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.