added support for multiple accounts, guardian login bugfixes, ui changes
This commit is contained in:
+203
-205
@@ -10,7 +10,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:loader_overlay/loader_overlay.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
@@ -34,20 +33,19 @@ import 'push/push_registration.dart';
|
||||
import 'push/push_registration_store.dart';
|
||||
import 'push/push_renderer.dart';
|
||||
import 'routing/app_routes.dart';
|
||||
import 'session/account_codec.dart';
|
||||
import 'session/session_lifecycle.dart';
|
||||
import 'session/session_manager.dart';
|
||||
import 'share_intent/share_intent_listener.dart';
|
||||
import 'state/app/modules/account/account_scope.dart';
|
||||
import 'state/app/modules/account/bloc/account_bloc.dart';
|
||||
import 'state/app/modules/account/bloc/account_state.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/children/child_selection_cubit.dart';
|
||||
import 'state/app/modules/nextcloud_capabilities/bloc/nextcloud_capabilities_cubit.dart';
|
||||
import 'state/app/modules/parent_letters/bloc/parent_letters_bloc.dart';
|
||||
import 'state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import 'state/app/modules/timetable/primary/primary_timetable_scope.dart';
|
||||
import 'storage/hydrated_storage_bootstrap.dart';
|
||||
import 'storage/account_storage.dart';
|
||||
import 'storage/settings.dart';
|
||||
import 'theming/dark_app_theme.dart';
|
||||
import 'theming/light_app_theme.dart';
|
||||
@@ -59,9 +57,9 @@ import 'view/login/login.dart';
|
||||
import 'view/login/post_login_splash.dart';
|
||||
import 'widget/avatar_disk_cache.dart';
|
||||
import 'widget/breaker/breaker.dart';
|
||||
import 'widget/debug/cache_view.dart';
|
||||
import 'widget/downloads/download_tray.dart';
|
||||
import 'widget/emergency/emergency_notice_gate.dart';
|
||||
import 'widget/info_dialog.dart';
|
||||
import 'widget_data/widget_sync.dart';
|
||||
|
||||
/// Runs one startup step with a time limit. Anything that throws or hangs
|
||||
@@ -148,8 +146,7 @@ Future<void> main() async {
|
||||
]),
|
||||
),
|
||||
_startupStep('hydrated storage', () async {
|
||||
final path = (await getTemporaryDirectory()).path;
|
||||
HydratedBloc.storage = await buildHydratedStorageWithFallback(path);
|
||||
await AccountStorage.init((await getTemporaryDirectory()).path);
|
||||
}, timeout: null),
|
||||
_startupStep('documents dir', () async {
|
||||
AppPaths.documentsDir = (await getApplicationDocumentsDirectory()).path;
|
||||
@@ -170,6 +167,16 @@ Future<void> main() async {
|
||||
|
||||
log('starting app initialisation...');
|
||||
await Future.wait(initialisationTasks);
|
||||
// Snapshot: the session may still finish loading later (see _MainState),
|
||||
// the bloc and the storage must agree on the account they start with.
|
||||
final initialAccount = SessionManager().isLoaded
|
||||
? SessionManager().activeAccount
|
||||
: null;
|
||||
await _startupStep(
|
||||
'account storage',
|
||||
() => AccountStorage.activate(initialAccount),
|
||||
timeout: null,
|
||||
);
|
||||
log('app initialisation done!');
|
||||
|
||||
// Local notifications: init the plugin (with tap/action callbacks) and the
|
||||
@@ -236,7 +243,7 @@ Future<void> main() async {
|
||||
|
||||
// Created eagerly so the endpoint is configured before anything below can
|
||||
// issue a request (the primary timetable bloc loads on creation).
|
||||
final settingsCubit = SettingsCubit();
|
||||
final settingsCubit = SettingsCubit(storage: AccountStorage.global);
|
||||
_syncMarianumConnectEndpoint(settingsCubit.state);
|
||||
settingsCubit.stream.listen(_syncMarianumConnectEndpoint);
|
||||
|
||||
@@ -246,21 +253,13 @@ Future<void> main() async {
|
||||
providers: [
|
||||
BlocProvider<SettingsCubit>.value(value: settingsCubit),
|
||||
BlocProvider<AccountBloc>(
|
||||
create: (_) => AccountBloc(initialStatus: _initialAccountStatus()),
|
||||
create: (_) => AccountBloc(
|
||||
initialStatus: _initialAccountStatus(initialAccount),
|
||||
accountId: initialAccount?.id,
|
||||
),
|
||||
),
|
||||
BlocProvider<BreakerBloc>(create: (_) => BreakerBloc()),
|
||||
BlocProvider<CapabilitiesCubit>(create: (_) => CapabilitiesCubit()),
|
||||
BlocProvider<NextcloudCapabilitiesCubit>(
|
||||
create: (_) => NextcloudCapabilitiesCubit(),
|
||||
),
|
||||
BlocProvider<ChatListBloc>(create: (_) => ChatListBloc()),
|
||||
BlocProvider<ChatBloc>(
|
||||
create: (ctx) => ChatBloc(chatListBloc: ctx.read<ChatListBloc>()),
|
||||
),
|
||||
BlocProvider<ChildSelectionCubit>(create: (_) => ChildSelectionCubit()),
|
||||
BlocProvider<ParentLettersBloc>(create: (_) => ParentLettersBloc()),
|
||||
],
|
||||
child: const PrimaryTimetableScope(child: Main()),
|
||||
child: const Main(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -279,10 +278,11 @@ void _syncMarianumConnectEndpoint(Settings settings) {
|
||||
unawaited(WidgetSync.setMarianumConnectBaseUrl(url));
|
||||
}
|
||||
|
||||
AccountStatus _initialAccountStatus() {
|
||||
final session = SessionManager();
|
||||
if (session.isSignedIn) return AccountStatus.loggedIn;
|
||||
return session.isLoaded ? AccountStatus.loggedOut : AccountStatus.undefined;
|
||||
AccountStatus _initialAccountStatus(AccountEntry? account) {
|
||||
if (account != null) return AccountStatus.loggedIn;
|
||||
return SessionManager().isLoaded
|
||||
? AccountStatus.loggedOut
|
||||
: AccountStatus.undefined;
|
||||
}
|
||||
|
||||
class Main extends StatefulWidget {
|
||||
@@ -306,26 +306,30 @@ class _MainState extends State<Main> {
|
||||
Jiffy.setLocale('de');
|
||||
|
||||
SessionManager().unauthorizedSignal.addListener(_onUnauthorized);
|
||||
SessionManager().waitForLoad().then((session) {
|
||||
GuardianLinkListener.pending.addListener(_onGuardianLink);
|
||||
SessionManager().waitForLoad().then((session) async {
|
||||
if (!mounted) return;
|
||||
final accountBloc = context.read<AccountBloc>();
|
||||
accountBloc.setStatus(
|
||||
session != null ? AccountStatus.loggedIn : AccountStatus.loggedOut,
|
||||
);
|
||||
if (session != null) {
|
||||
_scheduleSessionValidation(accountBloc);
|
||||
// Cold start while already logged in: the account status doesn't
|
||||
// change, so the loggedIn listener below never fires.
|
||||
_onSessionActive();
|
||||
if (session == null) {
|
||||
accountBloc.setStatus(AccountStatus.loggedOut);
|
||||
return;
|
||||
}
|
||||
// Covers a session that finished loading after the startup snapshot;
|
||||
// otherwise the id is unchanged and nothing remounts.
|
||||
final account = SessionManager().activeAccount;
|
||||
await AccountStorage.activate(account);
|
||||
if (!mounted) return;
|
||||
accountBloc.activated(account?.id);
|
||||
_scheduleSessionValidation(accountBloc);
|
||||
unawaited(_onGuardianLink());
|
||||
});
|
||||
}
|
||||
|
||||
/// Pulls the capability flags of the active account, then registers push
|
||||
/// right away instead of deferring it to the next app start.
|
||||
void _onSessionActive() {
|
||||
final settingsCubit = context.read<SettingsCubit>();
|
||||
final capabilitiesCubit = context.read<CapabilitiesCubit>();
|
||||
/// Runs whenever the account-scoped tree mounts for a signed-in account:
|
||||
/// pulls the capability flags, then registers push for this account.
|
||||
void _onSessionActive(BuildContext scopeContext) {
|
||||
final settingsCubit = scopeContext.read<SettingsCubit>();
|
||||
final capabilitiesCubit = scopeContext.read<CapabilitiesCubit>();
|
||||
unawaited(
|
||||
capabilitiesCubit.load().then((_) {
|
||||
if (!mounted) return;
|
||||
@@ -333,7 +337,20 @@ class _MainState extends State<Main> {
|
||||
_promptGuardianNotifications();
|
||||
}),
|
||||
);
|
||||
unawaited(context.read<NextcloudCapabilitiesCubit>().load());
|
||||
unawaited(scopeContext.read<NextcloudCapabilitiesCubit>().load());
|
||||
unawaited(SessionLifecycle.refreshDisplayName());
|
||||
_prefetchBaseData(scopeContext);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _showTakeoverNotice());
|
||||
}
|
||||
|
||||
/// A forced sign-out that handed over to another account never reaches the
|
||||
/// login screen, which normally explains it.
|
||||
void _showTakeoverNotice() {
|
||||
final notice = SessionLifecycle.signOutNotice.value;
|
||||
final overlayContext = AppRoutes.overlayContext;
|
||||
if (notice == null || overlayContext == null) return;
|
||||
SessionLifecycle.signOutNotice.value = null;
|
||||
InfoDialog.show(overlayContext, notice, title: 'Abgemeldet');
|
||||
}
|
||||
|
||||
/// Waits for the post-login splash so the dialog never covers it; the
|
||||
@@ -379,9 +396,20 @@ class _MainState extends State<Main> {
|
||||
@override
|
||||
void dispose() {
|
||||
SessionManager().unauthorizedSignal.removeListener(_onUnauthorized);
|
||||
GuardianLinkListener.pending.removeListener(_onGuardianLink);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// A guardian login link tapped while signed in belongs to an "add account"
|
||||
/// flow (the app may have been killed meanwhile): reopen the login for it.
|
||||
Future<void> _onGuardianLink() async {
|
||||
if (GuardianLinkListener.pending.value == null || !mounted) return;
|
||||
final accountBloc = context.read<AccountBloc>();
|
||||
if (accountBloc.state.status != AccountStatus.loggedIn) return;
|
||||
await SessionLifecycle.beginAddAccount();
|
||||
accountBloc.setStatus(AccountStatus.addingAccount);
|
||||
}
|
||||
|
||||
void _onUnauthorized() {
|
||||
if (!mounted) return;
|
||||
final accountBloc = context.read<AccountBloc>();
|
||||
@@ -394,25 +422,52 @@ class _MainState extends State<Main> {
|
||||
}
|
||||
|
||||
/// Background credential check: a 401 means the password was rotated
|
||||
/// server-side, so the validator wipes the local session and flips the
|
||||
/// account bloc to `loggedOut` (sending the user to the login screen).
|
||||
/// server-side, so the validator signs the account out and the app moves
|
||||
/// on to another stored account or the login screen.
|
||||
void _scheduleSessionValidation(AccountBloc accountBloc) {
|
||||
unawaited(
|
||||
SessionValidator.probeStored(
|
||||
onInvalidated: () async {
|
||||
onInvalidated: (nextAccountId) async {
|
||||
if (!mounted) return;
|
||||
accountBloc.setStatus(AccountStatus.loggedOut);
|
||||
accountBloc.activated(nextAccountId);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onAccountChanged(BuildContext context, AccountState accountState) {
|
||||
if (accountState.status == AccountStatus.loggedIn &&
|
||||
accountState.freshLogin) {
|
||||
_showPostLoginSplash = true;
|
||||
_appMounted = false;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) setState(() => _appMounted = true);
|
||||
});
|
||||
}
|
||||
if (accountState.status == AccountStatus.addingAccount) {
|
||||
// Routes pushed on the root navigator (Settings) would otherwise keep
|
||||
// covering the login screen that replaces the home route.
|
||||
AppRoutes.rootNavigatorKey.currentState?.popUntil(
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
}
|
||||
if (accountState.status != AccountStatus.loggedOut) return;
|
||||
// A pending share would otherwise survive logout and be re-applied after
|
||||
// re-login with file paths the OS may already have evicted from the cache.
|
||||
ShareIntentListener.instance.clear();
|
||||
// Deferred until the account-scoped tree is torn down.
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => unawaited(_wipeDeviceState()),
|
||||
);
|
||||
}
|
||||
|
||||
String? _scopeAccountId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: BlocBuilder<SettingsCubit, Settings>(
|
||||
builder: (context, settings) {
|
||||
final devToolsSettings = settings.devToolsSettings;
|
||||
// Mirror the notification toggle into group-scoped storage so the FCM
|
||||
// background isolate and the iOS NSE can suppress rendering when off.
|
||||
unawaited(
|
||||
@@ -420,172 +475,115 @@ class _MainState extends State<Main> {
|
||||
settings.notificationSettings.enabled,
|
||||
),
|
||||
);
|
||||
return MaterialApp(
|
||||
showPerformanceOverlay: devToolsSettings.showPerformanceOverlay,
|
||||
checkerboardOffscreenLayers:
|
||||
devToolsSettings.checkerboardOffscreenLayers,
|
||||
checkerboardRasterCacheImages:
|
||||
devToolsSettings.checkerboardRasterCacheImages,
|
||||
debugShowCheckedModeBanner: false,
|
||||
navigatorKey: AppRoutes.rootNavigatorKey,
|
||||
// Used by ChatView.didPopNext to reclaim the global ChatBloc.
|
||||
// DownloadRouteObserver tracks full-page navigations so the downloads
|
||||
// chip only surfaces once the user leaves the screen they started on.
|
||||
navigatorObservers: [
|
||||
AppRoutes.chatRouteObserver,
|
||||
DownloadRouteObserver(),
|
||||
],
|
||||
localizationsDelegates: const [
|
||||
...GlobalMaterialLocalizations.delegates,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: const [Locale('de'), Locale('en')],
|
||||
locale: const Locale('de'),
|
||||
title: 'Marianum Fulda',
|
||||
themeMode: settings.appTheme,
|
||||
theme: LightAppTheme.theme,
|
||||
darkTheme: DarkAppTheme.theme,
|
||||
// Brand-colored backdrop behind every route. During the logout
|
||||
// home-swap and route pop animations the framework can briefly
|
||||
// expose the layer below the topmost Scaffold; without this
|
||||
// the dark Material default shows through and the user sees a
|
||||
// black flash.
|
||||
builder: (context, child) => ColoredBox(
|
||||
color: LightAppTheme.marianumRed,
|
||||
// Downloads tray mounted ABOVE the navigator so its chip floats over
|
||||
// every route (folder views, chat, viewer are full-page pushes that
|
||||
// would otherwise cover it).
|
||||
child: DownloadTrayHost(child: child ?? const SizedBox.shrink()),
|
||||
),
|
||||
home: EmergencyNoticeGate(
|
||||
child: LoaderOverlay(
|
||||
child: Breaker(
|
||||
breaker: BreakerArea.global,
|
||||
child: BlocConsumer<AccountBloc, AccountState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.status != current.status,
|
||||
listener: (context, accountState) {
|
||||
if (accountState.status == AccountStatus.loggedIn) {
|
||||
_onSessionActive();
|
||||
_showPostLoginSplash = true;
|
||||
_appMounted = false;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) setState(() => _appMounted = true);
|
||||
});
|
||||
_prefetchBaseData(context);
|
||||
}
|
||||
if (accountState.status != AccountStatus.loggedOut) return;
|
||||
// A pending share would otherwise survive logout and be
|
||||
// re-applied after re-login with file paths the OS may
|
||||
// already have evicted from the cache.
|
||||
ShareIntentListener.instance.clear();
|
||||
// Routes pushed via AppRoutes (e.g. Settings) live on the
|
||||
// root navigator and survive the home swap below, so they
|
||||
// would still cover the Login screen after logout. Pop
|
||||
// them here so the user immediately sees Login.
|
||||
final navigator = Navigator.of(context);
|
||||
if (navigator.canPop()) {
|
||||
navigator.popUntil((route) => route.isFirst);
|
||||
}
|
||||
// Capture bloc references before the post-frame callback
|
||||
// — by the time it runs the dialog/Settings context is
|
||||
// gone but this listener context is still valid.
|
||||
final settingsCubit = context.read<SettingsCubit>();
|
||||
final breakerBloc = context.read<BreakerBloc>();
|
||||
final capabilitiesCubit = context.read<CapabilitiesCubit>();
|
||||
final childSelectionCubit = context
|
||||
.read<ChildSelectionCubit>();
|
||||
final chatListBloc = context.read<ChatListBloc>();
|
||||
final parentLettersBloc = context.read<ParentLettersBloc>();
|
||||
final chatBloc = context.read<ChatBloc>();
|
||||
final nextcloudCapabilitiesCubit = context
|
||||
.read<NextcloudCapabilitiesCubit>();
|
||||
// Defer the actual wipe until after this frame so the
|
||||
// App tree (TimetableBloc/ChatListBloc watchers etc.)
|
||||
// is already torn down. Resetting blocs while App is
|
||||
// still in front caused a black-frame race.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
unawaited(
|
||||
_wipeUserState(
|
||||
settingsCubit: settingsCubit,
|
||||
childSelectionCubit: childSelectionCubit,
|
||||
chatListBloc: chatListBloc,
|
||||
parentLettersBloc: parentLettersBloc,
|
||||
chatBloc: chatBloc,
|
||||
breakerBloc: breakerBloc,
|
||||
capabilitiesCubit: capabilitiesCubit,
|
||||
nextcloudCapabilitiesCubit:
|
||||
nextcloudCapabilitiesCubit,
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
builder: (context, accountState) {
|
||||
switch (accountState.status) {
|
||||
case AccountStatus.loggedIn:
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (_appMounted)
|
||||
const App(key: ValueKey('app-shell')),
|
||||
if (_showPostLoginSplash)
|
||||
PostLoginSplash(
|
||||
key: const ValueKey('post-login-splash'),
|
||||
onComplete: () {
|
||||
setState(() => _showPostLoginSplash = false);
|
||||
_promptGuardianNotifications();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
case AccountStatus.loggedOut:
|
||||
return const Login();
|
||||
case AccountStatus.undefined:
|
||||
return const AccountLoadingScreen();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
return BlocConsumer<AccountBloc, AccountState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.status != current.status ||
|
||||
previous.accountId != current.accountId,
|
||||
listener: _onAccountChanged,
|
||||
buildWhen: (previous, current) =>
|
||||
previous.accountId != current.accountId,
|
||||
builder: (context, account) {
|
||||
if (account.accountId != _scopeAccountId) {
|
||||
_scopeAccountId = account.accountId;
|
||||
// The old navigator (and every route on it) goes with the old
|
||||
// account; a shared GlobalKey would carry it over instead.
|
||||
AppRoutes.rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||
}
|
||||
return AccountScope(
|
||||
key: ValueKey(account.accountId),
|
||||
onActive: _onSessionActive,
|
||||
child: _buildApp(settings),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildApp(Settings settings) {
|
||||
final devToolsSettings = settings.devToolsSettings;
|
||||
return MaterialApp(
|
||||
showPerformanceOverlay: devToolsSettings.showPerformanceOverlay,
|
||||
checkerboardOffscreenLayers: devToolsSettings.checkerboardOffscreenLayers,
|
||||
checkerboardRasterCacheImages:
|
||||
devToolsSettings.checkerboardRasterCacheImages,
|
||||
debugShowCheckedModeBanner: false,
|
||||
navigatorKey: AppRoutes.rootNavigatorKey,
|
||||
// Used by ChatView.didPopNext to reclaim the global ChatBloc.
|
||||
// DownloadRouteObserver tracks full-page navigations so the downloads
|
||||
// chip only surfaces once the user leaves the screen they started on.
|
||||
navigatorObservers: [
|
||||
AppRoutes.chatRouteObserver,
|
||||
DownloadRouteObserver(),
|
||||
],
|
||||
localizationsDelegates: const [
|
||||
...GlobalMaterialLocalizations.delegates,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: const [Locale('de'), Locale('en')],
|
||||
locale: const Locale('de'),
|
||||
title: 'Marianum Fulda',
|
||||
themeMode: settings.appTheme,
|
||||
theme: LightAppTheme.theme,
|
||||
darkTheme: DarkAppTheme.theme,
|
||||
// Brand-colored backdrop behind every route. During the logout
|
||||
// home-swap and route pop animations the framework can briefly
|
||||
// expose the layer below the topmost Scaffold; without this
|
||||
// the dark Material default shows through and the user sees a
|
||||
// black flash.
|
||||
builder: (context, child) => ColoredBox(
|
||||
color: LightAppTheme.marianumRed,
|
||||
// Downloads tray mounted ABOVE the navigator so its chip floats over
|
||||
// every route (folder views, chat, viewer are full-page pushes that
|
||||
// would otherwise cover it).
|
||||
child: DownloadTrayHost(child: child ?? const SizedBox.shrink()),
|
||||
),
|
||||
home: EmergencyNoticeGate(
|
||||
child: LoaderOverlay(
|
||||
child: Breaker(
|
||||
breaker: BreakerArea.global,
|
||||
child: BlocBuilder<AccountBloc, AccountState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.status != current.status,
|
||||
builder: (context, accountState) {
|
||||
switch (accountState.status) {
|
||||
case AccountStatus.loggedIn:
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (_appMounted) const App(key: ValueKey('app-shell')),
|
||||
if (_showPostLoginSplash)
|
||||
PostLoginSplash(
|
||||
key: const ValueKey('post-login-splash'),
|
||||
onComplete: () {
|
||||
setState(() => _showPostLoginSplash = false);
|
||||
_promptGuardianNotifications();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
case AccountStatus.loggedOut:
|
||||
return const Login();
|
||||
case AccountStatus.addingAccount:
|
||||
return const Login(addingAccount: true);
|
||||
case AccountStatus.undefined:
|
||||
return const AccountLoadingScreen();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _wipeUserState({
|
||||
required SettingsCubit settingsCubit,
|
||||
required ChildSelectionCubit childSelectionCubit,
|
||||
required ChatListBloc chatListBloc,
|
||||
required ParentLettersBloc parentLettersBloc,
|
||||
required ChatBloc chatBloc,
|
||||
required BreakerBloc breakerBloc,
|
||||
required CapabilitiesCubit capabilitiesCubit,
|
||||
required NextcloudCapabilitiesCubit nextcloudCapabilitiesCubit,
|
||||
}) async {
|
||||
/// Wipes what outlives accounts once the last one signed out. Account data
|
||||
/// itself is removed by [SessionLifecycle.signOut].
|
||||
Future<void> _wipeDeviceState() async {
|
||||
try {
|
||||
// Reset user-data blocs whose tree is no longer mounted after the
|
||||
// home swap. We do NOT touch SettingsCubit here — its outer BlocBuilder
|
||||
// wraps MaterialApp, so emit'ing a fresh state would tear down the
|
||||
// freshly-mounted Login tree and leave the user with a blank screen
|
||||
// (the MaterialApp.builder backdrop) until the next interaction.
|
||||
// The timetable bloc is not reset here: PrimaryTimetableScope replaces it
|
||||
// on the status change, and HydratedBloc.storage.clear() below drops the
|
||||
// cached plans of every subject.
|
||||
childSelectionCubit.reset();
|
||||
capabilitiesCubit.reset();
|
||||
nextcloudCapabilitiesCubit.reset();
|
||||
await Future.wait([
|
||||
chatListBloc.reset(),
|
||||
parentLettersBloc.reset(),
|
||||
chatBloc.reset(),
|
||||
breakerBloc.reset(),
|
||||
]);
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.clear();
|
||||
await HydratedBloc.storage.clear();
|
||||
await const CacheView().clear();
|
||||
// The chat background image lives outside HydratedStorage, so clear it too
|
||||
// (best-effort) to avoid orphaning the previous user's wallpaper.
|
||||
final backgroundImage = File(AppPaths.chatBackgroundImage);
|
||||
@@ -597,6 +595,6 @@ Future<void> _wipeUserState({
|
||||
await WidgetSync.clear();
|
||||
await WidgetSync.triggerUpdate();
|
||||
} catch (e, s) {
|
||||
log('User state wipe failed: $e', stackTrace: s);
|
||||
log('Device state wipe failed: $e', stackTrace: s);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user