mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-22 06:28:39 +02:00
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:
@@ -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}');
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user