Skip to content

Commit

Permalink
feat(snaps): Add custom dialog (#26304)
Browse files Browse the repository at this point in the history
## **Description**

This PR adds the support for custom dialogs.

Major changes:
- Add support for `Footer` and `Container` custom UI components
- Refactor `snap-ui-renderer` to handle display of the snap header
- Add the new `snap_resolveInterface` snap RPC method
- Add the new `default` dialog type

[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/26304?quickstart=1)

## **Related issues**

Fixes:

## **Manual testing steps**

1. Go to test-snaps
2. trigger the `custom` snap dialog

## **Screenshots/Recordings**

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

### **Before**

<!-- [screenshots/recordings] -->

### **After**

<!-- [screenshots/recordings] -->

## **Pre-merge author checklist**

- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.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-extension/blob/develop/.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: Maarten Zuidhoorn <maarten@zuidhoorn.com>
Co-authored-by: Frederik Bolding <frederik.bolding@gmail.com>
  • Loading branch information
3 people authored Aug 13, 2024
1 parent 0c2a55c commit a1d3b3c
Show file tree
Hide file tree
Showing 26 changed files with 599 additions and 210 deletions.
2 changes: 2 additions & 0 deletions app/scripts/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { ApprovalType } from '@metamask/controller-utils';
import PortStream from 'extension-port-stream';

import { ethErrors } from 'eth-rpc-errors';
import { DIALOG_APPROVAL_TYPES } from '@metamask/snaps-rpc-methods';
import {
ENVIRONMENT_TYPE_POPUP,
ENVIRONMENT_TYPE_NOTIFICATION,
Expand Down Expand Up @@ -1102,6 +1103,7 @@ export function setupController(
switch (type) {
case ApprovalType.SnapDialogAlert:
case ApprovalType.SnapDialogPrompt:
case DIALOG_APPROVAL_TYPES.default:
controller.approvalController.accept(id, null);
break;
case ApprovalType.SnapDialogConfirmation:
Expand Down
1 change: 1 addition & 0 deletions app/scripts/controllers/permissions/specifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,7 @@ export const unrestrictedMethods = Object.freeze([
'snap_createInterface',
'snap_updateInterface',
'snap_getInterfaceState',
'snap_resolveInterface',
///: BEGIN:ONLY_INCLUDE_IF(build-mmi)
'metamaskinstitutional_authenticate',
'metamaskinstitutional_reauthenticate',
Expand Down
7 changes: 7 additions & 0 deletions app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -1404,6 +1404,8 @@ export default class MetamaskController extends EventEmitter {
allowedActions: [
`${this.phishingController.name}:maybeUpdateState`,
`${this.phishingController.name}:testOrigin`,
`${this.approvalController.name}:hasRequest`,
`${this.approvalController.name}:acceptRequest`,
],
});

Expand Down Expand Up @@ -5633,6 +5635,11 @@ export default class MetamaskController extends EventEmitter {
'SnapInterfaceController:updateInterface',
origin,
),
resolveInterface: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapInterfaceController:resolveInterface',
origin,
),
getSnap: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapController:get',
Expand Down
6 changes: 5 additions & 1 deletion development/build/scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -929,7 +929,11 @@ function setupBundlerDefaults(
[
babelify,
{
only: ['./**/node_modules/firebase', './**/node_modules/@firebase'],
only: [
'./**/node_modules/firebase',
'./**/node_modules/@firebase',
'./**/node_modules/marked',
],
global: true,
},
],
Expand Down
15 changes: 5 additions & 10 deletions shared/constants/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DialogType } from '@metamask/snaps-sdk';
import { DIALOG_APPROVAL_TYPES } from '@metamask/snaps-rpc-methods';
import { RestrictedMethods } from './permissions';

/**
Expand Down Expand Up @@ -48,9 +48,10 @@ export const MESSAGE_TYPE = {
WALLET_REQUEST_PERMISSIONS: 'wallet_requestPermissions',
WATCH_ASSET: 'wallet_watchAsset',
WATCH_ASSET_LEGACY: 'metamask_watchAsset',
SNAP_DIALOG_ALERT: `${RestrictedMethods.snap_dialog}:alert`,
SNAP_DIALOG_CONFIRMATION: `${RestrictedMethods.snap_dialog}:confirmation`,
SNAP_DIALOG_PROMPT: `${RestrictedMethods.snap_dialog}:prompt`,
SNAP_DIALOG_ALERT: DIALOG_APPROVAL_TYPES.alert,
SNAP_DIALOG_CONFIRMATION: DIALOG_APPROVAL_TYPES.confirmation,
SNAP_DIALOG_PROMPT: DIALOG_APPROVAL_TYPES.prompt,
SNAP_DIALOG_DEFAULT: DIALOG_APPROVAL_TYPES.default,
///: BEGIN:ONLY_INCLUDE_IF(build-mmi)
MMI_AUTHENTICATE: 'metamaskinstitutional_authenticate',
MMI_REAUTHENTICATE: 'metamaskinstitutional_reauthenticate',
Expand All @@ -64,12 +65,6 @@ export const MESSAGE_TYPE = {
///: END:ONLY_INCLUDE_IF
} as const;

export const SNAP_DIALOG_TYPES = {
[DialogType.Alert]: MESSAGE_TYPE.SNAP_DIALOG_ALERT,
[DialogType.Confirmation]: MESSAGE_TYPE.SNAP_DIALOG_CONFIRMATION,
[DialogType.Prompt]: MESSAGE_TYPE.SNAP_DIALOG_PROMPT,
};

///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
export const SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES = {
confirmAccountCreation: 'snap_manageAccounts:confirmAccountCreation',
Expand Down
1 change: 1 addition & 0 deletions ui/components/app/app-components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
@import 'snaps/show-more/index';
@import 'snaps/insight-warnings/index';
@import 'snaps/snap-authorship-header/index';
@import 'snaps/snap-footer-button/index';
@import 'hold-to-reveal-button/index';
@import 'home-notification/index';
@import 'info-box/index';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import TextField from '../../ui/text-field';
import ConfirmationNetworkSwitch from '../../../pages/confirmations/confirmation/components/confirmation-network-switch';
import UrlIcon from '../../ui/url-icon';
import Tooltip from '../../ui/tooltip/tooltip';
import { AvatarIcon, Text } from '../../component-library';
import { AvatarIcon, FormTextField, Text } from '../../component-library';
import ActionableMessage from '../../ui/actionable-message/actionable-message';
import { AccountListItem } from '../../multichain';
import {
Expand All @@ -35,6 +35,7 @@ import { SnapUIDropdown } from '../snaps/snap-ui-dropdown';
import { SnapUIRadioGroup } from '../snaps/snap-ui-radio-group';
import { SnapUICheckbox } from '../snaps/snap-ui-checkbox';
import { SnapUITooltip } from '../snaps/snap-ui-tooltip';
import { SnapFooterButton } from '../snaps/snap-footer-button';
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
import { SnapAccountSuccessMessage } from '../../../pages/confirmations/components/snap-account-success-message';
import { SnapAccountErrorMessage } from '../../../pages/confirmations/components/snap-account-error-message';
Expand Down Expand Up @@ -91,6 +92,8 @@ export const safeComponentList = {
SnapUIRadioGroup,
SnapUICheckbox,
SnapUITooltip,
SnapFooterButton,
FormTextField,
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
CreateSnapAccount,
RemoveSnapAccount,
Expand Down
53 changes: 4 additions & 49 deletions ui/components/app/snaps/snap-avatar/snap-avatar.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,20 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { useSelector } from 'react-redux';

import {
TextColor,
IconColor,
AlignItems,
Display,
JustifyContent,
BackgroundColor,
} from '../../../../helpers/constants/design-system';
import {
AvatarFavicon,
BadgeWrapper,
BadgeWrapperPosition,
AvatarIcon,
AvatarBase,
IconName,
IconSize,
} from '../../../component-library';
import {
getSnapMetadata,
getTargetSubjectMetadata,
} from '../../../../selectors';
import { getAvatarFallbackLetter } from '../../../../helpers/utils/util';

import { SnapIcon } from '../snap-icon';

const SnapAvatar = ({
snapId,
Expand All @@ -33,19 +24,6 @@ const SnapAvatar = ({
className,
badgeBackgroundColor = BackgroundColor.backgroundAlternative,
}) => {
const subjectMetadata = useSelector((state) =>
getTargetSubjectMetadata(state, snapId),
);

const { name: snapName } = useSelector((state) =>
getSnapMetadata(state, snapId),
);

const iconUrl = subjectMetadata?.iconUrl;

// We choose the first non-symbol char as the fallback icon.
const fallbackIcon = getAvatarFallbackLetter(snapName);

return (
<BadgeWrapper
className={classnames('snap-avatar', className)}
Expand All @@ -63,30 +41,7 @@ const SnapAvatar = ({
}
position={BadgeWrapperPosition.bottomRight}
>
{iconUrl ? (
<AvatarFavicon
style={{
'background-color': 'var(--color-background-alternative-hover)',
}}
size={avatarSize}
src={iconUrl}
name={snapName}
/>
) : (
<AvatarBase
size={avatarSize}
display={Display.Flex}
alignItems={AlignItems.center}
justifyContent={JustifyContent.center}
color={TextColor.textAlternative}
style={{
borderWidth: '0px',
'background-color': 'var(--color-background-alternative-hover)',
}}
>
{fallbackIcon}
</AvatarBase>
)}
<SnapIcon snapId={snapId} avatarSize={avatarSize} />
</BadgeWrapper>
);
};
Expand Down
45 changes: 45 additions & 0 deletions ui/components/app/snaps/snap-footer-button/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
.snap-footer-button {
&.mm-button-primary {
box-shadow: none;
color: var(--color-text-alternative);
background-color: var(--color-icon-default);

&:hover:not(&--disabled) {
opacity: 80%;
box-shadow: none;
}

&:active:not(&--disabled) {
opacity: 60%;
}
}

&.mm-button-secondary {
border-color: var(--color-icon-default);
color: var(--color-icon-default);

& span {
color: var(--color-icon-default);
}

&:hover:not(&--disabled) {
border-color: var(--color-icon-default);
color: var(--color-background-default);
background-color: var(--color-background-default-hover);
box-shadow: none;

& span {
color: var(--color-icon-default);
}
}

&:active:not(&--disabled) {
border-color: var(--color-icon-default);
opacity: 60%;

& span {
color: var(--color-icon-default);
}
}
}
}
1 change: 1 addition & 0 deletions ui/components/app/snaps/snap-footer-button/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './snap-footer-button';
58 changes: 58 additions & 0 deletions ui/components/app/snaps/snap-footer-button/snap-footer-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, { FunctionComponent, MouseEvent as ReactMouseEvent } from 'react';
import { ButtonVariant, UserInputEventType } from '@metamask/snaps-sdk';
import { Button, ButtonSize, IconSize } from '../../../component-library';
import {
AlignItems,
Display,
FlexDirection,
} from '../../../../helpers/constants/design-system';
import { useSnapInterfaceContext } from '../../../../contexts/snaps';
import { SnapIcon } from '../snap-icon';

type SnapFooterButtonProps = {
name?: string;
isSnapAction?: boolean;
onCancel?: () => void;
};

export const SnapFooterButton: FunctionComponent<SnapFooterButtonProps> = ({
onCancel,
name,
children,
isSnapAction = false,
...props
}) => {
const { handleEvent, snapId } = useSnapInterfaceContext();

const handleSnapAction = (event: ReactMouseEvent<HTMLElement>) => {
event.preventDefault();

handleEvent({
event: UserInputEventType.ButtonClickEvent,
name,
});
};

const handleClick = isSnapAction ? handleSnapAction : onCancel;

return (
<Button
className="snap-footer-button"
{...props}
size={ButtonSize.Lg}
block
variant={isSnapAction ? ButtonVariant.Primary : ButtonVariant.Secondary}
onClick={handleClick}
textProps={{
display: Display.Flex,
alignItems: AlignItems.center,
flexDirection: FlexDirection.Row,
}}
>
{isSnapAction && (
<SnapIcon snapId={snapId} avatarSize={IconSize.Xs} marginRight={2} />
)}
{children}
</Button>
);
};
1 change: 1 addition & 0 deletions ui/components/app/snaps/snap-icon/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './snap-icon';
78 changes: 78 additions & 0 deletions ui/components/app/snaps/snap-icon/snap-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React, { FunctionComponent } from 'react';

import { useSelector } from 'react-redux';
import {
getSnapMetadata,
getTargetSubjectMetadata,
} from '../../../../selectors';
import { getAvatarFallbackLetter } from '../../../../helpers/utils/util';
import {
AvatarBase,
AvatarBaseSize,
AvatarFavicon,
AvatarFaviconProps,
AvatarFaviconSize,
IconSize,
} from '../../../component-library';
import {
AlignItems,
BackgroundColor,
Display,
JustifyContent,
TextColor,
} from '../../../../helpers/constants/design-system';

type SnapIconProps = {
snapId: string;
avatarSize?: IconSize;
borderWidth?: number;
className?: string;
badgeBackgroundColor?: BackgroundColor;
} & Omit<AvatarFaviconProps<'span'>, 'name'>;

export const SnapIcon: FunctionComponent<SnapIconProps> = ({
snapId,
avatarSize = IconSize.Lg,
...props
}) => {
const subjectMetadata = useSelector((state) =>
getTargetSubjectMetadata(state, snapId),
);

const { name: snapName } = useSelector((state) =>
/* @ts-expect-error wrong type on selector. */
getSnapMetadata(state, snapId),
);

const iconUrl = subjectMetadata?.iconUrl;

// We choose the first non-symbol char as the fallback icon.
const fallbackIcon = getAvatarFallbackLetter(snapName);

return iconUrl ? (
<AvatarFavicon
style={{
backgroundColor: 'var(--color-background-alternative-hover)',
}}
src={iconUrl}
name={snapName}
{...props}
size={avatarSize as unknown as AvatarFaviconSize}
/>
) : (
<AvatarBase
display={Display.Flex}
alignItems={AlignItems.center}
justifyContent={JustifyContent.center}
color={TextColor.textAlternative}
style={{
borderWidth: '0px',
backgroundColor: 'var(--color-background-alternative-hover)',
}}
{...props}
size={avatarSize as unknown as AvatarBaseSize}
>
{fallbackIcon}
</AvatarBase>
);
};
Loading

0 comments on commit a1d3b3c

Please sign in to comment.