mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 00:07:01 +00:00
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.
This commit is contained in:
@@ -17,6 +17,17 @@ const TRANSLATED_FILES: Record<string, string[]> = {
|
||||
// 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': [],
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<LoginShell {...shellProps} centered showFooter>
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function EmailStep({
|
||||
</View>
|
||||
|
||||
<Input
|
||||
placeholder="you@example.com"
|
||||
placeholder={t('login.username_placeholder', 'user@example.com')}
|
||||
value={value}
|
||||
onChangeText={onChange}
|
||||
autoFocus
|
||||
|
||||
@@ -13,6 +13,7 @@ import Constants from 'expo-constants';
|
||||
import { ArrowLeft, X } from 'lucide-react-native';
|
||||
import { spacing, typography, componentSizes, type ThemePalette } from '../../theme/tokens';
|
||||
import { useColors } from '../../theme/colors';
|
||||
import { useLocaleStore } from '../../stores/locale-store';
|
||||
|
||||
const APP_VERSION = Constants.expoConfig?.version ?? '0.0.0';
|
||||
|
||||
@@ -41,6 +42,7 @@ export default function LoginShell({
|
||||
}: LoginShellProps) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const t = useLocaleStore((s) => s.t);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
@@ -50,14 +52,26 @@ export default function LoginShell({
|
||||
>
|
||||
<View style={styles.header}>
|
||||
{onBack ? (
|
||||
<Pressable onPress={onBack} hitSlop={12} style={styles.headerButton}>
|
||||
<Pressable
|
||||
onPress={onBack}
|
||||
hitSlop={12}
|
||||
style={styles.headerButton}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t('common.back', 'Back')}
|
||||
>
|
||||
<ArrowLeft size={22} color={c.textSecondary} />
|
||||
</Pressable>
|
||||
) : (
|
||||
<View style={styles.headerButton} />
|
||||
)}
|
||||
{onClose ? (
|
||||
<Pressable onPress={onClose} hitSlop={12} style={styles.headerButton}>
|
||||
<Pressable
|
||||
onPress={onClose}
|
||||
hitSlop={12}
|
||||
style={styles.headerButton}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t('common.close', 'Close')}
|
||||
>
|
||||
<X size={22} color={c.textSecondary} />
|
||||
</Pressable>
|
||||
) : (
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function PasswordStep({
|
||||
|
||||
<Input
|
||||
label={t('login.mobile.username_label', 'Email or username')}
|
||||
placeholder="you@example.com"
|
||||
placeholder={t('login.username_placeholder', 'user@example.com')}
|
||||
value={email}
|
||||
onChangeText={onChangeEmail}
|
||||
autoCapitalize="none"
|
||||
|
||||
Reference in New Issue
Block a user