Skip to content

Commit 1fef224

Browse files
committed
feat(app): implement app version enforcement functionality
- Add PackageInfoService to retrieve current app version - Implement version comparison logic in AppBloc - Handle version enforcement during app startup and background checks - Transition app to 'updateRequired' state when version is outdated - Log appropriate messages for version check outcomes - Update imports and event handlers for new version check feature
1 parent 9e7179f commit 1fef224

File tree

1 file changed

+108
-19
lines changed

1 file changed

+108
-19
lines changed

lib/app/bloc/app_bloc.dart

Lines changed: 108 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import 'package:flutter_news_app_mobile_client_full_source_code/app/config/confi
1111
as local_config;
1212
import 'package:flutter_news_app_mobile_client_full_source_code/app/services/demo_data_initializer_service.dart';
1313
import 'package:flutter_news_app_mobile_client_full_source_code/app/services/demo_data_migration_service.dart';
14+
import 'package:flutter_news_app_mobile_client_full_source_code/app/services/package_info_service.dart';
1415
import 'package:logging/logging.dart';
16+
import 'package:pub_semver/pub_semver.dart';
1517

1618
part 'app_event.dart';
1719
part 'app_state.dart';
@@ -41,6 +43,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
4143
required GlobalKey<NavigatorState> navigatorKey,
4244
required RemoteConfig? initialRemoteConfig,
4345
required HttpException? initialRemoteConfigError,
46+
required PackageInfoService packageInfoService,
4447
this.demoDataMigrationService,
4548
this.demoDataInitializerService,
4649
this.initialUser,
@@ -51,6 +54,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
5154
_userRepository = userRepository,
5255
_environment = environment,
5356
_navigatorKey = navigatorKey,
57+
_packageInfoService = packageInfoService,
5458
_logger = Logger('AppBloc'),
5559
super(
5660
AppState(
@@ -71,6 +75,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
7175
on<AppUserContentPreferencesRefreshed>(_onUserContentPreferencesRefreshed);
7276
on<AppSettingsChanged>(_onAppSettingsChanged);
7377
on<AppPeriodicConfigFetchRequested>(_onAppPeriodicConfigFetchRequested);
78+
on<AppVersionCheckRequested>(_onAppVersionCheckRequested);
7479
on<AppUserFeedDecoratorShown>(_onAppUserFeedDecoratorShown);
7580
on<AppUserContentPreferencesChanged>(_onAppUserContentPreferencesChanged);
7681
on<AppLogoutRequested>(_onLogoutRequested);
@@ -91,6 +96,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
9196
final DataRepository<User> _userRepository;
9297
final local_config.AppEnvironment _environment;
9398
final GlobalKey<NavigatorState> _navigatorKey;
99+
final PackageInfoService _packageInfoService;
94100
final Logger _logger;
95101
final DemoDataMigrationService? demoDataMigrationService;
96102
final DemoDataInitializerService? demoDataInitializerService;
@@ -259,14 +265,13 @@ class AppBloc extends Bloc<AppEvent, AppState> {
259265
return;
260266
}
261267

262-
if (state.remoteConfig!.appStatus.isLatestVersionOnly) {
263-
// TODO(fulleni): Compare with actual app version.
264-
_logger.info(
265-
'[AppBloc] App update required. Transitioning to updateRequired state.',
266-
);
267-
emit(state.copyWith(status: AppLifeCycleStatus.updateRequired));
268-
return;
269-
}
268+
// Dispatch AppVersionCheckRequested to handle version enforcement.
269+
add(
270+
AppVersionCheckRequested(
271+
remoteConfig: state.remoteConfig!,
272+
isBackgroundCheck: false, // Not a background check during startup
273+
),
274+
);
270275

271276
// If we reach here, the app is not under maintenance or requires update.
272277
// Now, handle user-specific data loading.
@@ -547,17 +552,13 @@ class AppBloc extends Bloc<AppEvent, AppState> {
547552
return;
548553
}
549554

550-
if (remoteConfig.appStatus.isLatestVersionOnly) {
551-
// TODO(fulleni): Compare with actual app version.
552-
emit(
553-
state.copyWith(
554-
status: AppLifeCycleStatus.updateRequired,
555-
remoteConfig: remoteConfig,
556-
initialRemoteConfigError: null,
557-
),
558-
);
559-
return;
560-
}
555+
// Dispatch AppVersionCheckRequested to handle version enforcement.
556+
add(
557+
AppVersionCheckRequested(
558+
remoteConfig: remoteConfig,
559+
isBackgroundCheck: event.isBackgroundCheck,
560+
),
561+
);
561562

562563
final finalStatus = state.user!.appRole == AppUserRole.standardUser
563564
? AppLifeCycleStatus.authenticated
@@ -598,6 +599,94 @@ class AppBloc extends Bloc<AppEvent, AppState> {
598599
}
599600
}
600601

602+
/// Handles the [AppVersionCheckRequested] event to enforce app version updates.
603+
Future<void> _onAppVersionCheckRequested(
604+
AppVersionCheckRequested event,
605+
Emitter<AppState> emit,
606+
) async {
607+
final remoteConfig = event.remoteConfig;
608+
final isBackgroundCheck = event.isBackgroundCheck;
609+
610+
if (!remoteConfig.appStatus.isLatestVersionOnly) {
611+
_logger.info(
612+
'[AppBloc] Version enforcement not enabled. Skipping version check.',
613+
);
614+
return;
615+
}
616+
617+
final currentAppVersionString = await _packageInfoService.getAppVersion();
618+
619+
if (currentAppVersionString == null) {
620+
_logger.warning(
621+
'[AppBloc] Could not determine current app version. '
622+
'Skipping version comparison.',
623+
);
624+
// If we can't get the current version, we can't enforce.
625+
// Do not block the app, but log a warning.
626+
return;
627+
}
628+
629+
try {
630+
final currentVersion = Version.parse(currentAppVersionString);
631+
final latestRequiredVersion = Version.parse(
632+
remoteConfig.appStatus.latestAppVersion,
633+
);
634+
635+
if (currentVersion < latestRequiredVersion) {
636+
_logger.info(
637+
'[AppBloc] App version ($currentVersion) is older than '
638+
'required ($latestRequiredVersion). Transitioning to updateRequired state.',
639+
);
640+
emit(state.copyWith(status: AppLifeCycleStatus.updateRequired));
641+
} else {
642+
_logger.info(
643+
'[AppBloc] App version ($currentVersion) is up to date '
644+
'or newer than required ($latestRequiredVersion).',
645+
);
646+
// If the app is up to date, and it was previously in an updateRequired
647+
// state (e.g., after an update), transition it back to a normal state.
648+
if (state.status == AppLifeCycleStatus.updateRequired) {
649+
final finalStatus = state.user!.appRole == AppUserRole.standardUser
650+
? AppLifeCycleStatus.authenticated
651+
: AppLifeCycleStatus.anonymous;
652+
emit(state.copyWith(status: finalStatus));
653+
}
654+
}
655+
emit(state.copyWith(currentAppVersion: currentAppVersionString));
656+
} on FormatException catch (e, s) {
657+
_logger.severe(
658+
'[AppBloc] Failed to parse app version string: $currentAppVersionString '
659+
'or latest required version: ${remoteConfig.appStatus.latestAppVersion}.',
660+
e,
661+
s,
662+
);
663+
if (!isBackgroundCheck) {
664+
emit(
665+
state.copyWith(
666+
status: AppLifeCycleStatus.criticalError,
667+
initialRemoteConfigError: UnknownException(
668+
'Failed to parse app version: ${e.message}',
669+
),
670+
),
671+
);
672+
}
673+
} catch (e, s) {
674+
_logger.severe(
675+
'[AppBloc] Unexpected error during app version check.',
676+
e,
677+
s,
678+
);
679+
if (!isBackgroundCheck) {
680+
emit(
681+
state.copyWith(
682+
status: AppLifeCycleStatus.criticalError,
683+
initialRemoteConfigError: UnknownException(e.toString()),
684+
),
685+
);
686+
}
687+
}
688+
}
689+
601690
/// Handles updating the user's feed decorator status.
602691
Future<void> _onAppUserFeedDecoratorShown(
603692
AppUserFeedDecoratorShown event,

0 commit comments

Comments
 (0)