Skip to content

Commit 855593d

Browse files
fix(backup): align failure handling and localization
1 parent ad4338c commit 855593d

23 files changed

Lines changed: 588 additions & 197 deletions

FEATURES.md

Lines changed: 1 addition & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -60,54 +60,7 @@ graph TB
6060
BACKUPS --> TOR
6161
BACKUPS --> WALLETS
6262
BTC_PRICE --> SETTINGS
63-
BTCPAY --> BIP85_REGISTRY
64-
BTCPAY --> DETERMINISTIC_WALLETS
65-
BTCPAY --> KEYCHAIN_MANIFEST
66-
BTCPAY --> GET_PAID_SETTINGS
67-
BTCPAY --> WALLETS
68-
DETERMINISTIC_WALLETS --> BIP85
69-
KEYCHAIN_MANIFEST --> BIP85_REGISTRY
70-
KEYCHAIN_MANIFEST --> NOSTR_IDENTITY
71-
KEYCHAIN_RECOVERY --> BIP85_REGISTRY
72-
KEYCHAIN_RECOVERY --> DETERMINISTIC_WALLETS
73-
KEYCHAIN_RECOVERY --> KEYCHAIN_MANIFEST
7463
ONBOARDING --> BACKUPS
75-
NOSTR_IDENTITY --> BIP85_REGISTRY
76-
LIGHTNING_ADDRESS --> BIP85_REGISTRY
77-
LIGHTNING_ADDRESS --> BULLNYM
78-
LIGHTNING_ADDRESS --> DETERMINISTIC_WALLETS
79-
LIGHTNING_ADDRESS --> KEYCHAIN_MANIFEST
80-
LIGHTNING_ADDRESS --> NOSTR_IDENTITY
81-
LIGHTNING_ADDRESS --> GET_PAID_SETTINGS
82-
PAYMENT_PAGE --> BULLNYM
83-
PAYMENT_PAGE --> LIGHTNING_ADDRESS
84-
PAYMENT_PAGE --> DETERMINISTIC_WALLETS
85-
PAYMENT_PAGE --> KEYCHAIN_MANIFEST
86-
PAYMENT_PAGE --> BIP85_REGISTRY
87-
PAYMENT_PAGE --> GET_PAID_SETTINGS
88-
PAYMENT_PAGE --> NOSTR_IDENTITY
89-
POS --> BULLNYM
90-
POS --> LIGHTNING_ADDRESS
91-
POS --> DETERMINISTIC_WALLETS
92-
POS --> KEYCHAIN_MANIFEST
93-
POS --> BIP85_REGISTRY
94-
POS --> GET_PAID_SETTINGS
95-
POS --> NOSTR_IDENTITY
96-
INVOICES --> BULLNYM
97-
INVOICES --> NOSTR_IDENTITY
98-
INVOICES --> WALLETS
99-
INVOICES --> LABELS
100-
GET_PAID_SETTINGS --> KEYCHAIN_MANIFEST
101-
GET_PAID_SETTINGS --> NOSTR_RELAY_POLICY
102-
GET_PAID_SETTINGS --> CORE
103-
REMOTE_KEYCHAIN_RECOVERY --> KEYCHAIN_MANIFEST
104-
REMOTE_KEYCHAIN_RECOVERY --> KEYCHAIN_RECOVERY
105-
REMOTE_KEYCHAIN_RECOVERY --> NOSTR_RELAY_POLICY
106-
REMOTE_KEYCHAIN_RECOVERY --> LIGHTNING_ADDRESS
107-
REMOTE_KEYCHAIN_RECOVERY --> PAYMENT_PAGE
108-
REMOTE_KEYCHAIN_RECOVERY --> POS
109-
REMOTE_KEYCHAIN_RECOVERY --> GET_PAID_SETTINGS
110-
REMOTE_KEYCHAIN_RECOVERY --> CORE
11164
BUY --> EXCHANGE
11265
BUY --> RECEIVE
11366
COINS --> UTXO_MGMT
@@ -154,8 +107,7 @@ graph TB
154107
classDef featureStyle fill:#1a202c,stroke:#2d3748,stroke-width:2px,color:#e2e8f0
155108
156109
class CORE coreStyle
157-
class SETTINGS,TOR,PIN_CODE,LABELS,SECRETS,HW_WALLETS,BTC_PRICE,NETWORK,BIP85,FEES,WALLETS,EXCHANGE,APP_STARTUP,UTXO_MGMT,ADDRESS_MGMT,RECIPIENTS,FUNDING,BACKUPS,SWAPS,PAYJOIN,WITHDRAWAL,STATUS,SEND,RECEIVE,TRANSFER,TX_HISTORY,BG_TASKS,AUTOSWAPS,DCA,SELL,PAY,BUY,COINS featureStyle
158-
class SETTINGS,TOR,PIN_CODE,LABELS,SECRETS,HW_WALLETS,BTC_PRICE,BTCPAY,BIP85_REGISTRY,DETERMINISTIC_WALLETS,KEYCHAIN_MANIFEST,KEYCHAIN_RECOVERY,NOSTR_IDENTITY,NOSTR_RELAY_POLICY,BULLNYM,LIGHTNING_ADDRESS,PAYMENT_PAGE,POS,INVOICES,GET_PAID_SETTINGS,REMOTE_KEYCHAIN_RECOVERY,NETWORK,BIP85,FEES,WALLETS,EXCHANGE,APP_STARTUP,ONBOARDING,UTXO_MGMT,ADDRESS_MGMT,RECIPIENTS,FUNDING,BACKUPS,SWAPS,PAYJOIN,WITHDRAWAL,STATUS,SEND,RECEIVE,TRANSFER,TX_HISTORY,BG_TASKS,AUTOSWAPS,AUTOSWEEP,DCA,SELL,PAY,BUY,COINS featureStyle
110+
class SETTINGS,TOR,PIN_CODE,LABELS,SECRETS,HW_WALLETS,BTC_PRICE,NETWORK,BIP85,FEES,WALLETS,EXCHANGE,APP_STARTUP,ONBOARDING,UTXO_MGMT,ADDRESS_MGMT,RECIPIENTS,FUNDING,BACKUPS,SWAPS,PAYJOIN,WITHDRAWAL,STATUS,SEND,RECEIVE,TRANSFER,TX_HISTORY,BG_TASKS,AUTOSWAPS,DCA,SELL,PAY,BUY,COINS featureStyle
159111
```
160112

161113
## About Package Dependency Diagrams

docs/backup-health-reminders.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ Reminder state is stored locally in a versioned SharedPreferences record keyed b
7070
- a pending action start time; and
7171
- the pending action balance tier.
7272

73-
No mnemonic, seed, private key, vault key, or other secret is stored or logged by the reminder. Malformed or unsupported reminder records are ignored and replaced with an empty in-memory record, which favors showing another reminder over suppressing one indefinitely.
73+
No mnemonic, seed, private key, vault key, or other secret is stored or logged by the reminder. Malformed, unsupported, or unreadable reminder records are replaced with an empty in-memory record, which favors showing another reminder over suppressing one indefinitely. The underlying read failure is logged without exposing it to the user.
74+
75+
If a reminder action cannot be persisted, the overlay stays visible so the user can retry. A localized "close for now" action is then available as a session-only escape; it does not acknowledge the reminder, so the app evaluates it again on the next launch.
7476

7577
## Architecture
7678

lib/features/backup_settings/data/shared_preferences_backup_health_reminder_repository.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ class SharedPreferencesBackupHealthReminderRepository
5555
error: e,
5656
trace: st,
5757
);
58-
return Err(BackupSettingsPersistenceFailure(e.toString()));
58+
return Ok(
59+
BackupHealthReminderRecord(masterFingerprint: masterFingerprint),
60+
);
5961
}
6062
}
6163

lib/features/backup_settings/presentation/cubit/backup_health_reminder_cubit.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,16 @@ class BackupHealthReminderCubit extends Cubit<BackupHealthReminderState> {
105105
return false;
106106
}
107107
}
108+
109+
void dismissFailureForSession() {
110+
final current = state;
111+
if (current is! BackupHealthReminderVisible ||
112+
current.failure == null ||
113+
current.isSaving) {
114+
return;
115+
}
116+
117+
_sessionSuppressed = true;
118+
emit(const BackupHealthReminderHidden());
119+
}
108120
}

lib/features/backup_settings/ui/widgets/backup_health_reminder_overlay.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,17 @@ class _BackupHealthReminderBlocker extends StatelessWidget {
214214
),
215215
),
216216
],
217+
if (state.failure != null) ...[
218+
const Gap(8),
219+
TextButton(
220+
onPressed: state.isSaving
221+
? null
222+
: () => context
223+
.read<BackupHealthReminderCubit>()
224+
.dismissFailureForSession(),
225+
child: Text(context.loc.backupHealthCloseForNow),
226+
),
227+
],
217228
],
218229
),
219230
),

lib/features/onboarding/domain/onboarding_failure.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,8 @@ sealed class OnboardingFailure extends Failure {
77
final class OnboardingUnexpectedFailure extends OnboardingFailure {
88
const OnboardingUnexpectedFailure([super.logMessage]);
99
}
10+
11+
final class OnboardingBackupVerificationPersistenceFailure
12+
extends OnboardingFailure {
13+
const OnboardingBackupVerificationPersistenceFailure([super.logMessage]);
14+
}

lib/features/onboarding/domain/complete_physical_backup_verification_usecase.dart renamed to lib/features/onboarding/domain/usecases/complete_physical_backup_verification_usecase.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ class CompletePhysicalBackupVerificationUsecase {
1818
case Ok():
1919
return const Ok(null);
2020
case Err(:final failure):
21-
return Err(OnboardingUnexpectedFailure(failure.logMessage));
21+
return Err(
22+
OnboardingBackupVerificationPersistenceFailure(failure.logMessage),
23+
);
2224
}
2325
}
2426
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import 'package:bb_mobile/core/utils/logger.dart';
2+
import 'package:bb_mobile/core/utils/result.dart';
3+
import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';
4+
import 'package:bb_mobile/core/wallet/domain/usecases/create_default_wallets_usecase.dart';
5+
import 'package:bb_mobile/features/onboarding/domain/onboarding_failure.dart';
6+
import 'package:meta/meta.dart';
7+
8+
class CreateOnboardingWalletsUsecase {
9+
final CreateDefaultWalletsUsecase _createDefaultWalletsUsecase;
10+
11+
CreateOnboardingWalletsUsecase(this._createDefaultWalletsUsecase);
12+
13+
@useResult
14+
Future<Result<List<Wallet>, OnboardingFailure>> execute({
15+
List<String>? mnemonicWords,
16+
}) async {
17+
try {
18+
final wallets = mnemonicWords == null
19+
? await _createDefaultWalletsUsecase.execute()
20+
: await _createDefaultWalletsUsecase.execute(
21+
mnemonicWords: mnemonicWords,
22+
);
23+
if (wallets.isEmpty) {
24+
const failure = OnboardingUnexpectedFailure(
25+
'No wallets were created or restored',
26+
);
27+
log.severe(
28+
message: failure.logMessage,
29+
error: failure,
30+
trace: StackTrace.current,
31+
);
32+
return const Err(failure);
33+
}
34+
return Ok(wallets);
35+
} on CreateDefaultWalletsException catch (error, trace) {
36+
log.severe(
37+
message: 'createOnboardingWallets failed',
38+
error: error,
39+
trace: trace,
40+
);
41+
return Err(OnboardingUnexpectedFailure(error.toString()));
42+
}
43+
}
44+
}

lib/features/onboarding/onboarding_locator.dart

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:bb_mobile/core/wallet/domain/usecases/create_default_wallets_usecase.dart';
2-
import 'package:bb_mobile/features/onboarding/domain/complete_physical_backup_verification_usecase.dart';
2+
import 'package:bb_mobile/features/onboarding/domain/usecases/complete_physical_backup_verification_usecase.dart';
3+
import 'package:bb_mobile/features/onboarding/domain/usecases/create_onboarding_wallets_usecase.dart';
34
import 'package:bb_mobile/features/onboarding/presentation/bloc/onboarding_bloc.dart';
45
import 'package:bb_mobile/features/test_wallet_backup/public/test_wallet_backup_facade.dart';
56
import 'package:get_it/get_it.dart';
@@ -9,7 +10,8 @@ class OnboardingLocator {
910
// Blocs
1011
locator.registerFactory<OnboardingBloc>(
1112
() => OnboardingBloc(
12-
createDefaultWalletsUsecase: locator<CreateDefaultWalletsUsecase>(),
13+
createOnboardingWalletsUsecase:
14+
locator<CreateOnboardingWalletsUsecase>(),
1315
completePhysicalBackupVerificationUsecase:
1416
locator<CompletePhysicalBackupVerificationUsecase>(),
1517
),
@@ -21,5 +23,10 @@ class OnboardingLocator {
2123
locator<TestWalletBackupFacade>(),
2224
),
2325
);
26+
locator.registerFactory<CreateOnboardingWalletsUsecase>(
27+
() => CreateOnboardingWalletsUsecase(
28+
locator<CreateDefaultWalletsUsecase>(),
29+
),
30+
);
2431
}
2532
}
Lines changed: 50 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import 'dart:async';
22

3-
import 'package:bb_mobile/core/utils/logger.dart';
43
import 'package:bb_mobile/core/utils/result.dart';
5-
import 'package:bb_mobile/core/wallet/domain/usecases/create_default_wallets_usecase.dart';
6-
import 'package:bb_mobile/features/onboarding/domain/complete_physical_backup_verification_usecase.dart';
74
import 'package:bb_mobile/features/onboarding/domain/onboarding_failure.dart';
5+
import 'package:bb_mobile/features/onboarding/domain/usecases/complete_physical_backup_verification_usecase.dart';
6+
import 'package:bb_mobile/features/onboarding/domain/usecases/create_onboarding_wallets_usecase.dart';
87
import 'package:bip39_mnemonic/bip39_mnemonic.dart' as bip39;
98
import 'package:flutter_bloc/flutter_bloc.dart';
109
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -15,7 +14,7 @@ part 'onboarding_state.dart';
1514

1615
class OnboardingBloc extends Bloc<OnboardingEvent, OnboardingState> {
1716
OnboardingBloc({
18-
required this._createDefaultWalletsUsecase,
17+
required this._createOnboardingWalletsUsecase,
1918
required this._completePhysicalBackupVerificationUsecase,
2019
}) : super(const OnboardingState()) {
2120
on<OnboardingCreateNewWallet>(_onCreateNewWallet);
@@ -26,18 +25,17 @@ class OnboardingBloc extends Bloc<OnboardingEvent, OnboardingState> {
2625
});
2726
}
2827

29-
final CreateDefaultWalletsUsecase _createDefaultWalletsUsecase;
28+
final CreateOnboardingWalletsUsecase _createOnboardingWalletsUsecase;
3029

3130
final CompletePhysicalBackupVerificationUsecase
3231
_completePhysicalBackupVerificationUsecase;
3332

34-
void _handleError(Exception error, Emitter<OnboardingState> emit) {
35-
log.severe(error: error, trace: StackTrace.current);
33+
void _emitFailure(OnboardingFailure failure, Emitter<OnboardingState> emit) {
3634
emit(
3735
state.copyWith(
3836
onboardingStepStatus: OnboardingStepStatus.none,
3937
step: OnboardingStep.splash,
40-
failure: OnboardingUnexpectedFailure(error.toString()),
38+
failure: failure,
4139
),
4240
);
4341
}
@@ -50,18 +48,20 @@ class OnboardingBloc extends Bloc<OnboardingEvent, OnboardingState> {
5048
// dequeues, the 1st emit has already flipped the status to loading,
5149
// so this guard drops the duplicate (#2015).
5250
if (state.onboardingStepStatus == OnboardingStepStatus.loading) return;
53-
try {
54-
emit(
55-
state.copyWith(
56-
onboardingStepStatus: OnboardingStepStatus.loading,
57-
step: OnboardingStep.create,
58-
failure: null,
59-
),
60-
);
61-
await _createDefaultWalletsUsecase.execute();
62-
emit(state.copyWith(onboardingStepStatus: OnboardingStepStatus.success));
63-
} on Exception catch (e) {
64-
_handleError(e, emit);
51+
emit(
52+
state.copyWith(
53+
onboardingStepStatus: OnboardingStepStatus.loading,
54+
step: OnboardingStep.create,
55+
failure: null,
56+
),
57+
);
58+
switch (await _createOnboardingWalletsUsecase.execute()) {
59+
case Ok():
60+
emit(
61+
state.copyWith(onboardingStepStatus: OnboardingStepStatus.success),
62+
);
63+
case Err(:final failure):
64+
_emitFailure(failure, emit);
6565
}
6666
}
6767

@@ -71,41 +71,36 @@ class OnboardingBloc extends Bloc<OnboardingEvent, OnboardingState> {
7171
) async {
7272
// Same serialized-event guard as `_onCreateNewWallet` (#2015).
7373
if (state.onboardingStepStatus == OnboardingStepStatus.loading) return;
74-
try {
75-
emit(
76-
state.copyWith(
77-
onboardingStepStatus: OnboardingStepStatus.loading,
78-
step: OnboardingStep.recover,
79-
failure: null,
80-
),
81-
);
82-
final restoredWallets = await _createDefaultWalletsUsecase.execute(
83-
mnemonicWords: event.mnemonic.words,
84-
);
85-
if (restoredWallets.isEmpty) {
86-
_handleError(Exception('No wallets were restored'), emit);
87-
return;
88-
}
89-
final completed = await _completePhysicalBackupVerificationUsecase
90-
.execute(masterFingerprint: restoredWallets.first.masterFingerprint);
91-
if (completed case Err(:final failure)) {
92-
log.severe(
93-
message: failure.logMessage,
94-
error: failure,
95-
trace: StackTrace.current,
96-
);
97-
emit(
98-
state.copyWith(
99-
onboardingStepStatus: OnboardingStepStatus.none,
100-
step: OnboardingStep.splash,
101-
failure: failure,
102-
),
103-
);
104-
return;
105-
}
106-
emit(state.copyWith(onboardingStepStatus: OnboardingStepStatus.success));
107-
} on Exception catch (e) {
108-
_handleError(e, emit);
74+
emit(
75+
state.copyWith(
76+
onboardingStepStatus: OnboardingStepStatus.loading,
77+
step: OnboardingStep.recover,
78+
failure: null,
79+
),
80+
);
81+
switch (await _createOnboardingWalletsUsecase.execute(
82+
mnemonicWords: event.mnemonic.words,
83+
)) {
84+
case Err(:final failure):
85+
_emitFailure(failure, emit);
86+
case Ok(:final value):
87+
final completed = await _completePhysicalBackupVerificationUsecase
88+
.execute(masterFingerprint: value.first.masterFingerprint);
89+
switch (completed) {
90+
case Ok():
91+
emit(
92+
state.copyWith(
93+
onboardingStepStatus: OnboardingStepStatus.success,
94+
),
95+
);
96+
case Err(:final failure):
97+
emit(
98+
state.copyWith(
99+
onboardingStepStatus: OnboardingStepStatus.success,
100+
failure: failure,
101+
),
102+
);
103+
}
109104
}
110105
}
111106
}

0 commit comments

Comments
 (0)