Skip to content

Commit fe197c0

Browse files
authored
Merge pull request #2346 from SatoshiPortal/refactor-import-mnemonic-errors
refactor(import_mnemonic): sanitize user-facing error messages
2 parents b609c95 + dbdf37a commit fe197c0

39 files changed

Lines changed: 551 additions & 90 deletions
Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,31 @@
11
import 'package:bb_mobile/core/seed/data/repository/seed_repository.dart';
2-
import 'package:bb_mobile/features/import_mnemonic/errors.dart';
2+
import 'package:bb_mobile/core/utils/logger.dart';
3+
import 'package:bb_mobile/core/utils/result.dart';
4+
import 'package:bb_mobile/features/import_mnemonic/domain/import_mnemonic_failure.dart';
5+
import 'package:meta/meta.dart';
36

47
class CheckDuplicateMnemonicUsecase {
58
final SeedRepository _seedRepository;
69

710
CheckDuplicateMnemonicUsecase({required this._seedRepository});
811

9-
Future<void> execute({
12+
@useResult
13+
Future<Result<void, ImportMnemonicFailure>> execute({
1014
required List<String> mnemonicWords,
1115
String passphrase = '',
1216
}) async {
13-
final fingerprint = _seedRepository.fingerprintFor(
14-
mnemonicWords: mnemonicWords,
15-
passphrase: passphrase,
16-
);
17-
if (await _seedRepository.exists(fingerprint)) {
18-
throw DuplicateMnemonicException();
17+
try {
18+
final fingerprint = _seedRepository.fingerprintFor(
19+
mnemonicWords: mnemonicWords,
20+
passphrase: passphrase,
21+
);
22+
if (await _seedRepository.exists(fingerprint)) {
23+
return const Err(ImportMnemonicDuplicateFailure());
24+
}
25+
return const Ok(null);
26+
} catch (e, st) {
27+
log.severe(message: 'Duplicate mnemonic check failed', error: e, trace: st);
28+
return Err(ImportMnemonicUnexpectedFailure(e.toString()));
1929
}
2030
}
2131
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import 'package:bb_mobile/core/failures/failure.dart';
2+
3+
sealed class ImportMnemonicFailure extends Failure {
4+
const ImportMnemonicFailure([super.logMessage]);
5+
}
6+
7+
final class ImportMnemonicDuplicateFailure extends ImportMnemonicFailure {
8+
const ImportMnemonicDuplicateFailure();
9+
}
10+
11+
final class ImportMnemonicEmptyLabelFailure extends ImportMnemonicFailure {
12+
const ImportMnemonicEmptyLabelFailure();
13+
}
14+
15+
/// Guard: [ImportMnemonicCubit.import] called before mnemonic was set.
16+
final class ImportMnemonicNullMnemonicFailure extends ImportMnemonicFailure {
17+
const ImportMnemonicNullMnemonicFailure();
18+
}
19+
20+
/// Catch-all. [logMessage] is for logs/Sentry ONLY and MUST never reach the UI.
21+
final class ImportMnemonicUnexpectedFailure extends ImportMnemonicFailure {
22+
const ImportMnemonicUnexpectedFailure([super.logMessage]);
23+
}

lib/features/import_mnemonic/domain/import_wallet_usecase.dart

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import 'package:bb_mobile/core/seed/data/repository/seed_repository.dart';
22
import 'package:bb_mobile/core/settings/data/settings_repository.dart';
33
import 'package:bb_mobile/core/utils/logger.dart';
4+
import 'package:bb_mobile/core/utils/result.dart';
45
import 'package:bb_mobile/core/wallet/data/repositories/wallet_repository.dart';
56
import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';
67
import 'package:bb_mobile/features/import_mnemonic/domain/check_duplicate_mnemonic_usecase.dart';
7-
import 'package:bb_mobile/features/import_mnemonic/errors.dart';
8+
import 'package:bb_mobile/features/import_mnemonic/domain/import_mnemonic_failure.dart';
9+
import 'package:meta/meta.dart';
810

911
class ImportWalletUsecase {
1012
final CheckDuplicateMnemonicUsecase _checkDuplicateMnemonicUsecase;
@@ -19,18 +21,24 @@ class ImportWalletUsecase {
1921
required WalletRepository walletRepository,
2022
}) : _wallet = walletRepository;
2123

22-
Future<Wallet> execute({
24+
@useResult
25+
Future<Result<Wallet, ImportMnemonicFailure>> execute({
2326
required List<String> mnemonicWords,
2427
ScriptType scriptType = ScriptType.bip84,
2528
String passphrase = '',
2629
String? label,
2730
}) async {
28-
try {
29-
await _checkDuplicateMnemonicUsecase.execute(
30-
mnemonicWords: mnemonicWords,
31-
passphrase: passphrase,
32-
);
31+
switch (await _checkDuplicateMnemonicUsecase.execute(
32+
mnemonicWords: mnemonicWords,
33+
passphrase: passphrase,
34+
)) {
35+
case Err(:final failure):
36+
return Err(failure);
37+
case Ok():
38+
break;
39+
}
3340

41+
try {
3442
final settings = await _settingsRepository.fetch();
3543
final environment = settings.environment;
3644
final bitcoinNetwork = environment.isMainnet
@@ -42,7 +50,7 @@ class ImportWalletUsecase {
4250
passphrase: passphrase,
4351
);
4452

45-
final wallet = _wallet.createWallet(
53+
final wallet = await _wallet.createWallet(
4654
seed: seed,
4755
network: bitcoinNetwork,
4856
scriptType: scriptType,
@@ -53,10 +61,10 @@ class ImportWalletUsecase {
5361

5462
log.fine('Wallet imported');
5563

56-
return wallet;
57-
} catch (e) {
58-
if (e is DuplicateMnemonicException) rethrow;
59-
throw ImportWalletException(e.toString());
64+
return Ok(wallet);
65+
} catch (e, st) {
66+
log.severe(message: 'Import wallet failed', error: e, trace: st);
67+
return Err(ImportMnemonicUnexpectedFailure(e.toString()));
6068
}
6169
}
6270
}

lib/features/import_mnemonic/errors.dart

Lines changed: 0 additions & 21 deletions
This file was deleted.

lib/features/import_mnemonic/presentation/cubit.dart

Lines changed: 34 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';
1+
import 'package:bb_mobile/core/utils/result.dart';
22
import 'package:bb_mobile/core/wallet/domain/usecases/check_wallet_status_usecase.dart';
3+
import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';
34
import 'package:bb_mobile/features/import_mnemonic/domain/check_duplicate_mnemonic_usecase.dart';
5+
import 'package:bb_mobile/features/import_mnemonic/domain/import_mnemonic_failure.dart';
46
import 'package:bb_mobile/features/import_mnemonic/domain/import_wallet_usecase.dart';
5-
import 'package:bb_mobile/features/import_mnemonic/errors.dart';
67
import 'package:bb_mobile/features/import_mnemonic/presentation/state.dart';
78
import 'package:bip39_mnemonic/bip39_mnemonic.dart' as bip39;
89
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -18,30 +19,27 @@ class ImportMnemonicCubit extends Cubit<ImportMnemonicState> {
1819
required this._checkDuplicateMnemonicUsecase,
1920
}) : super(const ImportMnemonicState());
2021

21-
void clearError() => emit(state.copyWith(error: null));
22+
void clearFailure() => emit(state.copyWith(failure: null));
2223

2324
void reset() => emit(const ImportMnemonicState());
2425

2526
Future<void> updateMnemonic(Mnemonic mnemonic) async {
26-
try {
27-
if (mnemonic.label.isEmpty) throw EmptyMnemonicLabelError();
28-
29-
emit(state.copyWith(isLoading: true, error: null));
27+
if (mnemonic.label.isEmpty) {
28+
emit(state.copyWith(failure: const ImportMnemonicEmptyLabelFailure()));
29+
return;
30+
}
3031

31-
await _checkDuplicateMnemonicUsecase.execute(
32-
mnemonicWords: mnemonic.words,
33-
passphrase: mnemonic.passphrase,
34-
);
32+
emit(state.copyWith(isLoading: true, failure: null));
3533

36-
emit(state.copyWith(mnemonic: mnemonic, isLoading: false));
37-
_scanAllScriptTypes(mnemonic);
38-
} catch (e) {
39-
emit(
40-
state.copyWith(
41-
error: e is Exception ? e : ImportMnemonicError(e.toString()),
42-
isLoading: false,
43-
),
44-
);
34+
switch (await _checkDuplicateMnemonicUsecase.execute(
35+
mnemonicWords: mnemonic.words,
36+
passphrase: mnemonic.passphrase,
37+
)) {
38+
case Ok():
39+
emit(state.copyWith(mnemonic: mnemonic, isLoading: false));
40+
_scanAllScriptTypes(mnemonic);
41+
case Err(:final failure):
42+
emit(state.copyWith(failure: failure, isLoading: false));
4543
}
4644
}
4745

@@ -86,28 +84,24 @@ class ImportMnemonicCubit extends Cubit<ImportMnemonicState> {
8684
emit(state.copyWith(scriptType: scriptType));
8785

8886
Future<void> import() async {
89-
try {
90-
if (state.mnemonic == null) throw MnemonicIsNullError();
87+
if (state.mnemonic == null) {
88+
emit(state.copyWith(failure: const ImportMnemonicNullMnemonicFailure()));
89+
return;
90+
}
9191

92-
emit(state.copyWith(isLoading: true, error: null));
92+
emit(state.copyWith(isLoading: true, failure: null));
9393

94-
final mnemonic = state.mnemonic!;
95-
final wallet = await _importWalletUsecase.execute(
96-
mnemonicWords: mnemonic.words,
97-
label: mnemonic.label,
98-
passphrase: mnemonic.passphrase,
99-
scriptType: state.scriptType,
100-
);
101-
emit(state.copyWith(wallet: wallet, isLoading: false));
102-
} on DuplicateMnemonicException catch (e) {
103-
emit(state.copyWith(error: e, isLoading: false));
104-
} catch (e) {
105-
emit(
106-
state.copyWith(
107-
error: ImportMnemonicError(e.toString()),
108-
isLoading: false,
109-
),
110-
);
94+
final mnemonic = state.mnemonic!;
95+
switch (await _importWalletUsecase.execute(
96+
mnemonicWords: mnemonic.words,
97+
label: mnemonic.label,
98+
passphrase: mnemonic.passphrase,
99+
scriptType: state.scriptType,
100+
)) {
101+
case Ok(:final value):
102+
emit(state.copyWith(wallet: value, isLoading: false));
103+
case Err(:final failure):
104+
emit(state.copyWith(failure: failure, isLoading: false));
111105
}
112106
}
113107
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import 'package:bb_mobile/core/utils/build_context_x.dart';
2+
import 'package:bb_mobile/features/import_mnemonic/domain/import_mnemonic_failure.dart';
3+
import 'package:flutter/widgets.dart';
4+
5+
extension ImportMnemonicFailureL10n on ImportMnemonicFailure {
6+
String toTranslated(BuildContext context) => switch (this) {
7+
ImportMnemonicDuplicateFailure() =>
8+
context.loc.importMnemonicDuplicateError,
9+
ImportMnemonicEmptyLabelFailure() =>
10+
context.loc.importMnemonicEmptyLabelError,
11+
ImportMnemonicNullMnemonicFailure() => context.loc.oopsSomethingWentWrong,
12+
ImportMnemonicUnexpectedFailure() => context.loc.oopsSomethingWentWrong,
13+
};
14+
}

lib/features/import_mnemonic/presentation/state.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';
2+
import 'package:bb_mobile/features/import_mnemonic/domain/import_mnemonic_failure.dart';
23
import 'package:bip39_mnemonic/bip39_mnemonic.dart' as bip39;
34
import 'package:freezed_annotation/freezed_annotation.dart';
45

@@ -22,6 +23,6 @@ sealed class ImportMnemonicState with _$ImportMnemonicState {
2223
@Default(null) ({BigInt satoshis, int transactions})? bip44Status,
2324
@Default(null) ({BigInt satoshis, int transactions})? bip49Status,
2425
@Default(null) ({BigInt satoshis, int transactions})? bip84Status,
25-
Exception? error,
26+
@Default(null) ImportMnemonicFailure? failure,
2627
}) = _ImportMnemonicState;
2728
}

lib/features/import_mnemonic/ui/mnemonic_page.dart

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import 'package:bb_mobile/core/themes/app_theme.dart';
22
import 'package:bb_mobile/core/utils/build_context_x.dart';
33
import 'package:bb_mobile/core/widgets/mnemonic_widget.dart';
44
import 'package:bb_mobile/core/widgets/navbar/top_bar.dart';
5-
import 'package:bb_mobile/features/import_mnemonic/errors.dart';
65
import 'package:bb_mobile/features/import_mnemonic/presentation/cubit.dart';
6+
import 'package:bb_mobile/features/import_mnemonic/presentation/import_mnemonic_failure_l10n.dart';
77
import 'package:bb_mobile/features/import_mnemonic/presentation/state.dart';
88
import 'package:bb_mobile/core/widgets/snackbar_utils.dart';
99
import 'package:bip39_mnemonic/bip39_mnemonic.dart' as bip39;
@@ -29,11 +29,11 @@ class MnemonicPage extends StatelessWidget {
2929
),
3030
body: BlocConsumer<ImportMnemonicCubit, ImportMnemonicState>(
3131
listener: (context, state) {
32-
if (state.error != null) {
33-
final message = state.error is DuplicateMnemonicException
34-
? context.loc.importMnemonicDuplicateError
35-
: state.error!.toString();
36-
SnackBarUtils.showSnackBar(context, message);
32+
if (state.failure != null) {
33+
SnackBarUtils.showSnackBar(
34+
context,
35+
state.failure!.toTranslated(context),
36+
);
3737
}
3838
},
3939
builder: (context, state) {

lib/features/import_mnemonic/ui/select_purpose_page.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:bb_mobile/core/widgets/loading/loading_line_content.dart';
66
import 'package:bb_mobile/core/widgets/navbar/top_bar.dart';
77
import 'package:bb_mobile/core/widgets/text/text.dart';
88
import 'package:bb_mobile/features/import_mnemonic/presentation/cubit.dart';
9+
import 'package:bb_mobile/features/import_mnemonic/presentation/import_mnemonic_failure_l10n.dart';
910
import 'package:bb_mobile/features/import_mnemonic/presentation/state.dart';
1011
import 'package:bb_mobile/features/wallet/ui/wallet_router.dart';
1112
import 'package:bb_mobile/core/widgets/snackbar_utils.dart';
@@ -31,8 +32,11 @@ class SelectScriptTypePage extends StatelessWidget {
3132
),
3233
body: BlocConsumer<ImportMnemonicCubit, ImportMnemonicState>(
3334
listener: (context, state) {
34-
if (state.error != null) {
35-
SnackBarUtils.showSnackBar(context, state.error!.toString());
35+
if (state.failure != null) {
36+
SnackBarUtils.showSnackBar(
37+
context,
38+
state.failure!.toTranslated(context),
39+
);
3640
}
3741
},
3842
builder: (context, state) {

localization/app_ar.arb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12651,6 +12651,10 @@
1265112651
}
1265212652
}
1265312653
},
12654+
"importMnemonicEmptyLabelError": "يجب توفير تسمية لاستيراد عبارة التذكر",
12655+
"@importMnemonicEmptyLabelError": {
12656+
"description": "Error message when user tries to import a mnemonic without a label"
12657+
},
1265412658
"labelErrorSystemCannotDelete": "ولا يمكن حذف علامات النظام.",
1265512659
"@labelErrorSystemCannotDelete": {
1265612660
"description": "Error message when trying to delete a system label"

0 commit comments

Comments
 (0)