@@ -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,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