Skip to content

Commit

Permalink
Interactive Interfaces (#22795)
Browse files Browse the repository at this point in the history
## **Description**

This PR integrates the necessary changes for interactive Interfaces.

- Setup and update the various controllers.
- Update the `SnapUIRenderer` to handle an `interfaceId` rather than
direct content.
- Add the logic for interface state and event handling in a new
`InterfaceContext` react context.
- Update the different APIs to handle the newly returned values.
- Move the UI components mapping out of the `SnapUIRenderer` file
- Rework confirmations to take the `SnapUIRenderer`.

## **Related issues**

## **Manual testing steps**

1. Fire up the interactive UI test snap [coming soon]
2. Play with the snap

## **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 Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.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-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
- [ ] 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 <frederik.bolding@gmail.com>
  • Loading branch information
GuillaumeRx and FrederikBolding committed Feb 14, 2024
1 parent 9118389 commit ccbbe45
Show file tree
Hide file tree
Showing 54 changed files with 950 additions and 337 deletions.
19 changes: 19 additions & 0 deletions .storybook/test-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { NetworkType } from '@metamask/controller-utils';
import { NetworkStatus } from '@metamask/network-controller';
import { EthAccountType, EthMethod } from '@metamask/keyring-api';
import { CHAIN_IDS } from '../shared/constants/network';
import { copyable, divider, heading, panel, text } from '@metamask/snaps-sdk';

const state = {
invalidCustomNetwork: {
Expand Down Expand Up @@ -277,6 +278,24 @@ const state = {
],
},
},
interfaces: {
'test-interface': {
content: panel([
heading('Foo bar'),
text('Description'),
divider(),
text('More text'),
copyable('Text you can copy'),
]),
state: {},
snapId: 'local:http://localhost:8080/',
},
'error-interface': {
content: 'foo',
state: {},
snapId: 'local:http://localhost:8080/',
},
},
accountArray: [
{
name: 'This is a Really Long Account Name',
Expand Down
3 changes: 3 additions & 0 deletions app/scripts/lib/setupSentry.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,9 @@ export const SENTRY_BACKGROUND_STATE = {
snapStates: false,
snaps: false,
},
SnapInterface: {
interfaces: false,
},
SnapsRegistry: {
database: false,
lastUpdated: false,
Expand Down
58 changes: 54 additions & 4 deletions app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import {
JsonSnapsRegistry,
SnapController,
IframeExecutionService,
SnapInterfaceController,
} from '@metamask/snaps-controllers';
import {
createSnapsMethodMiddleware,
Expand Down Expand Up @@ -1176,8 +1177,6 @@ export default class MetamaskController extends EventEmitter {
`${this.permissionController.name}:grantPermissions`,
`${this.subjectMetadataController.name}:getSubjectMetadata`,
`${this.subjectMetadataController.name}:addSubjectMetadata`,
`${this.phishingController.name}:maybeUpdateState`,
`${this.phishingController.name}:testOrigin`,
'ExecutionService:executeSnap',
'ExecutionService:getRpcRequestHandler',
'ExecutionService:terminateSnap',
Expand All @@ -1187,6 +1186,8 @@ export default class MetamaskController extends EventEmitter {
'SnapsRegistry:getMetadata',
'SnapsRegistry:update',
'SnapsRegistry:resolveVersion',
`SnapInterfaceController:createInterface`,
`SnapInterfaceController:getInterface`,
],
});

Expand Down Expand Up @@ -1283,6 +1284,7 @@ export default class MetamaskController extends EventEmitter {
allowedEvents: [],
allowedActions: [],
});

this.snapsRegistry = new JsonSnapsRegistry({
state: initState.SnapsRegistry,
messenger: snapsRegistryMessenger,
Expand All @@ -1296,6 +1298,20 @@ export default class MetamaskController extends EventEmitter {
'0x025b65308f0f0fb8bc7f7ff87bfc296e0330eee5d3c1d1ee4a048b2fd6a86fa0a6',
});

const snapInterfaceControllerMessenger =
this.controllerMessenger.getRestricted({
name: 'SnapInterfaceController',
allowedActions: [
`${this.phishingController.name}:maybeUpdateState`,
`${this.phishingController.name}:testOrigin`,
],
});

this.snapInterfaceController = new SnapInterfaceController({
state: initState.SnapInterfaceController,
messenger: snapInterfaceControllerMessenger,
});

///: END:ONLY_INCLUDE_IF

// account tracker watches balances, nonces, and any code at their address
Expand Down Expand Up @@ -1944,6 +1960,7 @@ export default class MetamaskController extends EventEmitter {
CronjobController: this.cronjobController,
SnapsRegistry: this.snapsRegistry,
NotificationController: this.notificationController,
SnapInterfaceController: this.snapInterfaceController,
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(desktop)
DesktopController: this.desktopController.store,
Expand Down Expand Up @@ -1995,6 +2012,7 @@ export default class MetamaskController extends EventEmitter {
CronjobController: this.cronjobController,
SnapsRegistry: this.snapsRegistry,
NotificationController: this.notificationController,
SnapInterfaceController: this.snapInterfaceController,
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(desktop)
DesktopController: this.desktopController.store,
Expand Down Expand Up @@ -2253,11 +2271,11 @@ export default class MetamaskController extends EventEmitter {
this.controllerMessenger,
'SnapController:getSnapState',
),
showDialog: (origin, type, content, placeholder) =>
showDialog: (origin, type, id, placeholder) =>
this.approvalController.addAndShowApprovalRequest({
origin,
type: SNAP_DIALOG_TYPES[type],
requestData: { content, placeholder },
requestData: { id, placeholder },
}),
showNativeNotification: (origin, args) =>
this.controllerMessenger.call(
Expand Down Expand Up @@ -2304,6 +2322,14 @@ export default class MetamaskController extends EventEmitter {
origin,
).result;
},
createInterface: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapInterfaceController:createInterface',
),
getInterface: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapInterfaceController:getInterface',
),
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
getSnapKeyring: this.getSnapKeyring.bind(this),
Expand Down Expand Up @@ -3184,6 +3210,14 @@ export default class MetamaskController extends EventEmitter {

return phishingController.test(website);
},
deleteInterface: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapInterfaceController:deleteInterface',
),
updateInterfaceState: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapInterfaceController:updateInterfaceState',
),
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(desktop)
// Desktop
Expand Down Expand Up @@ -4941,6 +4975,22 @@ export default class MetamaskController extends EventEmitter {
this.subjectMetadataController,
origin,
),
createInterface: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapInterfaceController:createInterface',
origin,
),
getInterfaceState: (...args) =>
this.controllerMessenger.call(
'SnapInterfaceController:getInterface',
origin,
...args,
).state,
updateInterface: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapInterfaceController:updateInterface',
origin,
),
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(snaps)
}),
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/snaps/test-snap-dialog.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ describe('Test Snap Dialog', function () {
);

// fill '2323' in form field
await driver.pasteIntoField('.MuiInput-input', '2323');
await driver.pasteIntoField('.mm-input', '2323');

// click submit button
await driver.clickElement({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@
"snapStates": "object",
"unencryptedSnapStates": "object"
},
"SnapInterfaceController": "object",
"SnapsRegistry": { "database": "object", "lastUpdated": "object" },
"SubjectMetadataController": { "subjectMetadata": "object" },
"SwapsController": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@
"database": "object",
"lastUpdated": "object",
"notifications": "object",
"interfaces": "object",
"names": "object",
"nameSources": "object",
"userOperations": "object",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import { Copyable } from '../snaps/copyable';
import Spinner from '../../ui/spinner';
import { SnapUIMarkdown } from '../snaps/snap-ui-markdown';
import { SnapUIImage } from '../snaps/snap-ui-image';
import { SnapUIInput } from '../snaps/snap-ui-input';
import { SnapUIForm } from '../snaps/snap-ui-form';
import { SnapUIButton } from '../snaps/snap-ui-button';
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
import { CreateSnapAccount } from '../../../pages/create-snap-account';
Expand Down Expand Up @@ -66,6 +69,9 @@ export const safeComponentList = {
Spinner,
ConfirmInfoRow,
ConfirmInfoRowAddress,
SnapUIInput,
SnapUIButton,
SnapUIForm,
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
CreateSnapAccount,
Expand Down
4 changes: 2 additions & 2 deletions ui/components/app/snaps/insight-warnings/insight-warnings.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,12 @@ export default function InsightWarnings({
return (
<Box className="insights-warnings-modal__content">
{warnings.map((warning, idx) => {
const { snapId, content } = warning;
const { snapId, id } = warning;
return (
<SnapUIRenderer
key={`${snapId}-${idx}`}
snapId={snapId}
data={content}
interfaceId={id}
delineatorType={DelineatorType.Warning}
onClick={() => handleWarningClick(snapId)}
isCollapsable
Expand Down
20 changes: 15 additions & 5 deletions ui/components/app/snaps/snap-home-page/snap-home-renderer.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { Box, Text } from '../../../component-library';
import { SnapUIRenderer } from '../snap-ui-renderer';
import { getTargetSubjectMetadata } from '../../../../selectors';
Expand All @@ -10,9 +10,11 @@ import { DelineatorType } from '../../../../helpers/constants/snaps';
import { TextVariant } from '../../../../helpers/constants/design-system';
import { Copyable } from '../copyable';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import { deleteInterface } from '../../../../store/actions';
import { useSnapHome } from './useSnapHome';

export const SnapHomeRenderer = ({ snapId }) => {
const dispatch = useDispatch();
const t = useI18nContext();
const targetSubjectMetadata = useSelector((state) =>
getTargetSubjectMetadata(state, snapId),
Expand All @@ -21,7 +23,11 @@ export const SnapHomeRenderer = ({ snapId }) => {
const snapName = getSnapName(snapId, targetSubjectMetadata);
const { data, error, loading } = useSnapHome({ snapId });

const content = !loading && !error && data?.content;
const interfaceId = !loading && !error && data?.id;

useEffect(() => {
return () => interfaceId && dispatch(deleteInterface(interfaceId));
}, []);

return (
<Box>
Expand All @@ -33,8 +39,12 @@ export const SnapHomeRenderer = ({ snapId }) => {
<Copyable text={error.message} />
</SnapDelineator>
)}
{(content || loading) && (
<SnapUIRenderer snapId={snapId} data={content} isLoading={loading} />
{(interfaceId || loading) && (
<SnapUIRenderer
snapId={snapId}
interfaceId={interfaceId}
isLoading={loading}
/>
)}
</Box>
);
Expand Down
45 changes: 24 additions & 21 deletions ui/components/app/snaps/snap-insight/snap-insight.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,8 @@
import React, {
///: BEGIN:ONLY_INCLUDE_IF(build-flask)
useEffect,
///: END:ONLY_INCLUDE_IF
} from 'react';
import React, { useEffect } from 'react';

import PropTypes from 'prop-types';

import {
useSelector,
///: BEGIN:ONLY_INCLUDE_IF(build-flask)
useDispatch,
///: END:ONLY_INCLUDE_IF
} from 'react-redux';
import { useSelector, useDispatch } from 'react-redux';
import { Text } from '../../../component-library';
import {
AlignItems,
Expand All @@ -29,9 +20,14 @@ import { DelineatorType } from '../../../../helpers/constants/snaps';
import { getSnapName } from '../../../../helpers/utils/util';
import { Copyable } from '../copyable';
import { getTargetSubjectMetadata } from '../../../../selectors';
///: BEGIN:ONLY_INCLUDE_IF(build-flask)
import { trackInsightSnapUsage } from '../../../../store/actions';
///: END:ONLY_INCLUDE_IF
import {
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-mmi,build-beta)
deleteInterface,
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(build-flask)
trackInsightSnapUsage,
///: END:ONLY_INCLUDE_IF
} from '../../../../store/actions';
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-mmi,build-beta)
import { useTransactionInsightSnaps } from '../../../../hooks/snaps/useTransactionInsightSnaps';
///: END:ONLY_INCLUDE_IF
Expand All @@ -46,13 +42,13 @@ export const SnapInsight = ({
insightHookParams,
///: END:ONLY_INCLUDE_IF
}) => {
const dispatch = useDispatch();
const t = useI18nContext();
let error, content;
let error, interfaceId;
let isLoading = loading;
///: BEGIN:ONLY_INCLUDE_IF(build-flask)
error = data?.error;
content = data?.response?.content;
const dispatch = useDispatch();
interfaceId = data?.response?.id;
useEffect(() => {
const trackInsightUsage = async () => {
try {
Expand All @@ -68,8 +64,14 @@ export const SnapInsight = ({
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-mmi,build-beta)
const insights = useTransactionInsightSnaps(insightHookParams);
error = insights.data?.[0]?.error;
content = insights.data?.[0]?.response?.content;
interfaceId = insights.data?.[0]?.response?.id;
isLoading = insights.loading;

useEffect(() => {
return () => {
interfaceId && dispatch(deleteInterface(interfaceId));
};
}, []);
///: END:ONLY_INCLUDE_IF

const targetSubjectMetadata = useSelector((state) =>
Expand All @@ -78,7 +80,8 @@ export const SnapInsight = ({

const snapName = getSnapName(snapId, targetSubjectMetadata);

const hasNoData = !error && !isLoading && !content;
const hasNoData = !error && !isLoading && !interfaceId;

return (
<Box
flexDirection={FLEX_DIRECTION.COLUMN}
Expand All @@ -97,10 +100,10 @@ export const SnapInsight = ({
flexDirection={FLEX_DIRECTION.COLUMN}
className="snap-insight__container"
>
{isLoading || content ? (
{isLoading || interfaceId ? (
<SnapUIRenderer
snapId={snapId}
data={content}
interfaceId={interfaceId}
delineatorType={DelineatorType.Insights}
isLoading={isLoading}
/>
Expand Down
1 change: 1 addition & 0 deletions ui/components/app/snaps/snap-ui-button/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './snap-ui-button';

0 comments on commit ccbbe45

Please sign in to comment.