CW-782: Show error report popup without cooldown (#1739)

* improve exception throwing on broken wallets
- put _lastOpenedWallet to avoid issues on windows (file is currently open by)
- don't throw corruptedWalletsSeed - instead store it inside of secureStorage
- await ExceptionHandler.onError calls where possible to makse sure that popup won't be canceled by some UI element
- adjust BaseAlertDialog to be scrollable if the text is too long
- add ExceptionHandler.resetLastPopupDate - that can be called when we want to show error report screen (bypassing cooldown)

* fix: HiveError: Box has already been closed.

* await the alerts to be sure that each one of them is being shown
fix typo in secure storage

* Update lib/core/backup_service.dart

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* address comments on github

* don't store seeds in secure storage

* fix wallet password

* update monero_c
update corrupted seeds UI
prevent app from crashing when wallet is corrupted

* show alert with seeds

* Update corrupted wallet UI
Fix wallet opening cache

* remove unused code

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
cyan
2024-11-28 21:28:31 +02:00
committed by GitHub
co-authored by Omar Hatem
parent d8d4190608
commit 17d34beae9
51 changed files with 205 additions and 87 deletions
+1 -1
View File
@@ -56,7 +56,7 @@ class AnyPayApi {
final response = await post(url, headers: headers, body: utf8.encode(json.encode(body)));
if (response.statusCode != 200) {
ExceptionHandler.onError(FlutterErrorDetails(exception: response));
await ExceptionHandler.onError(FlutterErrorDetails(exception: response));
throw Exception('Unexpected response http code: ${response.statusCode}');
}
+8 -10
View File
@@ -230,17 +230,15 @@ class BackupService {
json.decode(transactionDescriptionFile.readAsStringSync()) as Map<String, dynamic>;
final descriptionsMap = jsonData.map((key, value) =>
MapEntry(key, TransactionDescription.fromJson(value as Map<String, dynamic>)));
if (!_transactionDescriptionBox.isOpen) {
final transactionDescriptionsBoxKey = await getEncryptionKey(secureStorage: secureStorageShared, forKey: TransactionDescription.boxKey);
final transactionDescriptionBox = await CakeHive.openBox<TransactionDescription>(
var box = _transactionDescriptionBox;
if (!box.isOpen) {
final transactionDescriptionsBoxKey =
await getEncryptionKey(secureStorage: _secureStorage, forKey: TransactionDescription.boxKey);
box = await CakeHive.openBox<TransactionDescription>(
TransactionDescription.boxName,
encryptionKey: transactionDescriptionsBoxKey,
);
await transactionDescriptionBox.putAll(descriptionsMap);
return;
}
await _transactionDescriptionBox.putAll(descriptionsMap);
encryptionKey: transactionDescriptionsBoxKey);
}
await box.putAll(descriptionsMap);
}
Future<void> _importPreferencesDump() async {
+55 -9
View File
@@ -2,15 +2,22 @@ import 'dart:async';
import 'package:cake_wallet/core/generate_wallet_password.dart';
import 'package:cake_wallet/core/key_service.dart';
import 'package:cake_wallet/core/secure_storage.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/main.dart';
import 'package:cake_wallet/reactions/on_authentication_state_change.dart';
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/utils/exception_handler.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
class WalletLoadingService {
@@ -58,24 +65,25 @@ class WalletLoadingService {
return wallet;
} catch (error, stack) {
ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stack));
await ExceptionHandler.resetLastPopupDate();
await ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stack));
// try fetching the seeds of the corrupted wallet to show it to the user
String corruptedWalletsSeeds = "Corrupted wallets seeds (if retrievable, empty otherwise):";
try {
corruptedWalletsSeeds += await _getCorruptedWalletSeeds(name, type);
} catch (e) {
corruptedWalletsSeeds += "\nFailed to fetch $name seeds: $e";
corruptedWalletsSeeds += "\nFailed to fetch $name seeds: $e";
}
// try opening another wallet that is not corrupted to give user access to the app
final walletInfoSource = await CakeHive.openBox<WalletInfo>(WalletInfo.boxName);
WalletBase? wallet;
for (var walletInfo in walletInfoSource.values) {
try {
final walletService = walletServiceFactory.call(walletInfo.type);
final walletPassword = password ?? (await keyService.getWalletPassword(walletName: name));
final wallet = await walletService.openWallet(walletInfo.name, walletPassword);
final walletPassword = await keyService.getWalletPassword(walletName: walletInfo.name);
wallet = await walletService.openWallet(walletInfo.name, walletPassword);
if (walletInfo.type == WalletType.monero) {
await updateMoneroWalletPassword(wallet);
@@ -88,8 +96,6 @@ class WalletLoadingService {
// if found a wallet that is not corrupted, then still display the seeds of the corrupted ones
authenticatedErrorStreamController.add(corruptedWalletsSeeds);
return wallet;
} catch (e) {
print(e);
// save seeds and show corrupted wallets' seeds to the user
@@ -99,16 +105,56 @@ class WalletLoadingService {
corruptedWalletsSeeds += seeds;
}
} catch (e) {
corruptedWalletsSeeds += "\nFailed to fetch $name seeds: $e";
corruptedWalletsSeeds += "\nFailed to fetch $name seeds: $e";
}
}
}
// if all user's wallets are corrupted throw exception
throw error.toString() + "\n\n" + corruptedWalletsSeeds;
final msg = error.toString() + "\n" + corruptedWalletsSeeds;
if (navigatorKey.currentContext != null) {
await showPopUp<void>(
context: navigatorKey.currentContext!,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle: "Corrupted seeds",
alertContent: S.of(context).corrupted_seed_notice,
leftButtonText: S.of(context).cancel,
rightButtonText: S.of(context).show_seed,
actionLeftButton: () => Navigator.of(context).pop(),
actionRightButton: () => showSeedsPopup(context, msg),
);
});
} else {
throw msg;
}
if (wallet == null) {
throw Exception("Wallet is null");
}
return wallet;
}
}
Future<void> showSeedsPopup(BuildContext context, String message) async {
Navigator.of(context).pop();
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle: "Corrupted seeds",
alertContent: message,
leftButtonText: S.of(context).copy,
rightButtonText: S.of(context).ok,
actionLeftButton: () async {
await Clipboard.setData(ClipboardData(text: message));
},
actionRightButton: () async {
Navigator.of(context).pop();
},
);
});
}
Future<void> updateMoneroWalletPassword(WalletBase wallet) async {
final key = PreferencesKey.moneroWalletUpdateV1Key(wallet.name);
var isPasswordUpdated = sharedPreferences.getBool(key) ?? false;
+3 -3
View File
@@ -575,7 +575,7 @@ Future<void> setup({
totpAuthPageState.changeProcessText('Loading the wallet');
if (loginError != null) {
totpAuthPageState.changeProcessText('ERROR: ${loginError.toString()}');
totpAuthPageState.changeProcessText('ERROR: ${loginError.toString()}'.trim());
}
ReactionDisposer? _reaction;
@@ -604,7 +604,7 @@ Future<void> setup({
authPageState.changeProcessText('Loading the wallet');
if (loginError != null) {
authPageState.changeProcessText('ERROR: ${loginError.toString()}');
authPageState.changeProcessText('ERROR: ${loginError.toString()}'.trim());
loginError = null;
}
@@ -624,7 +624,7 @@ Future<void> setup({
}
if (loginError != null) {
authPageState.changeProcessText('ERROR: ${loginError.toString()}');
authPageState.changeProcessText('ERROR: ${loginError.toString()}'.trim());
timer.cancel();
}
});
+1 -1
View File
@@ -89,7 +89,7 @@ Future<void> runAppWithZone({Key? topLevelKey}) async {
);
}
ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stackTrace));
await ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stackTrace));
});
}
@@ -43,8 +43,8 @@ void startAuthenticationStateChange(
if (!requireHardwareWalletConnection()) await loadCurrentWallet();
} catch (error, stack) {
loginError = error;
ExceptionHandler.onError(
FlutterErrorDetails(exception: error, stack: stack));
await ExceptionHandler.resetLastPopupDate();
await ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stack));
}
return;
}
@@ -81,7 +81,7 @@ void startAuthenticationStateChange(
.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
}
if (!(await authenticatedErrorStreamController.stream.isEmpty)) {
ExceptionHandler.showError(
await ExceptionHandler.showError(
(await authenticatedErrorStreamController.stream.first).toString());
authenticatedErrorStreamController.stream.drain();
}
+21 -2
View File
@@ -1,5 +1,6 @@
import 'package:another_flushbar/flushbar.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:mobx/mobx.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
@@ -9,6 +10,8 @@ import 'package:cake_wallet/view_model/auth_view_model.dart';
import 'package:cake_wallet/src/screens/pin_code/pin_code.dart';
import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:flutter/services.dart';
typedef OnAuthenticationFinished = void Function(bool, AuthPageState);
@@ -66,7 +69,6 @@ class AuthPagePinCodeStateImpl extends AuthPageState<AuthPage> {
dismissFlushBar(_authBar);
showBar<void>(
context, S.of(context).failed_authentication(state.error));
widget.onAuthenticationFinished(false, this);
});
}
@@ -77,12 +79,12 @@ class AuthPagePinCodeStateImpl extends AuthPageState<AuthPage> {
dismissFlushBar(_authBar);
showBar<void>(
context, S.of(context).failed_authentication(state.error));
widget.onAuthenticationFinished(false, this);
});
}
});
if (widget.authViewModel.isBiometricalAuthenticationAllowed) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
await Future<void>.delayed(Duration(milliseconds: 100));
@@ -93,6 +95,23 @@ class AuthPagePinCodeStateImpl extends AuthPageState<AuthPage> {
super.initState();
}
Future<void> _showSeedsPopup(BuildContext context, String message) async {
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle: "Corrupted seeds",
alertContent: message,
leftButtonText: S.of(context).copy,
rightButtonText: S.of(context).ok,
actionLeftButton: () async {
await Clipboard.setData(ClipboardData(text: message));
},
actionRightButton: () => Navigator.of(context).pop(),
);
});
}
@override
void dispose() {
_reaction?.reaction.dispose();
+1 -1
View File
@@ -154,7 +154,7 @@ class BackupPage extends BasePage {
File returnedFile = File(outputFile!);
await returnedFile.writeAsBytes(backup.content);
} catch (exception, stackTrace) {
ExceptionHandler.onError(FlutterErrorDetails(
await ExceptionHandler.onError(FlutterErrorDetails(
exception: exception,
stack: stackTrace,
library: "Export Backup",
@@ -11,6 +11,7 @@ import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.da
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/themes/extensions/menu_theme.dart';
import 'package:cake_wallet/utils/exception_handler.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart';
@@ -1,6 +1,7 @@
import 'package:another_flushbar/flushbar.dart';
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/core/new_wallet_arguments.dart';
import 'package:cake_wallet/core/secure_storage.dart';
import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart';
import 'package:cake_wallet/entities/wallet_list_order_types.dart';
import 'package:cake_wallet/generated/i18n.dart';
@@ -20,6 +21,7 @@ import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/filter_theme.dart';
import 'package:cake_wallet/themes/extensions/wallet_list_theme.dart';
import 'package:cake_wallet/utils/exception_handler.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
@@ -459,6 +461,9 @@ class WalletListBodyState extends State<WalletListBody> {
});
}
} catch (e) {
await ExceptionHandler.resetLastPopupDate();
final err = e.toString();
await ExceptionHandler.onError(FlutterErrorDetails(exception: err));
if (this.mounted) {
changeProcessText(S
.of(context)
+16 -9
View File
@@ -73,15 +73,22 @@ class BaseAlertDialog extends StatelessWidget {
}
Widget content(BuildContext context) {
return Text(
contentText,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.normal,
fontFamily: 'Lato',
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
decoration: TextDecoration.none,
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
contentText,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.normal,
fontFamily: 'Lato',
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
decoration: TextDecoration.none,
),
),
],
),
);
}
+41 -32
View File
@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:cake_wallet/core/secure_storage.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/main.dart';
@@ -20,7 +21,7 @@ class ExceptionHandler {
static const _coolDownDurationInDays = 7;
static File? _file;
static void _saveException(String? error, StackTrace? stackTrace, {String? library}) async {
static Future<void> _saveException(String? error, StackTrace? stackTrace, {String? library}) async {
final appDocDir = await getAppDir();
if (_file == null) {
@@ -90,7 +91,12 @@ class ExceptionHandler {
}
}
static void onError(FlutterErrorDetails errorDetails) async {
static Future<void> resetLastPopupDate() async {
final sharedPrefs = await SharedPreferences.getInstance();
await sharedPrefs.setString(PreferencesKey.lastPopupDate, DateTime(1971).toString());
}
static Future<void> onError(FlutterErrorDetails errorDetails) async {
if (kDebugMode || kProfileMode) {
FlutterError.presentError(errorDetails);
debugPrint(errorDetails.toString());
@@ -124,35 +130,40 @@ class ExceptionHandler {
}
_hasError = true;
sharedPrefs.setString(PreferencesKey.lastPopupDate, DateTime.now().toString());
await sharedPrefs.setString(PreferencesKey.lastPopupDate, DateTime.now().toString());
WidgetsBinding.instance.addPostFrameCallback(
(timeStamp) async {
if (navigatorKey.currentContext != null) {
await showPopUp<void>(
context: navigatorKey.currentContext!,
builder: (context) {
return AlertWithTwoActions(
isDividerExist: true,
alertTitle: S.of(context).error,
alertContent: S.of(context).error_dialog_content,
rightButtonText: S.of(context).send,
leftButtonText: S.of(context).do_not_send,
actionRightButton: () {
Navigator.of(context).pop();
_sendExceptionFile();
},
actionLeftButton: () {
Navigator.of(context).pop();
},
);
// Instead of using WidgetsBinding.instance.addPostFrameCallback we
// await Future.delayed(Duration.zero), which does essentially the same (
// but doesn't wait for actual frame to be rendered), but it allows us to
// properly await the execution - which is what we want, without awaiting
// other code may call functions like Navigator.pop(), and close the alert
// instead of the intended UI.
// WidgetsBinding.instance.addPostFrameCallback(
// (timeStamp) async {
await Future.delayed(Duration.zero);
if (navigatorKey.currentContext != null) {
await showPopUp<void>(
context: navigatorKey.currentContext!,
builder: (context) {
return AlertWithTwoActions(
isDividerExist: true,
alertTitle: S.of(context).error,
alertContent: S.of(context).error_dialog_content,
rightButtonText: S.of(context).send,
leftButtonText: S.of(context).do_not_send,
actionRightButton: () {
Navigator.of(context).pop();
_sendExceptionFile();
},
actionLeftButton: () {
Navigator.of(context).pop();
},
);
}
},
);
}
_hasError = false;
},
);
_hasError = false;
}
/// Ignore User related errors or system errors
@@ -272,20 +283,18 @@ class ExceptionHandler {
};
}
static void showError(String error, {int? delayInSeconds}) async {
static Future<void> showError(String error, {int? delayInSeconds}) async {
if (_hasError) {
return;
}
_hasError = true;
if (delayInSeconds != null) {
Future.delayed(Duration(seconds: delayInSeconds), () => _showCopyPopup(error));
return;
}
WidgetsBinding.instance.addPostFrameCallback(
(_) async => _showCopyPopup(error),
);
await Future.delayed(Duration.zero);
await _showCopyPopup(error);
}
static Future<void> _showCopyPopup(String content) async {
@@ -68,7 +68,7 @@ abstract class RestoreFromBackupViewModelBase with Store {
if (msg.toLowerCase().contains("message authentication code (mac)")) {
msg = 'Incorrect backup password';
} else {
ExceptionHandler.onError(FlutterErrorDetails(
await ExceptionHandler.onError(FlutterErrorDetails(
exception: e,
stack: s,
library: this.toString(),