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

feat: snaps controllers integration (Flask Only) #7942

Merged
merged 56 commits into from
Dec 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
08c7820
install snaps packages
owencraston Nov 29, 2023
27ca8dc
snaps logic form #7526
owencraston Nov 29, 2023
4d2546f
apply post-message-stream patch
owencraston Nov 29, 2023
4cd8e3c
fix USER_INTENT imports
owencraston Nov 30, 2023
b1422cc
code fencing
owencraston Dec 1, 2023
0d0b10e
Add EngineService change
owencraston Dec 1, 2023
708220c
snaps install approval flow
owencraston Dec 1, 2023
fc97d4f
Test ids
owencraston Dec 5, 2023
13276d1
fix file name for InstallSnapPermissionsRequest
owencraston Dec 5, 2023
67e75f9
SnapPermissions component
owencraston Dec 5, 2023
c46c4f7
strings
owencraston Dec 6, 2023
f202195
add SnapsController and SubjectMetaDataController to test state
owencraston Dec 6, 2023
3419d7c
SnapsSettings
owencraston Dec 6, 2023
816c509
cleanup
owencraston Dec 6, 2023
1d5fca3
losen up snaps package requirements
owencraston Dec 6, 2023
99628df
address feedback on engine file
owencraston Dec 7, 2023
d5154e0
migrate to new code fencing term
owencraston Dec 7, 2023
96a7133
ckeanup
owencraston Dec 7, 2023
6cd6b47
turn on allow list in main
owencraston Dec 7, 2023
5a3e808
remove duplicate snap http/local code
owencraston Dec 7, 2023
d2a6fb8
remove snapErrors from test state
owencraston Dec 7, 2023
3149fa0
fix requireAllowlist
owencraston Dec 7, 2023
d9f3928
registry as type URL
owencraston Dec 7, 2023
cc1e177
update metamask utils
owencraston Dec 7, 2023
cf5d76c
better typing for Engine file
owencraston Dec 7, 2023
c12bc36
typed Engine actions/events
owencraston Dec 8, 2023
ca99cae
even better types
owencraston Dec 8, 2023
dcce4cd
even better typing
owencraston Dec 8, 2023
d15c888
fix all type errors in Engine.ts
owencraston Dec 8, 2023
0daa002
resolve even more type errors
owencraston Dec 8, 2023
26be56c
update getSnapDerivationPathName
owencraston Dec 8, 2023
f7e2f71
check subject type
owencraston Dec 8, 2023
20d0289
conditional types
owencraston Dec 8, 2023
ca3ea16
Address jonathons comments
owencraston Dec 8, 2023
a4901dc
wrap all snaps code in fence
owencraston Dec 8, 2023
0a90500
fix SnapPermissions.test.tsx
owencraston Dec 9, 2023
7e4c44c
update specifications.test.js
owencraston Dec 9, 2023
9f21b0a
fix depcheck
owencraston Dec 9, 2023
04ad4aa
fix lint
owencraston Dec 9, 2023
5f7aaad
Fix npm.ts lint error
owencraston Dec 11, 2023
9fe508a
fix location.ts type error
owencraston Dec 11, 2023
938054e
fix engine lint
owencraston Dec 11, 2023
344ccbd
add events back to global type
owencraston Dec 11, 2023
3955e2b
Ignore window type and wrap remaining files in code fence
owencraston Dec 11, 2023
1317966
format location.ts
owencraston Dec 11, 2023
8d866aa
comment out localhost options
owencraston Dec 11, 2023
d7abbee
extract install styles into single file
owencraston Dec 12, 2023
e582e36
calculate snapName in parent component
owencraston Dec 12, 2023
22f7988
update tests to include snapName prop
owencraston Dec 12, 2023
036bf6e
check conditions before calculating the name
owencraston Dec 12, 2023
0d2c71d
address frederiks feedback
owencraston Dec 12, 2023
1bedcde
yarn dedupe
owencraston Dec 12, 2023
ad1fa63
disable default export in fenced file
owencraston Dec 12, 2023
f3903d1
Address most of Gutos comments
owencraston Dec 13, 2023
cf01a73
migrate to map in WebviewExecutionService
owencraston Dec 13, 2023
3a52b57
remove debug code and better approval types
owencraston Dec 13, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
///: BEGIN:ONLY_INCLUDE_IF(snaps)
export const SNAP_INSTALL_FLOW = 'snap-install-flow';
export const SNAP_INSTALL_OK = 'snap-install-ok';
///: END:ONLY_INCLUDE_IF
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
///: BEGIN:ONLY_INCLUDE_IF(snaps)
import { StyleSheet } from 'react-native';
import { Theme } from '../../../util/theme/models';
import Device from '../../../util/device';

