@@ -11,7 +11,9 @@ import 'package:flutter_news_app_mobile_client_full_source_code/app/config/confi
1111 as local_config;
1212import 'package:flutter_news_app_mobile_client_full_source_code/app/services/demo_data_initializer_service.dart' ;
1313import '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' ;
1415import 'package:logging/logging.dart' ;
16+ import 'package:pub_semver/pub_semver.dart' ;
1517
1618part 'app_event.dart' ;
1719part '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,14 @@ 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+ // Not a background check during startup
273+ isBackgroundCheck : false ,
274+ ),
275+ );
270276
271277 // If we reach here, the app is not under maintenance or requires update.
272278 // Now, handle user-specific data loading.
@@ -547,17 +553,13 @@ class AppBloc extends Bloc<AppEvent, AppState> {
547553 return ;
548554 }
549555
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- }
556+ // Dispatch AppVersionCheckRequested to handle version enforcement.
557+ add (
558+ AppVersionCheckRequested (
559+ remoteConfig: remoteConfig,
560+ isBackgroundCheck: event.isBackgroundCheck,
561+ ),
562+ );
561563
562564 final finalStatus = state.user! .appRole == AppUserRole .standardUser
563565 ? AppLifeCycleStatus .authenticated
@@ -598,6 +600,105 @@ class AppBloc extends Bloc<AppEvent, AppState> {
598600 }
599601 }
600602
603+ /// Handles the [AppVersionCheckRequested] event to enforce app version updates.
604+ Future <void > _onAppVersionCheckRequested (
605+ AppVersionCheckRequested event,
606+ Emitter <AppState > emit,
607+ ) async {
608+ final remoteConfig = event.remoteConfig;
609+ final isBackgroundCheck = event.isBackgroundCheck;
610+
611+ if (! remoteConfig.appStatus.isLatestVersionOnly) {
612+ _logger.info (
613+ '[AppBloc] Version enforcement not enabled. Skipping version check.' ,
614+ );
615+ return ;
616+ }
617+
618+ final currentAppVersionString = await _packageInfoService.getAppVersion ();
619+
620+ if (currentAppVersionString == null ) {
621+ _logger.warning (
622+ '[AppBloc] Could not determine current app version. '
623+ 'Skipping version comparison.' ,
624+ );
625+ // If we can't get the current version, we can't enforce.
626+ // Do not block the app, but log a warning.
627+ return ;
628+ }
629+
630+ try {
631+ final currentVersion = Version .parse (currentAppVersionString);
632+ final latestRequiredVersion = Version .parse (
633+ remoteConfig.appStatus.latestAppVersion,
634+ );
635+
636+ if (currentVersion >= latestRequiredVersion) {
637+ _logger.info (
638+ '[AppBloc] App version ($currentVersion ) is up to date '
639+ 'or newer than required ($latestRequiredVersion ).' ,
640+ );
641+ // If the app is up to date, and it was previously in an updateRequired
642+ // state (e.g., after an update), transition it back to a normal state.
643+ if (state.status == AppLifeCycleStatus .updateRequired) {
644+ final finalStatus = state.user! .appRole == AppUserRole .standardUser
645+ ? AppLifeCycleStatus .authenticated
646+ : AppLifeCycleStatus .anonymous;
647+ emit (
648+ state.copyWith (
649+ status: finalStatus,
650+ currentAppVersion: currentAppVersionString,
651+ ),
652+ );
653+ } else {
654+ emit (state.copyWith (currentAppVersion: currentAppVersionString));
655+ }
656+ } else {
657+ _logger.info (
658+ '[AppBloc] App version ($currentVersion ) is older than '
659+ 'required ($latestRequiredVersion ). Transitioning to updateRequired state.' ,
660+ );
661+ emit (
662+ state.copyWith (
663+ status: AppLifeCycleStatus .updateRequired,
664+ currentAppVersion: currentAppVersionString,
665+ ),
666+ );
667+ }
668+ } on FormatException catch (e, s) {
669+ _logger.severe (
670+ '[AppBloc] Failed to parse app version string: $currentAppVersionString '
671+ 'or latest required version: ${remoteConfig .appStatus .latestAppVersion }.' ,
672+ e,
673+ s,
674+ );
675+ if (! isBackgroundCheck) {
676+ emit (
677+ state.copyWith (
678+ status: AppLifeCycleStatus .criticalError,
679+ initialRemoteConfigError: UnknownException (
680+ 'Failed to parse app version: ${e .message }' ,
681+ ),
682+ ),
683+ );
684+ }
685+ } catch (e, s) {
686+ _logger.severe (
687+ '[AppBloc] Unexpected error during app version check.' ,
688+ e,
689+ s,
690+ );
691+ if (! isBackgroundCheck) {
692+ emit (
693+ state.copyWith (
694+ status: AppLifeCycleStatus .criticalError,
695+ initialRemoteConfigError: UnknownException (e.toString ()),
696+ ),
697+ );
698+ }
699+ }
700+ }
701+
601702 /// Handles updating the user's feed decorator status.
602703 Future <void > _onAppUserFeedDecoratorShown (
603704 AppUserFeedDecoratorShown event,
0 commit comments