From 8403843e76b3ca81d7f3ecd9656a65120b306e5f Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:35:30 +0200 Subject: [PATCH] fix: ask before signing out --- locales/rn/de.json | 5 ++++ locales/rn/en.json | 5 ++++ src/components/SidebarDrawer.tsx | 11 ++++++-- src/lib/__tests__/sign-out-confirm.test.ts | 23 ++++++++++++++++ src/lib/sign-out-confirm.ts | 31 ++++++++++++++++++++++ src/screens/SettingsScreen.tsx | 4 ++- 6 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 src/lib/__tests__/sign-out-confirm.test.ts create mode 100644 src/lib/sign-out-confirm.ts diff --git a/locales/rn/de.json b/locales/rn/de.json index 3e9674e2..488b521f 100644 --- a/locales/rn/de.json +++ b/locales/rn/de.json @@ -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": { diff --git a/locales/rn/en.json b/locales/rn/en.json index 00f6f632..abb546ec 100644 --- a/locales/rn/en.json +++ b/locales/rn/en.json @@ -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": { diff --git a/src/components/SidebarDrawer.tsx b/src/components/SidebarDrawer.tsx index 8595670e..8cb5ddbb 100644 --- a/src/components/SidebarDrawer.tsx +++ b/src/components/SidebarDrawer.tsx @@ -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(); }); + }} > @@ -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(); }); + }} > diff --git a/src/lib/__tests__/sign-out-confirm.test.ts b/src/lib/__tests__/sign-out-confirm.test.ts new file mode 100644 index 00000000..6ff29e04 --- /dev/null +++ b/src/lib/__tests__/sign-out-confirm.test.ts @@ -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) => + 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); + }); +}); diff --git a/src/lib/sign-out-confirm.ts b/src/lib/sign-out-confirm.ts new file mode 100644 index 00000000..d428cf65 --- /dev/null +++ b/src/lib/sign-out-confirm.ts @@ -0,0 +1,31 @@ +import { Alert } from 'react-native'; + +type Translate = (key: string, fallback: string, params?: Record) => 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, + }, + ]); +} diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index c5cd2a7e..b6bf0822 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -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 */} { if (onLogout) confirmSignOut(t, {}, onLogout); }} + accessibilityRole="button" style={({ pressed }) => [styles.logoutBtn, pressed && styles.logoutBtnPressed]} >