diff --git a/apps/web/components/user-menu.tsx b/apps/web/components/user-menu.tsx index a3258ba..f69c61e 100644 --- a/apps/web/components/user-menu.tsx +++ b/apps/web/components/user-menu.tsx @@ -16,8 +16,12 @@ import { useEffect, useRef, useState } from 'react'; interface MeUser { userId: string; - email: string; + // All three can be null on a fresh phone-only signup: name not collected, + // email not entered, phone is the only stable identifier. Anything that + // dereferences these MUST handle the null case. + email: string | null; name: string | null; + phone: string | null; plan?: 'hobby' | 'pro' | 'team' | 'enterprise'; isAdmin?: boolean; } @@ -81,9 +85,19 @@ export function UserMenu() { if (!user) return null; - const label = user.name?.trim() || user.email; - const initial = (user.name?.trim() || user.email).charAt(0).toUpperCase(); - const shade = avatarShade(user.email); + // Pick the best identifier we have. Phone-only signups have null email + // AND null name — used to crash with .charAt(null) here. Fallback chain: + // name → email → phone → "Account". + const identifier = + user.name?.trim() || + user.email || + user.phone || + 'Account'; + const label = identifier; + const initial = identifier.charAt(0).toUpperCase() || '?'; + // Avatar colour is derived from a stable identifier so it doesn't shift + // across sessions. Fall back to userId so we always have something. + const shade = avatarShade(user.email || user.phone || user.userId); return (