@@ -11,7 +11,9 @@ import 'package:flutter_news_app_mobile_client_full_source_code/app/config/confi
11
11
as local_config;
12
12
import 'package:flutter_news_app_mobile_client_full_source_code/app/services/demo_data_initializer_service.dart' ;
13
13
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' ;
14
15
import 'package:logging/logging.dart' ;
16
+ import 'package:pub_semver/pub_semver.dart' ;
15
17
16
18
part 'app_event.dart' ;
17
19
part 'app_state.dart' ;
@@ -41,6 +43,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
41
43
required GlobalKey <NavigatorState > navigatorKey,
42
44
required RemoteConfig ? initialRemoteConfig,
43
45
required HttpException ? initialRemoteConfigError,
46
+ required PackageInfoService packageInfoService,
44
47
this .demoDataMigrationService,
45
48
this .demoDataInitializerService,
46
49
this .initialUser,
@@ -51,6 +54,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
51
54
_userRepository = userRepository,
52
55
_environment = environment,
53
56
_navigatorKey = navigatorKey,
57
+ _packageInfoService = packageInfoService,
54
58
_logger = Logger ('AppBloc' ),
55
59
super (
56
60
AppState (
@@ -71,6 +75,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
71
75
on < AppUserContentPreferencesRefreshed > (_onUserContentPreferencesRefreshed);
72
76
on < AppSettingsChanged > (_onAppSettingsChanged);
73
77
on < AppPeriodicConfigFetchRequested > (_onAppPeriodicConfigFetchRequested);
78
+ on < AppVersionCheckRequested > (_onAppVersionCheckRequested);
74
79
on < AppUserFeedDecoratorShown > (_onAppUserFeedDecoratorShown);
75
80
on < AppUserContentPreferencesChanged > (_onAppUserContentPreferencesChanged);
76
81
on < AppLogoutRequested > (_onLogoutRequested);
@@ -91,6 +96,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
91
96
final DataRepository <User > _userRepository;
92
97
final local_config.AppEnvironment _environment;
93
98
final GlobalKey <NavigatorState > _navigatorKey;
99
+ final PackageInfoService _packageInfoService;
94
100
final Logger _logger;
95
101
final DemoDataMigrationService ? demoDataMigrationService;
96
102
final DemoDataInitializerService ? demoDataInitializerService;
@@ -259,14 +265,14 @@ class AppBloc extends Bloc<AppEvent, AppState> {
259
265
return ;
260
266
}
261
267
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
+ );
270
276
271
277
// If we reach here, the app is not under maintenance or requires update.
272
278
// Now, handle user-specific data loading.
@@ -547,17 +553,13 @@ class AppBloc extends Bloc<AppEvent, AppState> {
547
553
return ;
548
554
}
549
555
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
+ );
561
563
562
564
final finalStatus = state.user! .appRole == AppUserRole .standardUser
563
565
? AppLifeCycleStatus .authenticated
@@ -598,6 +600,105 @@ class AppBloc extends Bloc<AppEvent, AppState> {
598
600
}
599
601
}
600
602
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
+
601
702
/// Handles updating the user's feed decorator status.
602
703
Future <void > _onAppUserFeedDecoratorShown (
603
704
AppUserFeedDecoratorShown event,
0 commit comments