added guardian login with views for their assigned childs
This commit is contained in:
+89
-57
@@ -24,15 +24,16 @@ import 'api/marianumconnect/queries/get_breakers/get_breakers_response.dart';
|
||||
import 'api/marianumconnect/queries/report_client_error/client_error_reporter.dart';
|
||||
import 'api/marianumconnect/queries/telemetry_heartbeat/telemetry_heartbeat.dart';
|
||||
import 'app.dart';
|
||||
import 'auth_link/guardian_link_listener.dart';
|
||||
import 'background/widget_background_task.dart';
|
||||
import 'firebase_options.dart';
|
||||
import 'model/account_data.dart';
|
||||
import 'notification/notification_service.dart';
|
||||
import 'push/push_message_handler.dart';
|
||||
import 'push/push_registration.dart';
|
||||
import 'push/push_registration_store.dart';
|
||||
import 'push/push_renderer.dart';
|
||||
import 'routing/app_routes.dart';
|
||||
import 'session/session_manager.dart';
|
||||
import 'share_intent/share_intent_listener.dart';
|
||||
import 'state/app/modules/account/bloc/account_bloc.dart';
|
||||
import 'state/app/modules/account/bloc/account_state.dart';
|
||||
@@ -40,14 +41,16 @@ 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/settings/bloc/settings_cubit.dart';
|
||||
import 'state/app/modules/timetable/bloc/timetable_bloc.dart';
|
||||
import 'state/app/modules/timetable/primary/primary_timetable_scope.dart';
|
||||
import 'storage/hydrated_storage_bootstrap.dart';
|
||||
import 'storage/settings.dart';
|
||||
import 'theming/dark_app_theme.dart';
|
||||
import 'theming/light_app_theme.dart';
|
||||
import 'utils/app_paths.dart';
|
||||
import 'utils/debouncer.dart';
|
||||
import 'utils/downloads/download_manager.dart';
|
||||
import 'view/login/account_loading_screen.dart';
|
||||
import 'view/login/login.dart';
|
||||
@@ -149,17 +152,18 @@ Future<void> main() async {
|
||||
_startupStep('documents dir', () async {
|
||||
AppPaths.documentsDir = (await getApplicationDocumentsDirectory()).path;
|
||||
}),
|
||||
// The keychain may still be locked right after device unlock; AccountData
|
||||
// The keychain may still be locked right after device unlock; the session
|
||||
// keeps retrying, so on timeout the app starts on the loading screen and
|
||||
// flips to the real state once the session is readable (see _MainState).
|
||||
_startupStep(
|
||||
'account data',
|
||||
AccountData().waitForPopulation,
|
||||
SessionManager().waitForLoad,
|
||||
timeout: const Duration(seconds: 5),
|
||||
// Expected on every background wake of a locked device; not an error.
|
||||
report: false,
|
||||
),
|
||||
_startupStep('share intent', ShareIntentListener.instance.initialize),
|
||||
_startupStep('guardian link', GuardianLinkListener.instance.initialize),
|
||||
];
|
||||
|
||||
log('starting app initialisation...');
|
||||
@@ -215,7 +219,7 @@ Future<void> main() async {
|
||||
// has data ready by the time the user navigates to it. No-op when a
|
||||
// cached payload is already present, so this does not undo the day-long
|
||||
// root cache TTL.
|
||||
if (AccountData().isPopulated()) {
|
||||
if (SessionManager().hasNextcloud) {
|
||||
unawaited(
|
||||
ListFilesCache.prefetchRootListing().onError(
|
||||
(e, _) => log('Files root prefetch failed: $e'),
|
||||
@@ -228,11 +232,17 @@ Future<void> main() async {
|
||||
// placeholder flash.
|
||||
AvatarDiskCache.instance.warmUp();
|
||||
|
||||
// Created eagerly so the endpoint is configured before anything below can
|
||||
// issue a request (the primary timetable bloc loads on creation).
|
||||
final settingsCubit = SettingsCubit();
|
||||
_syncMarianumConnectEndpoint(settingsCubit.state);
|
||||
settingsCubit.stream.listen(_syncMarianumConnectEndpoint);
|
||||
|
||||
log('running app...');
|
||||
runApp(
|
||||
MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<SettingsCubit>(create: (_) => SettingsCubit()),
|
||||
BlocProvider<SettingsCubit>.value(value: settingsCubit),
|
||||
BlocProvider<AccountBloc>(
|
||||
create: (_) => AccountBloc(initialStatus: _initialAccountStatus()),
|
||||
),
|
||||
@@ -245,17 +255,31 @@ Future<void> main() async {
|
||||
BlocProvider<ChatBloc>(
|
||||
create: (ctx) => ChatBloc(chatListBloc: ctx.read<ChatListBloc>()),
|
||||
),
|
||||
BlocProvider<TimetableBloc>(create: (_) => TimetableBloc()),
|
||||
BlocProvider<ChildSelectionCubit>(create: (_) => ChildSelectionCubit()),
|
||||
],
|
||||
child: const Main(),
|
||||
child: const PrimaryTimetableScope(child: Main()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String? _syncedMcBaseUrl;
|
||||
|
||||
/// Keeps the MC dio singleton aligned with the selected endpoint (live /
|
||||
/// beta / custom), mirrored into WidgetSync so the background isolate
|
||||
/// refreshes against the same endpoint. Settings emit on every toggle; only
|
||||
/// an actual URL change is applied.
|
||||
void _syncMarianumConnectEndpoint(Settings settings) {
|
||||
final url = settings.devToolsSettings.resolveMarianumConnectBaseUrl();
|
||||
if (url == _syncedMcBaseUrl) return;
|
||||
_syncedMcBaseUrl = url;
|
||||
MarianumConnectEndpoint.update(url);
|
||||
unawaited(WidgetSync.setMarianumConnectBaseUrl(url));
|
||||
}
|
||||
|
||||
AccountStatus _initialAccountStatus() {
|
||||
final account = AccountData();
|
||||
if (account.isPopulated()) return AccountStatus.loggedIn;
|
||||
return account.isLoaded ? AccountStatus.loggedOut : AccountStatus.undefined;
|
||||
final session = SessionManager();
|
||||
if (session.isSignedIn) return AccountStatus.loggedIn;
|
||||
return session.isLoaded ? AccountStatus.loggedOut : AccountStatus.undefined;
|
||||
}
|
||||
|
||||
class Main extends StatefulWidget {
|
||||
@@ -278,35 +302,44 @@ class _MainState extends State<Main> {
|
||||
super.initState();
|
||||
Jiffy.setLocale('de');
|
||||
|
||||
AccountData().waitForPopulation().then((value) {
|
||||
SessionManager().unauthorizedSignal.addListener(_onUnauthorized);
|
||||
SessionManager().waitForLoad().then((session) {
|
||||
if (!mounted) return;
|
||||
final accountBloc = context.read<AccountBloc>();
|
||||
accountBloc.setStatus(
|
||||
value ? AccountStatus.loggedIn : AccountStatus.loggedOut,
|
||||
session != null ? AccountStatus.loggedIn : AccountStatus.loggedOut,
|
||||
);
|
||||
if (value) {
|
||||
if (session != null) {
|
||||
_scheduleSessionValidation(accountBloc);
|
||||
// Cold start while already logged in: the account status doesn't
|
||||
// change, so the loggedIn listener below never fires — refresh
|
||||
// capabilities here, then self-heal the push registration.
|
||||
final settingsCubit = context.read<SettingsCubit>();
|
||||
unawaited(
|
||||
context.read<CapabilitiesCubit>().load().then((_) {
|
||||
if (!mounted) return;
|
||||
_syncPush(settingsCubit, context.read<CapabilitiesCubit>());
|
||||
}),
|
||||
);
|
||||
unawaited(context.read<NextcloudCapabilitiesCubit>().load());
|
||||
// change, so the loggedIn listener below never fires.
|
||||
_onSessionActive();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Warms the core caches (timetable, chat list, files root) in the
|
||||
/// background so the first screen render hits populated data.
|
||||
/// 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>();
|
||||
unawaited(
|
||||
capabilitiesCubit.load().then((_) {
|
||||
if (!mounted) return;
|
||||
_syncPush(settingsCubit, capabilitiesCubit);
|
||||
}),
|
||||
);
|
||||
unawaited(context.read<NextcloudCapabilitiesCubit>().load());
|
||||
}
|
||||
|
||||
/// Warms the chat list and files root in the background so the first screen
|
||||
/// render hits populated data. The timetable needs no warm-up:
|
||||
/// PrimaryTimetableScope creates a freshly loading bloc per account.
|
||||
void _prefetchBaseData(BuildContext context) {
|
||||
context.read<TimetableBloc>().refresh();
|
||||
unawaited(context.read<ChatListBloc>().refresh(silent: true));
|
||||
unawaited(ListFilesCache.prefetchRootListing());
|
||||
if (SessionManager().hasNextcloud) {
|
||||
unawaited(ListFilesCache.prefetchRootListing());
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers/self-heals the push subscription whenever the backend advertises
|
||||
@@ -329,6 +362,23 @@ class _MainState extends State<Main> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
SessionManager().unauthorizedSignal.removeListener(_onUnauthorized);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onUnauthorized() {
|
||||
if (!mounted) return;
|
||||
final accountBloc = context.read<AccountBloc>();
|
||||
if (accountBloc.state.status != AccountStatus.loggedIn) return;
|
||||
Debouncer.throttle(
|
||||
'sessionUnauthorized',
|
||||
const Duration(seconds: 30),
|
||||
() => _scheduleSessionValidation(accountBloc),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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).
|
||||
@@ -349,14 +399,6 @@ class _MainState extends State<Main> {
|
||||
child: BlocBuilder<SettingsCubit, Settings>(
|
||||
builder: (context, settings) {
|
||||
final devToolsSettings = settings.devToolsSettings;
|
||||
// Keep the MC dio singleton aligned with the currently selected
|
||||
// endpoint (live / beta / custom). Idempotent when the URL is
|
||||
// unchanged so it's safe to call on every rebuild. Mirrored into
|
||||
// WidgetSync so the background isolate refreshes against the same
|
||||
// endpoint.
|
||||
final mcBaseUrl = devToolsSettings.resolveMarianumConnectBaseUrl();
|
||||
MarianumConnectEndpoint.update(mcBaseUrl);
|
||||
unawaited(WidgetSync.setMarianumConnectBaseUrl(mcBaseUrl));
|
||||
// Mirror the notification toggle into group-scoped storage so the FCM
|
||||
// background isolate and the iOS NSE can suppress rendering when off.
|
||||
unawaited(
|
||||
@@ -409,22 +451,8 @@ class _MainState extends State<Main> {
|
||||
listenWhen: (previous, current) =>
|
||||
previous.status != current.status,
|
||||
listener: (context, accountState) {
|
||||
// Fresh login (loggedOut -> loggedIn): pull capability flags
|
||||
// for the newly authenticated user, then register push right
|
||||
// away instead of deferring it to the next app start.
|
||||
if (accountState.status == AccountStatus.loggedIn) {
|
||||
final settingsCubit = context.read<SettingsCubit>();
|
||||
final capabilitiesCubit = context
|
||||
.read<CapabilitiesCubit>();
|
||||
unawaited(
|
||||
capabilitiesCubit.load().then((_) {
|
||||
if (!mounted) return;
|
||||
_syncPush(settingsCubit, capabilitiesCubit);
|
||||
}),
|
||||
);
|
||||
unawaited(
|
||||
context.read<NextcloudCapabilitiesCubit>().load(),
|
||||
);
|
||||
_onSessionActive();
|
||||
_showPostLoginSplash = true;
|
||||
_appMounted = false;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
@@ -449,11 +477,12 @@ class _MainState extends State<Main> {
|
||||
// — by the time it runs the dialog/Settings context is
|
||||
// gone but this listener context is still valid.
|
||||
final settingsCubit = context.read<SettingsCubit>();
|
||||
final timetableBloc = context.read<TimetableBloc>();
|
||||
final chatListBloc = context.read<ChatListBloc>();
|
||||
final chatBloc = context.read<ChatBloc>();
|
||||
final breakerBloc = context.read<BreakerBloc>();
|
||||
final capabilitiesCubit = context.read<CapabilitiesCubit>();
|
||||
final childSelectionCubit = context
|
||||
.read<ChildSelectionCubit>();
|
||||
final chatListBloc = context.read<ChatListBloc>();
|
||||
final chatBloc = context.read<ChatBloc>();
|
||||
final nextcloudCapabilitiesCubit = context
|
||||
.read<NextcloudCapabilitiesCubit>();
|
||||
// Defer the actual wipe until after this frame so the
|
||||
@@ -464,7 +493,7 @@ class _MainState extends State<Main> {
|
||||
unawaited(
|
||||
_wipeUserState(
|
||||
settingsCubit: settingsCubit,
|
||||
timetableBloc: timetableBloc,
|
||||
childSelectionCubit: childSelectionCubit,
|
||||
chatListBloc: chatListBloc,
|
||||
chatBloc: chatBloc,
|
||||
breakerBloc: breakerBloc,
|
||||
@@ -510,7 +539,7 @@ class _MainState extends State<Main> {
|
||||
|
||||
Future<void> _wipeUserState({
|
||||
required SettingsCubit settingsCubit,
|
||||
required TimetableBloc timetableBloc,
|
||||
required ChildSelectionCubit childSelectionCubit,
|
||||
required ChatListBloc chatListBloc,
|
||||
required ChatBloc chatBloc,
|
||||
required BreakerBloc breakerBloc,
|
||||
@@ -523,8 +552,11 @@ Future<void> _wipeUserState({
|
||||
// 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.
|
||||
await Future.wait([
|
||||
timetableBloc.reset(),
|
||||
childSelectionCubit.reset(),
|
||||
chatListBloc.reset(),
|
||||
chatBloc.reset(),
|
||||
breakerBloc.reset(),
|
||||
|
||||
Reference in New Issue
Block a user