From e682ad3e5b20f37795dac2178e7acc69f103c8dd Mon Sep 17 00:00:00 2001 From: lsdsjy <1356263+lsdsjy@users.noreply.github.com> Date: Mon, 21 Sep 2026 19:53:58 +0800 Subject: [PATCH] fix(ui-settings-account): keep sign-in errors in the login dialog --- .../ui-settings-account/README.i18n.yaml | 4 +- packages/client/ui-settings-account/README.md | 2 +- .../client/ui-settings-account/README.zh.md | 2 +- .../src/client/AccountMenu.tsx | 12 +++--- .../tests/account.client.spec.tsx | 43 ++++++++++++++++++- .../tests/expected/login-failed-en.txt | 1 + 6 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 packages/client/ui-settings-account/tests/expected/login-failed-en.txt diff --git a/packages/client/ui-settings-account/README.i18n.yaml b/packages/client/ui-settings-account/README.i18n.yaml index fb41dc5813..db8b7570f1 100644 --- a/packages/client/ui-settings-account/README.i18n.yaml +++ b/packages/client/ui-settings-account/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-settings-account/README.md -README.md: 99053a5ffb5e85bb9754a1d2b00dffbadbaf51da -README.zh.md: aa89bce650374e5f2de15277cd53c0eb9cf8bc38 +README.md: 825bb63e59a08e2d9fb05a3a730c17ae53af9c51 +README.zh.md: 3949c16008bb664e816d649c6096768311c53331 diff --git a/packages/client/ui-settings-account/README.md b/packages/client/ui-settings-account/README.md index 99053a5ffb..825bb63e59 100644 --- a/packages/client/ui-settings-account/README.md +++ b/packages/client/ui-settings-account/README.md @@ -44,7 +44,7 @@ The Account section appears first in Settings only while signed in; it is hidden The [credentials subsystem](../../../docs/subsystems/credentials.md) owns storage APIs; the [architecture](../../../docs/architecture.md) explains application composition. -Account login uses a dismissible dialog before the model onboarding credential editor. The dialog uses a compact, right-aligned action row with the primary action last. The sidebar displays the profile name, falling back to the server-masked phone number or email when the name is absent; it stays blank while the profile is loading. Dialog copy and the sidebar account label are not text-selectable; the copy-link button remains available and restores its label two seconds after each successful copy. Clipboard failures show Copy failed on the link for two seconds without interrupting sign-in. Waiting shows a copyable authorization link and a loading indicator; timeout and failure require an explicit retry. Closing a waiting dialog cancels its Host attempt. The sidebar can reopen the same API-key editor through the settings coordinator. Browser login preopens a tab on the click and navigates it only when that attempt receives its authorization URL; blocked popups retain the copy-link route. The original tab retains the authorization window handle and closes it on failure, timeout, or cancellation. Login feedback stays in the original tab; callbacks never navigate to a second Web UI. +Account login uses a dismissible dialog before the model onboarding credential editor. The dialog uses a compact, right-aligned action row with the primary action last. The sidebar displays the profile name, falling back to the server-masked phone number or email when the name is absent; it stays blank while the profile is loading. Dialog copy and the sidebar account label are not text-selectable; the copy-link button remains available and restores its label two seconds after each successful copy. Clipboard failures show Copy failed on the link for two seconds without interrupting sign-in. Waiting shows a copyable authorization link and a loading indicator; timeout and failure require an explicit retry. Sign-in failures render in that dialog; the launcher's inline failure line reports only a failed sign-out. Closing a waiting dialog cancels its Host attempt. The sidebar can reopen the same API-key editor through the settings coordinator. Browser login preopens a tab on the click and navigates it only when that attempt receives its authorization URL; blocked popups retain the copy-link route. The original tab retains the authorization window handle and closes it on failure, timeout, or cancellation. Login feedback stays in the original tab; callbacks never navigate to a second Web UI. The balance card shows recharge funds and positive bonus credit in separate rows. Empty or nonpositive bonus wallets hide the bonus row and its divider; currencies retain their own amounts. diff --git a/packages/client/ui-settings-account/README.zh.md b/packages/client/ui-settings-account/README.zh.md index aa89bce650..3949c16008 100644 --- a/packages/client/ui-settings-account/README.zh.md +++ b/packages/client/ui-settings-account/README.zh.md @@ -44,7 +44,7 @@ Desktop 的用量和充值操作在 48px 返回栏下方打开隔离的原生 Pl [凭证子系统](../../../docs/subsystems/credentials.zh.md)定义存储接口;[架构](../../../docs/architecture.zh.md)说明应用组合。 -账号登录在模型引导的凭证编辑器之前显示可关闭的弹窗。弹窗底部采用紧凑的右对齐横排操作,主操作位于末尾。侧边栏优先显示昵称,缺少昵称时显示服务端返回的脱敏手机号或邮箱;资料加载期间保持留空。 弹窗文字和侧边栏账号状态文字不可选中,复制登录链接按钮始终可用,并在每次复制成功两秒后恢复原文案。剪贴板失败时,链接原位显示“复制失败”两秒,不中断登录。等待状态提供可复制的授权链接和加载图标;超时和失败均须用户手动重试。关闭等待弹窗会取消对应 Host 登录流程。侧边栏通过设置协调器重新打开同一个 API Key 编辑器。浏览器登录在点击时预先打开标签页,只有该次流程收到授权地址后才导航;弹窗被拦截时保留复制链接入口。原标签页保留授权窗口引用,并在失败、超时或取消时关闭它。登录反馈留在原标签页,回调不会导航到第二个 Web UI。 +账号登录在模型引导的凭证编辑器之前显示可关闭的弹窗。弹窗底部采用紧凑的右对齐横排操作,主操作位于末尾。侧边栏优先显示昵称,缺少昵称时显示服务端返回的脱敏手机号或邮箱;资料加载期间保持留空。 弹窗文字和侧边栏账号状态文字不可选中,复制登录链接按钮始终可用,并在每次复制成功两秒后恢复原文案。剪贴板失败时,链接原位显示“复制失败”两秒,不中断登录。等待状态提供可复制的授权链接和加载图标;超时和失败均须用户手动重试。登录失败在该弹窗内呈现;侧边栏入口的内联失败提示仅用于退出登录失败。关闭等待弹窗会取消对应 Host 登录流程。侧边栏通过设置协调器重新打开同一个 API Key 编辑器。浏览器登录在点击时预先打开标签页,只有该次流程收到授权地址后才导航;弹窗被拦截时保留复制链接入口。原标签页保留授权窗口引用,并在失败、超时或取消时关闭它。登录反馈留在原标签页,回调不会导航到第二个 Web UI。 余额卡片分行展示充值余额与正额赠金余额。赠送钱包为空或没有正余额时,隐藏赠金行及其分隔线;不同币种分别显示。 diff --git a/packages/client/ui-settings-account/src/client/AccountMenu.tsx b/packages/client/ui-settings-account/src/client/AccountMenu.tsx index a6e80fb7ac..79caf52db6 100644 --- a/packages/client/ui-settings-account/src/client/AccountMenu.tsx +++ b/packages/client/ui-settings-account/src/client/AccountMenu.tsx @@ -25,14 +25,16 @@ export function AccountMenu({ ? profile.value.name ?? profile.value.contact ?? t('signedIn') : t('signedIn') const [open, setOpen] = useState(false) const [busy, setBusy] = useState(false) - const [failed, setFailed] = useState(false) + const [logoutFailed, setLogoutFailed] = useState(false) const logout = async () => { setBusy(true) - setFailed(false) + setLogoutFailed(false) try { await signOut(); setOpen(false) } - catch { setFailed(true) } + catch { setLogoutFailed(true) } finally { setBusy(false) } } + // The plugin's start publishes `loginFailed` before it rejects, so the dialog owns the report. + const beginSignIn = (): void => { setOpen(false); void start().catch(() => undefined) } return
{ if (id === 'settings') { setOpen(false); openSettings() } else if (id === 'contact') { setOpen(false); contactUs() } - else if (id === 'signin') { setOpen(false); void start().catch(() => { setFailed(true) }) } + else if (id === 'signin') beginSignIn() else if (id === 'signout') void logout() }} /> {account.loginVisible && !account.onboarding && { showLogin(false) }} useApiKey={() => { showLogin(false); openOnboarding('deepseek-official') }} />} - {failed && {t('failed')}} + {logoutFailed && {t('failed')}}
} diff --git a/packages/client/ui-settings-account/tests/account.client.spec.tsx b/packages/client/ui-settings-account/tests/account.client.spec.tsx index 9dfa3dd269..945315ad20 100644 --- a/packages/client/ui-settings-account/tests/account.client.spec.tsx +++ b/packages/client/ui-settings-account/tests/account.client.spec.tsx @@ -4,7 +4,7 @@ import { afterEach, expect, it, vi } from 'vitest' import type { GlobalStandardProps } from '@deepseek-ai/dsh-client-ui-slots' import type { AccountDetails, AccountView, SignInAttemptId } from '@deepseek-ai/dsh-deepseek-account/types' import type { PlatformBridge } from '../src/client/PlatformOverlay.tsx' -import { AccountSection, type AccountSectionInjected } from '../src/client/AccountSection.tsx' +import { AccountSection, type AccountSectionInjected, type AccountSnapshot } from '../src/client/AccountSection.tsx' import type {} from '../src/client/index.ts' import { en, zh, type AccountKey } from '../src/client/locales.ts' @@ -74,6 +74,47 @@ it.each([en, zh])('opens settings and signs out from the sidebar account menu', expect(screen.queryByRole('menu')).toBeNull() }) +it('reports a failed start in the login dialog, not as a sidebar alert', async () => { + const operations = mount({ status: 'signed-out', attempt: null }) + cleanup() + const { AccountMenu } = await import('../src/client/AccountMenu.tsx') + const links = { usageUrl: 'http://localhost:8081/usage', topUpUrl: 'http://localhost:8081/top_up' } + let snapshot: AccountSnapshot = { view: { status: 'signed-out', attempt: null, links }, details: undefined, failed: false } + // The plugin's start publishes the failure through the account snapshot, then rejects. + const start = vi.fn((): Promise => { + snapshot = { ...snapshot, loginVisible: true, loginFailed: true } + return Promise.reject(new Error('account start failed')) + }) + const view = render( selector(snapshot)} wide openOnboarding={() => {}} openSettings={() => {}} + t={key => key in en ? en[key as AccountKey] : key} />) + fireEvent.click(screen.getByRole('button', { name: en.menu })) + await act(async () => { fireEvent.click(screen.getByRole('menuitem', { name: en.signIn })) }) + expect(start).toHaveBeenCalledOnce() + expect(screen.queryByRole('alert')).toBeNull() + view.rerender( selector(snapshot)} wide openOnboarding={() => {}} openSettings={() => {}} + t={key => key in en ? en[key as AccountKey] : key} />) + expect(screen.getByRole('dialog').textContent).toContain(en.failed) + expect(screen.queryByRole('alert')).toBeNull() + // The dialog is the only place that reports the failure, so its copy appears once. + expect(document.body.textContent.split(en.failed)).toHaveLength(2) + await expect(`${document.body.textContent}\n`).toMatchFileSnapshot('./expected/login-failed-en.txt') +}) + +it('keeps the sidebar alert for a failed sign-out', async () => { + const operations = mount({ status: 'credential-stored', attempt: null }) + cleanup() + const { AccountMenu } = await import('../src/client/AccountMenu.tsx') + render( Promise.reject(new Error('account sign-out failed'))} + useAccount={selector => selector(operations.hooks.account.getSnapshot())} wide + openOnboarding={() => {}} openSettings={() => {}} t={key => key in en ? en[key as AccountKey] : key} />) + fireEvent.click(screen.getByRole('button', { name: en.menu })) + await act(async () => { fireEvent.click(screen.getByRole('menuitem', { name: en.signOut })) }) + expect(screen.getByRole('alert').textContent).toBe(en.failed) +}) + it.each([en, zh])('renders Platform profile and recharge wallet balances', async (copy) => { mount({ status: 'credential-stored', attempt: null }, copy, { profile: { status: 'ready', value: { id: null, name: 'Harness Mock (TEST ONLY)', contact: '138****0000' } }, diff --git a/packages/client/ui-settings-account/tests/expected/login-failed-en.txt b/packages/client/ui-settings-account/tests/expected/login-failed-en.txt new file mode 100644 index 0000000000..bc8b5ad67b --- /dev/null +++ b/packages/client/ui-settings-account/tests/expected/login-failed-en.txt @@ -0,0 +1 @@ +Not signed inCould not sign inCould not complete the operation. Try again.Add API KeySign in again