diff --git a/lib/app/bloc/app_bloc.dart b/lib/app/bloc/app_bloc.dart index a7d07bab..59a11508 100644 --- a/lib/app/bloc/app_bloc.dart +++ b/lib/app/bloc/app_bloc.dart @@ -31,7 +31,7 @@ class AppBloc extends Bloc { ) { on(_onAppUserChanged); on(_onAppSettingsRefreshed); - on<_AppConfigFetchRequested>(_onAppConfigFetchRequested); + on(_onAppConfigFetchRequested); on(_onAppUserAccountActionShown); // Added on(_onLogoutRequested); on(_onThemeModeChanged); @@ -70,14 +70,17 @@ class AppBloc extends Bloc { // Emit user and status update first emit(state.copyWith(status: status, user: event.user)); - // Load settings now that we have a user (anonymous or authenticated) if (event.user != null) { - add(const AppSettingsRefreshed()); + // User is present (authenticated or anonymous) + add(const AppSettingsRefreshed()); // Load user-specific settings + add(const AppConfigFetchRequested()); // Now attempt to fetch AppConfig + } else { + // User is null (unauthenticated or logged out) + // Clear appConfig if user is logged out, as it might be tied to auth context + // or simply to ensure fresh fetch on next login. + // Also ensure status is unauthenticated. + emit(state.copyWith(appConfig: null, clearAppConfig: true, status: AppStatus.unauthenticated)); } - // Fetch AppConfig regardless of user, as it's global config - // Or fetch it once at BLoC initialization if it doesn't depend on user at all. - // For now, fetching after user ensures some app state is set. - add(const _AppConfigFetchRequested()); } /// Handles refreshing/loading app settings (theme, font). @@ -298,31 +301,46 @@ class AppBloc extends Bloc { } Future _onAppConfigFetchRequested( - _AppConfigFetchRequested event, + AppConfigFetchRequested event, Emitter emit, ) async { - // Avoid refetching if already loaded, unless a refresh mechanism is added - if (state.appConfig != null && state.status != AppStatus.initial) return; + // Guard: Only fetch if a user (authenticated or anonymous) is present. + if (state.user == null) { + print('[AppBloc] User is null. Skipping AppConfig fetch because it requires authentication.'); + // If AppConfig was somehow present without a user, clear it. + // And ensure status isn't stuck on configFetching if this event was dispatched erroneously. + if (state.appConfig != null || state.status == AppStatus.configFetching) { + emit(state.copyWith(appConfig: null, clearAppConfig: true, status: AppStatus.unauthenticated)); + } + return; + } + + // Avoid refetching if already loaded for the current user session, unless explicitly trying to recover from a failed state. + if (state.appConfig != null && state.status != AppStatus.configFetchFailed) { + print('[AppBloc] AppConfig already loaded for user ${state.user?.id} and not in a failed state. Skipping fetch.'); + return; + } + + print('[AppBloc] Attempting to fetch AppConfig for user: ${state.user!.id}...'); + emit(state.copyWith(status: AppStatus.configFetching, appConfig: null, clearAppConfig: true)); try { - final appConfig = await _appConfigRepository.read(id: 'app_config'); - emit(state.copyWith(appConfig: appConfig)); - } on NotFoundException { - // If AppConfig is not found on the backend, use a local default. - // The AppConfig model has default values for its nested configurations. - emit(state.copyWith(appConfig: const AppConfig(id: 'app_config'))); - // Optionally, one might want to log this or attempt to create it on backend. - print( - '[AppBloc] AppConfig not found on backend, using local default.', - ); + final appConfig = await _appConfigRepository.read(id: 'app_config'); // API requires auth, so token will be used + print('[AppBloc] AppConfig fetched successfully. ID: ${appConfig.id} for user: ${state.user!.id}'); + + // Determine the correct status based on the existing user's role. + // This ensures that successfully fetching config doesn't revert auth status to 'initial'. + final newStatusBasedOnUser = state.user!.role == UserRole.standardUser + ? AppStatus.authenticated + : AppStatus.anonymous; + emit(state.copyWith(appConfig: appConfig, status: newStatusBasedOnUser)); } on HtHttpException catch (e) { - // Failed to fetch AppConfig, log error. App might be partially functional. - print('[AppBloc] Failed to fetch AppConfig: ${e.message}'); - // Emit state with null appConfig or keep existing if partially loaded before - emit(state.copyWith(appConfig: null, clearAppConfig: true)); - } catch (e) { - print('[AppBloc] Unexpected error fetching AppConfig: $e'); - emit(state.copyWith(appConfig: null, clearAppConfig: true)); + print('[AppBloc] Failed to fetch AppConfig (HtHttpException) for user ${state.user?.id}: ${e.runtimeType} - ${e.message}'); + emit(state.copyWith(status: AppStatus.configFetchFailed, appConfig: null, clearAppConfig: true)); + } catch (e, s) { + print('[AppBloc] Unexpected error fetching AppConfig for user ${state.user?.id}: $e'); + print('[AppBloc] Stacktrace: $s'); + emit(state.copyWith(status: AppStatus.configFetchFailed, appConfig: null, clearAppConfig: true)); } } diff --git a/lib/app/bloc/app_event.dart b/lib/app/bloc/app_event.dart index 263abae1..d342882b 100644 --- a/lib/app/bloc/app_event.dart +++ b/lib/app/bloc/app_event.dart @@ -92,12 +92,12 @@ class AppTextScaleFactorChanged extends AppEvent { List get props => [appTextScaleFactor]; } -/// {@template _app_config_fetch_requested} -/// Internal event to trigger fetching of the global AppConfig. +/// {@template app_config_fetch_requested} +/// Event to trigger fetching of the global AppConfig. /// {@endtemplate} -class _AppConfigFetchRequested extends AppEvent { - /// {@macro _app_config_fetch_requested} - const _AppConfigFetchRequested(); +class AppConfigFetchRequested extends AppEvent { + /// {@macro app_config_fetch_requested} + const AppConfigFetchRequested(); } /// {@template app_user_account_action_shown} diff --git a/lib/app/bloc/app_state.dart b/lib/app/bloc/app_state.dart index 04c01d8f..ccfcd344 100644 --- a/lib/app/bloc/app_state.dart +++ b/lib/app/bloc/app_state.dart @@ -13,6 +13,12 @@ enum AppStatus { /// The user is anonymous (signed in using an anonymous provider). anonymous, + + /// Fetching the essential AppConfig. + configFetching, + + /// Fetching the essential AppConfig failed. + configFetchFailed, } class AppState extends Equatable { diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 5d438e33..979f6c2c 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -1,6 +1,7 @@ // // ignore_for_file: deprecated_member_use +import 'package:flex_color_scheme/flex_color_scheme.dart'; // Added import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -12,6 +13,8 @@ import 'package:ht_main/authentication/bloc/authentication_bloc.dart'; import 'package:ht_main/l10n/l10n.dart'; import 'package:ht_main/router/router.dart'; import 'package:ht_main/shared/theme/app_theme.dart'; +import 'package:ht_main/shared/widgets/failure_state_widget.dart'; // Added +import 'package:ht_main/shared/widgets/loading_state_widget.dart'; // Added import 'package:ht_shared/ht_shared.dart'; // Shared models, FromJson, ToJson, etc. class App extends StatelessWidget { @@ -168,24 +171,93 @@ class _AppViewState extends State<_AppView> { // (specifically for updating the ValueNotifier) with a BlocListener. // The BlocBuilder remains for theme changes. return BlocListener( - // Only listen when the status actually changes + // Listen for status changes to update the GoRouter's ValueNotifier listenWhen: (previous, current) => previous.status != current.status, listener: (context, state) { - // Directly update the ValueNotifier when the AppBloc status changes. - // This triggers the GoRouter's refreshListenable. _statusNotifier.value = state.status; }, child: BlocBuilder( - // Build when theme-related properties change (including text scale factor) - buildWhen: - (previous, current) => - previous.themeMode != current.themeMode || - previous.flexScheme != current.flexScheme || - previous.fontFamily != current.fontFamily || - previous.appTextScaleFactor != current.appTextScaleFactor || - previous.locale != current.locale || - previous.settings != current.settings, // Added settings check + // Rebuild the UI based on AppBloc's state (theme, locale, and critical app statuses) builder: (context, state) { + // Defer l10n access until inside a MaterialApp context + + // Handle critical AppConfig loading states globally + if (state.status == AppStatus.configFetching) { + return MaterialApp( + debugShowCheckedModeBanner: false, + theme: lightTheme( + scheme: FlexScheme.material, // Default scheme + appTextScaleFactor: AppTextScaleFactor.medium, // Default + appFontWeight: AppFontWeight.regular, // Default + fontFamily: null, // System default font + ), + darkTheme: darkTheme( + scheme: FlexScheme.material, // Default scheme + appTextScaleFactor: AppTextScaleFactor.medium, // Default + appFontWeight: AppFontWeight.regular, // Default + fontFamily: null, // System default font + ), + themeMode: state.themeMode, // Still respect light/dark if available from system + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: Builder( // Use Builder to get context under MaterialApp + builder: (innerContext) { + final l10n = innerContext.l10n; + return LoadingStateWidget( + icon: Icons.settings_applications_outlined, + headline: l10n.headlinesFeedLoadingHeadline, // "Loading..." + subheadline: l10n.pleaseWait, // "Please wait..." + ); + }, + ), + ), + ); + } + + if (state.status == AppStatus.configFetchFailed) { + return MaterialApp( + debugShowCheckedModeBanner: false, + theme: lightTheme( + scheme: FlexScheme.material, // Default scheme + appTextScaleFactor: AppTextScaleFactor.medium, // Default + appFontWeight: AppFontWeight.regular, // Default + fontFamily: null, // System default font + ), + darkTheme: darkTheme( + scheme: FlexScheme.material, // Default scheme + appTextScaleFactor: AppTextScaleFactor.medium, // Default + appFontWeight: AppFontWeight.regular, // Default + fontFamily: null, // System default font + ), + themeMode: state.themeMode, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: Builder( // Use Builder to get context under MaterialApp + builder: (innerContext) { + final l10n = innerContext.l10n; + return FailureStateWidget( + message: l10n.unknownError, // "An unknown error occurred." + retryButtonText: "Retry", // Hardcoded for now + onRetry: () { + // Use outer context for BLoC access + context + .read() + .add(const AppConfigFetchRequested()); + }, + ); + }, + ), + ), + ); + } + + // If config is loaded (or not in a failed/fetching state for config), proceed with main app UI + // It's safe to access l10n here if needed for print statements, + // as this path implies we are about to build the main MaterialApp.router + // which provides localizations. + // final l10n = context.l10n; print('[_AppViewState] Building MaterialApp.router'); print('[_AppViewState] state.fontFamily: ${state.fontFamily}'); print( @@ -197,23 +269,22 @@ class _AppViewState extends State<_AppView> { return MaterialApp.router( debugShowCheckedModeBanner: false, themeMode: state.themeMode, - // Pass scheme and font family from state to theme functions theme: lightTheme( scheme: state.flexScheme, appTextScaleFactor: state.settings.displaySettings.textScaleFactor, - appFontWeight: state.settings.displaySettings.fontWeight, // Added + appFontWeight: state.settings.displaySettings.fontWeight, fontFamily: state.settings.displaySettings.fontFamily, ), darkTheme: darkTheme( scheme: state.flexScheme, appTextScaleFactor: state.settings.displaySettings.textScaleFactor, - appFontWeight: state.settings.displaySettings.fontWeight, // Added + appFontWeight: state.settings.displaySettings.fontWeight, fontFamily: state.settings.displaySettings.fontFamily, ), routerConfig: _router, - locale: state.locale, // Use locale from AppBloc state + locale: state.locale, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, );