fix(ui-settings-account): keep sign-in errors in the login dialog
This commit is contained in:
1 parent
a2bc1e412c
commit
e682ad3e5b
6 files changed
+54
-10
No files matched your search
@@ -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
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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。
|
||||
|
||||
余额卡片分行展示充值余额与正额赠金余额。赠送钱包为空或没有正余额时,隐藏赠金行及其分隔线;不同币种分别显示。
|
||||
|
||||
|
||||
@@ -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 <div className={css.root}>
|
||||
<Menu open={open} side="top" portal autoFocus className={css.anchor}
|
||||
anchor={<button type="button" className={css.trigger} data-collapsed={!wide} aria-label={t('menu')}
|
||||
@@ -50,11 +52,11 @@ export function AccountMenu({
|
||||
onSelect={(id) => {
|
||||
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 && <SignInDialog account={account} start={start} cancel={cancel} t={t}
|
||||
close={() => { showLogin(false) }} useApiKey={() => { showLogin(false); openOnboarding('deepseek-official') }} />}
|
||||
{failed && <span className={css.error} role="alert">{t('failed')}</span>}
|
||||
{logoutFailed && <span className={css.error} role="alert">{t('failed')}</span>}
|
||||
</div>
|
||||
}
|
||||
@@ -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<void> => {
|
||||
snapshot = { ...snapshot, loginVisible: true, loginFailed: true }
|
||||
return Promise.reject(new Error('account start failed'))
|
||||
})
|
||||
const view = render(<AccountMenu {...({} as GlobalStandardProps)} {...operations} start={start}
|
||||
useAccount={selector => 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(<AccountMenu {...({} as GlobalStandardProps)} {...operations} start={start}
|
||||
useAccount={selector => 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(<AccountMenu {...({} as GlobalStandardProps)} {...operations}
|
||||
signOut={() => 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' } },
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Not signed inCould not sign inCould not complete the operation. Try again.Add API KeySign in again
|
||||
Reference in new issue
Block a user