Skip to content

Commit

Permalink
Add support for ledger wallet in new signature designs
Browse files Browse the repository at this point in the history
  • Loading branch information
jpuri committed Feb 14, 2025
1 parent bf79f06 commit 346fdd2
Show file tree
Hide file tree
Showing 19 changed files with 515 additions and 82 deletions.
27 changes: 15 additions & 12 deletions app/components/Views/confirmations/Confirm/Confirm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { useStyles } from '../../../../component-library/hooks';
import BottomModal from '../components/UI/BottomModal';
import Footer from '../components/Confirm/Footer';
import Info from '../components/Confirm/Info';
import { QRHardwareContextProvider } from '../context/QRHardwareContext/QRHardwareContext';
import { LedgerContextProvider } from '../context/LedgerContext';
import { QRHardwareContextProvider } from '../context/QRHardwareContext';
import SignatureBlockaidBanner from '../components/Confirm/SignatureBlockaidBanner';
import Title from '../components/Confirm/Title';
import useApprovalRequest from '../hooks/useApprovalRequest';
Expand All @@ -19,17 +20,19 @@ const ConfirmWrapped = ({
styles: StyleSheet.NamedStyles<Record<string, unknown>>;
}) => (
<QRHardwareContextProvider>
<Title />
<View style={styles.scrollWrapper}>
<ScrollView
style={styles.scrollable}
contentContainerStyle={styles.scrollableSection}
>
<SignatureBlockaidBanner />
<Info />
</ScrollView>
</View>
<Footer />
<LedgerContextProvider>
<Title />
<View style={styles.scrollWrapper}>
<ScrollView
style={styles.scrollable}
contentContainerStyle={styles.scrollableSection}
>
<SignatureBlockaidBanner />
<Info />
</ScrollView>
</View>
<Footer />
</LedgerContextProvider>
</QRHardwareContextProvider>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import renderWithProvider from '../../../../../../util/test/renderWithProvider';
import { personalSignatureConfirmationState } from '../../../../../../util/test/confirm-data-helpers';
// eslint-disable-next-line import/no-namespace
import * as QRHardwareHook from '../../../context/QRHardwareContext/QRHardwareContext';
// eslint-disable-next-line import/no-namespace
import * as LedgerContext from '../../../context/LedgerContext/LedgerContext';
import Footer from './index';

const mockConfirmSpy = jest.fn();
Expand Down Expand Up @@ -46,13 +48,23 @@ describe('Footer', () => {
it('renders confirm button text "Get Signature" if QR signing is in progress', () => {
jest.spyOn(QRHardwareHook, 'useQRHardwareContext').mockReturnValue({
isQRSigningInProgress: true,
} as unknown as QRHardwareHook.QRHardwareContextType);
} as QRHardwareHook.QRHardwareContextType);
const { getByText } = renderWithProvider(<Footer />, {
state: personalSignatureConfirmationState,
});
expect(getByText('Get Signature')).toBeTruthy();
});

it('renders confirm button text "Sign with Ledger" if account used for signing is ledger account', () => {
jest.spyOn(LedgerContext, 'useLedgerContext').mockReturnValue({
isLedgerAccount: true,
} as LedgerContext.LedgerContextType);
const { getByText } = renderWithProvider(<Footer />, {
state: personalSignatureConfirmationState,
});
expect(getByText('Sign with Ledger')).toBeTruthy();
});

it('confirm button is disabled if `needsCameraPermission` is true', () => {
jest.spyOn(QRHardwareHook, 'useQRHardwareContext').mockReturnValue({
needsCameraPermission: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import { View } from 'react-native';

import { ConfirmationFooterSelectorIDs } from '../../../../../../../e2e/selectors/Confirmation/ConfirmationView.selectors';
Expand All @@ -10,8 +10,9 @@ import Button, {
} from '../../../../../../component-library/components/Buttons/Button';
import { useStyles } from '../../../../../../component-library/hooks';
import { useConfirmActions } from '../../../hooks/useConfirmActions';
import { useLedgerContext } from '../../../context/LedgerContext';
import { useSecurityAlertResponse } from '../../../hooks/useSecurityAlertResponse';
import { useQRHardwareContext } from '../../../context/QRHardwareContext/QRHardwareContext';
import { useQRHardwareContext } from '../../../context/QRHardwareContext';
import { ResultType } from '../../BlockaidBanner/BlockaidBanner.types';
import styleSheet from './Footer.styles';

Expand All @@ -20,9 +21,19 @@ const Footer = () => {
const { isQRSigningInProgress, needsCameraPermission } =
useQRHardwareContext();
const { securityAlertResponse } = useSecurityAlertResponse();

const { isLedgerAccount } = useLedgerContext();
const { styles } = useStyles(styleSheet, {});

const confirmButtonLabel = useMemo(() => {
if (isQRSigningInProgress) {
return strings('confirm.qr_get_sign');
}
if (isLedgerAccount) {
return strings('confirm.sign_with_ledger');
}
return strings('confirm.confirm');
}, [isLedgerAccount, isQRSigningInProgress]);

return (
<View style={styles.buttonsContainer}>
<Button
Expand All @@ -37,11 +48,7 @@ const Footer = () => {
<View style={styles.buttonDivider} />
<Button
onPress={onConfirm}
label={
isQRSigningInProgress
? strings('confirm.qr_get_sign')
: strings('confirm.confirm')
}
label={confirmButtonLabel}
style={styles.footerButton}
size={ButtonSize.Lg}
testID={ConfirmationFooterSelectorIDs.CONFIRM_BUTTON}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { Text } from 'react-native';

import renderWithProvider from '../../../../../../util/test/renderWithProvider';
import {
Expand All @@ -8,7 +9,6 @@ import {
// eslint-disable-next-line import/no-namespace
import * as QRHardwareHook from '../../../context/QRHardwareContext/QRHardwareContext';
import Info from './Info';
import { Text } from 'react-native';

jest.mock('@react-navigation/native', () => ({
...jest.requireActual('@react-navigation/native'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React from 'react';

import useApprovalRequest from '../../../hooks/useApprovalRequest';
import { useTransactionMetadataRequest } from '../../../hooks/useTransactionMetadataRequest';
import { useQRHardwareContext } from '../../../context/QRHardwareContext/QRHardwareContext';
import { useQRHardwareContext } from '../../../context/QRHardwareContext';
import PersonalSign from './PersonalSign';
import QRInfo from './QRInfo';
import TypedSignV1 from './TypedSignV1';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
useMetrics,
} from '../../../../../../hooks/useMetrics';
import { useStyles } from '../../../../../../hooks/useStyles';
import { useQRHardwareContext } from '../../../../context/QRHardwareContext/QRHardwareContext';
import { useQRHardwareContext } from '../../../../context/QRHardwareContext';
import styleSheet from './QRInfo.styles';

const QRInfo = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { StyleSheet } from 'react-native';

import { Theme } from '../../../../../../util/theme/models';

const styleSheet = (params: { theme: Theme }) => {
const { theme } = params;

return StyleSheet.create({
modal: {
height: 600,
margin: 0,
zIndex: 1000,
},
contentWrapper: {
zIndex: 1000,
paddingHorizontal: 8,
marginHorizontal: 8,
paddingBottom: 32,
borderRadius: 20,
backgroundColor: theme.colors.background.default,
},
});
};

export default styleSheet;
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React from 'react';
import { Button, Text, View } from 'react-native';
import { fireEvent } from '@testing-library/react-native';

// eslint-disable-next-line import/no-namespace
import * as rpcEventsFuncs from '../../../../../../actions/rpcEvents';
import renderWithProvider from '../../../../../../util/test/renderWithProvider';
import { personalSignatureConfirmationState } from '../../../../../../util/test/confirm-data-helpers';
// eslint-disable-next-line import/no-namespace
import * as ConfirmationActions from '../../../hooks/useConfirmActions';

import LedgerSignModal from './LedgerSignModal';

const mockDeviceId = 'MockDeviceId';
const mockCloseLedgerSignModal = jest.fn();
jest.mock('../../../context/LedgerContext', () => ({
useLedgerContext: () => ({
deviceId: mockDeviceId,
closeLedgerSignModal: mockCloseLedgerSignModal,
}),
}));

const MockView = View;
const MockText = Text;
const MockButton = Button;
jest.mock('../../../../../UI/LedgerModals/LedgerConfirmationModal', () => ({
__esModule: true,
default: ({
onConfirmation,
onRejection,
deviceId,
}: {
onConfirmation: () => void;
onRejection: () => void;
deviceId: string;
}) => (
<MockView>
<MockText>Mock LedgerConfirmationModal</MockText>
<MockText>{deviceId}</MockText>
<MockButton onPress={onConfirmation} title="onConfirmation" />
<MockButton onPress={onRejection} title="onRejection" />
</MockView>
),
}));

describe('LedgerMessageSignModal', () => {
it('should render LedgerConfirmationModal correctly', () => {
const { getByText } = renderWithProvider(<LedgerSignModal />, {
state: personalSignatureConfirmationState,
});
expect(getByText('Mock LedgerConfirmationModal')).toBeTruthy();
expect(getByText(mockDeviceId)).toBeTruthy();
});

it('should call onConfirm when request is confirmed', () => {
const mockOnConfirm = jest.fn();
jest.spyOn(ConfirmationActions, 'useConfirmActions').mockReturnValue({
onConfirm: mockOnConfirm,
onReject: jest.fn(),
});
const { getByText } = renderWithProvider(<LedgerSignModal />, {
state: personalSignatureConfirmationState,
});
fireEvent.press(getByText('onConfirmation'));
expect(mockOnConfirm).toHaveBeenCalledTimes(1);
});

it('should call onReject when request is rejected', () => {
const mockOnReject = jest.fn();
jest.spyOn(ConfirmationActions, 'useConfirmActions').mockReturnValue({
onConfirm: jest.fn(),
onReject: mockOnReject,
});
jest
.spyOn(rpcEventsFuncs, 'resetEventStage')
.mockImplementation(() => ({ rpcName: 'dummy', type: 'DUMMY' }));
const { getByText } = renderWithProvider(<LedgerSignModal />, {
state: personalSignatureConfirmationState,
});
fireEvent.press(getByText('onRejection'));
expect(mockOnReject).toHaveBeenCalledTimes(1);
expect(rpcEventsFuncs.resetEventStage).toHaveBeenCalledTimes(1);
expect(mockCloseLedgerSignModal).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React, { useCallback, useEffect } from 'react';
import Modal from 'react-native-modal';
import { View } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';

import { RootState } from '../../../../../../reducers';
import {
RPCStageTypes,
iEventGroup,
} from '../../../../../../reducers/rpcEvents';
import { resetEventStage } from '../../../../../../actions/rpcEvents';
import LedgerConfirmationModal from '../../../../../UI/LedgerModals/LedgerConfirmationModal';
import { useStyles } from '../../../../../hooks/useStyles';
import { useConfirmActions } from '../../../hooks/useConfirmActions';
import { useLedgerContext } from '../../../context/LedgerContext';
import styleSheet from './LedgerSignModal.styles';

const LedgerSignModal = () => {
const dispatch = useDispatch();
const { styles } = useStyles(styleSheet, {});
const { signingEvent }: iEventGroup = useSelector(
(state: RootState) => state.rpcEvents,
);
const { closeLedgerSignModal, deviceId } = useLedgerContext();
const { onConfirm, onReject } = useConfirmActions();

const completeRequest = useCallback(() => {
closeLedgerSignModal();
dispatch(resetEventStage(signingEvent.rpcName));
}, [closeLedgerSignModal, dispatch, signingEvent.rpcName]);

useEffect(() => {
//Close the modal when the signMessageStage is complete or error, error will return the error message to the user
if (
signingEvent.eventStage === RPCStageTypes.COMPLETE ||
signingEvent.eventStage === RPCStageTypes.ERROR
) {
completeRequest();
}
}, [signingEvent.eventStage, completeRequest]);

const onConfirmation = useCallback(async () => {
onConfirm();
}, [onConfirm]);

const onRejection = useCallback(() => {
onReject();
completeRequest();
}, [completeRequest, onReject]);

if (!deviceId) {
return null;
}

return (
<Modal isVisible style={styles.modal}>
<View style={styles.contentWrapper}>
<LedgerConfirmationModal
onConfirmation={onConfirmation}
onRejection={onRejection}
deviceId={deviceId}
/>
</View>
</Modal>
);
};

export default LedgerSignModal;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './LedgerSignModal';
Loading

0 comments on commit 346fdd2

Please sign in to comment.