/**
*
* @param params Style sheet params.
* @param params.theme App theme from ThemeContext.
* @param params.vars Inputs that the style sheet depends on.
* @returns StyleSheet object.
*/
const styleSheet = (params: { theme: Theme }) => {
const { theme } = params;
const { colors } = theme;
return StyleSheet.create({
root: {
backgroundColor: colors.background.default,
paddingTop: 24,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
minHeight: 200,
paddingBottom: Device.isIphoneX() ? 20 : 0,
},
accountCardWrapper: {
paddingHorizontal: 24,
},
actionContainer: {
flex: 0,
paddingVertical: 16,
justifyContent: 'center',
},
description: {
textAlign: 'center',
paddingBottom: 16,
},
snapCell: {
marginVertical: 16,
},
snapPermissionContainer: {
maxHeight: 300,
borderWidth: 1,
borderRadius: 8,
borderColor: colors.border.muted,
},
iconContainer: {
justifyContent: 'center',
alignItems: 'center',
},
iconWrapper: {
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: colors.success.muted,
justifyContent: 'center',
alignItems: 'center',
},
});
};

export default styleSheet;
///: END:ONLY_INCLUDE_IF
140 changes: 140 additions & 0 deletions app/components/Approvals/InstallSnapApproval/InstallSnapApproval.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
///: BEGIN:ONLY_INCLUDE_IF(snaps)
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 { SnapInstallState } from './InstallSnapApproval.types';
import {
InstallSnapConnectionRequest,
InstallSnapError,
InstallSnapPermissionsRequest,
InstallSnapSuccess,
} from './components';
import { SNAP_INSTALL_FLOW } from './InstallSnapApproval.constants';
import { ApprovalRequest } from '@metamask/approval-controller';

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 getSnapName = (request: ApprovalRequest<any>): string => {
// We first look for the name inside the snapId approvalRequest data
const snapId = request?.requestData?.snapId;
if (typeof snapId === 'string') {
const colonIndex = snapId.indexOf(':');
if (colonIndex !== -1) {
return snapId.substring(colonIndex + 1);
}
}
// If there is no snapId present in the approvalRequest data, we look for the name inside the snapIds caveat
const snapIdsCaveat =
request?.requestData?.permissions?.wallet_snap?.caveats?.find(
(c: any) => c.type === 'snapIds',
);
// return an empty string if we can't find the snap name in the approvalRequest data
return snapIdsCaveat?.value ? Object.keys(snapIdsCaveat.value)[0] : '';
};

if (!approvalRequest) return null;

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

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 snapName = getSnapName(approvalRequest);

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

const content = renderModalContent();

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

export default InstallSnapApproval;
///: END:ONLY_INCLUDE_IF
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
///: BEGIN:ONLY_INCLUDE_IF(snaps)
interface InstallSnapFlowProps {
approvalRequest: any;
snapName: string;
onConfirm: () => void;
onCancel: () => void;
chainId?: string;
error?: Error;
}

export enum SnapInstallState {
Confirm = 'Confirm',
AcceptPermissions = 'AcceptPermissions',
SnapInstalled = 'SnapInstalled',
SnapInstallError = 'SnapInstallError',
}

