Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 96 additions & 45 deletions lib/app/bloc/app_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,17 @@ class AppBloc extends Bloc<AppEvent, AppState> {
'[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 '
Expand Down Expand Up @@ -185,12 +190,17 @@ class AppBloc extends Bloc<AppEvent, AppState> {
'[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 '
Expand Down Expand Up @@ -313,46 +323,86 @@ class AppBloc extends Bloc<AppEvent, AppState> {

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) {
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);
// 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(
'Demo mode: Data initialization complete for ${newUser.id}.',
'[AppBloc] Demo mode: Initializing user-specific data for '
'user: ${newUser.id}',
);
} catch (e, s) {
_logger.severe('ERROR: Failed to initialize demo user data.', e, s);
try {
await demoDataInitializerService!.initializeUserSpecificData(newUser);
_logger.info(
'[AppBloc] Demo mode: User-specific data initialized for '
'user: ${newUser.id}.',
);
} catch (e, s) {
_logger.severe(
'[AppBloc] ERROR: Failed to initialize demo user data.',
e,
s,
);
emit(
state.copyWith(
status: AppLifeCycleStatus.criticalError,
initialUserPreferencesError: UnknownException(
'Failed to initialize demo user data: $e',
),
),
);
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 != 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,
// 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.',
);
_logger.info('Demo mode: Data migration completed for ${newUser.id}.');
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',
),
),
);
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));
}
}

Expand Down Expand Up @@ -406,6 +456,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {

final updatedSettings = event.settings;

// Optimistically update the state.
emit(state.copyWith(settings: updatedSettings));

try {
Expand Down
105 changes: 32 additions & 73 deletions lib/app/services/demo_data_initializer_service.dart
Original file line number Diff line number Diff line change
@@ -1,58 +1,51 @@
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
/// (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,
/// the backend API is responsible for initializing user data.
/// {@endtemplate}
class DemoDataInitializerService {
/// {@macro demo_data_initializer_service}
const DemoDataInitializerService({
DemoDataInitializerService({
required DataRepository<UserAppSettings> userAppSettingsRepository,
required DataRepository<UserContentPreferences>
userContentPreferencesRepository,
required DataRepository<User> userRepository,
}) : _userAppSettingsRepository = userAppSettingsRepository,
_userContentPreferencesRepository = userContentPreferencesRepository,
_userRepository = userRepository;
_logger = Logger('DemoDataInitializerService'); // Initialize logger

final DataRepository<UserAppSettings> _userAppSettingsRepository;
final DataRepository<UserContentPreferences>
_userContentPreferencesRepository;
final DataRepository<User> _userRepository;
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<void> 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([
_ensureUserAppSettingsExist(user.id),
_ensureUserContentPreferencesExist(user.id),
_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}',
);
}

Expand All @@ -61,12 +54,10 @@ class DemoDataInitializerService {
Future<void> _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(
Expand Down Expand Up @@ -95,14 +86,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;
}
Expand All @@ -113,12 +106,10 @@ class DemoDataInitializerService {
Future<void> _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(
Expand All @@ -132,48 +123,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',
);
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<User>` client might not have it
/// immediately.
Future<void> _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);
print(
'[DemoDataInitializerService] User object found and updated in '
'user client for ID: ${user.id}.',
);
} on NotFoundException {
print(
'[DemoDataInitializerService] 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 '
'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 UserContentPreferences '
'exist for user ID: $userId: $e',
e,
s,
);
rethrow;
}
Expand Down
Loading
Loading