fix(dashboard): приветствие без пустого имени для email-пользователей

This commit is contained in:
kewldan
2026-07-23 03:36:26 +03:00
parent f04675b249
commit 147b1fbb2e
7 changed files with 96 additions and 35 deletions

View File

@@ -250,6 +250,7 @@
"dashboard": { "dashboard": {
"title": "Dashboard", "title": "Dashboard",
"welcome": "Welcome, {{name}}!", "welcome": "Welcome, {{name}}!",
"welcomeNoName": "Welcome!",
"yourSubscription": "Your Subscription", "yourSubscription": "Your Subscription",
"quickActions": "Quick Actions", "quickActions": "Quick Actions",
"viewSubscription": "Manage Subscription", "viewSubscription": "Manage Subscription",

View File

@@ -235,6 +235,7 @@
"dashboard": { "dashboard": {
"title": "داشبورد", "title": "داشبورد",
"welcome": "خوش آمدید، {{name}}!", "welcome": "خوش آمدید، {{name}}!",
"welcomeNoName": "خوش آمدید!",
"yourSubscription": "اشتراک شما", "yourSubscription": "اشتراک شما",
"quickActions": "دسترسی سریع", "quickActions": "دسترسی سریع",
"viewSubscription": "مدیریت اشتراک", "viewSubscription": "مدیریت اشتراک",

View File

@@ -253,6 +253,7 @@
"dashboard": { "dashboard": {
"title": "Личный кабинет", "title": "Личный кабинет",
"welcome": "Добро пожаловать, {{name}}!", "welcome": "Добро пожаловать, {{name}}!",
"welcomeNoName": "Добро пожаловать!",
"yourSubscription": "Ваша подписка", "yourSubscription": "Ваша подписка",
"quickActions": "Быстрые действия", "quickActions": "Быстрые действия",
"viewSubscription": "Управление подпиской", "viewSubscription": "Управление подпиской",

View File

@@ -235,6 +235,7 @@
"dashboard": { "dashboard": {
"title": "个人中心", "title": "个人中心",
"welcome": "欢迎,{{name}}", "welcome": "欢迎,{{name}}",
"welcomeNoName": "欢迎!",
"yourSubscription": "您的订阅", "yourSubscription": "您的订阅",
"quickActions": "快捷操作", "quickActions": "快捷操作",
"viewSubscription": "管理订阅", "viewSubscription": "管理订阅",

View File

@@ -251,12 +251,14 @@ export default function Dashboard() {
setShowOnboarding(false); setShowOnboarding(false);
}; };
const userName = displayName(user);
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} {/* Header */}
<div data-onboarding="welcome"> <div data-onboarding="welcome">
<h1 className="text-2xl font-bold text-dark-50 sm:text-3xl"> <h1 className="text-2xl font-bold text-dark-50 sm:text-3xl">
{t('dashboard.welcome', { name: displayName(user) })} {userName ? t('dashboard.welcome', { name: userName }) : t('dashboard.welcomeNoName')}
</h1> </h1>
<div className="mt-1 flex flex-wrap items-center gap-2"> <div className="mt-1 flex flex-wrap items-center gap-2">
<p className="text-dark-400">{t('dashboard.yourSubscription')}</p> <p className="text-dark-400">{t('dashboard.yourSubscription')}</p>
@@ -328,9 +330,8 @@ export default function Dashboard() {
)} )}
{/* Subscription Status Card — hidden in multi-tariff (managed via /subscriptions) */} {/* Subscription Status Card — hidden in multi-tariff (managed via /subscriptions) */}
{!isMultiTariff && ( {!isMultiTariff &&
<> (subLoading ? (
{subLoading ? (
<div className="bento-card"> <div className="bento-card">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<div className="skeleton h-5 w-20" /> <div className="skeleton h-5 w-20" />
@@ -359,9 +360,7 @@ export default function Dashboard() {
trafficRefreshCooldown={trafficRefreshCooldown} trafficRefreshCooldown={trafficRefreshCooldown}
connectedDevices={devicesData?.total ?? 0} connectedDevices={devicesData?.total ?? 0}
/> />
) : null} ) : null)}
</>
)}
{/* Нет подписок: показываем триал (если доступен) и ВСЕГДА одну явную {/* Нет подписок: показываем триал (если доступен) и ВСЕГДА одну явную
кнопку покупки. Триал не обязателен, чтобы попасть в витрину — раньше кнопку покупки. Триал не обязателен, чтобы попасть в витрину — раньше

View File

@@ -0,0 +1,51 @@
import { describe, expect, it } from 'vitest';
import { displayName } from './displayName';
describe('displayName', () => {
it('returns empty string for missing user', () => {
expect(displayName(undefined)).toBe('');
expect(displayName(null)).toBe('');
});
it('combines first_name and last_name', () => {
expect(displayName({ first_name: 'Иван', last_name: 'Петров' })).toBe('Иван Петров');
expect(displayName({ first_name: 'Иван', last_name: null })).toBe('Иван');
expect(displayName({ first_name: null, last_name: 'Петров' })).toBe('Петров');
});
it('falls back to username when there is no name', () => {
expect(displayName({ first_name: null, last_name: null, username: 'ivan' })).toBe('ivan');
});
it('falls back to telegram_id when there is no name and no username', () => {
expect(displayName({ first_name: null, username: null, telegram_id: 123456 })).toBe('#123456');
});
it('falls back to the email local part for email-only users', () => {
expect(
displayName({
first_name: null,
last_name: null,
username: null,
telegram_id: null,
email: 'vasya@gmail.com',
}),
).toBe('vasya');
});
it('prefers telegram_id over email', () => {
expect(displayName({ telegram_id: 123456, email: 'vasya@gmail.com' })).toBe('#123456');
});
it('returns empty string when every source is empty', () => {
expect(
displayName({
first_name: ' ',
last_name: null,
username: null,
telegram_id: null,
email: null,
}),
).toBe('');
});
});

View File

@@ -3,21 +3,28 @@
* *
* Why: single-letter first_name (e.g., "О") looked confusing alone ("Добро пожаловать, О!"). * Why: single-letter first_name (e.g., "О") looked confusing alone ("Добро пожаловать, О!").
* Combining with last_name makes truncated/short first names readable. * Combining with last_name makes truncated/short first names readable.
*
* Email/OAuth registration does not collect a name, so for such users every
* Telegram-derived field is null — fall back to the email local part
* ("vasya@gmail.com" → "vasya") to avoid an empty name in greetings.
*/ */
export interface NameSource { export interface NameSource {
first_name?: string | null; first_name?: string | null;
last_name?: string | null; last_name?: string | null;
username?: string | null; username?: string | null;
telegram_id?: number | null; telegram_id?: number | null;
email?: string | null;
} }
export function displayName(user?: NameSource | null): string { export function displayName(user?: NameSource | null): string {
if (!user) return ''; if (!user) return '';
const fullName = [user.first_name, user.last_name] const fullName = [user.first_name, user.last_name]
.filter((part): part is string => Boolean(part && part.trim())) .filter((part): part is string => Boolean(part?.trim()))
.join(' '); .join(' ');
if (fullName) return fullName; if (fullName) return fullName;
if (user.username) return user.username; if (user.username) return user.username;
if (user.telegram_id) return `#${user.telegram_id}`; if (user.telegram_id) return `#${user.telegram_id}`;
const emailLocalPart = user.email?.trim().split('@')[0];
if (emailLocalPart) return emailLocalPart;
return ''; return '';
} }