// eslint-disable-next-line import/prefer-default-export
export type { InstallSnapFlowProps };
///: END:ONLY_INCLUDE_IF
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
///: BEGIN:ONLY_INCLUDE_IF(snaps)
export const SNAP_INSTALL_CONNECTION_REQUEST =
'snap-install-connection-request';
export const SNAP_INSTALL_CANCEL = 'snap-install-cancel';
export const SNAP_INSTALL_CONNECT = 'snap-install-connect';
///: END:ONLY_INCLUDE_IF
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
///: BEGIN:ONLY_INCLUDE_IF(snaps)
import React, { useMemo } from 'react';
import { ImageSourcePropType, View } from 'react-native';
import { InstallSnapFlowProps } from '../../InstallSnapApproval.types';
import styleSheet from '../../InstallSnapApproval.styles';
import { strings } from '../../../../../../locales/i18n';
import SheetHeader from '../../../../../component-library/components/Sheet/SheetHeader';
import Text, {
TextVariant,
} from '../../../../../component-library/components/Texts/Text';
import TagUrl from '../../../../../component-library/components/Tags/TagUrl';
import { getUrlObj, prefixUrlWithProtocol } from '../../../../../util/browser';
import { IconName } from '../../../../../component-library/components/Icons/Icon';
import Cell, {
CellVariant,
} from '../../../../../component-library/components/Cells/Cell';
import { AvatarVariant } from '../../../../../component-library/components/Avatars/Avatar';
import {
ButtonSize,
ButtonVariants,
} from '../../../../../component-library/components/Buttons/Button';
import BottomSheetFooter, {
ButtonsAlignment,
} from '../../../../../component-library/components/BottomSheets/BottomSheetFooter';
import { ButtonProps } from '../../../../../component-library/components/Buttons/Button/Button.types';
import { useStyles } from '../../../../hooks/useStyles';
import {
SNAP_INSTALL_CANCEL,
SNAP_INSTALL_CONNECT,
SNAP_INSTALL_CONNECTION_REQUEST,
} from './InstallSnapConnectionRequest.constants';

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

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

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

const urlWithProtocol = prefixUrlWithProtocol(origin);

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

const cancelButtonProps: ButtonProps = {
variant: ButtonVariants.Secondary,
label: strings('accountApproval.cancel'),
size: ButtonSize.Lg,
onPress: onCancel,
testID: SNAP_INSTALL_CANCEL,
};

const connectButtonProps: ButtonProps = {
variant: ButtonVariants.Primary,
label: strings('accountApproval.connect'),
size: ButtonSize.Lg,
onPress: onConfirm,
testID: SNAP_INSTALL_CONNECT,
};

return (
<View testID={SNAP_INSTALL_CONNECTION_REQUEST} style={styles.root}>
<View style={styles.accountCardWrapper}>
<TagUrl
imageSource={favicon}
label={urlWithProtocol}
iconName={secureIcon}
/>
<SheetHeader title={strings('install_snap.title')} />
<Text style={styles.description} variant={TextVariant.BodyMD}>
{strings('install_snap.description', {
origin,
snap: snapName,
})}
</Text>
<Cell
style={styles.snapCell}
variant={CellVariant.Display}
title={snapName}
avatarProps={{
variant: AvatarVariant.Icon,
name: IconName.Snaps,
}}
/>
<View style={styles.actionContainer}>
<BottomSheetFooter
buttonsAlignment={ButtonsAlignment.Horizontal}
buttonPropsArray={[cancelButtonProps, connectButtonProps]}
/>
</View>
</View>
</View>
);
};

export default React.memo(InstallSnapConnectionRequest);
///: END:ONLY_INCLUDE_IF
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
///: BEGIN:ONLY_INCLUDE_IF(snaps)
/* eslint-disable import/prefer-default-export */
import InstallSnapConnectionRequest from './InstallSnapConnectionRequest';

export { InstallSnapConnectionRequest };
///: END:ONLY_INCLUDE_IF
Loading
Loading