Skip to content

Commit

Permalink
Revamp Snap connection screen
Browse files Browse the repository at this point in the history
  • Loading branch information
FrederikBolding committed Jul 1, 2024
1 parent f451c92 commit 5827b59
Show file tree
Hide file tree
Showing 16 changed files with 264 additions and 51 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
///: BEGIN:ONLY_INCLUDE_IF(external-snaps)
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-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
@@ -1,4 +1,4 @@
///: BEGIN:ONLY_INCLUDE_IF(external-snaps)
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
import { StyleSheet } from 'react-native';
import { Theme } from '../../../util/theme/models';
import Device from '../../../util/device';
Expand Down Expand Up @@ -37,6 +37,10 @@ const styleSheet = (params: { theme: Theme }) => {
snapCell: {
marginVertical: 16,
},
snapAvatar: {
alignSelf: 'center',
marginTop: 16,
},
snapPermissionContainer: {
maxHeight: 300,
borderWidth: 1,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,50 @@
///: BEGIN:ONLY_INCLUDE_IF(external-snaps)
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
import React, { useEffect, useState } from 'react';
import ApprovalModal from '../ApprovalModal';
import useApprovalRequest from '../../Views/confirmations/hooks/useApprovalRequest';
import { ApprovalTypes } from '../../../core/RPCMethods/RPCMethodMiddleware';
import Logger from '../../../util/Logger';
import { SnapInstallState } from './InstallSnapApproval.types';
import {
InstallSnapConnectionRequest,
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(external-snaps)
InstallSnapError,
InstallSnapPermissionsRequest,
InstallSnapSuccess,
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
} from './components';
import { SNAP_INSTALL_FLOW } from './InstallSnapApproval.constants';
import { ApprovalRequest } from '@metamask/approval-controller';
import { useSelector } from 'react-redux';
import { selectSnapsMetadata } from '../../../selectors/snaps/snapController';
import {
WALLET_SNAP_PERMISSION_KEY,
stripSnapPrefix,
} from '@metamask/snaps-utils';

const InstallSnapApproval = () => {
const snapsMetadata = useSelector(selectSnapsMetadata);

const [installState, setInstallState] = useState<
SnapInstallState | undefined
>(undefined);
const [isFinished, setIsFinished] = useState<boolean>(false);
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(external-snaps)
const [installError, setInstallError] = useState<Error | undefined>(
undefined,
);
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
const { approvalRequest, onConfirm, onReject } = useApprovalRequest();

useEffect(() => {
if (approvalRequest) {
if (approvalRequest.type === ApprovalTypes.REQUEST_PERMISSIONS) {
if (
approvalRequest.type === ApprovalTypes.REQUEST_PERMISSIONS &&
Object.keys(approvalRequest?.requestData?.permissions).includes(
WALLET_SNAP_PERMISSION_KEY,
)
) {
setInstallState(SnapInstallState.Confirm);
} else if (
approvalRequest.type === ApprovalTypes.INSTALL_SNAP &&
Expand All @@ -41,14 +59,11 @@ const InstallSnapApproval = () => {

// TODO: Replace "any" with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const getSnapName = (request: ApprovalRequest<any>): string => {
const getSnapId = (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);
}
return snapId;
}
// If there is no snapId present in the approvalRequest data, we look for the name inside the snapIds caveat
const snapIdsCaveat =
Expand All @@ -57,14 +72,19 @@ const InstallSnapApproval = () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(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] : '';
return Object.keys(snapIdsCaveat.value)[0];
};

const getSnapMetadata = (snapId: string) =>
snapsMetadata[snapId] ?? { name: stripSnapPrefix(snapId) };

if (!approvalRequest) return null;

///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(external-snaps)

const onInstallSnapFinished = () => {
setIsFinished(true);
setInstallState(SnapInstallState.SnapInstallFinished);
};

const onPermissionsConfirm = async () => {
Expand All @@ -75,34 +95,38 @@ const InstallSnapApproval = () => {
});
setInstallState(SnapInstallState.SnapInstalled);
} catch (error) {
Logger.error(
error as Error,
`${SNAP_INSTALL_FLOW} Failed to install snap`,
);
setInstallError(error as Error);
setInstallState(SnapInstallState.SnapInstallError);
}
};
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)

if (!approvalRequest) return null;
if (!approvalRequest || installState === undefined) return null;

const snapName = getSnapName(approvalRequest);
const snapId = getSnapId(approvalRequest);
const snapName = getSnapMetadata(snapId).name;

// TODO: This component should support connecting to multiple Snaps at once.
const renderModalContent = () => {
switch (installState) {
case SnapInstallState.Confirm:
return (
<InstallSnapConnectionRequest
approvalRequest={approvalRequest}
snapId={snapId}
snapName={snapName}
onConfirm={onConfirm}
onCancel={onReject}
/>
);
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(external-snaps)
case SnapInstallState.AcceptPermissions:
return (
<InstallSnapPermissionsRequest
approvalRequest={approvalRequest}
snapId={snapId}
snapName={snapName}
onConfirm={onPermissionsConfirm}
onCancel={onReject}
Expand All @@ -123,6 +147,8 @@ const InstallSnapApproval = () => {
error={installError}
/>
);
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
default:
return null;
}
Expand All @@ -132,7 +158,10 @@ const InstallSnapApproval = () => {

return content ? (
<ApprovalModal
isVisible={installState !== undefined && !isFinished}
isVisible={
installState !== undefined &&
installState !== SnapInstallState.SnapInstallFinished
}
onCancel={onReject}
>
{content}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
///: BEGIN:ONLY_INCLUDE_IF(external-snaps)
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
interface InstallSnapFlowProps {
// TODO: Replace "any" with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
approvalRequest: any;
snapId: string;
snapName: string;
onConfirm: () => void;
onCancel: () => void;
Expand All @@ -15,6 +16,7 @@ export enum SnapInstallState {
AcceptPermissions = 'AcceptPermissions',
SnapInstalled = 'SnapInstalled',
SnapInstallError = 'SnapInstallError',
SnapInstallFinished = 'SnapInstallFinished',
}

// eslint-disable-next-line import/prefer-default-export
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
///: BEGIN:ONLY_INCLUDE_IF(external-snaps)
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
import React, { useMemo } from 'react';
import { ImageSourcePropType, View } from 'react-native';
import { View } from 'react-native';
import { InstallSnapFlowProps } from '../../InstallSnapApproval.types';
import styleSheet from '../../InstallSnapApproval.styles';
import { strings } from '../../../../../../locales/i18n';
Expand All @@ -11,10 +11,6 @@ import 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,
Expand All @@ -29,15 +25,18 @@ import {
SNAP_INSTALL_CONNECT,
SNAP_INSTALL_CONNECTION_REQUEST,
} from './InstallSnapConnectionRequest.constants';
import { useFavicon } from '../../../../hooks/useFavicon';
import { SnapAvatar } from '../../../../UI/Snaps/SnapAvatar/SnapAvatar';

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

Expand All @@ -46,10 +45,7 @@ const InstallSnapConnectionRequest = ({
[approvalRequest.origin],
);

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

const urlWithProtocol = prefixUrlWithProtocol(origin);

Expand Down Expand Up @@ -85,22 +81,18 @@ const InstallSnapConnectionRequest = ({
label={urlWithProtocol}
iconName={secureIcon}
/>
<SnapAvatar
snapId={snapId}
snapName={snapName}
style={styles.snapAvatar}
/>
<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}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
///: BEGIN:ONLY_INCLUDE_IF(external-snaps)
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
/* eslint-disable import/prefer-default-export */
import InstallSnapConnectionRequest from './InstallSnapConnectionRequest';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
///: BEGIN:ONLY_INCLUDE_IF(external-snaps)
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import InstallSnapConnectionRequest from '../InstallSnapConnectionRequest';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
///: BEGIN:ONLY_INCLUDE_IF(external-snaps)
/* eslint-disable import/prefer-default-export */
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps)
import { InstallSnapConnectionRequest } from './InstallSnapConnectionRequest';
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(external-snaps)
import { InstallSnapSuccess } from './InstallSnapSuccess';
import { InstallSnapError } from './InstallSnapError';
import { InstallSnapPermissionsRequest } from './InstallSnapPermissionsRequest';
///: END:ONLY_INCLUDE_IF

///: BEGIN:ONLY_INCLUDE_IF(external-snaps)
export { InstallSnapPermissionsRequest };
export { InstallSnapError };
export { InstallSnapSuccess };
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps)
export { InstallSnapConnectionRequest };
///: END:ONLY_INCLUDE_IF
2 changes: 1 addition & 1 deletion app/components/Approvals/InstallSnapApproval/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
///: BEGIN:ONLY_INCLUDE_IF(external-snaps)
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
export { default } from './InstallSnapApproval';
///: END:ONLY_INCLUDE_IF
4 changes: 2 additions & 2 deletions app/components/Nav/Main/RootRPCMethodsUI.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ import { selectShouldUseSmartTransaction } from '../../../selectors/smartTransac
import { STX_NO_HASH_ERROR } from '../../../util/smart-transactions/smart-publish-hook';
import { getSmartTransactionMetricsProperties } from '../../../util/smart-transactions';

///: BEGIN:ONLY_INCLUDE_IF(external-snaps)
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
import InstallSnapApproval from '../../Approvals/InstallSnapApproval';
///: END:ONLY_INCLUDE_IF

Expand Down Expand Up @@ -487,7 +487,7 @@ const RootRPCMethodsUI = (props) => {
<FlowLoaderModal />
<TemplateConfirmationModal />
{
///: BEGIN:ONLY_INCLUDE_IF(external-snaps)
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
}
<InstallSnapApproval />
{
Expand Down
39 changes: 39 additions & 0 deletions app/components/UI/Snaps/SnapAvatar/SnapAvatar.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
import { StyleSheet } from 'react-native';
import { Theme } from '../../../../util/theme/models';

/**
*
* @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({
avatar: {
backgroundColor: colors.background.alternativeHover,
},
fallbackAvatar: {
backgroundColor: colors.background.alternativeHover,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: colors.text.alternative,
},
fallbackAvatarText: {
textTransform: 'uppercase',
},
badge: {
backgroundColor: colors.info.default,
color: colors.info.inverse,
borderColor: colors.background.alternative,
borderWidth: 2,
},
});
};

export default styleSheet;
///: END:ONLY_INCLUDE_IF
Loading

0 comments on commit 5827b59

Please sign in to comment.