Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Fix the mobile crash due to ledger bluetooth relative exception #11769

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
22 changes: 4 additions & 18 deletions app/components/UI/LedgerModals/LedgerConfirmationModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<LedgerConfirmationModal
onConfirmation={jest.fn()}
onRejection={onRejection}
deviceId={'test'}
/>,
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();
});
});
9 changes: 7 additions & 2 deletions app/components/UI/LedgerModals/LedgerConfirmationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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
}
/>
</View>
Expand Down
1 change: 1 addition & 0 deletions app/components/hooks/Ledger/useLedgerBluetooth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ function useLedgerBluetooth(deviceId: string): UseLedgerBluetoothHook {
setLedgerError(LedgerCommunicationErrors.LedgerIsLocked);
break;
default:
setLedgerError(LedgerCommunicationErrors.UserRefusedConfirmation);
break;
}
} else if (e.name === 'TransportRaceCondition') {
Expand Down
32 changes: 32 additions & 0 deletions app/core/ErrorHandler/ErrorHandler.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
25 changes: 25 additions & 0 deletions app/core/ErrorHandler/ErrorHandler.ts
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions app/core/ErrorHandler/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ErrorHandler';
14 changes: 13 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();

4 changes: 3 additions & 1 deletion locales/languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading