diff --git a/app/components/UI/LedgerModals/LedgerConfirmationModal.test.tsx b/app/components/UI/LedgerModals/LedgerConfirmationModal.test.tsx index 3cefe4a7158..165c4032f0f 100644 --- a/app/components/UI/LedgerModals/LedgerConfirmationModal.test.tsx +++ b/app/components/UI/LedgerModals/LedgerConfirmationModal.test.tsx @@ -407,24 +407,10 @@ describe('LedgerConfirmationModal', () => { }); it('calls onRejection when user refuses confirmation', async () => { - const onRejection = jest.fn(); - (useLedgerBluetooth as jest.Mock).mockReturnValue({ - isSendingLedgerCommands: true, - isAppLaunchConfirmationNeeded: false, - ledgerLogicToRun: jest.fn(), - error: LedgerCommunicationErrors.UserRefusedConfirmation, - }); - - renderWithProvider( - , + checkLedgerCommunicationErrorFlow( + LedgerCommunicationErrors.UserRefusedConfirmation, + strings('ledger.user_reject_transaction'), + strings('ledger.user_reject_transaction_message'), ); - // eslint-disable-next-line no-empty-function - await act(async () => {}); - - expect(onRejection).toHaveBeenCalled(); }); }); diff --git a/app/components/UI/LedgerModals/LedgerConfirmationModal.tsx b/app/components/UI/LedgerModals/LedgerConfirmationModal.tsx index a1526426502..b35967f8c40 100644 --- a/app/components/UI/LedgerModals/LedgerConfirmationModal.tsx +++ b/app/components/UI/LedgerModals/LedgerConfirmationModal.tsx @@ -160,7 +160,10 @@ const LedgerConfirmationModal = ({ }); break; case LedgerCommunicationErrors.UserRefusedConfirmation: - onReject(); + setErrorDetails({ + title: strings('ledger.user_reject_transaction'), + subtitle: strings('ledger.user_reject_transaction_message'), + }); break; case LedgerCommunicationErrors.LedgerHasPendingConfirmation: setErrorDetails({ @@ -275,7 +278,9 @@ const LedgerConfirmationModal = ({ isRetryHide={ ledgerError === LedgerCommunicationErrors.UnknownError || ledgerError === LedgerCommunicationErrors.NonceTooLow || - ledgerError === LedgerCommunicationErrors.NotSupported + ledgerError === LedgerCommunicationErrors.NotSupported || + ledgerError === LedgerCommunicationErrors.BlindSignError || + ledgerError === LedgerCommunicationErrors.UserRefusedConfirmation } /> diff --git a/app/components/hooks/Ledger/useLedgerBluetooth.ts b/app/components/hooks/Ledger/useLedgerBluetooth.ts index 9b1010f15fc..fb1ff459242 100644 --- a/app/components/hooks/Ledger/useLedgerBluetooth.ts +++ b/app/components/hooks/Ledger/useLedgerBluetooth.ts @@ -214,6 +214,7 @@ function useLedgerBluetooth(deviceId: string): UseLedgerBluetoothHook { setLedgerError(LedgerCommunicationErrors.LedgerIsLocked); break; default: + setLedgerError(LedgerCommunicationErrors.UserRefusedConfirmation); break; } } else if (e.name === 'TransportRaceCondition') { diff --git a/app/core/ErrorHandler/ErrorHandler.test.ts b/app/core/ErrorHandler/ErrorHandler.test.ts new file mode 100644 index 00000000000..5c73a967bfb --- /dev/null +++ b/app/core/ErrorHandler/ErrorHandler.test.ts @@ -0,0 +1,32 @@ +import { getReactNativeDefaultHandler, handleCustomError, setReactNativeDefaultHandler } from './ErrorHandler'; +import { ErrorHandlerCallback } from 'react-native'; + +describe('ErrorHandler', () => { + const mockHandler: ErrorHandlerCallback = jest.fn(); + + it('sets the default error handler', () => { + setReactNativeDefaultHandler(mockHandler); + expect(getReactNativeDefaultHandler()).toBe(mockHandler); + }); + + it('handles Ledger error without crashing the app', () => { + const mockError = { name: 'EthAppPleaseEnableContractData', message: 'Enable contract data' }; + console.error = jest.fn(); + handleCustomError(mockError, true); + expect(console.error).toHaveBeenCalledWith('Ledger error: ', 'Enable contract data'); + }); + + it('passes non-Ledger error to the default handler', () => { + setReactNativeDefaultHandler(mockHandler); + const mockError = new Error('Some other error'); + handleCustomError(mockError, true); + expect(mockHandler).toHaveBeenCalledWith(mockError, true); + }); + + it('handles TransportStatusError without crashing the app', () => { + const mockError = { name: 'TransportStatusError', message: 'Transport error' }; + console.error = jest.fn(); + handleCustomError(mockError, true); + expect(console.error).toHaveBeenCalledWith('Ledger error: ', 'Transport error'); + }); +}); diff --git a/app/core/ErrorHandler/ErrorHandler.ts b/app/core/ErrorHandler/ErrorHandler.ts new file mode 100644 index 00000000000..8d9e5d685e5 --- /dev/null +++ b/app/core/ErrorHandler/ErrorHandler.ts @@ -0,0 +1,25 @@ +import { ErrorHandlerCallback } from 'react-native'; + +let reactNativeDefaultHandler: ErrorHandlerCallback; + +/** + * Set the default error handler from react-native + * @param handler + */ +export const setReactNativeDefaultHandler = (handler: ErrorHandlerCallback) => { + reactNativeDefaultHandler = handler; +}; + +export const handleCustomError = (error: Error, isFatal: boolean) => { + // Check whether the error is from the Ledger native bluetooth errors. + if(error.name === 'EthAppPleaseEnableContractData' || error.name === 'TransportStatusError') { + // dont pass the error to react native error handler to prevent app crash + console.error('Ledger error: ', error.message); + // Handle the error + } else { + // Pass the error to react native error handler + reactNativeDefaultHandler(error, isFatal); + } +}; + +export const getReactNativeDefaultHandler = () => reactNativeDefaultHandler; diff --git a/app/core/ErrorHandler/index.ts b/app/core/ErrorHandler/index.ts new file mode 100644 index 00000000000..836458d0019 --- /dev/null +++ b/app/core/ErrorHandler/index.ts @@ -0,0 +1 @@ +export * from './ErrorHandler'; diff --git a/index.js b/index.js index 50460ab3fe0..88c32221566 100644 --- a/index.js +++ b/index.js @@ -14,12 +14,13 @@ import * as Sentry from '@sentry/react-native'; // eslint-disable-line import/no import { setupSentry } from './app/util/sentry/utils'; setupSentry(); -import { AppRegistry, LogBox } from 'react-native'; +import { AppRegistry, LogBox, ErrorUtils } from 'react-native'; import Root from './app/components/Views/Root'; import { name } from './app.json'; import { isTest } from './app/util/test/utils.js'; import { Performance } from './app/core/Performance'; +import { handleCustomError, setReactNativeDefaultHandler } from './app/core/ErrorHandler'; Performance.setupPerformanceObservers(); LogBox.ignoreAllLogs(); @@ -91,3 +92,14 @@ AppRegistry.registerComponent(name, () => // Disable Sentry for E2E tests isTest ? Root : Sentry.wrap(Root), ); + +function setupGlobalErrorHandler() { + const reactNativeDefaultHandler = global.ErrorUtils.getGlobalHandler(); + // set the base handler to the react native ExceptionsManager.handleException(), please refer to setupErrorHandling.js under react-native/Libraries/Core/ for details. + setReactNativeDefaultHandler(reactNativeDefaultHandler); + // override the global handler to provide custom error handling + global.ErrorUtils.setGlobalHandler(handleCustomError); +} + +setupGlobalErrorHandler(); + diff --git a/locales/languages/en.json b/locales/languages/en.json index e2bbea8e5c3..3fe12eb8c7d 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -3233,7 +3233,9 @@ "ledger_bip44_path": "BIP44(e.g. MetaMask, Trezor)", "ledger_legacy_label": " (legacy)", "blind_sign_error": "Blind signing error", - "blind_sign_error_message": "Blind signing is not enabled on your Ledger device. Please enable it in the settings." + "blind_sign_error_message": "Blind signing is not enabled on your Ledger device. Please enable it in the settings.", + "user_reject_transaction": "User rejected the transaction", + "user_reject_transaction_message": "The user has rejected the transaction on the Ledger device." }, "account_actions": { "edit_name": "Edit account name",