From 57bd78d08e1795641a19a8a6f16000ac7923246b Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 18 Sep 2025 11:13:35 +0100 Subject: [PATCH 01/15] refactor(dependencies): make InlineAdCacheService nullable - Change inlineAdCacheService from late final to nullable - This modification allows for more flexible initialization and potential null checks --- lib/bootstrap.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 402f2c9a..ab976cff 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -49,7 +49,7 @@ Future bootstrap( // Initialize InlineAdCacheService early as it's a singleton and needs AdService. // It will be fully configured once AdService is available. - late final InlineAdCacheService inlineAdCacheService; + InlineAdCacheService? inlineAdCacheService; // 2. Initialize HttpClient. Its tokenProvider now directly reads from // kvStorage, breaking the circular dependency with AuthRepository. From edac84e80760195f2d52019d0a6ab9c042490bf0 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 18 Sep 2025 11:13:47 +0100 Subject: [PATCH 02/15] refactor(app): replace print statements with logging in DemoDataInitializerService - Import Logger from logging package - Initialize Logger in DemoDataInitializerService constructor - Replace print statements with appropriate log level methods (info, severe) - Remove redundant comments and simplify code --- .../demo_data_initializer_service.dart | 78 ++++++++++--------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/lib/app/services/demo_data_initializer_service.dart b/lib/app/services/demo_data_initializer_service.dart index 9212b623..9f28d4c2 100644 --- a/lib/app/services/demo_data_initializer_service.dart +++ b/lib/app/services/demo_data_initializer_service.dart @@ -1,6 +1,6 @@ import 'package:core/core.dart'; import 'package:data_repository/data_repository.dart'; -// Required for ThemeMode, AppBaseTheme, etc. +import 'package:logging/logging.dart'; // Import Logger /// {@template demo_data_initializer_service} /// A service responsible for ensuring that essential user-specific data @@ -14,19 +14,21 @@ import 'package:data_repository/data_repository.dart'; /// {@endtemplate} class DemoDataInitializerService { /// {@macro demo_data_initializer_service} - const DemoDataInitializerService({ + DemoDataInitializerService({ required DataRepository userAppSettingsRepository, required DataRepository userContentPreferencesRepository, required DataRepository userRepository, }) : _userAppSettingsRepository = userAppSettingsRepository, _userContentPreferencesRepository = userContentPreferencesRepository, - _userRepository = userRepository; + _userRepository = userRepository, + _logger = Logger('DemoDataInitializerService'); // Initialize logger final DataRepository _userAppSettingsRepository; final DataRepository _userContentPreferencesRepository; final DataRepository _userRepository; + final Logger _logger; // Add logger instance /// Initializes essential user-specific data in the in-memory clients /// for the given [user]. @@ -39,9 +41,8 @@ class DemoDataInitializerService { /// access these user-specific data points for a newly signed-in anonymous /// user in the demo environment. Future initializeUserSpecificData(User user) async { - print( - '[DemoDataInitializerService] Initializing user-specific data for ' - 'user ID: ${user.id}', + _logger.info( + 'Initializing user-specific data for user ID: ${user.id}', ); await Future.wait([ @@ -50,9 +51,8 @@ class DemoDataInitializerService { _ensureUserClientUserExists(user), ]); - print( - '[DemoDataInitializerService] User-specific data initialization ' - 'completed for user ID: ${user.id}', + _logger.info( + 'User-specific data initialization completed for user ID: ${user.id}', ); } @@ -61,12 +61,12 @@ class DemoDataInitializerService { Future _ensureUserAppSettingsExist(String userId) async { try { await _userAppSettingsRepository.read(id: userId, userId: userId); - print( - '[DemoDataInitializerService] UserAppSettings found for user ID: $userId.', + _logger.info( + 'UserAppSettings found for user ID: $userId.', ); } on NotFoundException { - print( - '[DemoDataInitializerService] UserAppSettings not found for user ID: ' + _logger.info( + 'UserAppSettings not found for user ID: ' '$userId. Creating default settings.', ); final defaultSettings = UserAppSettings( @@ -95,14 +95,16 @@ class DemoDataInitializerService { item: defaultSettings, userId: userId, ); - print( - '[DemoDataInitializerService] Default UserAppSettings created for ' + _logger.info( + 'Default UserAppSettings created for ' 'user ID: $userId.', ); } catch (e, s) { - print( - '[DemoDataInitializerService] Error ensuring UserAppSettings exist ' - 'for user ID: $userId: $e\n$s', + _logger.severe( + 'Error ensuring UserAppSettings exist ' + 'for user ID: $userId: $e', + e, + s, ); rethrow; } @@ -113,12 +115,12 @@ class DemoDataInitializerService { Future _ensureUserContentPreferencesExist(String userId) async { try { await _userContentPreferencesRepository.read(id: userId, userId: userId); - print( - '[DemoDataInitializerService] UserContentPreferences found for user ID: $userId.', + _logger.info( + 'UserContentPreferences found for user ID: $userId.', ); } on NotFoundException { - print( - '[DemoDataInitializerService] UserContentPreferences not found for ' + _logger.info( + 'UserContentPreferences not found for ' 'user ID: $userId. Creating default preferences.', ); final defaultPreferences = UserContentPreferences( @@ -132,14 +134,16 @@ class DemoDataInitializerService { item: defaultPreferences, userId: userId, ); - print( - '[DemoDataInitializerService] Default UserContentPreferences created ' + _logger.info( + 'Default UserContentPreferences created ' 'for user ID: $userId.', ); } catch (e, s) { - print( - '[DemoDataInitializerService] Error ensuring UserContentPreferences ' - 'exist for user ID: $userId: $e\n$s', + _logger.severe( + 'Error ensuring UserContentPreferences ' + 'exist for user ID: $userId: $e', + e, + s, ); rethrow; } @@ -156,24 +160,26 @@ class DemoDataInitializerService { await _userRepository.read(id: user.id, userId: user.id); // If user exists, ensure it's up-to-date (e.g., if roles changed) await _userRepository.update(id: user.id, item: user, userId: user.id); - print( - '[DemoDataInitializerService] User object found and updated in ' + _logger.info( + 'User object found and updated in ' 'user client for ID: ${user.id}.', ); } on NotFoundException { - print( - '[DemoDataInitializerService] User object not found in user client ' + _logger.info( + 'User object not found in user client ' 'for ID: ${user.id}. Creating it.', ); await _userRepository.create(item: user, userId: user.id); - print( - '[DemoDataInitializerService] User object created in user client ' + _logger.info( + 'User object created in user client ' 'for ID: ${user.id}.', ); } catch (e, s) { - print( - '[DemoDataInitializerService] Error ensuring User object exists in ' - 'user client for ID: ${user.id}: $e\n$s', + _logger.severe( + 'Error ensuring User object exists in ' + 'user client for ID: ${user.id}: $e', + e, + s, ); rethrow; } From d5c57a84dbb7873457cd83edef82c7d77c3f822f Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 18 Sep 2025 11:14:16 +0100 Subject: [PATCH 03/15] fix(headlines-feed): pass HeadlinesFilterBloc to child filter page - Modify navigation to child filter page by passing the current HeadlinesFilterBloc instance as an extra argument - This ensures the child page can access the bloc directly, maintaining state consistency - Update comments to reflect the new implementation --- lib/headlines-feed/view/headlines_filter_page.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/headlines-feed/view/headlines_filter_page.dart b/lib/headlines-feed/view/headlines_filter_page.dart index 9cbac191..e7ef22ad 100644 --- a/lib/headlines-feed/view/headlines_filter_page.dart +++ b/lib/headlines-feed/view/headlines_filter_page.dart @@ -82,9 +82,13 @@ class _HeadlinesFilterView extends StatelessWidget { enabled: enabled, onTap: enabled ? () { - // Navigate to the child filter page. The child page will read - // the current selections from HeadlinesFilterBloc directly. - context.pushNamed(routeName); + // Navigate to the child filter page, passing the current + // HeadlinesFilterBloc instance as an extra argument. + // This ensures the child page can access the bloc directly. + context.pushNamed( + routeName, + extra: context.read(), + ); } : null, ); From 70028e5411337da4cf4f9d35c52227642679d245 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 18 Sep 2025 11:14:40 +0100 Subject: [PATCH 04/15] refactor(headlines-feed): improve country filter page architecture - Add HeadlinesFilterBloc requirement to CountryFilterPage - Introduce _CountryFilterView widget for actual page content - Update documentation and widget tree structure --- .../view/country_filter_page.dart | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/headlines-feed/view/country_filter_page.dart b/lib/headlines-feed/view/country_filter_page.dart index ce7d8f0e..4048b466 100644 --- a/lib/headlines-feed/view/country_filter_page.dart +++ b/lib/headlines-feed/view/country_filter_page.dart @@ -14,11 +14,36 @@ import 'package:ui_kit/ui_kit.dart'; /// {@endtemplate} class CountryFilterPage extends StatelessWidget { /// {@macro country_filter_page} - const CountryFilterPage({required this.title, super.key}); + /// + /// Requires a [title] for the app bar and the [filterBloc] instance + /// passed from the parent route. + const CountryFilterPage({ + required this.title, + required this.filterBloc, + super.key, + }); /// The title to display in the app bar for this filter page. final String title; + /// The instance of [HeadlinesFilterBloc] provided by the parent route. + final HeadlinesFilterBloc filterBloc; + + @override + Widget build(BuildContext context) { + // Provide the existing filterBloc to this subtree. + return BlocProvider.value( + value: filterBloc, + child: _CountryFilterView(title: title), + ); + } +} + +class _CountryFilterView extends StatelessWidget { + const _CountryFilterView({required this.title}); + + final String title; + @override Widget build(BuildContext context) { final l10n = AppLocalizationsX(context).l10n; From 443ae38bed2889086dc4dcacc57bb27551bd3a40 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 18 Sep 2025 11:14:51 +0100 Subject: [PATCH 05/15] feat(headlines-feed): implement BlocProvider in SourceFilterPage - Add required HeadlinesFilterBloc parameter to SourceFilterPage constructor - Implement BlocProvider to pass the existing filterBloc to the subtree - Update documentation to reflect the new requirement and addition --- lib/headlines-feed/view/source_filter_page.dart | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/headlines-feed/view/source_filter_page.dart b/lib/headlines-feed/view/source_filter_page.dart index 2c108836..3884edfe 100644 --- a/lib/headlines-feed/view/source_filter_page.dart +++ b/lib/headlines-feed/view/source_filter_page.dart @@ -19,11 +19,20 @@ import 'package:ui_kit/ui_kit.dart'; /// {@endtemplate} class SourceFilterPage extends StatelessWidget { /// {@macro source_filter_page} - const SourceFilterPage({super.key}); + /// + /// Requires the [filterBloc] instance passed from the parent route. + const SourceFilterPage({required this.filterBloc, super.key}); + + /// The instance of [HeadlinesFilterBloc] provided by the parent route. + final HeadlinesFilterBloc filterBloc; @override Widget build(BuildContext context) { - return const _SourceFilterView(); + // Provide the existing filterBloc to this subtree. + return BlocProvider.value( + value: filterBloc, + child: const _SourceFilterView(), + ); } } From e7907f26b6190a0627bdd72cc3bcac6e0d7f2558 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 18 Sep 2025 11:14:57 +0100 Subject: [PATCH 06/15] feat(headlines-feed): update TopicFilterPage to use provided filterBloc - Modify TopicFilterPage to require a HeadlinesFilterBloc instance - Implement dependency injection for HeadlinesFilterBloc - Rename original TopicFilterPage to _TopicFilterView --- .../view/topic_filter_page.dart | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/headlines-feed/view/topic_filter_page.dart b/lib/headlines-feed/view/topic_filter_page.dart index ff95c445..f0b055ba 100644 --- a/lib/headlines-feed/view/topic_filter_page.dart +++ b/lib/headlines-feed/view/topic_filter_page.dart @@ -14,7 +14,25 @@ import 'package:ui_kit/ui_kit.dart'; /// {@endtemplate} class TopicFilterPage extends StatelessWidget { /// {@macro topic_filter_page} - const TopicFilterPage({super.key}); + /// + /// Requires the [filterBloc] instance passed from the parent route. + const TopicFilterPage({required this.filterBloc, super.key}); + + /// The instance of [HeadlinesFilterBloc] provided by the parent route. + final HeadlinesFilterBloc filterBloc; + + @override + Widget build(BuildContext context) { + // Provide the existing filterBloc to this subtree. + return BlocProvider.value( + value: filterBloc, + child: const _TopicFilterView(), + ); + } +} + +class _TopicFilterView extends StatelessWidget { + const _TopicFilterView(); @override Widget build(BuildContext context) { From e502773a31ac4bebf88696ad8ff2974a1381ed3f Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 18 Sep 2025 11:15:10 +0100 Subject: [PATCH 07/15] refactor(app): improve demo mode user data initialization - Move demo mode data initialization before fetching user-specific data - Handle NotFoundException for user settings and content preferences in demo mode - Add critical error handling if demo user data initialization fails - Remove unnecessary demo mode initialization after user fetch --- lib/app/bloc/app_bloc.dart | 75 ++++++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 28 deletions(-) diff --git a/lib/app/bloc/app_bloc.dart b/lib/app/bloc/app_bloc.dart index a23e9189..f25b1f11 100644 --- a/lib/app/bloc/app_bloc.dart +++ b/lib/app/bloc/app_bloc.dart @@ -143,12 +143,17 @@ class AppBloc extends Bloc { '[AppBloc] Failed to fetch user settings (HttpException) ' 'for user ${user.id}: ${e.runtimeType} - ${e.message}', ); - emit( - state.copyWith( - status: AppLifeCycleStatus.criticalError, - initialUserPreferencesError: e, - ), - ); + // In demo mode, NotFoundException for user settings is expected if not yet initialized. + // Do not transition to criticalError immediately. + if (_environment != local_config.AppEnvironment.demo || + e is! NotFoundException) { + emit( + state.copyWith( + status: AppLifeCycleStatus.criticalError, + initialUserPreferencesError: e, + ), + ); + } } catch (e, s) { _logger.severe( '[AppBloc] Unexpected error during user settings fetch ' @@ -185,12 +190,17 @@ class AppBloc extends Bloc { '[AppBloc] Failed to fetch user content preferences (HttpException) ' 'for user ${user.id}: ${e.runtimeType} - ${e.message}', ); - emit( - state.copyWith( - status: AppLifeCycleStatus.criticalError, - initialUserPreferencesError: e, - ), - ); + // In demo mode, NotFoundException for user content preferences is expected if not yet initialized. + // Do not transition to criticalError immediately. + if (_environment != local_config.AppEnvironment.demo || + e is! NotFoundException) { + emit( + state.copyWith( + status: AppLifeCycleStatus.criticalError, + initialUserPreferencesError: e, + ), + ); + } } catch (e, s) { _logger.severe( '[AppBloc] Unexpected error during user content preferences fetch ' @@ -313,29 +323,37 @@ class AppBloc extends Bloc { emit(state.copyWith(status: newStatus)); - // If a new user is present, fetch their specific data. + // If a new user is present, handle their data. if (newUser != null) { + // In demo mode, ensure user-specific data is initialized BEFORE fetching. + if (_environment == local_config.AppEnvironment.demo && + demoDataInitializerService != null) { + try { + _logger.info('Demo mode: Initializing data for user ${newUser.id}.'); + await demoDataInitializerService!.initializeUserSpecificData(newUser); + _logger.info( + 'Demo mode: Data initialization complete for ${newUser.id}.', + ); + } catch (e, s) { + _logger.severe('ERROR: Failed to initialize demo user data.', e, s); + // If demo data initialization fails, it's a critical error for demo mode. + emit( + state.copyWith( + status: AppLifeCycleStatus.criticalError, + initialUserPreferencesError: UnknownException( + 'Failed to initialize demo user data: ${e.toString()}', + ), + ), + ); + return; // Stop further processing if demo data init failed critically. + } + } await _fetchAndSetUserData(newUser, emit); } else { // If user logs out, clear user-specific data from state. emit(state.copyWith(settings: null, userContentPreferences: null)); } - // In demo mode, ensure user-specific data is initialized. - if (_environment == local_config.AppEnvironment.demo && - demoDataInitializerService != null && - newUser != null) { - try { - _logger.info('Demo mode: Initializing data for user ${newUser.id}.'); - await demoDataInitializerService!.initializeUserSpecificData(newUser); - _logger.info( - 'Demo mode: Data initialization complete for ${newUser.id}.', - ); - } catch (e, s) { - _logger.severe('ERROR: Failed to initialize demo user data.', e, s); - } - } - // Handle data migration if an anonymous user signs in. if (oldUser != null && oldUser.appRole == AppUserRole.guestUser && @@ -406,6 +424,7 @@ class AppBloc extends Bloc { final updatedSettings = event.settings; + // Optimistically update the state. emit(state.copyWith(settings: updatedSettings)); try { From 8043434c3119aee8b694b6670983aa548bd7466c Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 18 Sep 2025 11:15:21 +0100 Subject: [PATCH 08/15] feat(router): pass HeadlinesFilterBloc to filter routes - Add HeadlinesFilterBloc import to router.dart - Modify TopicFilterPage, SourceFilterPage, and CountryFilterPage routes to pass HeadlinesFilterBloc via state.extra - Update page builders to utilize the passed HeadlinesFilterBloc --- lib/router/router.dart | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/router/router.dart b/lib/router/router.dart index f4f3e6ce..97210dfc 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -27,6 +27,7 @@ import 'package:flutter_news_app_mobile_client_full_source_code/headline-details import 'package:flutter_news_app_mobile_client_full_source_code/headline-details/bloc/similar_headlines_bloc.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/headline-details/view/headline_details_page.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/bloc/headlines_feed_bloc.dart'; +import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/bloc/headlines_filter_bloc.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/view/country_filter_page.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/view/headlines_feed_page.dart'; import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/view/headlines_filter_page.dart'; @@ -455,22 +456,33 @@ GoRouter createRouter({ GoRoute( path: Routes.feedFilterTopics, name: Routes.feedFilterTopicsName, - builder: (context, state) => const TopicFilterPage(), + builder: (context, state) { + final filterBloc = + state.extra! as HeadlinesFilterBloc; + return TopicFilterPage(filterBloc: filterBloc); + }, ), // Sub-route for source selection GoRoute( path: Routes.feedFilterSources, name: Routes.feedFilterSourcesName, - builder: (context, state) => const SourceFilterPage(), + builder: (context, state) { + final filterBloc = + state.extra! as HeadlinesFilterBloc; + return SourceFilterPage(filterBloc: filterBloc); + }, ), GoRoute( path: Routes.feedFilterEventCountries, name: Routes.feedFilterEventCountriesName, pageBuilder: (context, state) { final l10n = context.l10n; + final filterBloc = + state.extra! as HeadlinesFilterBloc; return MaterialPage( child: CountryFilterPage( title: l10n.headlinesFeedFilterEventCountryLabel, + filterBloc: filterBloc, ), ); }, From 137bed015bc1d409dfd425eef575c1b8184bbcf5 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 18 Sep 2025 12:13:17 +0100 Subject: [PATCH 09/15] feat(service): add documentation for DemoDataInitializerService - Explain the purpose of DemoDataInitializerService in the code comments - Describe its role in initializing essential user-specific data in demo environment --- lib/bootstrap.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index ab976cff..3ace5d94 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -420,6 +420,10 @@ Future bootstrap( : null; // Conditionally instantiate DemoDataInitializerService + // This service is responsible for ensuring that essential user-specific data + // (like UserAppSettings, UserContentPreferences) + // exists in the data in-memory clients when a user is first encountered + // in the demo environment. final demoDataInitializerService = appConfig.environment == app_config.AppEnvironment.demo ? DemoDataInitializerService( From 3a8bd0501d804d1eed6504745e77c75f6336827b Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 18 Sep 2025 12:13:55 +0100 Subject: [PATCH 10/15] refactor(app): enhance logging in DemoDataMigrationService - Replace print statements with Logger for better logging practices - Add logging for info and error events - Include stack trace in error logs - Inject Logger into DemoDataMigrationService --- .../services/demo_data_migration_service.dart | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/app/services/demo_data_migration_service.dart b/lib/app/services/demo_data_migration_service.dart index 58e84561..13e22cba 100644 --- a/lib/app/services/demo_data_migration_service.dart +++ b/lib/app/services/demo_data_migration_service.dart @@ -1,5 +1,6 @@ import 'package:core/core.dart'; import 'package:data_repository/data_repository.dart'; +import 'package:logging/logging.dart'; // Import Logger /// {@template demo_data_migration_service} /// A service responsible for migrating user data (settings and preferences) @@ -11,16 +12,18 @@ import 'package:data_repository/data_repository.dart'; /// {@endtemplate} class DemoDataMigrationService { /// {@macro demo_data_migration_service} - const DemoDataMigrationService({ + DemoDataMigrationService({ required DataRepository userAppSettingsRepository, required DataRepository userContentPreferencesRepository, }) : _userAppSettingsRepository = userAppSettingsRepository, - _userContentPreferencesRepository = userContentPreferencesRepository; + _userContentPreferencesRepository = userContentPreferencesRepository, + _logger = Logger('DemoDataMigrationService'); final DataRepository _userAppSettingsRepository; final DataRepository _userContentPreferencesRepository; + final Logger _logger; /// Migrates user settings and content preferences from an old anonymous /// user ID to a new authenticated user ID. @@ -31,7 +34,7 @@ class DemoDataMigrationService { required String oldUserId, required String newUserId, }) async { - print( + _logger.info( '[DemoDataMigrationService] Attempting to migrate data from ' 'anonymous user ID: $oldUserId to authenticated user ID: $newUserId', ); @@ -70,19 +73,21 @@ class DemoDataMigrationService { } await _userAppSettingsRepository.delete(id: oldUserId, userId: oldUserId); - print( + _logger.info( '[DemoDataMigrationService] UserAppSettings migrated successfully ' 'from $oldUserId to $newUserId.', ); } on NotFoundException { - print( + _logger.info( '[DemoDataMigrationService] No UserAppSettings found for old user ID: ' '$oldUserId. Skipping migration for settings.', ); } catch (e, s) { - print( + _logger.severe( '[DemoDataMigrationService] Error migrating UserAppSettings from ' - '$oldUserId to $newUserId: $e\n$s', + '$oldUserId to $newUserId: $e', + e, + s, ); } @@ -123,19 +128,21 @@ class DemoDataMigrationService { id: oldUserId, userId: oldUserId, ); - print( + _logger.info( '[DemoDataMigrationService] UserContentPreferences migrated ' 'successfully from $oldUserId to $newUserId.', ); } on NotFoundException { - print( + _logger.info( '[DemoDataMigrationService] No UserContentPreferences found for old ' 'user ID: $oldUserId. Skipping migration for preferences.', ); } catch (e, s) { - print( + _logger.severe( '[DemoDataMigrationService] Error migrating UserContentPreferences ' - 'from $oldUserId to $newUserId: $e\n$s', + 'from $oldUserId to $newUserId: $e', + e, + s, ); } } From 9e6402fba1d7231a5f77a58b05699b36cb759edf Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 18 Sep 2025 12:14:50 +0100 Subject: [PATCH 11/15] refactor(DemoDataInitializerService): remove User object initialization - Remove _ensureUserClientUserExists method - Remove User repository dependency - Update comments and documentation to reflect changes - Simplify logger calls by removing unnecessary line breaks --- .../demo_data_initializer_service.dart | 64 +++---------------- 1 file changed, 10 insertions(+), 54 deletions(-) diff --git a/lib/app/services/demo_data_initializer_service.dart b/lib/app/services/demo_data_initializer_service.dart index 9f28d4c2..de3d3316 100644 --- a/lib/app/services/demo_data_initializer_service.dart +++ b/lib/app/services/demo_data_initializer_service.dart @@ -4,9 +4,8 @@ import 'package:logging/logging.dart'; // Import Logger /// {@template demo_data_initializer_service} /// A service responsible for ensuring that essential user-specific data -/// (like [UserAppSettings], [UserContentPreferences], and the [User] object -/// itself) exists in the data in-memory clients when a user is first encountered -/// in the demo environment. +/// (like [UserAppSettings] and [UserContentPreferences]) exists in the +/// data in-memory clients when a user is first encountered in the demo environment. /// /// This service is specifically designed for the in-memory data clients /// used in the demo environment. In production/development environments, @@ -21,34 +20,31 @@ class DemoDataInitializerService { required DataRepository userRepository, }) : _userAppSettingsRepository = userAppSettingsRepository, _userContentPreferencesRepository = userContentPreferencesRepository, - _userRepository = userRepository, + _userRepository = userRepository, // Retained for consistency in constructor, but not used internally by this service. _logger = Logger('DemoDataInitializerService'); // Initialize logger final DataRepository _userAppSettingsRepository; final DataRepository _userContentPreferencesRepository; - final DataRepository _userRepository; + final DataRepository _userRepository; // Retained for consistency in constructor, but not used internally by this service. final Logger _logger; // Add logger instance /// Initializes essential user-specific data in the in-memory clients /// for the given [user]. /// - /// This method checks if [UserAppSettings], [UserContentPreferences], - /// and the [User] object itself exist for the provided user ID. If any - /// are missing, it creates them with default values. + /// This method checks if [UserAppSettings] and [UserContentPreferences] + /// exist for the provided user ID. If any are missing, it creates them + /// with default values. /// /// This prevents "READ FAILED" errors when the application attempts to /// access these user-specific data points for a newly signed-in anonymous /// user in the demo environment. Future initializeUserSpecificData(User user) async { - _logger.info( - 'Initializing user-specific data for user ID: ${user.id}', - ); + _logger.info('Initializing user-specific data for user ID: ${user.id}'); await Future.wait([ _ensureUserAppSettingsExist(user.id), _ensureUserContentPreferencesExist(user.id), - _ensureUserClientUserExists(user), ]); _logger.info( @@ -61,9 +57,7 @@ class DemoDataInitializerService { Future _ensureUserAppSettingsExist(String userId) async { try { await _userAppSettingsRepository.read(id: userId, userId: userId); - _logger.info( - 'UserAppSettings found for user ID: $userId.', - ); + _logger.info('UserAppSettings found for user ID: $userId.'); } on NotFoundException { _logger.info( 'UserAppSettings not found for user ID: ' @@ -115,9 +109,7 @@ class DemoDataInitializerService { Future _ensureUserContentPreferencesExist(String userId) async { try { await _userContentPreferencesRepository.read(id: userId, userId: userId); - _logger.info( - 'UserContentPreferences found for user ID: $userId.', - ); + _logger.info('UserContentPreferences found for user ID: $userId.'); } on NotFoundException { _logger.info( 'UserContentPreferences not found for ' @@ -148,40 +140,4 @@ class DemoDataInitializerService { rethrow; } } - - /// Ensures that the [User] object for the given [user] exists in the - /// user client. If not found, creates it. If found, updates it. - /// - /// This is important because the `AuthInmemory` client might create a - /// basic user, but the `DataInMemory` client might not have it - /// immediately. - Future _ensureUserClientUserExists(User user) async { - try { - await _userRepository.read(id: user.id, userId: user.id); - // If user exists, ensure it's up-to-date (e.g., if roles changed) - await _userRepository.update(id: user.id, item: user, userId: user.id); - _logger.info( - 'User object found and updated in ' - 'user client for ID: ${user.id}.', - ); - } on NotFoundException { - _logger.info( - 'User object not found in user client ' - 'for ID: ${user.id}. Creating it.', - ); - await _userRepository.create(item: user, userId: user.id); - _logger.info( - 'User object created in user client ' - 'for ID: ${user.id}.', - ); - } catch (e, s) { - _logger.severe( - 'Error ensuring User object exists in ' - 'user client for ID: ${user.id}: $e', - e, - s, - ); - rethrow; - } - } } From 6d02c3a7d648469d3c4823f48a9638a974dd5ff6 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 18 Sep 2025 12:18:49 +0100 Subject: [PATCH 12/15] refactor(DemoDataInitializerService): remove unused user repository - Remove unused DataRepository _userRepository field - Remove comment about unused repository --- lib/app/services/demo_data_initializer_service.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/app/services/demo_data_initializer_service.dart b/lib/app/services/demo_data_initializer_service.dart index de3d3316..19c19c26 100644 --- a/lib/app/services/demo_data_initializer_service.dart +++ b/lib/app/services/demo_data_initializer_service.dart @@ -17,16 +17,13 @@ class DemoDataInitializerService { required DataRepository userAppSettingsRepository, required DataRepository userContentPreferencesRepository, - required DataRepository userRepository, }) : _userAppSettingsRepository = userAppSettingsRepository, _userContentPreferencesRepository = userContentPreferencesRepository, - _userRepository = userRepository, // Retained for consistency in constructor, but not used internally by this service. _logger = Logger('DemoDataInitializerService'); // Initialize logger final DataRepository _userAppSettingsRepository; final DataRepository _userContentPreferencesRepository; - final DataRepository _userRepository; // Retained for consistency in constructor, but not used internally by this service. final Logger _logger; // Add logger instance /// Initializes essential user-specific data in the in-memory clients From 3e83d462465d6aa0ad56ae75c85ac6ece7ca99cb Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 18 Sep 2025 12:19:16 +0100 Subject: [PATCH 13/15] refactor(app): improve user data handling in demo mode - Enhance logging for demo data initialization and migration - Clarify comments on user-specific data initialization process - Add data migration for anonymous to authenticated user transition - Ensure user-specific data fetching after potential initialization and migration - Update logout process to clear user-specific data from state --- lib/app/bloc/app_bloc.dart | 82 ++++++++++++++++++++++++++------------ 1 file changed, 57 insertions(+), 25 deletions(-) diff --git a/lib/app/bloc/app_bloc.dart b/lib/app/bloc/app_bloc.dart index f25b1f11..7500e538 100644 --- a/lib/app/bloc/app_bloc.dart +++ b/lib/app/bloc/app_bloc.dart @@ -325,18 +325,28 @@ class AppBloc extends Bloc { // If a new user is present, handle their data. if (newUser != null) { - // In demo mode, ensure user-specific data is initialized BEFORE fetching. + // In demo mode, ensure essential user-specific data (settings, + // preferences, and the user object itself in the data client) + // are initialized if they don't already exist. This prevents + // NotFoundException during subsequent reads. if (_environment == local_config.AppEnvironment.demo && demoDataInitializerService != null) { + _logger.info( + '[AppBloc] Demo mode: Initializing user-specific data for ' + 'user: ${newUser.id}', + ); try { - _logger.info('Demo mode: Initializing data for user ${newUser.id}.'); await demoDataInitializerService!.initializeUserSpecificData(newUser); _logger.info( - 'Demo mode: Data initialization complete for ${newUser.id}.', + '[AppBloc] Demo mode: User-specific data initialized for ' + 'user: ${newUser.id}.', ); } catch (e, s) { - _logger.severe('ERROR: Failed to initialize demo user data.', e, s); - // If demo data initialization fails, it's a critical error for demo mode. + _logger.severe( + '[AppBloc] ERROR: Failed to initialize demo user data.', + e, + s, + ); emit( state.copyWith( status: AppLifeCycleStatus.criticalError, @@ -345,33 +355,55 @@ class AppBloc extends Bloc { ), ), ); - return; // Stop further processing if demo data init failed critically. + return; // Stop further processing if initialization failed critically. } } + + // Handle data migration if an anonymous user signs in. + if (oldUser != null && + oldUser.appRole == AppUserRole.guestUser && + newUser.appRole == AppUserRole.standardUser) { + _logger.info( + '[AppBloc] Anonymous user ${oldUser.id} transitioned to ' + 'authenticated user ${newUser.id}. Attempting data migration.', + ); + if (demoDataMigrationService != null && + _environment == local_config.AppEnvironment.demo) { + try { + await demoDataMigrationService!.migrateAnonymousData( + oldUserId: oldUser.id, + newUserId: newUser.id, + ); + _logger.info( + '[AppBloc] Demo mode: Data migration completed for ${newUser.id}.', + ); + } catch (e, s) { + _logger.severe( + '[AppBloc] ERROR: Failed to migrate demo user data.', + e, + s, + ); + // If demo data migration fails, it's a critical error for demo mode. + emit( + state.copyWith( + status: AppLifeCycleStatus.criticalError, + initialUserPreferencesError: UnknownException( + 'Failed to migrate demo user data: ${e.toString()}', + ), + ), + ); + return; // Stop further processing if migration failed critically. + } + } + } + + // After potential initialization and migration, + // ensure user-specific data (settings and preferences) are loaded. await _fetchAndSetUserData(newUser, emit); } else { // If user logs out, clear user-specific data from state. emit(state.copyWith(settings: null, userContentPreferences: null)); } - - // Handle data migration if an anonymous user signs in. - if (oldUser != null && - oldUser.appRole == AppUserRole.guestUser && - newUser != null && - newUser.appRole == AppUserRole.standardUser) { - _logger.info( - 'Anonymous user ${oldUser.id} transitioned to authenticated user ' - '${newUser.id}. Attempting data migration.', - ); - if (demoDataMigrationService != null && - _environment == local_config.AppEnvironment.demo) { - await demoDataMigrationService!.migrateAnonymousData( - oldUserId: oldUser.id, - newUserId: newUser.id, - ); - _logger.info('Demo mode: Data migration completed for ${newUser.id}.'); - } - } } /// Handles refreshing/loading app settings (theme, font). From dc880acb17f3008d2485eec51b96fe58f59afe0b Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 18 Sep 2025 12:20:28 +0100 Subject: [PATCH 14/15] refactor(bootstrap): remove unused parameters - Remove userRepository parameter from DemoDataInitializerService - Remove initialRemoteConfigError parameter from bootstrap function --- lib/bootstrap.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 3ace5d94..38db4711 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -429,7 +429,6 @@ Future bootstrap( ? DemoDataInitializerService( userAppSettingsRepository: userAppSettingsRepository, userContentPreferencesRepository: userContentPreferencesRepository, - userRepository: userRepository, ) : null; @@ -453,7 +452,6 @@ Future bootstrap( localAdRepository: localAdRepository, navigatorKey: navigatorKey, initialRemoteConfig: initialRemoteConfig, - initialRemoteConfigError: - initialRemoteConfigError, + initialRemoteConfigError: initialRemoteConfigError, ); } From d0f5d9c7b3f4576891f73aba1c51cc56b6f50c34 Mon Sep 17 00:00:00 2001 From: fulleni Date: Thu, 18 Sep 2025 12:20:51 +0100 Subject: [PATCH 15/15] style: format misc --- lib/app/bloc/app_bloc.dart | 4 ++-- lib/app/services/demo_data_migration_service.dart | 2 +- lib/app/view/app.dart | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/app/bloc/app_bloc.dart b/lib/app/bloc/app_bloc.dart index 7500e538..05bd25f6 100644 --- a/lib/app/bloc/app_bloc.dart +++ b/lib/app/bloc/app_bloc.dart @@ -351,7 +351,7 @@ class AppBloc extends Bloc { state.copyWith( status: AppLifeCycleStatus.criticalError, initialUserPreferencesError: UnknownException( - 'Failed to initialize demo user data: ${e.toString()}', + 'Failed to initialize demo user data: $e', ), ), ); @@ -388,7 +388,7 @@ class AppBloc extends Bloc { state.copyWith( status: AppLifeCycleStatus.criticalError, initialUserPreferencesError: UnknownException( - 'Failed to migrate demo user data: ${e.toString()}', + 'Failed to migrate demo user data: $e', ), ), ); diff --git a/lib/app/services/demo_data_migration_service.dart b/lib/app/services/demo_data_migration_service.dart index 13e22cba..83ec8b06 100644 --- a/lib/app/services/demo_data_migration_service.dart +++ b/lib/app/services/demo_data_migration_service.dart @@ -18,7 +18,7 @@ class DemoDataMigrationService { userContentPreferencesRepository, }) : _userAppSettingsRepository = userAppSettingsRepository, _userContentPreferencesRepository = userContentPreferencesRepository, - _logger = Logger('DemoDataMigrationService'); + _logger = Logger('DemoDataMigrationService'); final DataRepository _userAppSettingsRepository; final DataRepository diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index db7c0cec..0d382aa1 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -444,7 +444,9 @@ class _AppViewState extends State<_AppView> { builder: (context) { return LoadingStateWidget( icon: Icons.sync, - headline: AppLocalizations.of(context).settingsLoadingHeadline, + headline: AppLocalizations.of( + context, + ).settingsLoadingHeadline, subheadline: AppLocalizations.of( context, ).settingsLoadingSubheadline,