import 'dart:async'; import 'dart:developer'; import 'dart:io'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter_app_badge/flutter_app_badge.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../api/marianumcloud/talk/share_files_to_chat.dart'; import '../background/widget_background_task.dart'; import '../notification/notification_service.dart'; import '../push/chat_thread_store.dart'; import '../push/nid_store.dart'; import '../push/push_keypair.dart'; import '../push/push_tap_router.dart'; import '../routing/app_routes.dart'; import '../share_intent/share_intent_listener.dart'; import '../state/app/modules/breaker/bloc/breaker_bloc.dart'; import '../state/app/modules/capabilities/bloc/capabilities_cubit.dart'; import '../state/app/modules/chat/bloc/chat_bloc.dart'; import '../state/app/modules/chat_list/bloc/chat_list_bloc.dart'; import '../state/app/modules/nextcloud_capabilities/bloc/nextcloud_capabilities_cubit.dart'; import '../state/app/modules/timetable/bloc/timetable_bloc.dart'; import '../utils/app_paths.dart'; import '../utils/downloads/download_manager.dart'; import '../utils/file_clipboard.dart'; import '../widget/debug/cache_view.dart'; import '../widget_data/widget_sync.dart'; /// Removes everything the signed-out account left on the device. Every step /// is isolated: one failing step used to skip all later ones and leave the /// previous account's data behind. abstract final class SessionWipe { static Future? _running; /// Completes once a running wipe finished. A login awaits it, otherwise the /// wipe could clear what the next account just stored (widget job, caches). static Future get done => _running ?? Future.value(); /// State that must be gone before the next frame (a login screen can be /// reached right away): pending navigation and in-memory singletons. static void clearImmediate() { _step('share intents', ShareIntentListener.instance.clearAll); _step('share folder cache', resetTalkShareFolderCache); _step('pending navigation', () { AppRoutes.pendingChatToken.value = null; PushTapRouter.pendingChatToken.value = null; PushTapRouter.pendingNewsletterId.value = null; }); _step('file clipboard', FileClipboard.instance.clear); _step('downloads', DownloadManager.instance.clearAll); } static Future run({ required TimetableBloc timetableBloc, required ChatListBloc chatListBloc, required ChatBloc chatBloc, required BreakerBloc breakerBloc, required CapabilitiesCubit capabilitiesCubit, required NextcloudCapabilitiesCubit nextcloudCapabilitiesCubit, }) { final wipe = _run( timetableBloc: timetableBloc, chatListBloc: chatListBloc, chatBloc: chatBloc, breakerBloc: breakerBloc, capabilitiesCubit: capabilitiesCubit, nextcloudCapabilitiesCubit: nextcloudCapabilitiesCubit, ); _running = wipe; return wipe.whenComplete(() { if (identical(_running, wipe)) _running = null; }); } static Future _run({ required TimetableBloc timetableBloc, required ChatListBloc chatListBloc, required ChatBloc chatBloc, required BreakerBloc breakerBloc, required CapabilitiesCubit capabilitiesCubit, required NextcloudCapabilitiesCubit nextcloudCapabilitiesCubit, }) async { // SettingsCubit is deliberately left alone: its BlocBuilder wraps // MaterialApp, and emitting a fresh state here tore down the freshly // mounted Login tree (blank screen until the next interaction). await Future.wait([ _stepAsync('timetable', timetableBloc.reset), _stepAsync('chat list', chatListBloc.reset), _stepAsync('chat', chatBloc.reset), _stepAsync('breakers', breakerBloc.reset), _stepAsync('capabilities', capabilitiesCubit.reset), _stepAsync('nc capabilities', nextcloudCapabilitiesCubit.reset), ]); await _stepAsync('shared preferences', () async { await (await SharedPreferences.getInstance()).clear(); }); await _stepAsync('hydrated storage', HydratedBloc.storage.clear); await _stepAsync('request cache', const CacheView().clear); await _stepAsync('chat background', () async { final image = File(AppPaths.chatBackgroundImage); if (image.existsSync()) await image.delete(); }); await _stepAsync('download files', () async { final dir = Directory('${(await getTemporaryDirectory()).path}/downloads'); if (dir.existsSync()) await dir.delete(recursive: true); }); // Push: the tray and its bookkeeping belong to the previous account — a // tap or inline reply there would otherwise act as the next one. A new // keypair and FCM token make pushes still addressed to the previous // registration undecryptable and undeliverable (an offline sign-out // could not unregister it). await _stepAsync( 'notification tray', NotificationService().flutterLocalNotificationsPlugin.cancelAll, ); await _stepAsync('app badge', () => FlutterAppBadge.count(0)); await _stepAsync('push nid store', NidStore().clear); await _stepAsync('push thread store', ChatThreadStore().clearAll); await _stepAsync('push keypair', const PushKeypair().clear); await _stepAsync('fcm token', FirebaseMessaging.instance.deleteToken); // Stop the periodic widget refresh job so the background isolate doesn't // wake up every 30 minutes only to write `loggedIn=false`. Re-registers // on the next successful login. await _stepAsync('widget task', WidgetBackgroundTask.cancelAll); await _stepAsync('widget data', WidgetSync.clear); await _stepAsync('widget update', WidgetSync.triggerUpdate); } static void _step(String name, void Function() step) { try { step(); } catch (e, s) { log('Session wipe step "$name" failed: $e', stackTrace: s); } } static Future _stepAsync( String name, Future Function() step, ) async { try { await step(); } catch (e, s) { log('Session wipe step "$name" failed: $e', stackTrace: s); } } }