Skip to content

Commit

Permalink
feat: QR hardware signing in new designs (#13261)
Browse files Browse the repository at this point in the history
## **Description**

QR hardware signing in new designs

## **Related issues**

Fixes: MetaMask/MetaMask-planning#4058

## **Manual testing steps**

1. Connect to QR wallet
2. Import QR account
3. Sign QR sign request

## **Screenshots/Recordings**
TODO

## **Pre-merge author checklist**

- [X] I’ve followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [X] I've completed the PR template to the best of my ability
- [X] I’ve included tests if applicable
- [X] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [X] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

---------

Co-authored-by: digiwand <[email protected]>
Co-authored-by: OGPoyraz <[email protected]>
  • Loading branch information
3 people authored Feb 7, 2025
1 parent 2392f57 commit a010f9c
Show file tree
Hide file tree
Showing 23 changed files with 1,006 additions and 46 deletions.
28 changes: 22 additions & 6 deletions app/components/Views/confirmations/Confirm/Confirm.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ import { StyleSheet } from 'react-native';
import Device from '../../../../util/device';
import { Theme } from '../../../../util/theme/models';

const styleSheet = (params: { theme: Theme }) => {
const { theme } = params;
const styleSheet = (params: {
theme: Theme;
vars: { isFlatConfirmation: boolean };
}) => {
const {
theme,
vars: { isFlatConfirmation },
} = params;

return StyleSheet.create({
mainContainer: {
flatContainer: {
position: 'absolute',
top: 0,
left: 0,
Expand All @@ -18,15 +24,25 @@ const styleSheet = (params: { theme: Theme }) => {
justifyContent: 'space-between',
paddingHorizontal: 16,
},
container: {
modalContainer: {
backgroundColor: theme.colors.background.alternative,
paddingHorizontal: 16,
paddingVertical: 24,
minHeight: '70%',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
paddingBottom: Device.isIphoneX() ? 20 : 0,
justifyContent: 'space-between',
maxHeight: '90%',
},
scrollableSection: {
padding: 4,
},
scrollable: {
minHeight: '100%',
},
scrollWrapper: {
minHeight: isFlatConfirmation ? '80%' : '75%',
maxHeight: isFlatConfirmation ? '80%' : '75%',
margin: 0,
},
});
};
Expand Down
31 changes: 28 additions & 3 deletions app/components/Views/confirmations/Confirm/Confirm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import {
securityAlertResponse,
typedSignV1ConfirmationState,
} from '../../../../util/test/confirm-data-helpers';
// eslint-disable-next-line import/no-namespace
import * as ConfirmationRedesignEnabled from '../hooks/useConfirmationRedesignEnabled';

import Confirm from './index';

jest.mock('../../../../core/Engine', () => ({
Expand All @@ -32,8 +35,17 @@ jest.mock('react-native-gzip', () => ({
deflate: (str: string) => str,
}));

jest.mock('@react-navigation/native', () => ({
...jest.requireActual('@react-navigation/native'),
useNavigation: () => ({
navigate: jest.fn(),
addListener: jest.fn(),
dispatch: jest.fn(),
}),
}));

describe('Confirm', () => {
it('should render correct information for personal sign', async () => {
it('should render correct information for personal sign', () => {
const { getAllByRole, getByText } = renderWithProvider(<Confirm />, {
state: personalSignatureConfirmationState,
});
Expand All @@ -48,7 +60,7 @@ describe('Confirm', () => {
expect(getAllByRole('button')).toHaveLength(2);
});

it('should render correct information for typed sign v1', async () => {
it('should render correct information for typed sign v1', () => {
const { getAllByRole, getAllByText, getByText, queryByText } =
renderWithProvider(<Confirm />, {
state: typedSignV1ConfirmationState,
Expand All @@ -62,7 +74,7 @@ describe('Confirm', () => {
expect(queryByText('This is a deceptive request')).toBeNull();
});

it('should render blockaid banner if confirmation has blockaid error response', async () => {
it('should render blockaid banner if confirmation has blockaid error response', () => {
const { getByText } = renderWithProvider(<Confirm />, {
state: {
...typedSignV1ConfirmationState,
Expand All @@ -72,4 +84,17 @@ describe('Confirm', () => {
expect(getByText('Signature request')).toBeDefined();
expect(getByText('This is a deceptive request')).toBeDefined();
});

it('returns null if re-design is not enabled for confirmation', () => {
jest
.spyOn(ConfirmationRedesignEnabled, 'useConfirmationRedesignEnabled')
.mockReturnValue({ isRedesignedEnabled: false });
const { queryByText } = renderWithProvider(<Confirm />, {
state: {
...typedSignV1ConfirmationState,
signatureRequest: { securityAlertResponse },
},
});
expect(queryByText('Signature request')).toBeNull();
});
});
49 changes: 30 additions & 19 deletions app/components/Views/confirmations/Confirm/Confirm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { View, ScrollView } from 'react-native';
import { ScrollView, StyleSheet, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { TransactionType } from '@metamask/transaction-controller';

Expand All @@ -8,6 +8,7 @@ import AccountNetworkInfo from '../components/Confirm/AccountNetworkInfo';
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 SignatureBlockaidBanner from '../components/Confirm/SignatureBlockaidBanner';
import Title from '../components/Confirm/Title';
import useApprovalRequest from '../hooks/useApprovalRequest';
Expand All @@ -21,43 +22,53 @@ const FLAT_CONFIRMATIONS: TransactionType[] = [
// To be filled with flat confirmations
];

const ConfirmWrapped = () => (
<>
<ScrollView>
<Title />
<SignatureBlockaidBanner />
<AccountNetworkInfo />
<Info />
</ScrollView>
const ConfirmWrapped = ({
styles,
}: {
styles: StyleSheet.NamedStyles<Record<string, unknown>>;
}) => (
<QRHardwareContextProvider>
<Title />
<View style={styles.scrollWrapper}>
<ScrollView
style={styles.scrollable}
contentContainerStyle={styles.scrollableSection}
>
<SignatureBlockaidBanner />
<AccountNetworkInfo />
<Info />
</ScrollView>
</View>
<Footer />
</>
</QRHardwareContextProvider>
);

const Confirm = () => {
const { approvalRequest } = useApprovalRequest();
const { isRedesignedEnabled } = useConfirmationRedesignEnabled();
const { styles } = useStyles(styleSheet, {});

if (!isRedesignedEnabled) {
return null;
}

const isFlatConfirmation = FLAT_CONFIRMATIONS.includes(
approvalRequest?.type as TransactionType,
);

const { styles } = useStyles(styleSheet, { isFlatConfirmation });

if (!isRedesignedEnabled) {
return null;
}

if (isFlatConfirmation) {
return (
<SafeAreaView style={styles.mainContainer}>
<ConfirmWrapped />
<SafeAreaView style={styles.flatContainer}>
<ConfirmWrapped styles={styles} />
</SafeAreaView>
);
}

return (
<BottomModal canCloseOnBackdropClick={false}>
<View style={styles.container} testID={approvalRequest?.type}>
<ConfirmWrapped />
<View style={styles.modalContainer} testID={approvalRequest?.type}>
<ConfirmWrapped styles={styles} />
</View>
</BottomModal>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import React from 'react';
import { fireEvent } from '@testing-library/react-native';

import { ConfirmationFooterSelectorIDs } from '../../../../../../../e2e/selectors/Confirmation/ConfirmationView.selectors';
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';
import Footer from './index';
import { fireEvent } from '@testing-library/react-native';

const mockConfirmSpy = jest.fn();
const mockRejectSpy = jest.fn();
Expand All @@ -15,7 +18,7 @@ jest.mock('../../../hooks/useConfirmActions', () => ({
}));

describe('Footer', () => {
it('should render correctly', async () => {
it('should render correctly', () => {
const { getByText, getAllByRole } = renderWithProvider(<Footer />, {
state: personalSignatureConfirmationState,
});
Expand All @@ -24,19 +27,41 @@ describe('Footer', () => {
expect(getAllByRole('button')).toHaveLength(2);
});

it('should call onConfirm when confirm button is clicked', async () => {
it('should call onConfirm when confirm button is clicked', () => {
const { getByText } = renderWithProvider(<Footer />, {
state: personalSignatureConfirmationState,
});
fireEvent.press(getByText('Confirm'));
expect(mockConfirmSpy).toHaveBeenCalledTimes(1);
});

it('should call onReject when reject button is clicked', async () => {
it('should call onReject when reject button is clicked', () => {
const { getByText } = renderWithProvider(<Footer />, {
state: personalSignatureConfirmationState,
});
fireEvent.press(getByText('Reject'));
expect(mockRejectSpy).toHaveBeenCalledTimes(1);
});

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);
const { getByText } = renderWithProvider(<Footer />, {
state: personalSignatureConfirmationState,
});
expect(getByText('Get Signature')).toBeTruthy();
});

it('confirm button is disabled if `needsCameraPermission` is true', () => {
jest.spyOn(QRHardwareHook, 'useQRHardwareContext').mockReturnValue({
needsCameraPermission: true,
} as unknown as QRHardwareHook.QRHardwareContextType);
const { getByTestId } = renderWithProvider(<Footer />, {
state: personalSignatureConfirmationState,
});
expect(
getByTestId(ConfirmationFooterSelectorIDs.CONFIRM_BUTTON).props.disabled,
).toBe(true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ import Button, {
ButtonWidthTypes,
} from '../../../../../../component-library/components/Buttons/Button';
import { useStyles } from '../../../../../../component-library/hooks';
import { ResultType } from '../../../constants/signatures';
import { useConfirmActions } from '../../../hooks/useConfirmActions';
import { useSecurityAlertResponse } from '../../../hooks/useSecurityAlertResponse';
import { useQRHardwareContext } from '../../../context/QRHardwareContext/QRHardwareContext';
import { ResultType } from '../../BlockaidBanner/BlockaidBanner.types';
import styleSheet from './Footer.styles';

const Footer = () => {
const { onConfirm, onReject } = useConfirmActions();
const { isQRSigningInProgress, needsCameraPermission } =
useQRHardwareContext();
const { securityAlertResponse } = useSecurityAlertResponse();

const { styles } = useStyles(styleSheet, {});
Expand All @@ -34,13 +37,18 @@ const Footer = () => {
<View style={styles.buttonDivider} />
<Button
onPress={onConfirm}
label={strings('confirm.confirm')}
label={
isQRSigningInProgress
? strings('confirm.qr_get_sign')
: strings('confirm.confirm')
}
style={styles.footerButton}
size={ButtonSize.Lg}
testID={ConfirmationFooterSelectorIDs.CONFIRM_BUTTON}
variant={ButtonVariants.Primary}
width={ButtonWidthTypes.Full}
isDanger={securityAlertResponse?.result_type === ResultType.Malicious}
disabled={needsCameraPermission}
/>
</View>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,30 @@ import React from 'react';

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';
import Info from './Info';
import { Text } from 'react-native';

const MockText = Text;
jest.mock('./QRInfo', () => () => <MockText>QR Scanning Component</MockText>);

describe('Info', () => {
it('should render correctly for personal sign', async () => {
it('renders correctly for personal sign', () => {
const { getByText } = renderWithProvider(<Info />, {
state: personalSignatureConfirmationState,
});
expect(getByText('Message')).toBeDefined();
expect(getByText('Example `personal_sign` message')).toBeDefined();
});

it('renders QRInfo if user is signing using QR hardware', () => {
jest.spyOn(QRHardwareHook, 'useQRHardwareContext').mockReturnValue({
isSigningQRObject: true,
} as unknown as QRHardwareHook.QRHardwareContextType);
const { getByText } = renderWithProvider(<Info />, {
state: personalSignatureConfirmationState,
});
expect(getByText('QR Scanning Component')).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { TransactionType } from '@metamask/transaction-controller';
import React from 'react';

import useApprovalRequest from '../../../hooks/useApprovalRequest';
import { useQRHardwareContext } from '../../../context/QRHardwareContext/QRHardwareContext';
import PersonalSign from './PersonalSign';
import QRInfo from './QRInfo';
import TypedSignV1 from './TypedSignV1';
import TypedSignV3V4 from './TypedSignV3V4';

Expand All @@ -16,11 +18,16 @@ const ConfirmationInfoComponentMap = {

const Info = () => {
const { approvalRequest } = useApprovalRequest();
const { isSigningQRObject } = useQRHardwareContext();

if (!approvalRequest?.type) {
return null;
}

if (isSigningQRObject) {
return <QRInfo />;
}

const { requestData } = approvalRequest ?? {
requestData: {},
};
Expand Down
Loading

0 comments on commit a010f9c

Please sign in to comment.