@@ -3,6 +3,7 @@ import 'dart:async';
33import 'package:bb_mobile/core/storage/data/datasources/key_value_storage/keychain_locked_exception.dart' ;
44import 'package:bb_mobile/core/utils/logger.dart' ;
55import 'package:bb_mobile/core/utils/result.dart' ;
6+ import 'package:bb_mobile/core/utils/startup_phase_tracker.dart' ;
67import 'package:bb_mobile/features/app_startup/domain/usecases/check_for_existing_default_wallets_usecase.dart' ;
78import 'package:bb_mobile/features/app_startup/domain/usecases/check_legacy_install_usecase.dart' ;
89import 'package:bb_mobile/features/app_startup/domain/usecases/initialize_required_tor_usecase.dart' ;
@@ -30,7 +31,9 @@ class AppStartupBloc extends Bloc<AppStartupEvent, AppStartupState>
3031 required this ._checkLegacyInstallUsecase,
3132 required this ._checkBackupUsecase,
3233 required this ._initializeRequiredTorUsecase,
34+ StartupPhaseTracker ? startupPhaseTracker,
3335 }) : super (const AppStartupState .initial ()) {
36+ _startupPhaseTracker = startupPhaseTracker ?? StartupPhaseTracker ();
3437 on < AppStartupStarted > (_onAppStartupStarted);
3538 WidgetsBinding .instance.addObserver (this );
3639 }
@@ -42,6 +45,7 @@ class AppStartupBloc extends Bloc<AppStartupEvent, AppStartupState>
4245 final CheckLegacyInstallUsecase _checkLegacyInstallUsecase;
4346 final CheckBackupUsecase _checkBackupUsecase;
4447 final InitializeRequiredTorUsecase _initializeRequiredTorUsecase;
48+ late final StartupPhaseTracker _startupPhaseTracker;
4549
4650 /// True while we're sitting on the splash because a startup step
4751 /// threw `KeychainLockedException` (iOS pre-first-unlock pre-warm).
@@ -69,28 +73,37 @@ class AppStartupBloc extends Bloc<AppStartupEvent, AppStartupState>
6973 Emitter <AppStartupState > emit,
7074 ) async {
7175 emit (const AppStartupState .loadingInProgress ());
76+ _startupPhaseTracker.mark (StartupPhase .loading);
7277
7378 try {
7479 // Log app version on startup
7580 final packageInfo = await PackageInfo .fromPlatform ();
7681 log.info (
7782 'App started: ${packageInfo .appName } v${packageInfo .version }+${packageInfo .buildNumber }' ,
7883 );
84+ _startupPhaseTracker.mark (StartupPhase .packageLoaded);
7985
8086 final doDefaultWalletsExist = await _checkForExistingDefaultWalletsUsecase
8187 .execute ();
88+ _startupPhaseTracker.mark (StartupPhase .walletsChecked);
8289
8390 // Pre-v5 ("BULL") installs are no longer migrated: gate them behind a
8491 // backup screen. Only when the new DB is empty — the legacy marker can
8592 // survive a failed migration while the user has since set up working
8693 // v5+ wallets, and those current seeds are not legacy-format: gating
8794 // such an install would show a backup screen missing its live wallets
8895 // and instruct deleting them.
89- if (! doDefaultWalletsExist &&
90- await _checkLegacyInstallUsecase.execute ()) {
91- log.warning ('Legacy (pre-v5) install detected — backup gate shown' );
92- emit (const AppStartupState .legacyBackupRequired ());
93- return ;
96+ if (! doDefaultWalletsExist) {
97+ final isLegacyInstall = await _checkLegacyInstallUsecase.execute ();
98+ _startupPhaseTracker.mark (StartupPhase .legacyResolved);
99+ if (isLegacyInstall) {
100+ log.warning ('Legacy (pre-v5) install detected — backup gate shown' );
101+ emit (const AppStartupState .legacyBackupRequired ());
102+ _startupPhaseTracker.mark (StartupPhase .terminalResolved);
103+ return ;
104+ }
105+ } else {
106+ _startupPhaseTracker.mark (StartupPhase .legacyResolved);
94107 }
95108
96109 bool isPinCodeSet = false ;
@@ -99,6 +112,7 @@ class AppStartupBloc extends Bloc<AppStartupEvent, AppStartupState>
99112 switch (await _checkPinCodeExistsUsecase.execute ()) {
100113 case Ok (: final value):
101114 isPinCodeSet = value;
115+ _startupPhaseTracker.mark (StartupPhase .credentialsResolved);
102116 case Err (failure: AppUnlockKeychainLockedFailure ()):
103117 _waitForKeychainUnlock ();
104118 return ;
@@ -111,18 +125,21 @@ class AppStartupBloc extends Bloc<AppStartupEvent, AppStartupState>
111125 // there from a previous install.
112126 // (e.g. secure storage data on iOS like the pin code)
113127 await _resetAppDataUsecase.execute ();
128+ _startupPhaseTracker.mark (StartupPhase .credentialsResolved);
114129 }
115130
116131 // Warm the embedded client without delaying the startup screen. The
117132 // coordinator makes this single-flight with any concurrent consumer.
118133 unawaited (_initializeTorInBackground ());
119134
135+ _startupPhaseTracker.mark (StartupPhase .torCheckCompleted);
120136 emit (
121137 AppStartupState .success (
122138 isPinCodeSet: isPinCodeSet,
123139 hasDefaultWallets: doDefaultWalletsExist,
124140 ),
125141 );
142+ _startupPhaseTracker.mark (StartupPhase .terminalResolved);
126143 } on KeychainLockedException catch (_) {
127144 // iOS pre-first-unlock pre-warm: the keychain is locked, so any
128145 // seed read (CheckForExistingDefaultWalletsUsecase →
0 commit comments