diff --git a/packages/examples/packages/bip32/snap.manifest.json b/packages/examples/packages/bip32/snap.manifest.json index f296af262e..d16700c9aa 100644 --- a/packages/examples/packages/bip32/snap.manifest.json +++ b/packages/examples/packages/bip32/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "Ard/F/8RqOYcKlxb3xx6OB5XLtmv7t/Asrn8bhkCooQ=", + "shasum": "te1oZPWHfniZ/ST6D/blxr8w6yhoR4qaJeeNoOP7O6A=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/bip32/src/index.test.ts b/packages/examples/packages/bip32/src/index.test.ts index 0862e16d16..7aca481873 100644 --- a/packages/examples/packages/bip32/src/index.test.ts +++ b/packages/examples/packages/bip32/src/index.test.ts @@ -1,7 +1,6 @@ import { expect } from '@jest/globals'; -import { installSnap } from '@metamask/snaps-jest'; +import { assertIsConfirmationDialog, installSnap } from '@metamask/snaps-jest'; import { copyable, heading, panel, text } from '@metamask/snaps-sdk'; -import { assert } from '@metamask/utils'; describe('onRpcRequest', () => { it('throws an error if the requested method does not exist', async () => { @@ -109,6 +108,7 @@ describe('onRpcRequest', () => { }); const ui = await response.getInterface(); + assertIsConfirmationDialog(ui); expect(ui).toRender( panel([ heading('Signature request'), @@ -141,6 +141,7 @@ describe('onRpcRequest', () => { }); const ui = await response.getInterface(); + assertIsConfirmationDialog(ui); expect(ui).toRender( panel([ heading('Signature request'), @@ -173,6 +174,7 @@ describe('onRpcRequest', () => { }); const ui = await response.getInterface(); + assertIsConfirmationDialog(ui); expect(ui).toRender( panel([ heading('Signature request'), @@ -205,7 +207,7 @@ describe('onRpcRequest', () => { }); const ui = await response.getInterface(); - assert(ui.type === 'confirmation'); + assertIsConfirmationDialog(ui); await ui.cancel(); expect(await response).toRespondWithError({ diff --git a/packages/examples/packages/bip44/snap.manifest.json b/packages/examples/packages/bip44/snap.manifest.json index 919ec21e0f..4b507f1c3b 100644 --- a/packages/examples/packages/bip44/snap.manifest.json +++ b/packages/examples/packages/bip44/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "jJuoqF4p++cEfWb3YdEfuwlOEw7bByfpEMh6adH8sOI=", + "shasum": "yzw5UM4aWmD8vLglR9gazcNC0yKntyPp5A5sPSmouL4=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/bip44/src/index.test.ts b/packages/examples/packages/bip44/src/index.test.ts index 7986891bb7..92f8794857 100644 --- a/packages/examples/packages/bip44/src/index.test.ts +++ b/packages/examples/packages/bip44/src/index.test.ts @@ -1,7 +1,6 @@ import { expect } from '@jest/globals'; -import { installSnap } from '@metamask/snaps-jest'; +import { assertIsConfirmationDialog, installSnap } from '@metamask/snaps-jest'; import { copyable, heading, panel, text } from '@metamask/snaps-sdk'; -import { assert } from '@metamask/utils'; describe('onRpcRequest', () => { it('throws an error if the requested method does not exist', async () => { @@ -84,6 +83,8 @@ describe('onRpcRequest', () => { }); const ui = await response.getInterface(); + assertIsConfirmationDialog(ui); + expect(ui).toRender( panel([ heading('Signature request'), @@ -114,6 +115,8 @@ describe('onRpcRequest', () => { }); const ui = await response.getInterface(); + assertIsConfirmationDialog(ui); + expect(ui).toRender( panel([ heading('Signature request'), @@ -144,7 +147,8 @@ describe('onRpcRequest', () => { }); const ui = await response.getInterface(); - assert(ui.type === 'confirmation'); + assertIsConfirmationDialog(ui); + await ui.cancel(); expect(await response).toRespondWithError({ diff --git a/packages/examples/packages/browserify-plugin/snap.manifest.json b/packages/examples/packages/browserify-plugin/snap.manifest.json index b27aa6c458..ab2c6affa9 100644 --- a/packages/examples/packages/browserify-plugin/snap.manifest.json +++ b/packages/examples/packages/browserify-plugin/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "nBoIUO4s8aakybthHavEuRjxCoNVzevKVv1mr1u2uNM=", + "shasum": "a+jOIwfxC3ITF6MbF5ltQvM0ZvCJ5/NQJlbEsqWTQrk=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/browserify/snap.manifest.json b/packages/examples/packages/browserify/snap.manifest.json index 3209fa4f40..a7cb3b7d50 100644 --- a/packages/examples/packages/browserify/snap.manifest.json +++ b/packages/examples/packages/browserify/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "Kq4I9q6BdkSUrUsjysqqsfjlQ1gu6vM4sXSo7pgNruI=", + "shasum": "HPCtZ7reZrbut7cl09SnaoAHkzrvlvjc60leUfkcsVI=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/client-status/snap.manifest.json b/packages/examples/packages/client-status/snap.manifest.json index e418f0e16b..cbaa5b762a 100644 --- a/packages/examples/packages/client-status/snap.manifest.json +++ b/packages/examples/packages/client-status/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "RNHnGSZaZRAEkH7h4eqFfmic8Ixq30CXj4JWnhWkQbI=", + "shasum": "7BLeIk79FlkIK9fNKHcCdX8UChmjU7kOkGVLxOc29Kw=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/cronjobs/snap.manifest.json b/packages/examples/packages/cronjobs/snap.manifest.json index 214ad6018a..39a629f55f 100644 --- a/packages/examples/packages/cronjobs/snap.manifest.json +++ b/packages/examples/packages/cronjobs/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "0YLZlM7w5GpjDWYmmJFxiYB2kJnNG55ru3/8ywKVT9I=", + "shasum": "DMmgoUR+Q1/eYJTjZQ96jI406CpgObFs454UR1nKGM4=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/dialogs/package.json b/packages/examples/packages/dialogs/package.json index 229d73fd8b..9a7f03eca1 100644 --- a/packages/examples/packages/dialogs/package.json +++ b/packages/examples/packages/dialogs/package.json @@ -42,7 +42,6 @@ "@metamask/eslint-config-typescript": "^12.1.0", "@metamask/snaps-cli": "workspace:^", "@metamask/snaps-jest": "workspace:^", - "@metamask/utils": "^8.3.0", "@swc/core": "1.3.78", "@swc/jest": "^0.2.26", "@typescript-eslint/eslint-plugin": "^5.42.1", diff --git a/packages/examples/packages/dialogs/snap.config.ts b/packages/examples/packages/dialogs/snap.config.ts index 666bf6eb00..559cfc9b90 100644 --- a/packages/examples/packages/dialogs/snap.config.ts +++ b/packages/examples/packages/dialogs/snap.config.ts @@ -2,7 +2,7 @@ import type { SnapConfig } from '@metamask/snaps-cli'; import { resolve } from 'path'; const config: SnapConfig = { - input: resolve(__dirname, 'src/index.ts'), + input: resolve(__dirname, 'src/index.tsx'), server: { port: 8005, }, diff --git a/packages/examples/packages/dialogs/snap.manifest.json b/packages/examples/packages/dialogs/snap.manifest.json index d2dec66ab4..a7eecfb84d 100644 --- a/packages/examples/packages/dialogs/snap.manifest.json +++ b/packages/examples/packages/dialogs/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "5cc1fSlz4S5sDjlkjWhBUNjOQu6VdCqS5ln/DJjssSk=", + "shasum": "F5K30FZGcmERlgvw8D8v4Vuqh2FP2fxSeq0KVOT4LYg=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/dialogs/src/components/custom.tsx b/packages/examples/packages/dialogs/src/components/custom.tsx new file mode 100644 index 0000000000..70473b8952 --- /dev/null +++ b/packages/examples/packages/dialogs/src/components/custom.tsx @@ -0,0 +1,32 @@ +import { + Box, + Button, + Container, + Footer, + Heading, + Input, + Text, + type SnapComponent, +} from '@metamask/snaps-sdk/jsx'; + +/** + * A custom dialog component. + * + * @returns The custom dialog component. + */ +export const CustomDialog: SnapComponent = () => ( + + + Custom Dialog + + This is a custom dialog. It has a custom Footer and can be resolved to + any value. + + + + + +); diff --git a/packages/examples/packages/dialogs/src/components/index.ts b/packages/examples/packages/dialogs/src/components/index.ts new file mode 100644 index 0000000000..c7226ae4e2 --- /dev/null +++ b/packages/examples/packages/dialogs/src/components/index.ts @@ -0,0 +1 @@ +export * from './custom'; diff --git a/packages/examples/packages/dialogs/src/index.test.ts b/packages/examples/packages/dialogs/src/index.test.tsx similarity index 76% rename from packages/examples/packages/dialogs/src/index.test.ts rename to packages/examples/packages/dialogs/src/index.test.tsx index cf53bc4475..0c28a1de84 100644 --- a/packages/examples/packages/dialogs/src/index.test.ts +++ b/packages/examples/packages/dialogs/src/index.test.tsx @@ -1,7 +1,14 @@ import { expect } from '@jest/globals'; -import { installSnap } from '@metamask/snaps-jest'; +import { + assertIsAlertDialog, + assertIsConfirmationDialog, + assertIsCustomDialog, + assertIsPromptDialog, + installSnap, +} from '@metamask/snaps-jest'; import { heading, panel, text } from '@metamask/snaps-sdk'; -import { assert } from '@metamask/utils'; + +import { CustomDialog } from './components'; describe('onRpcRequest', () => { it('throws an error if the requested method does not exist', async () => { @@ -31,7 +38,7 @@ describe('onRpcRequest', () => { }); const ui = await response.getInterface(); - assert(ui.type === 'alert'); + assertIsAlertDialog(ui); expect(ui).toRender( panel([ @@ -55,7 +62,7 @@ describe('onRpcRequest', () => { }); const ui = await response.getInterface(); - assert(ui.type === 'confirmation'); + assertIsConfirmationDialog(ui); expect(ui).toRender( panel([ @@ -79,7 +86,7 @@ describe('onRpcRequest', () => { }); const ui = await response.getInterface(); - assert(ui.type === 'confirmation'); + assertIsConfirmationDialog(ui); await ui.cancel(); expect(await response).toRespondWith(false); @@ -95,7 +102,7 @@ describe('onRpcRequest', () => { }); const ui = await response.getInterface(); - assert(ui.type === 'prompt'); + assertIsPromptDialog(ui); expect(ui).toRender( panel([ @@ -119,10 +126,34 @@ describe('onRpcRequest', () => { }); const ui = await response.getInterface(); - assert(ui.type === 'prompt'); + assertIsPromptDialog(ui); + await ui.cancel(); expect(await response).toRespondWith(null); }); }); + + describe('showCustom', () => { + it('shows a custom dialog and can return the input value', async () => { + const value = 'Hello, world!'; + + const { request } = await installSnap(); + + const response = request({ + method: 'showCustom', + }); + + const ui = await response.getInterface(); + assertIsCustomDialog(ui); + + expect(ui).toRender(); + + await ui.typeInField('custom-input', value); + + await ui.clickElement('confirm'); + + expect(await response).toRespondWith(value); + }); + }); }); diff --git a/packages/examples/packages/dialogs/src/index.ts b/packages/examples/packages/dialogs/src/index.tsx similarity index 63% rename from packages/examples/packages/dialogs/src/index.ts rename to packages/examples/packages/dialogs/src/index.tsx index b367d39a5e..a1849576e3 100644 --- a/packages/examples/packages/dialogs/src/index.ts +++ b/packages/examples/packages/dialogs/src/index.tsx @@ -1,12 +1,18 @@ -import type { OnRpcRequestHandler } from '@metamask/snaps-sdk'; +import type { + OnRpcRequestHandler, + OnUserInputHandler, +} from '@metamask/snaps-sdk'; import { DialogType, panel, text, heading, MethodNotFoundError, + UserInputEventType, } from '@metamask/snaps-sdk'; +import { CustomDialog } from './components'; + /** * Handle incoming JSON-RPC requests from the dapp, sent through the * `wallet_invokeSnap` method. This handler handles three methods, one for each @@ -15,6 +21,7 @@ import { * - `showAlert`: Show an alert dialog. * - `showConfirmation`: Show a confirmation dialog. * - `showPrompt`: Show a prompt dialog. + * - `showCustom`: Show a custom dialog with the resolution handled by the snap. * * The dialogs are shown using the [`snap_dialog`](https://docs.metamask.io/snaps/reference/rpc-api/#snap_dialog) * method. @@ -75,7 +82,62 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { }, }); + case 'showCustom': + return snap.request({ + method: 'snap_dialog', + params: { + content: , + }, + }); + default: throw new MethodNotFoundError({ method: request.method }); } }; + +/** + * Handle incoming user events coming from the MetaMask clients open interfaces. + * + * @param params - The event parameters. + * @param params.id - The Snap interface ID where the event was fired. + * @param params.event - The event object containing the event type, name and value. + * @see https://docs.metamask.io/snaps/reference/exports/#onuserinput + */ +export const onUserInput: OnUserInputHandler = async ({ id, event }) => { + if (event.type === UserInputEventType.ButtonClickEvent) { + switch (event.name) { + case 'cancel': + await snap.request({ + method: 'snap_resolveInterface', + params: { + id, + value: null, + }, + }); + break; + + case 'confirm': { + const state = await snap.request({ + method: 'snap_getInterfaceState', + params: { + id, + }, + }); + + const value = state['custom-input']; + + await snap.request({ + method: 'snap_resolveInterface', + params: { + id, + value, + }, + }); + break; + } + + default: + break; + } + } +}; diff --git a/packages/examples/packages/dialogs/tsconfig.json b/packages/examples/packages/dialogs/tsconfig.json index 1cb4c3315f..32e2e013f3 100644 --- a/packages/examples/packages/dialogs/tsconfig.json +++ b/packages/examples/packages/dialogs/tsconfig.json @@ -2,7 +2,10 @@ "extends": "../../tsconfig.json", "compilerOptions": { "baseUrl": "./", + "jsx": "react-jsxdev", "paths": { + "@metamask/*/jsx": ["../../../*/src/jsx"], + "@metamask/*/jsx-dev-runtime": ["../../../*/src/jsx"], "@metamask/*": ["../../../*/src"] } }, diff --git a/packages/examples/packages/ethereum-provider/snap.manifest.json b/packages/examples/packages/ethereum-provider/snap.manifest.json index 18ae1cb98d..b1fd03740b 100644 --- a/packages/examples/packages/ethereum-provider/snap.manifest.json +++ b/packages/examples/packages/ethereum-provider/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "8L6HiuUzKAQVeog5uIIYiqBOuEb2Qm3+9HH+fXs5PlM=", + "shasum": "iHt9Se2I/fFZeIQW6p5qyNGt+wD7S2NKlNPLYjSTZMc=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/ethers-js/package.json b/packages/examples/packages/ethers-js/package.json index 3289714eca..451602ed1d 100644 --- a/packages/examples/packages/ethers-js/package.json +++ b/packages/examples/packages/ethers-js/package.json @@ -43,7 +43,6 @@ "@metamask/eslint-config-typescript": "^12.1.0", "@metamask/snaps-cli": "workspace:^", "@metamask/snaps-jest": "workspace:^", - "@metamask/utils": "^8.3.0", "@swc/core": "1.3.78", "@swc/jest": "^0.2.26", "@typescript-eslint/eslint-plugin": "^5.42.1", diff --git a/packages/examples/packages/ethers-js/snap.manifest.json b/packages/examples/packages/ethers-js/snap.manifest.json index 9b4c451f4a..881b8871b0 100644 --- a/packages/examples/packages/ethers-js/snap.manifest.json +++ b/packages/examples/packages/ethers-js/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "5fi+IIMm/FubpDnpYP+YGW/dCUgTT4bVIxxDmcuybr8=", + "shasum": "K1gDsaTcrhG5vRH5Tb1uswHg6QnxYIqKjynyvzLlbfM=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/ethers-js/src/index.test.ts b/packages/examples/packages/ethers-js/src/index.test.ts index e9acffcae7..b4c4ac5a30 100644 --- a/packages/examples/packages/ethers-js/src/index.test.ts +++ b/packages/examples/packages/ethers-js/src/index.test.ts @@ -1,7 +1,6 @@ import { expect } from '@jest/globals'; -import { installSnap } from '@metamask/snaps-jest'; +import { assertIsConfirmationDialog, installSnap } from '@metamask/snaps-jest'; import { copyable, heading, panel, text } from '@metamask/snaps-sdk'; -import { assert } from '@metamask/utils'; describe('onRpcRequest', () => { it('throws an error if the requested method does not exist', async () => { @@ -48,6 +47,8 @@ describe('onRpcRequest', () => { }); const ui = await response.getInterface(); + assertIsConfirmationDialog(ui); + expect(ui).toRender( panel([ heading('Signature request'), @@ -74,7 +75,7 @@ describe('onRpcRequest', () => { }); const ui = await response.getInterface(); - assert(ui.type === 'confirmation'); + assertIsConfirmationDialog(ui); await ui.cancel(); expect(await response).toRespondWithError({ diff --git a/packages/examples/packages/file-upload/snap.manifest.json b/packages/examples/packages/file-upload/snap.manifest.json index 003b56b552..36d27cf828 100644 --- a/packages/examples/packages/file-upload/snap.manifest.json +++ b/packages/examples/packages/file-upload/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "+yioSnJPHDQRsStHxbK9pm9bUTAgvCFrpWjwvWZNWqE=", + "shasum": "XWQyeVfzXAXLSNiE9je6OjMU8dqlDw0Mw4q6QApCgFo=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/get-entropy/snap.manifest.json b/packages/examples/packages/get-entropy/snap.manifest.json index f04ad0eeaa..e157546178 100644 --- a/packages/examples/packages/get-entropy/snap.manifest.json +++ b/packages/examples/packages/get-entropy/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "nlhNiocqRC/yaz73W2wMGxIXswHm6YCCigdd6X/maeU=", + "shasum": "WXUTcCOLbtQnTMVKVz2X2IeQG8ICXkmDozXlil/Z3rI=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/get-file/snap.manifest.json b/packages/examples/packages/get-file/snap.manifest.json index b64a2641b0..ac0669a257 100644 --- a/packages/examples/packages/get-file/snap.manifest.json +++ b/packages/examples/packages/get-file/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "opNYkgiLjqm7OBH2Wi4eAewEntFg0trEfw8yk8sQJGk=", + "shasum": "dhEHU8KvnRAcZlEq4cnrhaVaEeRux9b0HA8LrA5GPnI=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/home-page/snap.manifest.json b/packages/examples/packages/home-page/snap.manifest.json index 3c686aeb78..8f21ca554b 100644 --- a/packages/examples/packages/home-page/snap.manifest.json +++ b/packages/examples/packages/home-page/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "undUBL7PBSvSR81E5AfodCp7nY0658OuaCe6eY2Xjaw=", + "shasum": "YYz045/kftDaFs5MVv7EJzcMOtu7KBZa9RgceCoD9Gc=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/images/snap.manifest.json b/packages/examples/packages/images/snap.manifest.json index a080b64903..b86659ec87 100644 --- a/packages/examples/packages/images/snap.manifest.json +++ b/packages/examples/packages/images/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "yOiOfKpriNZCSbXVfddcEw8EOkKd+bjPlvtl/yowGNo=", + "shasum": "17xibkzbvbonxdKRhYc1/hcy07SMuF1kUnWTww01Wh8=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/interactive-ui/snap.manifest.json b/packages/examples/packages/interactive-ui/snap.manifest.json index 58c199dd59..c3551b8670 100644 --- a/packages/examples/packages/interactive-ui/snap.manifest.json +++ b/packages/examples/packages/interactive-ui/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "qonHJAYzv7XPR96X+J3qOs5wxnWkfmWkUqK3ooYdkGI=", + "shasum": "RjotFvCYvRLQkQh0/MfN+KnVdoYaX/iG5xJ72hgea+8=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/interactive-ui/src/index.test.tsx b/packages/examples/packages/interactive-ui/src/index.test.tsx index b2bc211989..2bc197547c 100644 --- a/packages/examples/packages/interactive-ui/src/index.test.tsx +++ b/packages/examples/packages/interactive-ui/src/index.test.tsx @@ -1,6 +1,5 @@ import { expect } from '@jest/globals'; -import { installSnap } from '@metamask/snaps-jest'; -import { assert } from '@metamask/utils'; +import { assertIsConfirmationDialog, installSnap } from '@metamask/snaps-jest'; import { Insight, @@ -37,7 +36,6 @@ describe('onRpcRequest', () => { }); const formScreen = await response.getInterface(); - assert(formScreen.type === 'confirmation'); expect(formScreen).toRender(); @@ -50,6 +48,7 @@ describe('onRpcRequest', () => { await formScreen.clickElement('submit'); const resultScreen = await response.getInterface(); + assertIsConfirmationDialog(resultScreen); expect(resultScreen).toRender( { }); const formScreen = await response.getInterface(); - assert(formScreen.type === 'confirmation'); expect(formScreen).toRender(); await formScreen.clickElement('submit'); const resultScreen = await response.getInterface(); + assertIsConfirmationDialog(resultScreen); expect(resultScreen).toRender( { // This is useful if you're using TypeScript, since it will infer the type // of the user interface. - assert(ui.type === 'alert'); + assertIsAlertDialog(ui); expect(ui).toRender(text('Hello, world!')); // "Click" the OK button. diff --git a/packages/snaps-jest/src/helpers.test.tsx b/packages/snaps-jest/src/helpers.test.tsx index f1703ce90e..5429a84721 100644 --- a/packages/snaps-jest/src/helpers.test.tsx +++ b/packages/snaps-jest/src/helpers.test.tsx @@ -2,9 +2,13 @@ import { NodeProcessExecutionService } from '@metamask/snaps-controllers/node'; import { DialogType } from '@metamask/snaps-sdk'; import { Text } from '@metamask/snaps-sdk/jsx'; import { getSnapManifest } from '@metamask/snaps-utils/test-utils'; -import { assert } from '@metamask/utils'; -import { installSnap } from './helpers'; +import { + assertIsAlertDialog, + assertIsConfirmationDialog, + assertIsPromptDialog, + installSnap, +} from './helpers'; import type { InstallSnapOptions } from './internals'; import { handleInstallSnap } from './internals'; import { getMockServer } from './test-utils'; @@ -397,7 +401,7 @@ describe('installSnap', () => { }); const ui = await response.getInterface(); - assert(ui.type === DialogType.Prompt); + assertIsPromptDialog(ui); expect(ui).toStrictEqual({ type: DialogType.Prompt, content: Hello, world!, @@ -457,7 +461,7 @@ describe('installSnap', () => { }); const ui = await response.getInterface(); - assert(ui.type === DialogType.Confirmation); + assertIsConfirmationDialog(ui); expect(ui).toStrictEqual({ type: DialogType.Confirmation, content: Hello, world!, @@ -517,6 +521,7 @@ describe('installSnap', () => { }); const ui = await response.getInterface(); + assertIsAlertDialog(ui); expect(ui).toStrictEqual({ type: DialogType.Alert, content: Hello, world!, diff --git a/packages/snaps-jest/src/helpers.ts b/packages/snaps-jest/src/helpers.ts index 438c892dac..16869acdce 100644 --- a/packages/snaps-jest/src/helpers.ts +++ b/packages/snaps-jest/src/helpers.ts @@ -1,7 +1,13 @@ import type { AbstractExecutionService } from '@metamask/snaps-controllers'; -import type { SnapId } from '@metamask/snaps-sdk'; -import { HandlerType, logInfo } from '@metamask/snaps-utils'; -import { assertStruct, createModuleLogger } from '@metamask/utils'; +import { DialogType, type SnapId } from '@metamask/snaps-sdk'; +import type { FooterElement } from '@metamask/snaps-sdk/jsx'; +import { HandlerType, getJsxChildren, logInfo } from '@metamask/snaps-utils'; +import { + assertStruct, + createModuleLogger, + hasProperty, + assert, +} from '@metamask/utils'; import { create } from 'superstruct'; import { @@ -12,6 +18,7 @@ import { JsonRpcMockOptionsStruct, SignatureOptionsStruct, SnapResponseWithInterfaceStruct, + getElementByType, } from './internals'; import type { InstallSnapOptions } from './internals'; import { @@ -25,6 +32,15 @@ import type { Snap, SnapResponse, TransactionOptions, + SnapInterface, + SnapAlertInterface, + SnapInterfaceActions, + SnapConfirmationInterface, + SnapPromptInterface, + DefaultSnapInterface, + DefaultSnapInterfaceWithFooter, + DefaultSnapInterfaceWithPartialFooter, + DefaultSnapInterfaceWithoutFooter, } from './types'; const log = createModuleLogger(rootLogger, 'helpers'); @@ -62,6 +78,89 @@ function assertIsResponseWithInterface( assertStruct(response, SnapResponseWithInterfaceStruct); } +/** + * Ensure that the actual interface is an alert dialog. + * + * @param ui - The interface to verify. + */ +export function assertIsAlertDialog( + ui: SnapInterface, +): asserts ui is SnapAlertInterface & SnapInterfaceActions { + assert(hasProperty(ui, 'type') && ui.type === DialogType.Alert); +} + +/** + * Ensure that the actual interface is a confirmation dialog. + * + * @param ui - The interface to verify. + */ +export function assertIsConfirmationDialog( + ui: SnapInterface, +): asserts ui is SnapConfirmationInterface & SnapInterfaceActions { + assert(hasProperty(ui, 'type') && ui.type === DialogType.Confirmation); +} + +/** + * Ensure that the actual interface is a Prompt dialog. + * + * @param ui - The interface to verify. + */ +export function assertIsPromptDialog( + ui: SnapInterface, +): asserts ui is SnapPromptInterface & SnapInterfaceActions { + assert(hasProperty(ui, 'type') && ui.type === DialogType.Prompt); +} + +/** + * Ensure that the actual interface is a custom dialog. + * + * @param ui - The interface to verify. + */ +export function assertIsCustomDialog( + ui: SnapInterface, +): asserts ui is DefaultSnapInterface & SnapInterfaceActions { + assert(!hasProperty(ui, 'type')); +} + +/** + * Ensure that the actual interface is a custom dialog with a complete footer. + * + * @param ui - The interface to verify. + */ +export function assertCustomDialogHasFooter( + ui: DefaultSnapInterface & SnapInterfaceActions, +): asserts ui is DefaultSnapInterfaceWithFooter & SnapInterfaceActions { + const footer = getElementByType(ui.content, 'Footer'); + + assert(footer && getJsxChildren(footer).length === 2); +} + +/** + * Ensure that the actual interface is a custom dialog with a partial footer. + * + * @param ui - The interface to verify. + */ +export function assertCustomDialogHasPartialFooter( + ui: DefaultSnapInterface & SnapInterfaceActions, +): asserts ui is DefaultSnapInterfaceWithPartialFooter & SnapInterfaceActions { + const footer = getElementByType(ui.content, 'Footer'); + + assert(footer && getJsxChildren(footer).length === 1); +} + +/** + * Ensure that the actual interface is a custom dialog without a footer. + * + * @param ui - The interface to verify. + */ +export function assertCustomDialogHasNoFooter( + ui: DefaultSnapInterface & SnapInterfaceActions, +): asserts ui is DefaultSnapInterfaceWithoutFooter & SnapInterfaceActions { + const footer = getElementByType(ui.content, 'Footer'); + + assert(!footer); +} + /** * Load a snap into the environment. This is the main entry point for testing * snaps: It returns a {@link Snap} object that can be used to interact with the diff --git a/packages/snaps-jest/src/internals/simulation/controllers.test.ts b/packages/snaps-jest/src/internals/simulation/controllers.test.ts index 0c1ccab84a..968e5d8a9c 100644 --- a/packages/snaps-jest/src/internals/simulation/controllers.test.ts +++ b/packages/snaps-jest/src/internals/simulation/controllers.test.ts @@ -9,11 +9,13 @@ import { getControllers } from './controllers'; import type { MiddlewareHooks } from './simulation'; const MOCK_HOOKS: MiddlewareHooks = { + getIsLocked: jest.fn(), getMnemonic: jest.fn(), getSnapFile: jest.fn(), createInterface: jest.fn(), updateInterface: jest.fn(), getInterfaceState: jest.fn(), + resolveInterface: jest.fn(), }; describe('getControllers', () => { diff --git a/packages/snaps-jest/src/internals/simulation/controllers.ts b/packages/snaps-jest/src/internals/simulation/controllers.ts index 633e76bfcd..804279c36d 100644 --- a/packages/snaps-jest/src/internals/simulation/controllers.ts +++ b/packages/snaps-jest/src/internals/simulation/controllers.ts @@ -82,6 +82,8 @@ export function getControllers(options: GetControllersOptions): Controllers { allowedActions: [ 'PhishingController:maybeUpdateState', 'PhishingController:testOrigin', + 'ApprovalController:hasRequest', + 'ApprovalController:acceptRequest', ], allowedEvents: [], }), diff --git a/packages/snaps-jest/src/internals/simulation/interface.test.tsx b/packages/snaps-jest/src/internals/simulation/interface.test.tsx index 9e89e88e01..a177b647c1 100644 --- a/packages/snaps-jest/src/internals/simulation/interface.test.tsx +++ b/packages/snaps-jest/src/internals/simulation/interface.test.tsx @@ -20,6 +20,8 @@ import { FileInput, Checkbox, Form, + Container, + Footer, } from '@metamask/snaps-sdk/jsx'; import { getJsxElementFromComponent, @@ -27,10 +29,18 @@ import { WrappedSnapError, } from '@metamask/snaps-utils'; import { MOCK_SNAP_ID } from '@metamask/snaps-utils/test-utils'; -import { assert } from '@metamask/utils'; import type { SagaIterator } from 'redux-saga'; import { take } from 'redux-saga/effects'; +import { + assertIsAlertDialog, + assertIsConfirmationDialog, + assertIsCustomDialog, + assertIsPromptDialog, + assertCustomDialogHasFooter, + assertCustomDialogHasPartialFooter, + assertCustomDialogHasNoFooter, +} from '../../helpers'; import { getMockOptions, getRestrictedSnapInterfaceControllerMessenger, @@ -42,6 +52,7 @@ import { getInterface, getInterfaceResponse, mergeValue, + resolveWithSaga, selectInDropdown, typeInField, uploadFile, @@ -79,6 +90,7 @@ describe('getInterfaceResponse', () => { foo, interfaceActions, ); + assertIsAlertDialog(response); expect(response).toStrictEqual({ type: DialogType.Alert, @@ -104,6 +116,7 @@ describe('getInterfaceResponse', () => { interfaceActions, ); + assertIsConfirmationDialog(response); expect(response).toStrictEqual({ type: DialogType.Confirmation, content: foo, @@ -129,7 +142,7 @@ describe('getInterfaceResponse', () => { interfaceActions, ); - assert(response.type === DialogType.Confirmation); + assertIsConfirmationDialog(response); expect(response).toStrictEqual({ type: DialogType.Confirmation, content: foo, @@ -155,6 +168,7 @@ describe('getInterfaceResponse', () => { interfaceActions, ); + assertIsPromptDialog(response); expect(response).toStrictEqual({ type: DialogType.Prompt, content: foo, @@ -180,6 +194,7 @@ describe('getInterfaceResponse', () => { interfaceActions, ); + assertIsPromptDialog(response); expect(response).toStrictEqual({ type: DialogType.Prompt, content: foo, @@ -205,7 +220,7 @@ describe('getInterfaceResponse', () => { interfaceActions, ); - assert(response.type === DialogType.Prompt); + assertIsPromptDialog(response); expect(response).toStrictEqual({ type: DialogType.Prompt, content: foo, @@ -222,6 +237,116 @@ describe('getInterfaceResponse', () => { expect(await promise).toBeNull(); }); + it('returns no `ok` or `cancel` functions for custom dialogs with a complete footer', () => { + const { runSaga } = createStore(getMockOptions()); + const response = getInterfaceResponse( + runSaga, + DIALOG_APPROVAL_TYPES.default, + + + foo + +
+ + +
+
, + interfaceActions, + ); + + assertIsCustomDialog(response); + assertCustomDialogHasFooter(response); + + expect(response).toStrictEqual({ + content: ( + + + foo + +
+ + +
+
+ ), + clickElement: expect.any(Function), + typeInField: expect.any(Function), + selectInDropdown: expect.any(Function), + uploadFile: expect.any(Function), + }); + }); + + it('returns a `cancel` functions for custom dialogs with a partial footer', () => { + const { runSaga } = createStore(getMockOptions()); + const response = getInterfaceResponse( + runSaga, + DIALOG_APPROVAL_TYPES.default, + + + foo + +
+ +
+
, + interfaceActions, + ); + + assertIsCustomDialog(response); + assertCustomDialogHasPartialFooter(response); + + expect(response).toStrictEqual({ + content: ( + + + foo + +
+ +
+
+ ), + clickElement: expect.any(Function), + typeInField: expect.any(Function), + selectInDropdown: expect.any(Function), + uploadFile: expect.any(Function), + cancel: expect.any(Function), + }); + }); + + it('returns a `ok` and `cancel` functions for custom dialogs without a footer', () => { + const { runSaga } = createStore(getMockOptions()); + const response = getInterfaceResponse( + runSaga, + DIALOG_APPROVAL_TYPES.default, + + + foo + + , + interfaceActions, + ); + + assertIsCustomDialog(response); + assertCustomDialogHasNoFooter(response); + + expect(response).toStrictEqual({ + content: ( + + + foo + + + ), + clickElement: expect.any(Function), + typeInField: expect.any(Function), + selectInDropdown: expect.any(Function), + uploadFile: expect.any(Function), + cancel: expect.any(Function), + ok: expect.any(Function), + }); + }); + it('throws an error for unknown dialog types', () => { const { runSaga } = createStore(getMockOptions()); @@ -232,6 +357,27 @@ describe('getInterfaceResponse', () => { }); }); +describe('resolveWithSaga', () => { + it('resolves the user interface', async () => { + const { runSaga, store } = createStore( + getMockOptions({ + state: { + ui: { + current: { + type: DIALOG_APPROVAL_TYPES.default, + id: 'foo', + }, + }, + }, + }), + ); + + await runSaga(resolveWithSaga, 'hello').toPromise(); + + expect(store.getState().ui.current).toBeNull(); + }); +}); + describe('getElement', () => { it('gets an element at the root', () => { const content = button({ value: 'foo', name: 'bar' }); diff --git a/packages/snaps-jest/src/internals/simulation/interface.ts b/packages/snaps-jest/src/internals/simulation/interface.ts index 2fd7850275..e3d529391f 100644 --- a/packages/snaps-jest/src/internals/simulation/interface.ts +++ b/packages/snaps-jest/src/internals/simulation/interface.ts @@ -9,7 +9,8 @@ import type { File, } from '@metamask/snaps-sdk'; import { DialogType, UserInputEventType, assert } from '@metamask/snaps-sdk'; -import type { FormElement, JSXElement } from '@metamask/snaps-sdk/jsx'; +import type { FooterElement } from '@metamask/snaps-sdk/jsx'; +import { type FormElement, type JSXElement } from '@metamask/snaps-sdk/jsx'; import { HandlerType, getJsxChildren, @@ -47,7 +48,7 @@ const MAX_FILE_SIZE = 10_000_000; // 10 MB */ export function getInterfaceResponse( runSaga: RunSagaFunction, - type: DialogApprovalTypes[DialogType], + type: DialogApprovalTypes[DialogType | 'default'], content: JSXElement, interfaceActions: SnapInterfaceActions, ): SnapInterface { @@ -80,11 +81,52 @@ export function getInterfaceResponse( cancel: resolveWith(runSaga, null), }; + case DIALOG_APPROVAL_TYPES.default: { + const footer = getElementByType(content, 'Footer'); + + // No Footer defined so we apply a default footer. + if (!footer) { + return { + ...interfaceActions, + content, + + ok: resolveWith(runSaga, null), + cancel: resolveWith(runSaga, null), + }; + } + + // Only one button in footer so we apply a default cancel button. + if (getJsxChildren(footer).length === 1) { + return { + ...interfaceActions, + content, + + cancel: resolveWith(runSaga, null), + }; + } + + // We have two buttons in the footer so we assume the snap handles the approval of the interface. + return { + ...interfaceActions, + content, + }; + } + default: throw new Error(`Unknown or unsupported dialog type: "${String(type)}".`); } } +/** + * Resolve the current user interface with the given value. + * + * @param value - The value to resolve the user interface with. + * @yields Puts the resolve user interface action. + */ +export function* resolveWithSaga(value: unknown): SagaIterator { + yield put(resolveInterface(value)); +} + /** * Resolve the current user interface with the given value. This returns a * function that can be used to resolve the user interface. @@ -94,17 +136,8 @@ export function getInterfaceResponse( * @returns A function that can be used to resolve the user interface. */ function resolveWith(runSaga: RunSagaFunction, value: unknown) { - /** - * Resolve the current user interface with the given value. - * - * @yields Puts the resolve user interface action. - */ - function* resolveWithSaga(): SagaIterator { - yield put(resolveInterface(value)); - } - return async () => { - await runSaga(resolveWithSaga).toPromise(); + await runSaga(resolveWithSaga, value).toPromise(); }; } @@ -116,16 +149,6 @@ function resolveWith(runSaga: RunSagaFunction, value: unknown) { * @returns A function that can be used to resolve the user interface. */ function resolveWithInput(runSaga: RunSagaFunction) { - /** - * Resolve the current user interface with the given value. - * - * @param value - The value to resolve the user interface with. - * @yields Puts the resolve user interface action. - */ - function* resolveWithSaga(value: string): SagaIterator { - yield put(resolveInterface(value)); - } - return async (value = '') => { await runSaga(resolveWithSaga, value).toPromise(); }; @@ -242,6 +265,27 @@ export function getElement( return undefined; }); } + +/** + * Get an element from a JSX tree with the given type. + * + * @param content - The interface content. + * @param type - The element type. + * @returns The element with the given type. + */ +export function getElementByType( + content: JSXElement, + type: string, +) { + return walkJsx(content, (element) => { + if (element.type === type) { + return element as Element; + } + + return undefined; + }); +} + /** * Handle submitting event requests to OnUserInput including unwrapping potential errors. * @@ -711,7 +755,7 @@ export function* getInterface( runSaga: RunSagaFunction, snapId: SnapId, controllerMessenger: RootControllerMessenger, -): SagaIterator { +): SagaIterator { const storedInterface = yield call( getStoredInterface, controllerMessenger, diff --git a/packages/snaps-jest/src/internals/simulation/simulation.test.ts b/packages/snaps-jest/src/internals/simulation/simulation.test.ts index 2e9d1968dc..5482742032 100644 --- a/packages/snaps-jest/src/internals/simulation/simulation.test.ts +++ b/packages/snaps-jest/src/internals/simulation/simulation.test.ts @@ -5,6 +5,7 @@ import { NodeThreadExecutionService, SnapInterfaceController, } from '@metamask/snaps-controllers/node'; +import { DIALOG_APPROVAL_TYPES } from '@metamask/snaps-rpc-methods'; import { AuxiliaryFileEncoding, text } from '@metamask/snaps-sdk'; import { VirtualFile } from '@metamask/snaps-utils'; import { getSnapManifest } from '@metamask/snaps-utils/test-utils'; @@ -17,6 +18,7 @@ import { } from '../../test-utils'; import { DEFAULT_SRP } from './constants'; import { getHooks, handleInstallSnap, registerActions } from './simulation'; +import { createStore, setInterface } from './store'; describe('handleInstallSnap', () => { it('installs a Snap and returns the execution service', async () => { @@ -203,6 +205,57 @@ describe('getHooks', () => { await close(); }); + it('returns the `resolveInterface` hook', async () => { + // eslint-disable-next-line no-new + const snapInterfaceController = new SnapInterfaceController({ + messenger: + getRestrictedSnapInterfaceControllerMessenger(controllerMessenger), + }); + + jest.spyOn(controllerMessenger, 'call'); + + const { snapId, close } = await getMockServer({ + manifest: getSnapManifest(), + }); + const id = await snapInterfaceController.createInterface( + snapId, + text('foo'), + ); + + const location = detectSnapLocation(snapId, { + allowLocal: true, + }); + const snapFiles = await fetchSnap(snapId, location); + + const { resolveInterface } = getHooks( + getMockOptions({ + state: { + ui: { + current: { + id, + type: DIALOG_APPROVAL_TYPES.default, + }, + }, + }, + }), + snapFiles, + snapId, + controllerMessenger, + ); + + await resolveInterface(id, 'foobar'); + + expect(controllerMessenger.call).toHaveBeenNthCalledWith( + 2, + 'SnapInterfaceController:resolveInterface', + snapId, + id, + 'foobar', + ); + + await close(); + }); + it('returns the `getIsLocked` hook', async () => { const { snapId, close } = await getMockServer(); @@ -224,12 +277,42 @@ describe('getHooks', () => { }); describe('registerActions', () => { + const { runSaga, store } = createStore(getMockOptions()); const controllerMessenger = getRootControllerMessenger(false); + it('registers `PhishingController:testOrigin`', async () => { - registerActions(controllerMessenger); + registerActions(controllerMessenger, runSaga); expect( controllerMessenger.call('PhishingController:testOrigin', 'foo'), ).toStrictEqual({ result: false, type: 'all' }); }); + + it('registers `ApprovalController:hasRequest`', async () => { + registerActions(controllerMessenger, runSaga); + + store.dispatch( + setInterface({ type: DIALOG_APPROVAL_TYPES.default, id: 'foo' }), + ); + + expect( + controllerMessenger.call('ApprovalController:hasRequest', { id: 'foo' }), + ).toBe(true); + }); + + it('registers `ApprovalController:acceptRequest`', async () => { + registerActions(controllerMessenger, runSaga); + + store.dispatch( + setInterface({ type: DIALOG_APPROVAL_TYPES.default, id: 'foo' }), + ); + + expect( + await controllerMessenger.call( + 'ApprovalController:acceptRequest', + 'foo', + 'bar', + ), + ).toStrictEqual({ value: 'bar' }); + }); }); diff --git a/packages/snaps-jest/src/internals/simulation/simulation.ts b/packages/snaps-jest/src/internals/simulation/simulation.ts index 93cbce907c..beafcdaf82 100644 --- a/packages/snaps-jest/src/internals/simulation/simulation.ts +++ b/packages/snaps-jest/src/internals/simulation/simulation.ts @@ -12,6 +12,7 @@ import { NodeThreadExecutionService, setupMultiplex, } from '@metamask/snaps-controllers/node'; +import { DIALOG_APPROVAL_TYPES } from '@metamask/snaps-rpc-methods'; import type { SnapId, AuxiliaryFileEncoding, @@ -20,18 +21,22 @@ import type { } from '@metamask/snaps-sdk'; import type { FetchedSnapFiles } from '@metamask/snaps-utils'; import { logError } from '@metamask/snaps-utils'; +import type { Json } from '@metamask/utils'; import type { Duplex } from 'readable-stream'; import { pipeline } from 'readable-stream'; +import type { SagaIterator } from 'redux-saga'; +import { select } from 'redux-saga/effects'; import type { RootControllerMessenger } from './controllers'; import { getControllers, registerSnap } from './controllers'; import { getSnapFile } from './files'; +import { resolveWithSaga } from './interface'; import { getEndowments } from './methods'; import { createJsonRpcEngine } from './middleware'; import type { SimulationOptions, SimulationUserOptions } from './options'; import { getOptions } from './options'; -import type { RunSagaFunction, Store } from './store'; -import { createStore } from './store'; +import type { Interface, RunSagaFunction, Store } from './store'; +import { createStore, getCurrentInterface } from './store'; /** * Options for the execution service, without the options that are shared @@ -110,6 +115,7 @@ export type MiddlewareHooks = { createInterface: (content: Component) => Promise; updateInterface: (id: string, content: Component) => Promise; getInterfaceState: (id: string) => InterfaceState; + resolveInterface: (id: string, value: Json) => Promise; }; /** @@ -152,7 +158,7 @@ export async function handleInstallSnap< const controllerMessenger = new ControllerMessenger(); - registerActions(controllerMessenger); + registerActions(controllerMessenger, runSaga); // Set up controllers and JSON-RPC stack. const hooks = getHooks(options, snapFiles, snapId, controllerMessenger); @@ -258,6 +264,12 @@ export function getHooks( snapId, ...args, ).state, + resolveInterface: async (...args) => + controllerMessenger.call( + 'SnapInterfaceController:resolveInterface', + snapId, + ...args, + ), }; } @@ -265,8 +277,12 @@ export function getHooks( * Register mocked action handlers. * * @param controllerMessenger - The controller messenger. + * @param runSaga - The run saga function. */ -export function registerActions(controllerMessenger: RootControllerMessenger) { +export function registerActions( + controllerMessenger: RootControllerMessenger, + runSaga: RunSagaFunction, +) { controllerMessenger.registerActionHandler( 'PhishingController:maybeUpdateState', async () => Promise.resolve(), @@ -276,4 +292,37 @@ export function registerActions(controllerMessenger: RootControllerMessenger) { 'PhishingController:testOrigin', () => ({ result: false, type: 'all' }), ); + + controllerMessenger.registerActionHandler( + 'ApprovalController:hasRequest', + (opts) => { + /** + * Get the current interface from the store. + * + * @yields Selects the current interface from the store. + * @returns The current interface. + */ + function* getCurrentInterfaceSaga(): SagaIterator { + const currentInterface: Interface = yield select(getCurrentInterface); + return currentInterface; + } + + const currentInterface: Interface | undefined = runSaga( + getCurrentInterfaceSaga, + ).result(); + return ( + currentInterface?.type === DIALOG_APPROVAL_TYPES.default && + currentInterface?.id === opts?.id + ); + }, + ); + + controllerMessenger.registerActionHandler( + 'ApprovalController:acceptRequest', + async (_id: string, value: unknown) => { + await runSaga(resolveWithSaga, value).toPromise(); + + return { value }; + }, + ); } diff --git a/packages/snaps-jest/src/internals/simulation/store/ui.ts b/packages/snaps-jest/src/internals/simulation/store/ui.ts index 9c12a4ec13..e1f4d85829 100644 --- a/packages/snaps-jest/src/internals/simulation/store/ui.ts +++ b/packages/snaps-jest/src/internals/simulation/store/ui.ts @@ -6,7 +6,7 @@ import { createAction, createSelector, createSlice } from '@reduxjs/toolkit'; import type { ApplicationState } from './store'; export type Interface = { - type: DialogApprovalTypes[DialogType]; + type: DialogApprovalTypes[DialogType | 'default']; id: string; }; diff --git a/packages/snaps-jest/src/test-utils/controller.ts b/packages/snaps-jest/src/test-utils/controller.ts index 8fe8d0fbe8..7dc0f08e4e 100644 --- a/packages/snaps-jest/src/test-utils/controller.ts +++ b/packages/snaps-jest/src/test-utils/controller.ts @@ -24,6 +24,16 @@ export const getRootControllerMessenger = (mocked = true) => { 'ExecutionService:handleRpcRequest', jest.fn(), ); + + messenger.registerActionHandler( + 'ApprovalController:hasRequest', + () => true, + ); + + messenger.registerActionHandler( + 'ApprovalController:acceptRequest', + async (_id: string, value: unknown) => ({ value }), + ); } return messenger; @@ -43,6 +53,8 @@ export const getRestrictedSnapInterfaceControllerMessenger = ( allowedActions: [ 'PhishingController:testOrigin', 'PhishingController:maybeUpdateState', + 'ApprovalController:hasRequest', + 'ApprovalController:acceptRequest', ], allowedEvents: [], }); diff --git a/packages/snaps-jest/src/types.ts b/packages/snaps-jest/src/types.ts index 5cd9be62fd..81ba68520e 100644 --- a/packages/snaps-jest/src/types.ts +++ b/packages/snaps-jest/src/types.ts @@ -216,10 +216,52 @@ export type SnapPromptInterface = { cancel(): Promise; }; +/** + * A `snap_dialog` default interface that has a Footer with two buttons defined. + * The approval of this confirmation is handled by the snap. + */ +export type DefaultSnapInterfaceWithFooter = { + /** + * The content to show in the interface. + */ + content: JSXElement; +}; + +/** + * A `snap_dialog` default interface that has a Footer with one button defined. + * A cancel button is automatically applied to the interface in this case. + */ +export type DefaultSnapInterfaceWithPartialFooter = + DefaultSnapInterfaceWithFooter & { + /** + * Cancel the dialog. + */ + cancel(): Promise; + }; + +/** + * A `snap_dialog` default interface that has no Footer defined. + * A cancel and ok button is automatically applied to the interface in this case. + */ +export type DefaultSnapInterfaceWithoutFooter = + DefaultSnapInterfaceWithPartialFooter & { + /** + * Close the dialog. + * + */ + ok(): Promise; + }; + +export type DefaultSnapInterface = + | DefaultSnapInterfaceWithFooter + | DefaultSnapInterfaceWithPartialFooter + | DefaultSnapInterfaceWithoutFooter; + export type SnapInterface = ( | SnapAlertInterface | SnapConfirmationInterface | SnapPromptInterface + | DefaultSnapInterface ) & SnapInterfaceActions; diff --git a/packages/snaps-sdk/src/jsx/validation.ts b/packages/snaps-sdk/src/jsx/validation.ts index befdbd5508..afa480d954 100644 --- a/packages/snaps-sdk/src/jsx/validation.ts +++ b/packages/snaps-sdk/src/jsx/validation.ts @@ -494,7 +494,10 @@ export const BoxChildStruct = nullUnion([ * For now, the allowed JSX elements at the root are the same as the allowed * children of the Box component. */ -export const RootJSXElementStruct = BoxChildStruct; +export const RootJSXElementStruct = nullUnion([ + BoxChildStruct, + ContainerStruct, +]); /** * A struct for the {@link JSXElement} type. diff --git a/yarn.lock b/yarn.lock index af52e093a5..2c52d7f368 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4291,7 +4291,6 @@ __metadata: "@metamask/snaps-cli": "workspace:^" "@metamask/snaps-jest": "workspace:^" "@metamask/snaps-sdk": "workspace:^" - "@metamask/utils": ^8.3.0 "@swc/core": 1.3.78 "@swc/jest": ^0.2.26 "@typescript-eslint/eslint-plugin": ^5.42.1 @@ -4526,7 +4525,6 @@ __metadata: "@metamask/snaps-cli": "workspace:^" "@metamask/snaps-jest": "workspace:^" "@metamask/snaps-sdk": "workspace:^" - "@metamask/utils": ^8.3.0 "@swc/core": 1.3.78 "@swc/jest": ^0.2.26 "@typescript-eslint/eslint-plugin": ^5.42.1