From c656bd31893359607b4cd407e4bd38b28d3e61e5 Mon Sep 17 00:00:00 2001 From: Marco Sadjadi Date: Mon, 25 May 2026 22:59:45 +0200 Subject: [PATCH] fix(web): UserMenu crashes for phone-only signups (null email + name) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dashboard layout threw TypeError: Cannot read properties of null (reading 'charAt') the moment a phone-only user reached any dashboard page — user.email and user.name are both null for fresh SMS signups, and the initial-letter computation didn't tolerate it. Fallback chain for the visible identifier: name → email → phone → 'Account'. Avatar colour seed falls back to userId. The secondary line under the name also uses phone when email is null. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/web/components/user-menu.tsx | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) 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 (
@@ -121,7 +135,7 @@ export function UserMenu() { {label}
- {user.email} + {user.email || user.phone || user.userId.slice(0, 8)}