diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 10ea07a68..d0abc28d3 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -3409,6 +3409,9 @@
"snapsToggle": {
"message": "A snap will only run if it is enabled"
},
+ "snapsUIError": {
+ "message": "The UI specified by the snap is invalid."
+ },
"someNetworksMayPoseSecurity": {
"message": "Some networks may pose security and/or privacy risks. Understand the risks before adding & using a network."
},
diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json
index 042f03002..e9e421f86 100644
--- a/lavamoat/browserify/beta/policy.json
+++ b/lavamoat/browserify/beta/policy.json
@@ -2435,7 +2435,7 @@
"TextEncoder": true
},
"packages": {
- "@metamask/snaps-utils>superstruct": true,
+ "@metamask/snaps-ui>superstruct": true,
"browserify>buffer": true,
"nock>debug": true
}
diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json
index 6d7ff9b63..57ba6ae23 100644
--- a/lavamoat/browserify/flask/policy.json
+++ b/lavamoat/browserify/flask/policy.json
@@ -1275,9 +1275,9 @@
"@metamask/permission-controller": true,
"@metamask/rpc-methods>@metamask/key-tree": true,
"@metamask/rpc-methods>nanoid": true,
+ "@metamask/snaps-ui>superstruct": true,
"@metamask/snaps-utils": true,
"@metamask/snaps-utils>@noble/hashes": true,
- "@metamask/snaps-utils>superstruct": true,
"eth-block-tracker>@metamask/utils": true,
"eth-rpc-errors": true
}
@@ -1766,11 +1766,11 @@
"URL": true
},
"packages": {
+ "@metamask/snaps-ui>superstruct": true,
"@metamask/snaps-utils>@noble/hashes": true,
"@metamask/snaps-utils>@scure/base": true,
"@metamask/snaps-utils>cron-parser": true,
"@metamask/snaps-utils>rfdc": true,
- "@metamask/snaps-utils>superstruct": true,
"@metamask/snaps-utils>validate-npm-package-name": true,
"eth-block-tracker>@metamask/utils": true,
"semver": true
@@ -2804,7 +2804,7 @@
"TextEncoder": true
},
"packages": {
- "@metamask/snaps-utils>superstruct": true,
+ "@metamask/snaps-ui>superstruct": true,
"browserify>buffer": true,
"nock>debug": true
}
diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json
index 042f03002..e9e421f86 100644
--- a/lavamoat/browserify/main/policy.json
+++ b/lavamoat/browserify/main/policy.json
@@ -2435,7 +2435,7 @@
"TextEncoder": true
},
"packages": {
- "@metamask/snaps-utils>superstruct": true,
+ "@metamask/snaps-ui>superstruct": true,
"browserify>buffer": true,
"nock>debug": true
}
diff --git a/package.json b/package.json
index 24e3f5066..09854b447 100644
--- a/package.json
+++ b/package.json
@@ -226,6 +226,7 @@
"@metamask/slip44": "^2.1.0",
"@metamask/smart-transactions-controller": "^3.0.0",
"@metamask/snaps-controllers": "^0.26.1",
+ "@metamask/snaps-ui": "^0.26.1",
"@metamask/snaps-utils": "^0.26.1",
"@metamask/subject-metadata-controller": "^1.0.0",
"@ngraveio/bc-ur": "^1.1.6",
diff --git a/ui/components/app/app-components.scss b/ui/components/app/app-components.scss
index 47289aa8a..d4d22e37c 100644
--- a/ui/components/app/app-components.scss
+++ b/ui/components/app/app-components.scss
@@ -38,6 +38,7 @@
@import 'flask/snap-content-footer/index';
@import 'flask/snap-install-warning/index';
@import 'flask/snap-remove-warning/index';
+@import 'flask/snap-ui-renderer/index';
@import 'flask/snap-delineator/index';
@import 'flask/snap-settings-card/index';
@import 'flask/update-snap-permission-list/index';
diff --git a/ui/components/app/flask/snap-ui-renderer/index.js b/ui/components/app/flask/snap-ui-renderer/index.js
new file mode 100644
index 000000000..572ebe69a
--- /dev/null
+++ b/ui/components/app/flask/snap-ui-renderer/index.js
@@ -0,0 +1 @@
+export { SnapUIRenderer } from './snap-ui-renderer';
diff --git a/ui/components/app/flask/snap-ui-renderer/index.scss b/ui/components/app/flask/snap-ui-renderer/index.scss
new file mode 100644
index 000000000..883777fde
--- /dev/null
+++ b/ui/components/app/flask/snap-ui-renderer/index.scss
@@ -0,0 +1,17 @@
+.snap-ui-renderer {
+ &__error {
+ margin-top: 0 !important;
+ }
+
+ &__spinner {
+ width: 30px;
+ }
+
+ &__divider {
+ width: 100%;
+ }
+
+ &__panel {
+ height: 100%;
+ }
+}
diff --git a/ui/components/app/flask/snap-ui-renderer/snap-ui-renderer.js b/ui/components/app/flask/snap-ui-renderer/snap-ui-renderer.js
new file mode 100644
index 000000000..b92530f47
--- /dev/null
+++ b/ui/components/app/flask/snap-ui-renderer/snap-ui-renderer.js
@@ -0,0 +1,104 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { useSelector } from 'react-redux';
+import nanoid from 'nanoid';
+import { isComponent } from '@metamask/snaps-ui';
+import MetaMaskTemplateRenderer from '../../metamask-template-renderer/metamask-template-renderer';
+import {
+ TYPOGRAPHY,
+ FONT_WEIGHT,
+ DISPLAY,
+ FLEX_DIRECTION,
+} from '../../../../helpers/constants/design-system';
+import { SnapDelineator } from '../snap-delineator';
+import { useI18nContext } from '../../../../hooks/useI18nContext';
+import ActionableMessage from '../../../ui/actionable-message/actionable-message';
+import { getSnap } from '../../../../selectors';
+
+export const UI_MAPPING = {
+ panel: (props) => ({
+ element: 'Box',
+ // eslint-disable-next-line no-use-before-define
+ children: props.children.map(mapToTemplate),
+ props: {
+ display: DISPLAY.FLEX,
+ flexDirection: FLEX_DIRECTION.COLUMN,
+ className: 'snap-ui-renderer__panel',
+ },
+ }),
+ heading: (props) => ({
+ element: 'Typography',
+ children: props.value,
+ props: {
+ variant: TYPOGRAPHY.H3,
+ fontWeight: FONT_WEIGHT.BOLD,
+ },
+ }),
+ text: (props) => ({
+ element: 'Typography',
+ children: props.value,
+ props: {
+ variant: TYPOGRAPHY.H6,
+ },
+ }),
+ spinner: () => ({
+ element: 'Spinner',
+ props: {
+ className: 'snap-ui-renderer__spinner',
+ },
+ }),
+ divider: () => ({
+ element: 'hr',
+ props: {
+ className: 'snap-ui-renderer__divider',
+ },
+ }),
+ copyable: (props) => ({
+ element: 'Copyable',
+ props: {
+ text: props.value,
+ },
+ }),
+};
+
+const mapToTemplate = (data) => {
+ const { type } = data;
+ const mapped = UI_MAPPING[type](data);
+ // TODO: We may want to have deterministic keys at some point
+ return { ...mapped, key: nanoid() };
+};
+
+// Component that maps Snaps UI JSON format to MetaMask Template Renderer format
+export const SnapUIRenderer = ({ snapId, data }) => {
+ const t = useI18nContext();
+ const snap = useSelector((state) => getSnap(state, snapId));
+
+ const snapName = snap.manifest.proposedName;
+
+ if (!isComponent(data)) {
+ return (
+
+
+
+ );
+ }
+
+ const sections = mapToTemplate(data);
+
+ return (
+
+
+
+ );
+};
+
+SnapUIRenderer.propTypes = {
+ snapId: PropTypes.string,
+ data: PropTypes.object,
+};
diff --git a/ui/components/app/flask/snap-ui-renderer/snap-ui-renderer.stories.js b/ui/components/app/flask/snap-ui-renderer/snap-ui-renderer.stories.js
new file mode 100644
index 000000000..941d473c6
--- /dev/null
+++ b/ui/components/app/flask/snap-ui-renderer/snap-ui-renderer.stories.js
@@ -0,0 +1,34 @@
+import React from 'react';
+import { Provider } from 'react-redux';
+import { object } from '@storybook/addon-knobs';
+import { panel, text, heading, divider, copyable } from '@metamask/snaps-ui';
+import configureStore from '../../../../store/store';
+import testData from '../../../../../.storybook/test-data';
+import { SnapUIRenderer } from '.';
+
+const store = configureStore(testData);
+
+export default {
+ title: 'Components/App/SnapUIRenderer',
+ id: __filename,
+ decorators: [(story) => {story()}],
+};
+
+const DATA = panel([
+ heading('Foo bar'),
+ text('Description'),
+ divider(),
+ text('More text'),
+ copyable('Text you can copy'),
+]);
+
+export const DefaultStory = () => (
+
+);
+
+export const ErrorStory = () => (
+
+);
diff --git a/ui/components/app/flask/snap-ui-renderer/snap-ui-renderer.test.js b/ui/components/app/flask/snap-ui-renderer/snap-ui-renderer.test.js
new file mode 100644
index 000000000..3a1e83150
--- /dev/null
+++ b/ui/components/app/flask/snap-ui-renderer/snap-ui-renderer.test.js
@@ -0,0 +1,9 @@
+import { NodeType } from '@metamask/snaps-ui';
+import { UI_MAPPING } from './snap-ui-renderer';
+
+describe('Snap UI mapping', () => {
+ it('supports all exposed components', () => {
+ const nodes = Object.values(NodeType);
+ expect(Object.keys(UI_MAPPING).sort()).toStrictEqual(nodes.sort());
+ });
+});
diff --git a/ui/components/app/metamask-template-renderer/safe-component-list.js b/ui/components/app/metamask-template-renderer/safe-component-list.js
index 243eac2da..f7e190160 100644
--- a/ui/components/app/metamask-template-renderer/safe-component-list.js
+++ b/ui/components/app/metamask-template-renderer/safe-component-list.js
@@ -15,6 +15,7 @@ import Tooltip from '../../ui/tooltip/tooltip';
///: BEGIN:ONLY_INCLUDE_IN(flask)
import { SnapDelineator } from '../flask/snap-delineator';
import { Copyable } from '../flask/copyable';
+import Spinner from '../../ui/spinner';
///: END:ONLY_INCLUDE_IN
export const safeComponentList = {
@@ -41,5 +42,7 @@ export const safeComponentList = {
///: BEGIN:ONLY_INCLUDE_IN(flask)
SnapDelineator,
Copyable,
+ Spinner,
+ hr: 'hr',
///: END:ONLY_INCLUDE_IN
};
diff --git a/yarn.lock b/yarn.lock
index b7858ae4d..bcb8a11ae 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4371,6 +4371,16 @@ __metadata:
languageName: node
linkType: hard
+"@metamask/snaps-ui@npm:^0.26.1":
+ version: 0.26.1
+ resolution: "@metamask/snaps-ui@npm:0.26.1"
+ dependencies:
+ "@metamask/utils": ^3.3.1
+ superstruct: ^0.16.7
+ checksum: 73dc68f02670ae075abf54740c4b3265741699b10cf2f9dc16a56393651d6595361feaa3f80cec4900884bd351e9177b0f1eb27e4511cdc7dc6b05be2e107210
+ languageName: node
+ linkType: hard
+
"@metamask/snaps-utils@npm:^0.26.1":
version: 0.26.1
resolution: "@metamask/snaps-utils@npm:0.26.1"
@@ -23354,6 +23364,7 @@ __metadata:
"@metamask/slip44": ^2.1.0
"@metamask/smart-transactions-controller": ^3.0.0
"@metamask/snaps-controllers": ^0.26.1
+ "@metamask/snaps-ui": ^0.26.1
"@metamask/snaps-utils": ^0.26.1
"@metamask/subject-metadata-controller": ^1.0.0
"@metamask/test-dapp": ^5.2.1