From 5801d6c97a047a100d4e50fe5af0cf20f23b71ee Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 24 Sep 2026 20:38:10 +0200 Subject: [PATCH] fix: show a translated notice when the sign-in session expired The sign-in screen showed the auth store's raw English error ("Session expired", "Invalid username or password") as the notice title. It now runs it through describeLoginError like every other sign-in failure, which also learns to explain an expired session with the webmail's login.session_expired copy. The back and close buttons of the sign-in steps get screen reader labels, and the email placeholders use the webmail's translated example address. --- src/components/__tests__/screens-i18n.test.ts | 11 +++++++++++ src/lib/__tests__/login-errors.test.ts | 5 +++++ src/lib/login-errors.ts | 6 ++++++ src/screens/LoginScreen.tsx | 3 ++- src/screens/login/EmailStep.tsx | 2 +- src/screens/login/LoginShell.tsx | 18 ++++++++++++++++-- src/screens/login/PasswordStep.tsx | 2 +- 7 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/components/__tests__/screens-i18n.test.ts b/src/components/__tests__/screens-i18n.test.ts index 45ef0a83..effbec40 100644 --- a/src/components/__tests__/screens-i18n.test.ts +++ b/src/components/__tests__/screens-i18n.test.ts @@ -17,6 +17,17 @@ const TRANSLATED_FILES: Record = { // The link prompt shows an example URL. 'screens/ComposeScreen.tsx': ['https://example.com'], 'components/RichTextEditor.tsx': [], + 'screens/LoginScreen.tsx': [], + // Product name and example host. + 'screens/login/ChooseStep.tsx': ['Bulwark Mail'], + 'screens/login/ConfirmStep.tsx': [], + 'screens/login/EmailStep.tsx': [], + 'screens/login/LoginNotice.tsx': [], + 'screens/login/LoginShell.tsx': ['Bulwark Mobile v'], + 'screens/login/OptionTile.tsx': [], + 'screens/login/PasswordStep.tsx': [], + 'screens/login/ServerStep.tsx': ['mail.example.com'], + 'screens/login/SigningInStep.tsx': [], 'screens/ScheduledScreen.tsx': [], 'screens/EmailSourceScreen.tsx': [], 'components/OfflineBanner.tsx': [], diff --git a/src/lib/__tests__/login-errors.test.ts b/src/lib/__tests__/login-errors.test.ts index cef5c52b..d455b081 100644 --- a/src/lib/__tests__/login-errors.test.ts +++ b/src/lib/__tests__/login-errors.test.ts @@ -14,6 +14,11 @@ describe('describeLoginError', () => { expect(copy.detail).toMatch(/app password/i); }); + it('explains the auth store\'s "Session expired" notice', () => { + expect(describeLoginError('Session expired').title).toBe('Your session has expired. Please sign in again.'); + expect(describeLoginError('Session expired for this account').title).toBe('Your session has expired. Please sign in again.'); + }); + it('names the host it could not reach', () => { const copy = describeLoginError(named('NetworkError', 'Network request failed'), { serverUrl: 'https://mail.example.com', diff --git a/src/lib/login-errors.ts b/src/lib/login-errors.ts index fd7e357f..70646643 100644 --- a/src/lib/login-errors.ts +++ b/src/lib/login-errors.ts @@ -84,6 +84,12 @@ export function describeLoginError(err: unknown, context: LoginErrorContext = {} }; } + // A saved session the server no longer accepts (the auth store's + // "Session expired" notice when the app lands back on sign-in). + if (lower.includes('session expired')) { + return { title: t('login.session_expired', 'Your session has expired. Please sign in again.') }; + } + if (lower.includes('certificate') || lower.includes('ssl') || lower.includes('tls')) { return { title: t('login.mobile.err_cert_title', "Couldn't verify {host}", { host }), diff --git a/src/screens/LoginScreen.tsx b/src/screens/LoginScreen.tsx index 9c6ee4c2..12d28cae 100644 --- a/src/screens/LoginScreen.tsx +++ b/src/screens/LoginScreen.tsx @@ -333,7 +333,8 @@ export default function LoginScreen({ onLogin, isAddMode = false, onCancel }: Lo if (step === 'choose') { // A leftover store error ("Session expired") is the reason some people // land back here, so it belongs on this screen. - const chooseNotice = notice ?? (storeError ? { title: storeError } : null); + // The store keeps the raw English message; show the translated copy. + const chooseNotice = notice ?? (storeError ? describeLoginError(storeError, { t }) : null); return ( <> diff --git a/src/screens/login/EmailStep.tsx b/src/screens/login/EmailStep.tsx index 0581e9a8..14627ec7 100644 --- a/src/screens/login/EmailStep.tsx +++ b/src/screens/login/EmailStep.tsx @@ -49,7 +49,7 @@ export default function EmailStep({ makeStyles(c), [c]); + const t = useLocaleStore((s) => s.t); return ( @@ -50,14 +52,26 @@ export default function LoginShell({ > {onBack ? ( - + ) : ( )} {onClose ? ( - + ) : ( diff --git a/src/screens/login/PasswordStep.tsx b/src/screens/login/PasswordStep.tsx index 8fcc5ddb..a77e8193 100644 --- a/src/screens/login/PasswordStep.tsx +++ b/src/screens/login/PasswordStep.tsx @@ -55,7 +55,7 @@ export default function PasswordStep({