Skip to content

Commit

Permalink
[Mobile snaps] Update snaps packages to version 2.0.2 (#7609)
Browse files Browse the repository at this point in the history
## **Description**

Update snaps controller to version 2.0.2. 

This also involved updating some of the peer dependancies such as
`@metamask/slip44`, `@metamask/snaps-utils"`, `@metamask/rpc-methods`.

Updated the snaps permissions titles to include the newer permissions.
The list of permissions can be found
[here](https://www.notion.so/bac3299d2c5241c599d2e5e7986e72f7?v=ef742a61bd844435b7171bd2e90b447e).

This changed how snap are installed now...

Before:
- installing a snap would be would trigger a `wallet_installSnap`
approval. From this point we would parse the approval request data to
show the requested permissions. We would then handle the install state
internally.

Now:
- Installing a a snap triggers a `wallet_requestPermissions` approval
type. For this we show the `InstallSnapConnectionRequest`. Approving
this triggers a second approval request of type `wallet_installSnap`.
This is where we show the requested permissions inside the
`InstallSnapPermissionsRequest`. Once approved we show a success/error
screen


Since the install logic relies so heavily on the the
`useApprovalRequest` response, I moved all of the install logic from
`app/components/UI/...` into
`app/components/Approvals/InstallSnapApproval`

## **Related issues**

Progresses: MetaMask/accounts-planning#116
Fixes: MetaMask/accounts-planning#115
Fixes: MetaMask/accounts-planning#114

## **Manual testing steps**

1. build branch
2. run `yarn setup`
3. navigate to browser tab
4. go to http://metamask.github.io/snaps/test-snaps/1.1.0
5. install BIP-32 snap
6. go through approval flow
7. the snap should be installed
8. navigate to settings/snaps
9. the snap should be available in settings
10. you should be able to preview all of the snaps permissions in the
snap settings

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->


https://github.com/MetaMask/metamask-mobile/assets/22918444/317d7eca-c0b4-44d1-a60f-e6f1352331c3




## **Pre-merge author checklist**

- [ ] I’ve followed [MetaMask Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've clearly explained what problem this PR is solving and how it
is solved.
- [ ] I've linked related issues
- [ ] I've included manual testing steps
- [ ] I've included screenshots/recordings if applicable
- [ ] 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)).
- [ ] I’ve properly set the pull request status:
  - [ ] In case it's not yet "ready for review", I've set it to "draft".
- [ ] In case it's "ready for review", I've changed it from "draft" to
"non-draft".

## **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: Frederik Bolding <[email protected]>
  • Loading branch information
owencraston and FrederikBolding authored Nov 7, 2023
1 parent 2e82b71 commit 49cc2ec
Show file tree
Hide file tree
Showing 45 changed files with 885 additions and 1,143 deletions.
114 changes: 98 additions & 16 deletions app/components/Approvals/InstallSnapApproval/InstallSnapApproval.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,113 @@
import React, { useState } from 'react';
import { InstallSnapApprovalFlow } from '../../UI/InstallSnapApprovalFlow';
import React, { useEffect, useState } from 'react';
import ApprovalModal from '../ApprovalModal';
import useApprovalRequest from '../../hooks/useApprovalRequest';
import { ApprovalTypes } from '../../../core/RPCMethods/RPCMethodMiddleware';
import Logger from '../../../util/Logger';
import { SNAP_INSTALL_FLOW } from '../../../constants/test-ids';
import { SnapInstallState } from './InstallSnapApproval.types';
import {
InstallSnapConnectionRequest,
InstallSnapError,
InstallSnapPermissionsRequest,
InstallSnapSuccess,
} from './components';

const InstallSnapApproval = () => {
const [installState, setInstallState] = useState<
SnapInstallState | undefined
>(undefined);
const [isFinished, setIsFinished] = useState<boolean>(false);
const [installError, setInstallError] = useState<Error | undefined>(
undefined,
);
const { approvalRequest, onConfirm, onReject } = useApprovalRequest();

useEffect(() => {
if (approvalRequest) {
if (approvalRequest.type === ApprovalTypes.REQUEST_PERMISSIONS) {
setInstallState(SnapInstallState.Confirm);
} else if (
approvalRequest.type === ApprovalTypes.INSTALL_SNAP &&
approvalRequest.requestState.permissions
) {
setInstallState(SnapInstallState.AcceptPermissions);
}
} else {
setInstallState(undefined);
}
}, [approvalRequest]);

const onInstallSnapFinished = () => {
setIsFinished(true);
};

const isModalVisible =
approvalRequest?.type === ApprovalTypes.INSTALL_SNAP ||
(!isFinished && approvalRequest);

return (
<ApprovalModal isVisible={isModalVisible} onCancel={onReject}>
<InstallSnapApprovalFlow
onCancel={onReject}
onConfirm={onConfirm}
onFinish={onInstallSnapFinished}
requestData={approvalRequest}
/>
const onPermissionsConfirm = async () => {
try {
await onConfirm(undefined, {
...approvalRequest.requestData,
permissions: approvalRequest.requestState.permissions,
});
setInstallState(SnapInstallState.SnapInstalled);
} catch (error) {
Logger.error(
error as Error,
`${SNAP_INSTALL_FLOW} Failed to install snap`,
);
setInstallError(error as Error);
setInstallState(SnapInstallState.SnapInstallError);
}
};

if (!approvalRequest) return null;

const renderModalContent = () => {
switch (installState) {
case SnapInstallState.Confirm:
return (
<InstallSnapConnectionRequest
approvalRequest={approvalRequest}
onConfirm={onConfirm}
onCancel={onReject}
/>
);
case SnapInstallState.AcceptPermissions:
return (
<InstallSnapPermissionsRequest
approvalRequest={approvalRequest}
onConfirm={onPermissionsConfirm}
onCancel={onReject}
/>
);
case SnapInstallState.SnapInstalled:
return (
<InstallSnapSuccess
approvalRequest={approvalRequest}
onConfirm={onInstallSnapFinished}
/>
);
case SnapInstallState.SnapInstallError:
return (
<InstallSnapError
approvalRequest={approvalRequest}
onConfirm={onInstallSnapFinished}
error={installError}
/>
);
default:
return null;
}
};

const content = renderModalContent();

return content ? (
<ApprovalModal
isVisible={installState !== undefined && !isFinished}
onCancel={onReject}
>
{content}
</ApprovalModal>
);
) : null;
};

export default React.memo(InstallSnapApproval);
export default InstallSnapApproval;
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
interface InstallSnapApprovalArgs {
requestData: any;
onConfirm: () => void;
onFinish: () => void;
onCancel: () => void;
chainId?: string;
}

interface InstallSnapFlowProps {
requestData: any;
approvalRequest: any;
onConfirm: () => void;
onCancel: () => void;
chainId?: string;
Expand All @@ -22,4 +14,4 @@ export enum SnapInstallState {
}

// eslint-disable-next-line import/prefer-default-export
export type { InstallSnapApprovalArgs, InstallSnapFlowProps };
export type { InstallSnapFlowProps };
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useMemo } from 'react';
import { ImageSourcePropType, View } from 'react-native';
import { InstallSnapFlowProps } from '../../InstallSnapApprovalFlow.types';
import { InstallSnapFlowProps } from '../../InstallSnapApproval.types';
import styleSheet from './InstallSnapConnectionRequest.styles';
import { strings } from '../../../../../../locales/i18n';
import {
Expand Down Expand Up @@ -30,38 +30,41 @@ import { ButtonProps } from '../../../../../component-library/components/Buttons
import { useStyles } from '../../../../hooks/useStyles';

const InstallSnapConnectionRequest = ({
requestData,
approvalRequest,
onConfirm,
onCancel,
}: InstallSnapFlowProps) => {
}: Pick<
InstallSnapFlowProps,
'approvalRequest' | 'onConfirm' | 'onCancel'
>) => {
const { styles } = useStyles(styleSheet, {});

const snapName = useMemo(() => {
const colonIndex = requestData.snapId.indexOf(':');
if (colonIndex !== -1) {
return requestData.snapId.substring(colonIndex + 1);
}
return requestData.snapId;
}, [requestData.snapId]);
const snapName: string | null =
Object.keys(
approvalRequest?.requestData?.permissions?.wallet_snap?.caveats?.find(
(c: { type: string; value: Record<string, any> }) =>
c.type === 'snapIds',
)?.value ?? {},
)[0] || null;

const dappOrigin = useMemo(
() => requestData.metadata.dappOrigin,
[requestData.metadata.dappOrigin],
const origin = useMemo(
() => approvalRequest.origin,
[approvalRequest.origin],
);

const favicon: ImageSourcePropType = useMemo(() => {
const iconUrl = `https://api.faviconkit.com/${dappOrigin}/50`;
const iconUrl = `https://api.faviconkit.com/${origin}/50`;
return { uri: iconUrl };
}, [dappOrigin]);
}, [origin]);

const urlWithProtocol = prefixUrlWithProtocol(dappOrigin);
const urlWithProtocol = prefixUrlWithProtocol(origin);

const secureIcon = useMemo(
() =>
(getUrlObj(dappOrigin) as URL).protocol === 'https:'
getUrlObj(origin).protocol === 'https:'
? IconName.Lock
: IconName.LockSlash,
[dappOrigin],
[origin],
);

const cancelButtonProps: ButtonProps = {
Expand Down Expand Up @@ -91,14 +94,14 @@ const InstallSnapConnectionRequest = ({
<SheetHeader title={strings('install_snap.title')} />
<Text style={styles.description} variant={TextVariant.BodyMD}>
{strings('install_snap.description', {
origin: dappOrigin,
origin,
snap: snapName,
})}
</Text>
<Cell
style={styles.snapCell}
variant={CellVariant.Display}
title={snapName}
title={snapName ?? ''}
avatarProps={{
variant: AvatarVariants.Icon,
name: IconName.Snaps,
Expand All @@ -115,4 +118,4 @@ const InstallSnapConnectionRequest = ({
);
};

export default InstallSnapConnectionRequest;
export default React.memo(InstallSnapConnectionRequest);
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,33 @@ import {
} from '../../../../../../constants/test-ids';

describe('InstallSnapConnectionRequest', () => {
const requestData = {
metadata: {
id: 'uNadWHqPnwOM4NER3mERI',
origin: 'npm:@lavamoat/tss-snap',
dappOrigin: 'tss.ac',
},
permissions: {
snap_manageState: {},
'endowment:rpc': {
caveats: [
{
type: 'rpcOrigin',
value: {
dapps: true,
snaps: true,
const requestPermissionsData = {
id: 'jUU9-fsMO1dkSAKdKxiK_',
origin: 'metamask.github.io',
type: 'wallet_requestPermissions',
time: 1699041698637,
requestData: {
metadata: {
id: 'jUU9-fsMO1dkSAKdKxiK_',
origin: 'metamask.github.io',
},
permissions: {
wallet_snap: {
caveats: [
{
type: 'snapIds',
value: {
'npm:@metamask/bip32-example-snap': {
version: '1.0.0',
},
},
},
},
],
],
},
},
},
snapId: 'npm:@lavamoat/tss-snap',
requestState: null,
expectsResult: false,
};

const onConfirm = jest.fn();
Expand All @@ -41,7 +47,7 @@ describe('InstallSnapConnectionRequest', () => {
it('renders correctly', () => {
const { getByTestId } = render(
<InstallSnapConnectionRequest
requestData={requestData}
approvalRequest={requestPermissionsData}
onConfirm={onConfirm}
onCancel={onCancel}
/>,
Expand All @@ -52,7 +58,7 @@ describe('InstallSnapConnectionRequest', () => {
it('calls onConfirm when the connect button is pressed', () => {
const { getByTestId } = render(
<InstallSnapConnectionRequest
requestData={requestData}
approvalRequest={requestPermissionsData}
onConfirm={onConfirm}
onCancel={onCancel}
/>,
Expand All @@ -65,7 +71,7 @@ describe('InstallSnapConnectionRequest', () => {
it('calls onCancel when the cancel button is pressed', () => {
const { getByTestId } = render(
<InstallSnapConnectionRequest
requestData={requestData}
approvalRequest={requestPermissionsData}
onConfirm={onConfirm}
onCancel={onCancel}
/>,
Expand All @@ -78,13 +84,13 @@ describe('InstallSnapConnectionRequest', () => {
it('correctly prefixes dappOrigin with protocol', () => {
const { getByText } = render(
<InstallSnapConnectionRequest
requestData={requestData}
approvalRequest={requestPermissionsData}
onConfirm={onConfirm}
onCancel={onCancel}
/>,
);

const expectedUrl = 'https://tss.ac';
const expectedUrl = 'https://metamask.github.io';
expect(getByText(expectedUrl)).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,22 @@ import BottomSheetFooter, {
} from '../../../../../component-library/components/BottomSheets/BottomSheetFooter';
import { ButtonProps } from '../../../../../component-library/components/Buttons/Button/Button.types';
import { useStyles } from '../../../../hooks/useStyles';
import { InstallSnapFlowProps } from '../../InstallSnapApprovalFlow.types';
import { InstallSnapFlowProps } from '../../InstallSnapApproval.types';

const InstallSnapError = ({
requestData,
approvalRequest,
onConfirm,
error,
}: InstallSnapFlowProps) => {
}: Pick<InstallSnapFlowProps, 'approvalRequest' | 'onConfirm' | 'error'>) => {
const { styles } = useStyles(stylesheet, {});

const snapName = useMemo(() => {
const colonIndex = requestData.snapId.indexOf(':');
const colonIndex = approvalRequest.requestData.snapId.indexOf(':');
if (colonIndex !== -1) {
return requestData.snapId.substring(colonIndex + 1);
return approvalRequest.requestData.snapId.substring(colonIndex + 1);
}
return requestData.snapId;
}, [requestData.snapId]);
return approvalRequest.requestData.snapId;
}, [approvalRequest.requestData.snapId]);

const okButtonProps: ButtonProps = {
variant: ButtonVariants.Primary,
Expand Down Expand Up @@ -97,4 +97,4 @@ const InstallSnapError = ({
);
};

export default InstallSnapError;
export default React.memo(InstallSnapError);
Loading

0 comments on commit 49cc2ec

Please sign in to comment.