fix: ask before signing out

This commit is contained in:
Linus Rath
2026-10-15 02:35:30 +02:00
parent 756ceaa436
commit 8403843e76
6 changed files with 76 additions and 3 deletions
+5
View File
@@ -1129,6 +1129,11 @@
"remove_account_message": "Von {account} auf diesem Gerät abmelden. Ihre E-Mails bleiben auf dem Server.",
"remove_account_title": "Konto entfernen?",
"show_unread": "Ungelesene anzeigen",
"sign_out_all_confirm_message": "Alle Konten werden von diesem Gerät entfernt. Sie können sich jederzeit wieder anmelden.",
"sign_out_all_confirm_title": "Von allen Konten abmelden?",
"sign_out_confirm_message": "Das Konto wird von diesem Gerät entfernt. Sie können sich jederzeit wieder anmelden.",
"sign_out_confirm_title": "Abmelden?",
"sign_out_confirm_title_account": "Von {account} abmelden?",
"tab_unavailable": "{name} (nicht verfügbar)"
},
"unified_mailbox": {
+5
View File
@@ -1148,6 +1148,11 @@
"remove_account_message": "Sign out of {account} on this device. Your mail stays on the server.",
"remove_account_title": "Remove account?",
"show_unread": "Show unread",
"sign_out_all_confirm_message": "Every account is removed from this device. You can sign in again at any time.",
"sign_out_all_confirm_title": "Sign out of all accounts?",
"sign_out_confirm_message": "The account is removed from this device. You can sign in again at any time.",
"sign_out_confirm_title": "Sign out?",
"sign_out_confirm_title_account": "Sign out of {account}?",
"tab_unavailable": "{name} (unavailable)"
},
"unified_mailbox": {
+9 -2
View File
@@ -38,6 +38,7 @@ import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import type { RootStackParamList } from '../navigation/types';
import type { Mailbox } from '../api/types';
import { MODAL_ORIENTATIONS } from '../lib/modal-orientations';
import { confirmSignOut } from '../lib/sign-out-confirm';
const CHEVRON_SLOT = 20;
const INDENT_STEP = 12;
@@ -919,7 +920,10 @@ export default function SidebarDrawer({ visible, onClose, docked = false }: Side
styles.accountMenuAction,
pressed && styles.accountMenuActionPressed,
]}
onPress={() => { setAccountMenuOpen(false); onClose(); void logout(); }}
onPress={() => {
setAccountMenuOpen(false);
confirmSignOut(t, { account: accountEmailFull || undefined }, () => { onClose(); void logout(); });
}}
>
<LogOut size={16} color={c.textSecondary} />
<Text style={styles.accountMenuActionText} numberOfLines={1}>
@@ -932,7 +936,10 @@ export default function SidebarDrawer({ visible, onClose, docked = false }: Side
styles.accountMenuAction,
pressed && styles.accountMenuActionPressed,
]}
onPress={() => { setAccountMenuOpen(false); onClose(); void logoutAll(); }}
onPress={() => {
setAccountMenuOpen(false);
confirmSignOut(t, { all: true }, () => { onClose(); void logoutAll(); });
}}
>
<LogOut size={16} color={c.error} />
<Text style={[styles.accountMenuActionText, { color: c.error }]}>
@@ -0,0 +1,23 @@
import { describe, it, expect, vi } from 'vitest';
const alert = vi.fn();
vi.mock('react-native', () => ({ Alert: { alert: (...a: unknown[]) => alert(...a) } }));
import { confirmSignOut } from '../sign-out-confirm';
const t = (_key: string, fallback: string, params?: Record<string, string | number>) =>
fallback.replace(/\{(\w+)\}/g, (_, k) => String(params?.[k] ?? ''));
describe('confirmSignOut', () => {
it('asks first and signs out only from the destructive button', () => {
const onConfirm = vi.fn();
confirmSignOut(t, { account: 'usera@example.org' }, onConfirm);
expect(onConfirm).not.toHaveBeenCalled();
const [title, , buttons] = alert.mock.calls[0] as [string, string, Array<{ style?: string; onPress?: () => void }>];
expect(title).toBe('Sign out of usera@example.org?');
buttons.find((b) => b.style === 'cancel')?.onPress?.();
expect(onConfirm).not.toHaveBeenCalled();
buttons.find((b) => b.style === 'destructive')!.onPress!();
expect(onConfirm).toHaveBeenCalledTimes(1);
});
});
+31
View File
@@ -0,0 +1,31 @@
import { Alert } from 'react-native';
type Translate = (key: string, fallback: string, params?: Record<string, string | number>) => string;
/**
* Asks before signing out: it drops the account's cached mail and push
* registration on this device, and a stray tap in the account menu or at the
* end of Settings did it straight away.
*/
export function confirmSignOut(
t: Translate,
{ account, all = false }: { account?: string; all?: boolean },
onConfirm: () => void,
): void {
const title = all
? t('sidebar.sign_out_all_confirm_title', 'Sign out of all accounts?')
: account
? t('sidebar.sign_out_confirm_title_account', 'Sign out of {account}?', { account })
: t('sidebar.sign_out_confirm_title', 'Sign out?');
const message = all
? t('sidebar.sign_out_all_confirm_message', 'Every account is removed from this device. You can sign in again at any time.')
: t('sidebar.sign_out_confirm_message', 'The account is removed from this device. You can sign in again at any time.');
Alert.alert(title, message, [
{ text: t('common.cancel', 'Cancel'), style: 'cancel' },
{
text: all ? t('sidebar.sign_out_all', 'Sign out of all accounts') : t('sidebar.sign_out', 'Sign out'),
style: 'destructive',
onPress: onConfirm,
},
]);
}
+3 -1
View File
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import { View, Text, StyleSheet, ScrollView, Pressable, TextInput, Keyboard } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useLayout } from '../lib/use-layout';
import { confirmSignOut } from '../lib/sign-out-confirm';
import {
ArrowLeft, LogOut, Settings, ChevronRight, Search, X,
Palette, User, Shield, UserPen, Palmtree, Calendar,
@@ -506,7 +507,8 @@ export default function SettingsScreen({ onLogout, onBack, onTabSelect }: Settin
{/* Logout - matches webmail: border-t, destructive text, icon + label */}
<View style={styles.logoutSection}>
<Pressable
onPress={onLogout}
onPress={() => { if (onLogout) confirmSignOut(t, {}, onLogout); }}
accessibilityRole="button"
style={({ pressed }) => [styles.logoutBtn, pressed && styles.logoutBtnPressed]}
>
<LogOut size={16} color={c.error} />