-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Redesign Signature Decoding Simulation (#12994)
## **Description** Add Signature Decoding Simulation UI This logic mirrors the extension with slight modifications due to mobile/extension parity differences. In addition: - updated ValueDisplay value logic to no longer use useMemo Todo in follow-up PRs: - Add additional tests #13023 - Add "Unlimited" text support #13022 - Investigate "useExternalServices" setting. This does not seem to exist in mobile #13024 ## **Related issues** Fixes: MetaMask/MetaMask-planning#3876 Related: #12994 (Replaces Ramp/Box usages with View) ## **Manual testing steps** 1. Set REDESIGNED_SIGNATURE_REQUEST to true in js.env 2. Enable confirmation_redesign in Launch Darkly 3. Turn on Improved Signatures setting 4. Turn on Simulation setting 5. Test various v3 and v4 signTypedData signatures - Example dapp: https://develop.d3bkcslj57l47p.amplifyapp.com/ → "Permit 2 - Single" button ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <img width="320" src="https://github.com/user-attachments/assets/4b511681-66d3-47c4-8807-5ea227a9eaa9"> <img width="320" src="https://github.com/user-attachments/assets/439477e6-b02f-4b46-9660-dd1e77d4df2a"> The values in this screenshot will be replaced by "Unlimited" <img width="320" src="https://github.com/user-attachments/assets/0f37d8cd-7817-4d88-a4ab-79356268265d"> ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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.
- Loading branch information
Showing
28 changed files
with
873 additions
and
76 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
...nents/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/Simulation.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import React from 'react'; | ||
|
||
import { useTypedSignSimulationEnabled } from '../../../../../hooks/useTypedSignSimulationEnabled'; | ||
import { isRecognizedPermit } from '../../../../../utils/signature'; | ||
import { useSignatureRequest } from '../../../../../hooks/useSignatureRequest'; | ||
import DecodedSimulation from './TypedSignDecoded'; | ||
import PermitSimulation from './TypedSignPermit'; | ||
|
||
const TypedSignV3V4Simulation: React.FC<object> = () => { | ||
const signatureRequest = useSignatureRequest(); | ||
const isPermit = signatureRequest && isRecognizedPermit(signatureRequest); | ||
const isSimulationSupported = useTypedSignSimulationEnabled(); | ||
|
||
if (!isSimulationSupported || !signatureRequest) { | ||
return null; | ||
} | ||
|
||
const { decodingData, decodingLoading } = signatureRequest; | ||
const hasDecodingData = !( | ||
(!decodingLoading && decodingData === undefined) || | ||
decodingData?.error | ||
); | ||
|
||
if (!hasDecodingData && isPermit) { | ||
return <PermitSimulation />; | ||
} | ||
|
||
return <DecodedSimulation />; | ||
}; | ||
|
||
export default TypedSignV3V4Simulation; |
55 changes: 55 additions & 0 deletions
55
...ts/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/Static/Static.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import React from 'react'; | ||
import { StyleSheet, View } from 'react-native'; | ||
|
||
import { useStyles } from '../../../../../../../../../component-library/hooks'; | ||
import InfoRow from '../../../../../UI/InfoRow'; | ||
import InfoSection from '../../../../../UI/InfoRow/InfoSection'; | ||
import Loader from '../../../../../../../../../component-library/components-temp/Loader'; | ||
|
||
const styleSheet = () => StyleSheet.create({ | ||
base: { | ||
display: 'flex', | ||
justifyContent: 'space-between', | ||
}, | ||
loaderContainer: { | ||
display: 'flex', | ||
justifyContent: 'center', | ||
}, | ||
}); | ||
|
||
const StaticSimulation: React.FC<{ | ||
title: string; | ||
titleTooltip: string; | ||
description?: string; | ||
simulationElements: React.ReactNode; | ||
isLoading?: boolean; | ||
isCollapsed?: boolean; | ||
}> = ({ | ||
title, | ||
titleTooltip, | ||
description, | ||
simulationElements, | ||
isLoading, | ||
isCollapsed = false, | ||
}) => { | ||
const { styles } = useStyles(styleSheet, {}); | ||
|
||
return( | ||
<View style={isCollapsed ? styles.base : {}}> | ||
<InfoSection> | ||
<InfoRow label={title} tooltip={titleTooltip}> | ||
{description} | ||
</InfoRow> | ||
{isLoading ? ( | ||
<View style={styles.loaderContainer}> | ||
<Loader size={'small'} /> | ||
</View> | ||
) : ( | ||
simulationElements | ||
)} | ||
</InfoSection> | ||
</View> | ||
); | ||
}; | ||
|
||
export default StaticSimulation; |
1 change: 1 addition & 0 deletions
1
...ents/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/Static/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from './Static'; |
167 changes: 167 additions & 0 deletions
167
...mponents/Confirm/Info/TypedSignV3V4/Simulation/TypedSignDecoded/TypedSignDecoded.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
import React from 'react'; | ||
import cloneDeep from 'lodash/cloneDeep'; | ||
import { | ||
DecodingDataChangeType, | ||
DecodingDataStateChanges, | ||
SignatureRequest, | ||
} from '@metamask/signature-controller'; | ||
|
||
import { strings } from '../../../../../../../../../../locales/i18n'; | ||
import { typedSignV4ConfirmationState } from '../../../../../../../../../util/test/confirm-data-helpers'; | ||
import renderWithProvider from '../../../../../../../../../util/test/renderWithProvider'; | ||
import TypedSignDecoded, { getStateChangeToolip, getStateChangeType, StateChangeType } from './TypedSignDecoded'; | ||
|
||
const stateChangesApprove = [ | ||
{ | ||
assetType: 'ERC20', | ||
changeType: DecodingDataChangeType.Approve, | ||
address: '0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad', | ||
amount: '12345', | ||
contractAddress: '0x6b175474e89094c44da98b954eedeac495271d0f', | ||
}, | ||
]; | ||
|
||
const stateChangesListingERC1155: DecodingDataStateChanges = [ | ||
{ | ||
assetType: 'NATIVE', | ||
changeType: DecodingDataChangeType.Receive, | ||
address: '', | ||
amount: '900000000000000000', | ||
contractAddress: '', | ||
}, | ||
{ | ||
assetType: 'ERC1155', | ||
changeType: DecodingDataChangeType.Listing, | ||
address: '', | ||
amount: '', | ||
contractAddress: '0xafd4896984CA60d2feF66136e57f958dCe9482d5', | ||
tokenID: '77789', | ||
}, | ||
]; | ||
|
||
const stateChangesNftListing: DecodingDataStateChanges = [ | ||
{ | ||
assetType: 'ERC721', | ||
changeType: DecodingDataChangeType.Listing, | ||
address: '', | ||
amount: '', | ||
contractAddress: '0x922dC160f2ab743312A6bB19DD5152C1D3Ecca33', | ||
tokenID: '22222', | ||
}, | ||
{ | ||
assetType: 'ERC20', | ||
changeType: DecodingDataChangeType.Receive, | ||
address: '', | ||
amount: '950000000000000000', | ||
contractAddress: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', | ||
}, | ||
]; | ||
|
||
const stateChangesNftBidding: DecodingDataStateChanges = [ | ||
{ | ||
assetType: 'ERC20', | ||
changeType: DecodingDataChangeType.Bidding, | ||
address: '', | ||
amount: '', | ||
contractAddress: '0x922dC160f2ab743312A6bB19DD5152C1D3Ecca33', | ||
tokenID: '189', | ||
}, | ||
{ | ||
assetType: 'ERC721', | ||
changeType: DecodingDataChangeType.Receive, | ||
address: '', | ||
amount: '950000000000000000', | ||
contractAddress: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', | ||
}, | ||
]; | ||
|
||
const mockState = (mockStateChanges: DecodingDataStateChanges, stubDecodingLoading: boolean = false) => { | ||
const clonedMockState = cloneDeep(typedSignV4ConfirmationState); | ||
const request = clonedMockState.engine.backgroundState.SignatureController.signatureRequests['fb2029e1-b0ab-11ef-9227-05a11087c334'] as SignatureRequest; | ||
request.decodingLoading = stubDecodingLoading; | ||
request.decodingData = { | ||
stateChanges: mockStateChanges, | ||
}; | ||
|
||
return clonedMockState; | ||
}; | ||
|
||
describe('DecodedSimulation', () => { | ||
it('renders for ERC20 approval', async () => { | ||
const { getByText } = renderWithProvider(<TypedSignDecoded />, { | ||
state: mockState(stateChangesApprove), | ||
}); | ||
|
||
expect(await getByText('Estimated changes')).toBeDefined(); | ||
expect(await getByText('Spending cap')).toBeDefined(); | ||
expect(await getByText('12,345')).toBeDefined(); | ||
}); | ||
|
||
it('renders for ERC712 token', async () => { | ||
const { getByText } = renderWithProvider(<TypedSignDecoded />, { | ||
state: mockState(stateChangesNftListing), | ||
}); | ||
|
||
expect(await getByText('Estimated changes')).toBeDefined(); | ||
expect(await getByText('Listing price')).toBeDefined(); | ||
expect(await getByText('You list')).toBeDefined(); | ||
expect(await getByText('#22222')).toBeDefined(); | ||
}); | ||
|
||
it('renders for ERC1155 token', async () => { | ||
const { getByText } = renderWithProvider(<TypedSignDecoded />, { | ||
state: mockState(stateChangesListingERC1155), | ||
}); | ||
|
||
expect(await getByText('Estimated changes')).toBeDefined(); | ||
expect(await getByText('You receive')).toBeDefined(); | ||
expect(await getByText('You list')).toBeDefined(); | ||
expect(await getByText('#77789')).toBeDefined(); | ||
}); | ||
|
||
it('renders label only once if there are multiple state changes of same changeType', async () => { | ||
const { getAllByText } = renderWithProvider(<TypedSignDecoded />, { | ||
state: mockState([stateChangesApprove[0], stateChangesApprove[0], stateChangesApprove[0]]), | ||
}); | ||
|
||
expect(await getAllByText('12,345')).toHaveLength(3); | ||
expect(await getAllByText('Spending cap')).toHaveLength(1); | ||
}); | ||
|
||
it('renders unavailable message if no state change is returned', async () => { | ||
const { getByText } = renderWithProvider(<TypedSignDecoded />, { | ||
state: mockState([]), | ||
}); | ||
|
||
expect(await getByText('Estimated changes')).toBeDefined(); | ||
expect(await getByText('Unavailable')).toBeDefined(); | ||
}); | ||
|
||
describe('getStateChangeToolip', () => { | ||
it('return correct tooltip when permit is for listing NFT', () => { | ||
const tooltip = getStateChangeToolip( | ||
StateChangeType.NFTListingReceive, | ||
); | ||
expect(tooltip).toBe(strings('confirm.simulation.decoded_tooltip_list_nft')); | ||
}); | ||
|
||
it('return correct tooltip when permit is for bidding NFT', () => { | ||
const tooltip = getStateChangeToolip( | ||
StateChangeType.NFTBiddingReceive, | ||
); | ||
expect(tooltip).toBe(strings('confirm.simulation.decoded_tooltip_bid_nft')); | ||
}); | ||
}); | ||
|
||
describe('getStateChangeType', () => { | ||
it('return correct state change type for NFT listing receive', () => { | ||
const stateChange = getStateChangeType(stateChangesNftListing, stateChangesNftListing[1]); | ||
expect(stateChange).toBe(StateChangeType.NFTListingReceive); | ||
}); | ||
|
||
it('return correct state change type for NFT bidding receive', () => { | ||
const stateChange = getStateChangeType(stateChangesNftBidding, stateChangesNftBidding[1]); | ||
expect(stateChange).toBe(StateChangeType.NFTBiddingReceive); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.