From 1f00ab79b1bd863e857941d72a5979e66be4c0c4 Mon Sep 17 00:00:00 2001 From: YellowSnnowmann <167776381+YellowSnnowmann@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:38:08 +0530 Subject: [PATCH] feat(rewards): Discord connect + disconnect/re-link on the Rewards page (#3766) Co-authored-by: Claude Opus 4.8 --- .../rewards/RewardsCommunityTab.tsx | 145 ++++++++++++++++-- .../__tests__/RewardsCommunityTab.test.tsx | 135 +++++++++++++++- app/src/lib/i18n/ar.ts | 10 ++ app/src/lib/i18n/bn.ts | 11 ++ app/src/lib/i18n/de.ts | 11 ++ app/src/lib/i18n/en.ts | 10 ++ app/src/lib/i18n/es.ts | 10 ++ app/src/lib/i18n/fr.ts | 11 ++ app/src/lib/i18n/hi.ts | 11 ++ app/src/lib/i18n/id.ts | 10 ++ app/src/lib/i18n/it.ts | 10 ++ app/src/lib/i18n/ko.ts | 11 ++ app/src/lib/i18n/pl.ts | 11 ++ app/src/lib/i18n/pt.ts | 11 ++ app/src/lib/i18n/ru.ts | 10 ++ app/src/lib/i18n/zh-CN.ts | 10 ++ .../services/api/__tests__/rewardsApi.test.ts | 46 +++++- app/src/services/api/rewardsApi.ts | 24 +++ app/test/e2e/helpers/shared-flows.ts | 33 ++-- .../e2e/specs/navigation-smoothness.spec.ts | 34 +++- app/test/e2e/specs/navigation.spec.ts | 6 +- .../e2e/specs/webhooks-ingress-flow.spec.ts | 6 +- .../e2e/specs/webhooks-tunnel-flow.spec.ts | 6 +- app/test/playwright/specs/login-flow.spec.ts | 4 +- .../specs/runtime-picker-login.spec.ts | 2 +- 25 files changed, 554 insertions(+), 34 deletions(-) diff --git a/app/src/components/rewards/RewardsCommunityTab.tsx b/app/src/components/rewards/RewardsCommunityTab.tsx index 961128f1e..713d03654 100644 --- a/app/src/components/rewards/RewardsCommunityTab.tsx +++ b/app/src/components/rewards/RewardsCommunityTab.tsx @@ -2,6 +2,7 @@ import createDebug from 'debug'; import { useCallback, useState } from 'react'; import { useT } from '../../lib/i18n/I18nContext'; +import { rewardsApi } from '../../services/api/rewardsApi'; import { callCoreRpc } from '../../services/coreRpcClient'; import type { RewardsAchievement, RewardsSnapshot } from '../../types/rewards'; import { DISCORD_INVITE_URL } from '../../utils/links'; @@ -91,6 +92,9 @@ export default function RewardsCommunityTab({ }: RewardsCommunityTabProps) { const { t } = useT(); const [connectState, setConnectState] = useState<'idle' | 'connecting' | 'error'>('idle'); + const [disconnectState, setDisconnectState] = useState<'idle' | 'disconnecting' | 'error'>( + 'idle' + ); const rewardRoles: RewardsAchievement[] = snapshot?.achievements ?? []; const unlocked = snapshot?.summary.unlockedCount ?? rewardRoles.filter(role => role.unlocked).length; @@ -103,6 +107,13 @@ export default function RewardsCommunityTab({ const ringOffset = ringCircumference - (progressPercent / 100) * ringCircumference; const discordLinked = snapshot?.discord.linked ?? false; const discordUsername = snapshot?.discord.username ?? null; + const membershipStatus = snapshot?.discord.membershipStatus ?? null; + const assignedRoleCount = snapshot?.summary.assignedDiscordRoleCount ?? 0; + // A connected member who unlocked a role-bearing achievement but has not joined the + // server yet cannot receive the role — surface an actionable prompt to join. + const hasUnlockedConfiguredRole = rewardRoles.some(role => role.unlocked && Boolean(role.roleId)); + const showClaimBanner = + discordLinked && membershipStatus === 'not_in_guild' && hasUnlockedConfiguredRole; const handleConnectDiscord = useCallback(async () => { log('connect discord requested'); @@ -129,6 +140,23 @@ export default function RewardsCommunityTab({ setConnectState('error'); } }, []); + + const handleDisconnectDiscord = useCallback(async () => { + log('disconnect discord requested'); + setDisconnectState('disconnecting'); + try { + // Clears user.discordId/discordUsername on the backend (idempotent), which flips the + // rewards snapshot back to unlinked. + await rewardsApi.disconnectDiscord(); + log('disconnect discord ok; refreshing snapshot'); + setDisconnectState('idle'); + // Refetch the snapshot so the connected state flips back to the Connect button (re-link path). + onRetry?.(); + } catch (err) { + log('disconnect discord failed error=%s', err instanceof Error ? err.message : String(err)); + setDisconnectState('error'); + } + }, [onRetry]); return ( <>
@@ -143,16 +171,36 @@ export default function RewardsCommunityTab({
{discordLinked ? ( -
- - {discordUsername - ? t('rewards.community.discordConnectedAs').replace('{username}', discordUsername) - : t('rewards.community.discordConnected')} -
+ <> +
+ + {discordUsername + ? t('rewards.community.discordConnectedAs').replace( + '{username}', + discordUsername + ) + : t('rewards.community.discordConnected')} +
+ + ) : (
@@ -291,6 +347,30 @@ export default function RewardsCommunityTab({ {t('rewards.community.rolesAndRewards')}
+ {showClaimBanner ? ( +
+
+

+ {t('rewards.community.roleClaimTitle')} +

+

+ {t('rewards.community.roleClaimDesc')} +

+
+ +
+ ) : null} {isLoading ? (
@@ -300,6 +380,30 @@ export default function RewardsCommunityTab({ ) : rewardRoles.length > 0 ? ( rewardRoles.map((role, index) => { const tone = roleAccentTone(index); + // Surface Discord role-assignment status only for a linked user's unlocked + // achievements — locked badges have no role to claim yet. + const roleStatus = + discordLinked && role.unlocked + ? role.discordRoleStatus === 'assigned' + ? { + label: t('rewards.community.roleAssigned'), + classes: + 'bg-emerald-50 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-300', + } + : role.discordRoleStatus === 'not_assigned' + ? { + label: t('rewards.community.rolePending'), + classes: + 'bg-amber-50 text-amber-700 dark:bg-amber-500/10 dark:text-amber-300', + } + : role.discordRoleStatus === 'not_in_guild' + ? { + label: t('rewards.community.roleJoinToClaim'), + classes: + 'bg-blue-50 text-primary-700 dark:bg-blue-500/10 dark:text-primary-300', + } + : null + : null; return (
+ {roleStatus ? ( +
+ + {roleStatus.label} + +
+ ) : null}
); }) @@ -385,6 +498,18 @@ export default function RewardsCommunityTab({
) : null} + {discordLinked && membershipStatus === 'member' ? ( +
+ {t('rewards.community.rolesAndRewards')} + + {t('rewards.community.roleAssignmentCount') + .replace('{assigned}', String(assignedRoleCount)) + .replace('{unlocked}', String(unlocked))} + +
+ ) : null}
{t('rewards.community.currentStreak')} diff --git a/app/src/components/rewards/__tests__/RewardsCommunityTab.test.tsx b/app/src/components/rewards/__tests__/RewardsCommunityTab.test.tsx index f50d9eaf1..a03648039 100644 --- a/app/src/components/rewards/__tests__/RewardsCommunityTab.test.tsx +++ b/app/src/components/rewards/__tests__/RewardsCommunityTab.test.tsx @@ -9,14 +9,16 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { RewardsSnapshot } from '../../../types/rewards'; -const { openUrl, callCoreRpc, setOAuthReturnRoute } = vi.hoisted(() => ({ +const { openUrl, callCoreRpc, setOAuthReturnRoute, disconnectDiscord } = vi.hoisted(() => ({ openUrl: vi.fn(), callCoreRpc: vi.fn(), setOAuthReturnRoute: vi.fn(), + disconnectDiscord: vi.fn(), })); vi.mock('../../../utils/openUrl', () => ({ openUrl })); vi.mock('../../../services/coreRpcClient', () => ({ callCoreRpc })); +vi.mock('../../../services/api/rewardsApi', () => ({ rewardsApi: { disconnectDiscord } })); vi.mock('../../../utils/oauthReturnRoute', () => ({ setOAuthReturnRoute })); function buildSnapshot(): RewardsSnapshot { @@ -174,3 +176,134 @@ describe('RewardsCommunityTab — Connect Discord', () => { expect(screen.queryByTestId('rewards-connect-discord')).not.toBeInTheDocument(); }); }); + +describe('RewardsCommunityTab — Disconnect Discord', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('disconnects Discord and refreshes the snapshot', async () => { + disconnectDiscord.mockResolvedValueOnce(undefined); + const onRetry = vi.fn(); + const { default: RewardsCommunityTab } = await import('../RewardsCommunityTab'); + render( + + + + ); + + fireEvent.click(screen.getByTestId('rewards-disconnect-discord')); + + await waitFor(() => expect(disconnectDiscord).toHaveBeenCalledTimes(1)); + // Snapshot is refetched so the connected state can flip back to Connect (re-link path). + await waitFor(() => expect(onRetry).toHaveBeenCalledTimes(1)); + expect(screen.queryByTestId('rewards-disconnect-discord-error')).not.toBeInTheDocument(); + }); + + it('surfaces an error and does not refetch when disconnect fails', async () => { + disconnectDiscord.mockRejectedValueOnce(new Error('disconnect failed')); + const onRetry = vi.fn(); + const { default: RewardsCommunityTab } = await import('../RewardsCommunityTab'); + render( + + + + ); + + fireEvent.click(screen.getByTestId('rewards-disconnect-discord')); + + await waitFor(() => + expect(screen.getByTestId('rewards-disconnect-discord-error')).toBeInTheDocument() + ); + expect(onRetry).not.toHaveBeenCalled(); + }); +}); + +describe('RewardsCommunityTab — Discord role assignment', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('shows an assigned badge and the assigned-count for an in-guild member', async () => { + // buildSnapshot: member, role-1 unlocked + assigned, role-2 locked. + const { default: RewardsCommunityTab } = await import('../RewardsCommunityTab'); + render( + + + + ); + + expect(screen.getByTestId('rewards-role-status-role-1')).toHaveTextContent('Role assigned'); + // Locked achievements have no role to claim yet, so no badge. + expect(screen.queryByTestId('rewards-role-status-role-2')).not.toBeInTheDocument(); + expect(screen.getByTestId('rewards-roles-assigned')).toHaveTextContent('1 of 1 roles assigned'); + // Already in the guild -> no join-to-claim prompt. + expect(screen.queryByTestId('rewards-claim-roles-banner')).not.toBeInTheDocument(); + }); + + it('shows a pending badge when an unlocked achievement has no role assigned yet', async () => { + const snapshot = buildSnapshot(); + snapshot.achievements[0].discordRoleStatus = 'not_assigned'; + const { default: RewardsCommunityTab } = await import('../RewardsCommunityTab'); + render( + + + + ); + + expect(screen.getByTestId('rewards-role-status-role-1')).toHaveTextContent('Syncing role'); + }); + + it('prompts a connected non-member to join the server to claim unlocked roles', async () => { + const snapshot = buildSnapshot(); + snapshot.discord.membershipStatus = 'not_in_guild'; + snapshot.achievements[0].discordRoleStatus = 'not_in_guild'; + const { default: RewardsCommunityTab } = await import('../RewardsCommunityTab'); + render( + + + + ); + + expect(screen.getByTestId('rewards-claim-roles-banner')).toBeInTheDocument(); + expect(screen.getByTestId('rewards-role-status-role-1')).toHaveTextContent( + 'Join server to claim' + ); + // The member-only assigned-count row is hidden when the user is not in the guild. + expect(screen.queryByTestId('rewards-roles-assigned')).not.toBeInTheDocument(); + + fireEvent.click(screen.getByTestId('rewards-claim-roles-join')); + expect(openUrl).toHaveBeenCalledWith('https://discord.gg/example'); + }); + + it('hides role-assignment status entirely when Discord is not linked', async () => { + const snapshot = buildSnapshot(); + snapshot.discord = { + linked: false, + discordId: null, + username: null, + inviteUrl: 'https://discord.gg/example', + membershipStatus: 'not_linked', + }; + const { default: RewardsCommunityTab } = await import('../RewardsCommunityTab'); + render( + + + + ); + + expect(screen.queryByTestId('rewards-role-status-role-1')).not.toBeInTheDocument(); + expect(screen.queryByTestId('rewards-claim-roles-banner')).not.toBeInTheDocument(); + expect(screen.queryByTestId('rewards-roles-assigned')).not.toBeInTheDocument(); + }); +}); diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index 34a9d6c2c..a2ed7ca88 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -3181,6 +3181,9 @@ const messages: TranslationMap = { 'rewards.community.connectingDiscord': 'جارٍ الاتصال…', 'rewards.community.cumulativeTokens': 'الرموز التراكمية', 'rewards.community.currentStreak': 'السلسلة الحالية', + 'rewards.community.disconnectDiscord': 'قطع الاتصال', + 'rewards.community.disconnectDiscordError': 'تعذّر قطع اتصال Discord. يُرجى المحاولة مرة أخرى.', + 'rewards.community.disconnectingDiscord': 'جارٍ قطع الاتصال…', 'rewards.community.discordAccount': 'حساب Discord', 'rewards.community.discordConnected': 'تم الاتصال بـ Discord', 'rewards.community.discordConnectedAs': 'متصل باسم {username}', @@ -3196,6 +3199,13 @@ const messages: TranslationMap = { 'rewards.community.loadingRewards': 'جارٍ تحميل المكافآت…', 'rewards.community.locked': 'مفتوح', 'rewards.community.retrying': 'جارٍ إعادة المحاولة…', + 'rewards.community.roleAssigned': 'تم تعيين الرتبة', + 'rewards.community.roleAssignmentCount': 'تم تعيين {assigned} من {unlocked} رتبة', + 'rewards.community.roleClaimDesc': + 'لقد فتحت رتب Discord لكنك لم تنضم بعد إلى خادم OpenHuman. انضم ليتم تعيينها تلقائيًا.', + 'rewards.community.roleClaimTitle': 'احصل على رتب Discord الخاصة بك', + 'rewards.community.roleJoinToClaim': 'انضم إلى الخادم للحصول عليها', + 'rewards.community.rolePending': 'جارٍ مزامنة الرتبة…', 'rewards.community.rolesAndRewards': 'الأدوار والمكافآت', 'rewards.community.streakDays': '{n}', 'rewards.community.syncPending': 'مزامنة المكافآت معلقة', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index 84594d572..4ec047b81 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -3252,6 +3252,10 @@ const messages: TranslationMap = { 'rewards.community.connectingDiscord': 'সংযুক্ত হচ্ছে…', 'rewards.community.cumulativeTokens': 'সঞ্চিত টোকেন', 'rewards.community.currentStreak': 'বর্তমান স্ট্রিক', + 'rewards.community.disconnectDiscord': 'সংযোগ বিচ্ছিন্ন করুন', + 'rewards.community.disconnectDiscordError': + 'Discord সংযোগ বিচ্ছিন্ন করা যায়নি। অনুগ্রহ করে আবার চেষ্টা করুন।', + 'rewards.community.disconnectingDiscord': 'সংযোগ বিচ্ছিন্ন করা হচ্ছে…', 'rewards.community.discordAccount': 'Discord অ্যাকাউন্ট', 'rewards.community.discordConnected': 'Discord সংযুক্ত', 'rewards.community.discordConnectedAs': '{username} হিসেবে সংযুক্ত', @@ -3267,6 +3271,13 @@ const messages: TranslationMap = { 'rewards.community.loadingRewards': 'পুরস্কার লোড হচ্ছে…', 'rewards.community.locked': 'আনলক করা', 'rewards.community.retrying': 'আবার চেষ্টা হচ্ছে…', + 'rewards.community.roleAssigned': 'রোল বরাদ্দ হয়েছে', + 'rewards.community.roleAssignmentCount': '{unlocked}টির মধ্যে {assigned}টি রোল বরাদ্দ হয়েছে', + 'rewards.community.roleClaimDesc': + 'আপনি Discord রোল আনলক করেছেন কিন্তু এখনও OpenHuman সার্ভারে যোগ দেননি। যোগ দিন যাতে সেগুলি স্বয়ংক্রিয়ভাবে বরাদ্দ হয়।', + 'rewards.community.roleClaimTitle': 'আপনার Discord রোল দাবি করুন', + 'rewards.community.roleJoinToClaim': 'দাবি করতে সার্ভারে যোগ দিন', + 'rewards.community.rolePending': 'রোল সিঙ্ক হচ্ছে…', 'rewards.community.rolesAndRewards': 'রোল ও পুরস্কার', 'rewards.community.streakDays': '{n}', 'rewards.community.syncPending': 'পুরস্কার সিঙ্ক মুলতুবি', diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index b381176f2..9a2da8787 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -3334,6 +3334,10 @@ const messages: TranslationMap = { 'rewards.community.connectingDiscord': 'Verbinden…', 'rewards.community.cumulativeTokens': 'Kumulierte Token', 'rewards.community.currentStreak': 'Aktuelle Serie', + 'rewards.community.disconnectDiscord': 'Trennen', + 'rewards.community.disconnectDiscordError': + 'Discord konnte nicht getrennt werden. Bitte versuche es erneut.', + 'rewards.community.disconnectingDiscord': 'Wird getrennt…', 'rewards.community.discordAccount': 'Discord-Konto', 'rewards.community.discordConnected': 'Discord verbunden', 'rewards.community.discordConnectedAs': 'Verbunden als {username}', @@ -3350,6 +3354,13 @@ const messages: TranslationMap = { 'rewards.community.loadingRewards': 'Prämien werden geladen…', 'rewards.community.locked': 'Gesperrt', 'rewards.community.retrying': 'Erneuter Versuch…', + 'rewards.community.roleAssigned': 'Rolle zugewiesen', + 'rewards.community.roleAssignmentCount': '{assigned} von {unlocked} Rollen zugewiesen', + 'rewards.community.roleClaimDesc': + 'Du hast Discord-Rollen freigeschaltet, bist dem OpenHuman-Server aber noch nicht beigetreten. Tritt bei, damit sie automatisch zugewiesen werden.', + 'rewards.community.roleClaimTitle': 'Hol dir deine Discord-Rollen', + 'rewards.community.roleJoinToClaim': 'Server beitreten zum Abholen', + 'rewards.community.rolePending': 'Rolle wird synchronisiert…', 'rewards.community.rolesAndRewards': 'Rollen und Belohnungen', 'rewards.community.streakDays': '{n} Tage', 'rewards.community.syncPending': 'Synchronisierung der Prämien steht aus', diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index 1dd2d506c..d411a8281 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -3851,6 +3851,9 @@ const en: TranslationMap = { 'rewards.community.connectingDiscord': 'Connecting…', 'rewards.community.cumulativeTokens': 'Cumulative tokens', 'rewards.community.currentStreak': 'Current streak', + 'rewards.community.disconnectDiscord': 'Disconnect', + 'rewards.community.disconnectDiscordError': 'Could not disconnect Discord. Please try again.', + 'rewards.community.disconnectingDiscord': 'Disconnecting…', 'rewards.community.discordAccount': 'Discord account', 'rewards.community.discordConnected': 'Discord connected', 'rewards.community.discordConnectedAs': 'Connected as {username}', @@ -3866,6 +3869,13 @@ const en: TranslationMap = { 'rewards.community.loadingRewards': 'Loading rewards…', 'rewards.community.locked': 'Locked', 'rewards.community.retrying': 'Retrying…', + 'rewards.community.roleAssigned': 'Role assigned', + 'rewards.community.roleAssignmentCount': '{assigned} of {unlocked} roles assigned', + 'rewards.community.roleClaimDesc': + 'You unlocked Discord roles but have not joined the OpenHuman server yet. Join to get them assigned automatically.', + 'rewards.community.roleClaimTitle': 'Claim your Discord roles', + 'rewards.community.roleJoinToClaim': 'Join server to claim', + 'rewards.community.rolePending': 'Syncing role…', 'rewards.community.rolesAndRewards': 'Roles & Rewards', 'rewards.community.streakDays': '{n}', 'rewards.community.syncPending': 'Rewards sync pending', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index 636c88194..9ec576577 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -3313,6 +3313,9 @@ const messages: TranslationMap = { 'rewards.community.connectingDiscord': 'Conectando…', 'rewards.community.cumulativeTokens': 'Tokens acumulados', 'rewards.community.currentStreak': 'Racha actual', + 'rewards.community.disconnectDiscord': 'Desconectar', + 'rewards.community.disconnectDiscordError': 'No se pudo desconectar Discord. Inténtalo de nuevo.', + 'rewards.community.disconnectingDiscord': 'Desconectando…', 'rewards.community.discordAccount': 'Cuenta de Discord', 'rewards.community.discordConnected': 'Discord conectado', 'rewards.community.discordConnectedAs': 'Conectado como {username}', @@ -3328,6 +3331,13 @@ const messages: TranslationMap = { 'rewards.community.loadingRewards': 'Cargando recompensas…', 'rewards.community.locked': 'Desbloqueado', 'rewards.community.retrying': 'Reintentando…', + 'rewards.community.roleAssigned': 'Rol asignado', + 'rewards.community.roleAssignmentCount': '{assigned} de {unlocked} roles asignados', + 'rewards.community.roleClaimDesc': + 'Has desbloqueado roles de Discord, pero aún no te has unido al servidor de OpenHuman. Únete para que se asignen automáticamente.', + 'rewards.community.roleClaimTitle': 'Reclama tus roles de Discord', + 'rewards.community.roleJoinToClaim': 'Únete al servidor para reclamar', + 'rewards.community.rolePending': 'Sincronizando rol…', 'rewards.community.rolesAndRewards': 'Roles y recompensas', 'rewards.community.streakDays': '{n}', 'rewards.community.syncPending': 'Sincronización de recompensas pendiente', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index 7609b2f42..a37147247 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -3325,6 +3325,10 @@ const messages: TranslationMap = { 'rewards.community.connectingDiscord': 'Connexion…', 'rewards.community.cumulativeTokens': 'Tokens cumulés', 'rewards.community.currentStreak': 'Série actuelle', + 'rewards.community.disconnectDiscord': 'Déconnecter', + 'rewards.community.disconnectDiscordError': + 'Impossible de déconnecter Discord. Veuillez réessayer.', + 'rewards.community.disconnectingDiscord': 'Déconnexion…', 'rewards.community.discordAccount': 'Compte Discord', 'rewards.community.discordConnected': 'Discord connecté', 'rewards.community.discordConnectedAs': 'Connecté en tant que {username}', @@ -3340,6 +3344,13 @@ const messages: TranslationMap = { 'rewards.community.loadingRewards': 'Chargement des récompenses…', 'rewards.community.locked': 'Débloqué', 'rewards.community.retrying': 'Nouvelle tentative…', + 'rewards.community.roleAssigned': 'Rôle attribué', + 'rewards.community.roleAssignmentCount': '{assigned} sur {unlocked} rôles attribués', + 'rewards.community.roleClaimDesc': + "Vous avez débloqué des rôles Discord mais n'avez pas encore rejoint le serveur OpenHuman. Rejoignez-le pour qu'ils soient attribués automatiquement.", + 'rewards.community.roleClaimTitle': 'Réclamez vos rôles Discord', + 'rewards.community.roleJoinToClaim': 'Rejoindre le serveur pour réclamer', + 'rewards.community.rolePending': 'Synchronisation du rôle…', 'rewards.community.rolesAndRewards': 'Rôles & Récompenses', 'rewards.community.streakDays': '{n}', 'rewards.community.syncPending': 'Synchronisation des récompenses en attente', diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index 313d02f3d..1c8ed7fa1 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -3255,6 +3255,10 @@ const messages: TranslationMap = { 'rewards.community.connectingDiscord': 'कनेक्ट हो रहा है…', 'rewards.community.cumulativeTokens': 'कुल टोकन', 'rewards.community.currentStreak': 'मौजूदा स्ट्रीक', + 'rewards.community.disconnectDiscord': 'डिस्कनेक्ट करें', + 'rewards.community.disconnectDiscordError': + 'Discord डिस्कनेक्ट नहीं हो सका। कृपया पुनः प्रयास करें।', + 'rewards.community.disconnectingDiscord': 'डिस्कनेक्ट हो रहा है…', 'rewards.community.discordAccount': 'Discord खाता', 'rewards.community.discordConnected': 'Discord कनेक्ट हो गया', 'rewards.community.discordConnectedAs': '{username} के रूप में कनेक्टेड', @@ -3270,6 +3274,13 @@ const messages: TranslationMap = { 'rewards.community.loadingRewards': 'रिवॉर्ड लोड हो रहे हैं…', 'rewards.community.locked': 'अनलॉक्ड', 'rewards.community.retrying': 'फिर से कोशिश हो रही है…', + 'rewards.community.roleAssigned': 'रोल असाइन हो गया', + 'rewards.community.roleAssignmentCount': '{unlocked} में से {assigned} रोल असाइन किए गए', + 'rewards.community.roleClaimDesc': + 'आपने Discord रोल अनलॉक किए हैं लेकिन अभी तक OpenHuman सर्वर में शामिल नहीं हुए हैं। शामिल हों ताकि वे अपने-आप असाइन हो जाएँ।', + 'rewards.community.roleClaimTitle': 'अपने Discord रोल क्लेम करें', + 'rewards.community.roleJoinToClaim': 'क्लेम करने के लिए सर्वर जॉइन करें', + 'rewards.community.rolePending': 'रोल सिंक हो रहा है…', 'rewards.community.rolesAndRewards': 'रोल्स और रिवॉर्ड', 'rewards.community.streakDays': '{n}', 'rewards.community.syncPending': 'रिवॉर्ड सिंक पेंडिंग', diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index a85e88f7b..365d58c1a 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -3260,6 +3260,9 @@ const messages: TranslationMap = { 'rewards.community.connectingDiscord': 'Menghubungkan…', 'rewards.community.cumulativeTokens': 'Token kumulatif', 'rewards.community.currentStreak': 'Streak saat ini', + 'rewards.community.disconnectDiscord': 'Putuskan', + 'rewards.community.disconnectDiscordError': 'Tidak dapat memutuskan Discord. Silakan coba lagi.', + 'rewards.community.disconnectingDiscord': 'Memutuskan…', 'rewards.community.discordAccount': 'Akun Discord', 'rewards.community.discordConnected': 'Discord terhubung', 'rewards.community.discordConnectedAs': 'Terhubung sebagai {username}', @@ -3275,6 +3278,13 @@ const messages: TranslationMap = { 'rewards.community.loadingRewards': 'Memuat hadiah...', 'rewards.community.locked': 'Terbuka', 'rewards.community.retrying': 'Mencoba ulang...', + 'rewards.community.roleAssigned': 'Role ditetapkan', + 'rewards.community.roleAssignmentCount': '{assigned} dari {unlocked} role ditetapkan', + 'rewards.community.roleClaimDesc': + 'Kamu telah membuka role Discord tetapi belum bergabung ke server OpenHuman. Bergabunglah agar role ditetapkan secara otomatis.', + 'rewards.community.roleClaimTitle': 'Klaim role Discord kamu', + 'rewards.community.roleJoinToClaim': 'Gabung server untuk klaim', + 'rewards.community.rolePending': 'Menyinkronkan role…', 'rewards.community.rolesAndRewards': 'Peran & Hadiah', 'rewards.community.streakDays': '{n}', 'rewards.community.syncPending': 'Sinkronisasi hadiah tertunda', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index 7d9e1d315..af50a803e 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -3305,6 +3305,9 @@ const messages: TranslationMap = { 'rewards.community.connectingDiscord': 'Connessione…', 'rewards.community.cumulativeTokens': 'Token cumulativi', 'rewards.community.currentStreak': 'Streak attuale', + 'rewards.community.disconnectDiscord': 'Disconnetti', + 'rewards.community.disconnectDiscordError': 'Impossibile disconnettere Discord. Riprova.', + 'rewards.community.disconnectingDiscord': 'Disconnessione…', 'rewards.community.discordAccount': 'Account Discord', 'rewards.community.discordConnected': 'Discord connesso', 'rewards.community.discordConnectedAs': 'Connesso come {username}', @@ -3320,6 +3323,13 @@ const messages: TranslationMap = { 'rewards.community.loadingRewards': 'Caricamento premi…', 'rewards.community.locked': 'Bloccato', 'rewards.community.retrying': 'Nuovo tentativo…', + 'rewards.community.roleAssigned': 'Ruolo assegnato', + 'rewards.community.roleAssignmentCount': '{assigned} di {unlocked} ruoli assegnati', + 'rewards.community.roleClaimDesc': + 'Hai sbloccato i ruoli Discord ma non ti sei ancora unito al server OpenHuman. Unisciti per assegnarli automaticamente.', + 'rewards.community.roleClaimTitle': 'Riscatta i tuoi ruoli Discord', + 'rewards.community.roleJoinToClaim': 'Unisciti al server per riscattare', + 'rewards.community.rolePending': 'Sincronizzazione ruolo…', 'rewards.community.rolesAndRewards': 'Ruoli e premi', 'rewards.community.streakDays': '{n}', 'rewards.community.syncPending': 'Sincronizzazione premi in attesa', diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index 62672893f..3290d9aaa 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -3224,6 +3224,10 @@ const messages: TranslationMap = { 'rewards.community.connectingDiscord': '연결 중…', 'rewards.community.cumulativeTokens': '누적 토큰', 'rewards.community.currentStreak': '현재 연속 기록', + 'rewards.community.disconnectDiscord': '연결 해제', + 'rewards.community.disconnectDiscordError': + 'Discord 연결을 해제하지 못했습니다. 다시 시도해 주세요.', + 'rewards.community.disconnectingDiscord': '연결 해제 중…', 'rewards.community.discordAccount': 'Discord 계정', 'rewards.community.discordConnected': 'Discord 연결됨', 'rewards.community.discordConnectedAs': '{username}(으)로 연결됨', @@ -3240,6 +3244,13 @@ const messages: TranslationMap = { 'rewards.community.loadingRewards': '보상 불러오는 중…', 'rewards.community.locked': '잠김', 'rewards.community.retrying': '다시 시도 중…', + 'rewards.community.roleAssigned': '역할이 부여됨', + 'rewards.community.roleAssignmentCount': '{unlocked}개 중 {assigned}개 역할 부여됨', + 'rewards.community.roleClaimDesc': + 'Discord 역할을 잠금 해제했지만 아직 OpenHuman 서버에 참여하지 않았습니다. 참여하면 자동으로 부여됩니다.', + 'rewards.community.roleClaimTitle': 'Discord 역할 받기', + 'rewards.community.roleJoinToClaim': '받으려면 서버 참여', + 'rewards.community.rolePending': '역할 동기화 중…', 'rewards.community.rolesAndRewards': '역할 및 보상', 'rewards.community.streakDays': '{n}일', 'rewards.community.syncPending': '보상 동기화 대기 중', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index b329354f8..2e46a3ed3 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -3301,6 +3301,10 @@ const messages: TranslationMap = { 'rewards.community.connectingDiscord': 'Łączenie…', 'rewards.community.cumulativeTokens': 'Łączna liczba tokenów', 'rewards.community.currentStreak': 'Aktualna seria', + 'rewards.community.disconnectDiscord': 'Rozłącz', + 'rewards.community.disconnectDiscordError': + 'Nie udało się rozłączyć konta Discord. Spróbuj ponownie.', + 'rewards.community.disconnectingDiscord': 'Rozłączanie…', 'rewards.community.discordAccount': 'Konto Discord', 'rewards.community.discordConnected': 'Discord połączony', 'rewards.community.discordConnectedAs': 'Połączono jako {username}', @@ -3317,6 +3321,13 @@ const messages: TranslationMap = { 'rewards.community.loadingRewards': 'Wczytywanie nagród…', 'rewards.community.locked': 'Zablokowane', 'rewards.community.retrying': 'Ponawianie…', + 'rewards.community.roleAssigned': 'Rola przypisana', + 'rewards.community.roleAssignmentCount': 'Przypisano {assigned} z {unlocked} ról', + 'rewards.community.roleClaimDesc': + 'Odblokowałeś role Discord, ale nie dołączyłeś jeszcze do serwera OpenHuman. Dołącz, aby zostały przypisane automatycznie.', + 'rewards.community.roleClaimTitle': 'Odbierz swoje role Discord', + 'rewards.community.roleJoinToClaim': 'Dołącz do serwera, aby odebrać', + 'rewards.community.rolePending': 'Synchronizowanie roli…', 'rewards.community.rolesAndRewards': 'Role i nagrody', 'rewards.community.streakDays': '{n} dni', 'rewards.community.syncPending': 'Synchronizacja nagród w toku', diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index 95363396e..d9aa1b4fd 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -3309,6 +3309,10 @@ const messages: TranslationMap = { 'rewards.community.connectingDiscord': 'Conectando…', 'rewards.community.cumulativeTokens': 'Tokens acumulados', 'rewards.community.currentStreak': 'Sequência atual', + 'rewards.community.disconnectDiscord': 'Desconectar', + 'rewards.community.disconnectDiscordError': + 'Não foi possível desconectar o Discord. Tente novamente.', + 'rewards.community.disconnectingDiscord': 'Desconectando…', 'rewards.community.discordAccount': 'Conta do Discord', 'rewards.community.discordConnected': 'Discord conectado', 'rewards.community.discordConnectedAs': 'Conectado como {username}', @@ -3324,6 +3328,13 @@ const messages: TranslationMap = { 'rewards.community.loadingRewards': 'Carregando recompensas…', 'rewards.community.locked': 'Desbloqueado', 'rewards.community.retrying': 'Tentando novamente…', + 'rewards.community.roleAssigned': 'Cargo atribuído', + 'rewards.community.roleAssignmentCount': '{assigned} de {unlocked} cargos atribuídos', + 'rewards.community.roleClaimDesc': + 'Você desbloqueou cargos do Discord, mas ainda não entrou no servidor do OpenHuman. Entre para que sejam atribuídos automaticamente.', + 'rewards.community.roleClaimTitle': 'Resgate seus cargos do Discord', + 'rewards.community.roleJoinToClaim': 'Entre no servidor para resgatar', + 'rewards.community.rolePending': 'Sincronizando cargo…', 'rewards.community.rolesAndRewards': 'Funções e Recompensas', 'rewards.community.streakDays': '{n}', 'rewards.community.syncPending': 'Sincronização de recompensas pendente', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index 3908f9af0..9d6564065 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -3281,6 +3281,9 @@ const messages: TranslationMap = { 'rewards.community.connectingDiscord': 'Подключение…', 'rewards.community.cumulativeTokens': 'Токенов всего', 'rewards.community.currentStreak': 'Текущая серия', + 'rewards.community.disconnectDiscord': 'Отключить', + 'rewards.community.disconnectDiscordError': 'Не удалось отключить Discord. Попробуйте ещё раз.', + 'rewards.community.disconnectingDiscord': 'Отключение…', 'rewards.community.discordAccount': 'Аккаунт Discord', 'rewards.community.discordConnected': 'Discord подключён', 'rewards.community.discordConnectedAs': 'Подключено как {username}', @@ -3296,6 +3299,13 @@ const messages: TranslationMap = { 'rewards.community.loadingRewards': 'Загрузка наград…', 'rewards.community.locked': 'Разблокировано', 'rewards.community.retrying': 'Повтор…', + 'rewards.community.roleAssigned': 'Роль назначена', + 'rewards.community.roleAssignmentCount': 'Назначено ролей: {assigned} из {unlocked}', + 'rewards.community.roleClaimDesc': + 'Вы разблокировали роли Discord, но ещё не присоединились к серверу OpenHuman. Присоединитесь, чтобы они назначились автоматически.', + 'rewards.community.roleClaimTitle': 'Получите свои роли в Discord', + 'rewards.community.roleJoinToClaim': 'Присоединиться к серверу', + 'rewards.community.rolePending': 'Синхронизация роли…', 'rewards.community.rolesAndRewards': 'Роли и награды', 'rewards.community.streakDays': '{n}', 'rewards.community.syncPending': 'Синхронизация наград ожидает', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index e82054d17..42137506a 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -3093,6 +3093,9 @@ const messages: TranslationMap = { 'rewards.community.connectingDiscord': '正在连接…', 'rewards.community.cumulativeTokens': '累计令牌', 'rewards.community.currentStreak': '当前连续天数', + 'rewards.community.disconnectDiscord': '断开连接', + 'rewards.community.disconnectDiscordError': '无法断开 Discord 连接,请重试。', + 'rewards.community.disconnectingDiscord': '正在断开连接…', 'rewards.community.discordAccount': 'Discord 账户', 'rewards.community.discordConnected': 'Discord 已连接', 'rewards.community.discordConnectedAs': '已连接为 {username}', @@ -3108,6 +3111,13 @@ const messages: TranslationMap = { 'rewards.community.loadingRewards': '正在加载奖励…', 'rewards.community.locked': '已解锁', 'rewards.community.retrying': '重试中…', + 'rewards.community.roleAssigned': '身份组已分配', + 'rewards.community.roleAssignmentCount': '已分配 {assigned}/{unlocked} 个身份组', + 'rewards.community.roleClaimDesc': + '你已解锁 Discord 身份组,但尚未加入 OpenHuman 服务器。加入后将自动为你分配。', + 'rewards.community.roleClaimTitle': '领取你的 Discord 身份组', + 'rewards.community.roleJoinToClaim': '加入服务器领取', + 'rewards.community.rolePending': '正在同步身份组…', 'rewards.community.rolesAndRewards': '角色与奖励', 'rewards.community.streakDays': '{n} 天', 'rewards.community.syncPending': '奖励同步待处理', diff --git a/app/src/services/api/__tests__/rewardsApi.test.ts b/app/src/services/api/__tests__/rewardsApi.test.ts index b6e5a2562..e953a095e 100644 --- a/app/src/services/api/__tests__/rewardsApi.test.ts +++ b/app/src/services/api/__tests__/rewardsApi.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest'; import { normalizeRewardsApiError, normalizeRewardsSnapshot, rewardsApi } from '../rewardsApi'; -vi.mock('../../apiClient', () => ({ apiClient: { get: vi.fn() } })); +vi.mock('../../apiClient', () => ({ apiClient: { get: vi.fn(), delete: vi.fn() } })); describe('normalizeRewardsSnapshot', () => { it('normalizes a backend rewards payload', () => { @@ -153,6 +153,50 @@ describe('rewardsApi', () => { }); }); +describe('rewardsApi.disconnectDiscord', () => { + it('resolves when the backend returns success', async () => { + const { apiClient } = await import('../../apiClient'); + vi.mocked(apiClient.delete).mockResolvedValueOnce({ success: true, data: null }); + + await expect(rewardsApi.disconnectDiscord()).resolves.toBeUndefined(); + expect(apiClient.delete).toHaveBeenCalledWith('/rewards/discord', { timeout: 15000 }); + }); + + it('throws a normalized error on transport failure', async () => { + const { apiClient } = await import('../../apiClient'); + vi.mocked(apiClient.delete).mockRejectedValueOnce(new Error('network error')); + + await expect(rewardsApi.disconnectDiscord()).rejects.toMatchObject({ + success: false, + error: 'network error', + }); + }); + + it('throws a RewardsApiError when the backend reports failure', async () => { + const { apiClient } = await import('../../apiClient'); + vi.mocked(apiClient.delete).mockResolvedValueOnce({ + success: false, + data: null, + error: 'Unable to disconnect Discord', + }); + + await expect(rewardsApi.disconnectDiscord()).rejects.toMatchObject({ + success: false, + error: 'Unable to disconnect Discord', + }); + }); + + it('falls back to a default message when backend error has no message', async () => { + const { apiClient } = await import('../../apiClient'); + vi.mocked(apiClient.delete).mockResolvedValueOnce({ success: false, data: null }); + + await expect(rewardsApi.disconnectDiscord()).rejects.toMatchObject({ + success: false, + error: 'Unable to disconnect Discord', + }); + }); +}); + describe('normalizeRewardsApiError', () => { it('keeps useful backend errors intact', () => { expect(normalizeRewardsApiError({ error: 'Rewards service unavailable' })).toEqual({ diff --git a/app/src/services/api/rewardsApi.ts b/app/src/services/api/rewardsApi.ts index 2c99c40e2..109c67874 100644 --- a/app/src/services/api/rewardsApi.ts +++ b/app/src/services/api/rewardsApi.ts @@ -191,4 +191,28 @@ export const rewardsApi = { ); return normalizeRewardsSnapshot(response.data); }, + + async disconnectDiscord(): Promise { + let response: ApiResponse; + try { + response = await apiClient.delete>('/rewards/discord', { + timeout: REWARDS_SNAPSHOT_TIMEOUT_MS, + }); + } catch (transportError) { + const normalized = normalizeRewardsApiError(transportError); + log('disconnect transport failed error=%s', normalized.error); + throw normalized; + } + + if (!response.success) { + const appError: RewardsApiError = { + success: false, + error: response.error ?? response.message ?? 'Unable to disconnect Discord', + }; + log('disconnect backend error error=%s', appError.error); + throw appError; + } + + log('discord disconnected'); + }, }; diff --git a/app/test/e2e/helpers/shared-flows.ts b/app/test/e2e/helpers/shared-flows.ts index adfc6a473..9bec60cb5 100644 --- a/app/test/e2e/helpers/shared-flows.ts +++ b/app/test/e2e/helpers/shared-flows.ts @@ -81,7 +81,14 @@ export async function waitForHomePage(timeout = 15_000) { // Home page (Home.tsx) renders t('home.askAssistant') = 'Ask your assistant anything...' // as a stable CTA button. The animated typewriter heading ('Welcome, 👋' etc.) // and old strings ('Good morning', 'Message OpenHuman', 'Upgrade to Premium') are gone. - const candidates = ['Ask your assistant anything', 'Your device is connected']; + // After the /home → /chat redirect (AppRoutes.tsx), the chat new-window hero renders + // t('home.statusOk') instead, so include both the old CTA text and the new status copy. + const candidates = [ + 'Ask your assistant anything', + 'Your device is connected', + 'Your assistant is ready when you are', + 'Type something below to get started', + ]; const deadline = Date.now() + timeout; while (Date.now() < deadline) { for (const text of candidates) { @@ -122,14 +129,15 @@ export async function clickFirstMatch(candidates, timeout = 5_000) { * Appium Mac2 cannot run W3C Execute Script in WKWebView — use sidebar labels * instead. * - * Current IA (bottom-tab bar, see app/src/config/navConfig.ts): the six tabs - * are Home, Chat, Human, Brain, Connections, Settings. The earlier + * Current IA (bottom-tab bar, see app/src/config/navConfig.ts): the four tabs + * are Chat, Human, Brain, Connections. Settings is reached via the gear icon in + * the sidebar header. Home no longer has its own tab (it was merged into Chat in + * Phase 6 — /home redirects to /chat via HASH_REDIRECTS below). The earlier * "Assistant"/"Activity"/"Alerts" labels are gone. Only real tabs belong here; - * routes that redirect (e.g. /activity, /intelligence, /skills, /channels) are - * resolved through HASH_REDIRECTS below — they have no sidebar button. + * routes that redirect (e.g. /home, /activity, /intelligence, /skills, /channels) + * are resolved through HASH_REDIRECTS below — they have no sidebar button. */ const HASH_TO_SIDEBAR_LABEL = { - '/home': 'Home', '/chat': 'Chat', '/human': 'Human', '/brain': 'Brain', @@ -144,6 +152,7 @@ const HASH_TO_SIDEBAR_LABEL = { * app/src/AppRoutes.tsx. */ const HASH_REDIRECTS = { + '/home': '/chat', '/skills': '/connections', '/channels': '/connections', '/activity': '/settings/notifications', @@ -339,7 +348,8 @@ export async function navigateViaHash(hash) { return; } - const label = HASH_TO_SIDEBAR_LABEL[normalized]; + // Resolve redirect before label lookup so that e.g. /home → Chat works on Mac2. + const label = HASH_TO_SIDEBAR_LABEL[resolveRedirect(normalized)]; if (label) { try { await clickText(label, 12_000); @@ -357,20 +367,23 @@ export async function navigateViaHash(hash) { } export async function navigateToHome() { - await navigateViaHash('/home'); + // /home redirects to /chat (AppRoutes.tsx). Navigate directly to /chat so + // the sidebar button click path uses the 'Chat' label which exists, rather + // than 'Home' which no longer has a dedicated tab. + await navigateViaHash('/chat'); const homeText = await waitForHomePage(10_000); if (!homeText) { if (supportsExecuteScript()) { try { await browser.execute(() => { - window.location.hash = '/home'; + window.location.hash = '/chat'; }); } catch { /* ignore */ } } else { try { - await clickText('Home', 8_000); + await clickText('Chat', 8_000); } catch { /* ignore */ } diff --git a/app/test/e2e/specs/navigation-smoothness.spec.ts b/app/test/e2e/specs/navigation-smoothness.spec.ts index fcf248369..29f0af711 100644 --- a/app/test/e2e/specs/navigation-smoothness.spec.ts +++ b/app/test/e2e/specs/navigation-smoothness.spec.ts @@ -34,10 +34,21 @@ interface RouteCheck { const ROUTES: RouteCheck[] = [ // Chat composer header: "New" thread button, agent-profile "Reasoning" pill. - { hash: '/chat', markers: ['New', 'Chat', 'Message', 'Reasoning'] }, + { hash: '/chat', markers: ['New', 'Chat', 'Message', 'Reasoning', 'Your assistant is ready'] }, // Connections page (was /skills) — tabs: Apps, Messaging, Tools, Explorer { hash: '/connections', markers: ['Apps', 'Messaging', 'Tools', 'Connections'] }, - { hash: '/home', markers: ['Ask your assistant anything', 'Your device is connected', 'Home'] }, + // /home redirects to /chat (AppRoutes.tsx Phase 6); markers cover both old + // Home.tsx CTA text and the new chat new-window hero status copy. + { + hash: '/home', + markers: [ + 'Ask your assistant anything', + 'Your device is connected', + 'Your assistant is ready', + 'Home', + 'Chat', + ], + }, { hash: '/notifications', markers: ['Notifications', 'Alerts', 'Notification', 'No notifications'], @@ -48,7 +59,16 @@ const ROUTES: RouteCheck[] = [ // Subconscious surface and memory live here now). Tabs: Graph, Memory, // Sources, Subconscious, Sync. { hash: '/brain', markers: ['Graph', 'Memory', 'Subconscious', 'Sources'] }, - { hash: '/home', markers: ['Ask your assistant anything', 'Your device is connected', 'Home'] }, + { + hash: '/home', + markers: [ + 'Ask your assistant anything', + 'Your device is connected', + 'Your assistant is ready', + 'Home', + 'Chat', + ], + }, ]; async function rootTextLength(): Promise { @@ -129,14 +149,16 @@ describe('Navigation smoothness', () => { console.log(`${LOG_PREFIX} N1.2: passed — rapid cycle complete`); }); - it('N1.3 — final state is /home with correct content', async () => { + it('N1.3 — final state is /home (or /chat redirect) with correct content', async () => { console.log(`${LOG_PREFIX} N1.3: navigating to /home for final check`); await navigateViaHash('/home'); const homeText = await waitForHomePage(ROUTE_TIMEOUT); expect(homeText).toBeTruthy(); + // AppRoutes.tsx redirects /home → /chat, so the settled hash is #/chat. + // Accept either form to keep the test resilient across routing changes. const hash = await browser.execute(() => window.location.hash); - expect(hash).toMatch(/^#\/home/); - console.log(`${LOG_PREFIX} N1.3: passed — on /home, content: "${homeText}"`); + expect(hash).toMatch(/^#\/(home|chat)(\/|$)/); + console.log(`${LOG_PREFIX} N1.3: passed — on ${hash}, content: "${homeText}"`); }); }); diff --git a/app/test/e2e/specs/navigation.spec.ts b/app/test/e2e/specs/navigation.spec.ts index 7f31521a4..27f20827c 100644 --- a/app/test/e2e/specs/navigation.spec.ts +++ b/app/test/e2e/specs/navigation.spec.ts @@ -29,11 +29,15 @@ interface Route { } // Phase 2/3/6 IA revamp: +// /home → /chat (Phase 6 — /home is now the merged chat surface) // /human → /chat (Phase 6 — back-compat redirect) // /skills → /connections (Phase 2 — back-compat redirect) // /intelligence → /activity (Phase 3 — back-compat redirect) +// Note: /home is intentionally omitted here because AppRoutes.tsx redirects it +// to /chat — navigateViaHash('/home') settles on #/chat, which is covered by +// the /chat row. Keeping /home in ROUTES would cause the hash assertion to +// fail since the actual hash is #/chat, not #/home. const ROUTES: Route[] = [ - { hash: '/home' }, { hash: '/chat' }, { hash: '/connections' }, { hash: '/activity' }, diff --git a/app/test/e2e/specs/webhooks-ingress-flow.spec.ts b/app/test/e2e/specs/webhooks-ingress-flow.spec.ts index e895ddbe0..54409b6c5 100644 --- a/app/test/e2e/specs/webhooks-ingress-flow.spec.ts +++ b/app/test/e2e/specs/webhooks-ingress-flow.spec.ts @@ -37,9 +37,13 @@ describe('Webhooks ingress surface (stub-level)', () => { it('reaches the app shell after onboarding', async () => { // Home.tsx: t('home.askAssistant') is the stable home page CTA button text. + // After the /home → /chat redirect (AppRoutes.tsx), the chat new-window hero + // renders t('home.statusOk') instead of the old CTA button. const atHome = (await textExists('Ask your assistant anything')) || - (await textExists('Your device is connected')); + (await textExists('Your device is connected')) || + (await textExists('Your assistant is ready when you are')) || + (await textExists('Type something below to get started')); expect(atHome).toBe(true); }); diff --git a/app/test/e2e/specs/webhooks-tunnel-flow.spec.ts b/app/test/e2e/specs/webhooks-tunnel-flow.spec.ts index 01a429914..9a7871cb5 100644 --- a/app/test/e2e/specs/webhooks-tunnel-flow.spec.ts +++ b/app/test/e2e/specs/webhooks-tunnel-flow.spec.ts @@ -82,9 +82,13 @@ describe('Webhook tunnel CRUD (UI + core RPC + mock backend)', () => { it('reached the logged-in shell after onboarding', async () => { // Home.tsx: t('home.askAssistant') is the stable home page CTA button text. + // After the /home → /chat redirect (AppRoutes.tsx), the chat new-window hero + // renders t('home.statusOk') instead of the old CTA button. const atHome = (await textExists('Ask your assistant anything')) || - (await textExists('Your device is connected')); + (await textExists('Your device is connected')) || + (await textExists('Your assistant is ready when you are')) || + (await textExists('Type something below to get started')); expect(atHome).toBe(true); }); diff --git a/app/test/playwright/specs/login-flow.spec.ts b/app/test/playwright/specs/login-flow.spec.ts index c2b0015a1..7346c4229 100644 --- a/app/test/playwright/specs/login-flow.spec.ts +++ b/app/test/playwright/specs/login-flow.spec.ts @@ -52,7 +52,7 @@ test.describe('Login Flow', () => { await expect .poll(async () => page.evaluate(() => window.location.hash)) - .toMatch(/^#\/(home|chat)/); + .toMatch(/^#\/(home|chat)(\/|$)/); await expect(await waitForMockRequest('GET', '/auth/me')).toBeTruthy(); }); @@ -61,7 +61,7 @@ test.describe('Login Flow', () => { await expect .poll(async () => page.evaluate(() => window.location.hash)) - .toMatch(/^#\/(home|chat)/); + .toMatch(/^#\/(home|chat)(\/|$)/); const consumeCall = (await requests()).find( request => request.method === 'POST' && request.url.includes('/telegram/login-tokens/') diff --git a/app/test/playwright/specs/runtime-picker-login.spec.ts b/app/test/playwright/specs/runtime-picker-login.spec.ts index 9fda33b7e..e756e542b 100644 --- a/app/test/playwright/specs/runtime-picker-login.spec.ts +++ b/app/test/playwright/specs/runtime-picker-login.spec.ts @@ -112,7 +112,7 @@ test.describe('Runtime picker -> login -> logout', () => { await expect .poll(async () => page.evaluate(() => window.location.hash)) - .toMatch(/^#\/(home|chat)/); + .toMatch(/^#\/(home|chat)(\/|$)/); await expect(await waitForMockRequest('GET', '/auth/me')).toBeTruthy(); await page.goto('/#/settings/account');