diff --git a/packages/examples/packages/interactive-ui/snap.config.ts b/packages/examples/packages/interactive-ui/snap.config.ts index 00d56f12be..e4ad5de257 100644 --- a/packages/examples/packages/interactive-ui/snap.config.ts +++ b/packages/examples/packages/interactive-ui/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: 8028, }, diff --git a/packages/examples/packages/interactive-ui/snap.manifest.json b/packages/examples/packages/interactive-ui/snap.manifest.json index c7d7f864e4..e43fab4996 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": "WCcrRcQpQpNuWEmyN5TQuX1ckpHF/5JIsdkgMLCvjO0=", + "shasum": "ENUxWrgZruvIFCLM2uCYYFIjyBxp6vHYK7L1ZhiZDJA=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/interactive-ui/src/components/Insight.tsx b/packages/examples/packages/interactive-ui/src/components/Insight.tsx new file mode 100644 index 0000000000..540aa56ee8 --- /dev/null +++ b/packages/examples/packages/interactive-ui/src/components/Insight.tsx @@ -0,0 +1,21 @@ +import type { SnapComponent } from '@metamask/snaps-sdk/jsx'; +import { Button, Box, Text, Row, Address } from '@metamask/snaps-sdk/jsx'; + +type InsightProps = { + from: string; + to?: string; +}; + +export const Insight: SnapComponent = ({ from, to }) => { + return ( + + +
+ + + {to ?
: None} + + + + ); +}; diff --git a/packages/examples/packages/interactive-ui/src/components/InteractiveForm.tsx b/packages/examples/packages/interactive-ui/src/components/InteractiveForm.tsx new file mode 100644 index 0000000000..91158f938b --- /dev/null +++ b/packages/examples/packages/interactive-ui/src/components/InteractiveForm.tsx @@ -0,0 +1,25 @@ +import type { SnapComponent } from '@metamask/snaps-sdk/jsx'; +import { + Button, + Box, + Field, + Heading, + Form, + Input, +} from '@metamask/snaps-sdk/jsx'; + +export const InteractiveForm: SnapComponent = () => { + return ( + + Interactive UI Example Snap +
+ + + + +
+
+ ); +}; diff --git a/packages/examples/packages/interactive-ui/src/components/Result.tsx b/packages/examples/packages/interactive-ui/src/components/Result.tsx new file mode 100644 index 0000000000..c5ecb2b913 --- /dev/null +++ b/packages/examples/packages/interactive-ui/src/components/Result.tsx @@ -0,0 +1,21 @@ +import type { SnapComponent } from '@metamask/snaps-sdk/jsx'; +import { Heading, Button, Box, Text, Copyable } from '@metamask/snaps-sdk/jsx'; + +type ResultProps = { + values: Record; +}; + +export const Result: SnapComponent = ({ values }) => { + return ( + + Interactive UI Example Snap + You submitted the following values: + + {Object.values(values).map((value) => ( + + ))} + + + + ); +}; diff --git a/packages/examples/packages/interactive-ui/src/components/TransactionType.tsx b/packages/examples/packages/interactive-ui/src/components/TransactionType.tsx new file mode 100644 index 0000000000..c47e00510f --- /dev/null +++ b/packages/examples/packages/interactive-ui/src/components/TransactionType.tsx @@ -0,0 +1,21 @@ +import type { SnapComponent } from '@metamask/snaps-sdk/jsx'; +import { Bold, Box, Row, Button, Text } from '@metamask/snaps-sdk/jsx'; + +type TransactionTypeProps = { + type: string; +}; + +export const TransactionType: SnapComponent = ({ + type, +}) => { + return ( + + + + {type} + + + + + ); +}; diff --git a/packages/examples/packages/interactive-ui/src/components/index.ts b/packages/examples/packages/interactive-ui/src/components/index.ts new file mode 100644 index 0000000000..a62b48b712 --- /dev/null +++ b/packages/examples/packages/interactive-ui/src/components/index.ts @@ -0,0 +1,4 @@ +export * from './InteractiveForm'; +export * from './Insight'; +export * from './Result'; +export * from './TransactionType'; diff --git a/packages/examples/packages/interactive-ui/src/index.test.ts b/packages/examples/packages/interactive-ui/src/index.test.ts deleted file mode 100644 index 75d8f9b56b..0000000000 --- a/packages/examples/packages/interactive-ui/src/index.test.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { expect } from '@jest/globals'; -import { installSnap } from '@metamask/snaps-jest'; -import { - ButtonType, - address, - button, - copyable, - form, - heading, - input, - panel, - row, - text, -} from '@metamask/snaps-sdk'; -import { assert } from '@metamask/utils'; - -describe('onRpcRequest', () => { - it('throws an error if the requested method does not exist', async () => { - const { request } = await installSnap(); - - const response = await request({ - method: 'foo', - }); - - expect(response).toRespondWithError({ - code: -32601, - message: 'The method does not exist / is not available.', - stack: expect.any(String), - data: { - method: 'foo', - cause: null, - }, - }); - }); - - describe('dialog', () => { - it('creates a new Snap interface and use it in a confirmation dialog', async () => { - const { request } = await installSnap(); - - const response = request({ - method: 'dialog', - }); - - const startScreen = await response.getInterface(); - assert(startScreen.type === 'confirmation'); - - expect(startScreen).toRender( - panel([ - heading('Interactive UI Example Snap'), - button({ value: 'Update UI', name: 'update' }), - ]), - ); - - await startScreen.clickElement('update'); - - const formScreen = await response.getInterface(); - - expect(formScreen).toRender( - panel([ - heading('Interactive UI Example Snap'), - form({ - name: 'example-form', - children: [ - input({ - name: 'example-input', - placeholder: 'Enter something...', - }), - button('Submit', ButtonType.Submit, 'submit'), - ], - }), - ]), - ); - - await formScreen.typeInField('example-input', 'foobar'); - - await formScreen.clickElement('submit'); - - const resultScreen = await response.getInterface(); - - expect(resultScreen).toRender( - panel([ - heading('Interactive UI Example Snap'), - text('The submitted value is:'), - copyable('foobar'), - ]), - ); - await resultScreen.ok(); - - expect(await response).toRespondWith(true); - }); - - it('lets users input nothing', async () => { - const { request } = await installSnap(); - - const response = request({ - method: 'dialog', - }); - - const startScreen = await response.getInterface(); - assert(startScreen.type === 'confirmation'); - - expect(startScreen).toRender( - panel([ - heading('Interactive UI Example Snap'), - button({ value: 'Update UI', name: 'update' }), - ]), - ); - - await startScreen.clickElement('update'); - - const formScreen = await response.getInterface(); - - expect(formScreen).toRender( - panel([ - heading('Interactive UI Example Snap'), - form({ - name: 'example-form', - children: [ - input({ - name: 'example-input', - placeholder: 'Enter something...', - }), - button('Submit', ButtonType.Submit, 'submit'), - ], - }), - ]), - ); - - await formScreen.clickElement('submit'); - - const resultScreen = await response.getInterface(); - - expect(resultScreen).toRender( - panel([ - heading('Interactive UI Example Snap'), - text('The submitted value is:'), - copyable(''), - ]), - ); - await resultScreen.ok(); - - expect(await response).toRespondWith(true); - }); - }); - - describe('getState', () => { - it('gets the interface state', async () => { - const { request } = await installSnap(); - - const response = request({ - method: 'dialog', - }); - - const ui = await response.getInterface(); - - const getStateResponse = await request({ - method: 'getState', - }); - - await ui.ok(); - - expect(getStateResponse).toRespondWith({}); - }); - }); -}); - -describe('onHomePage', () => { - it('returns custom UI', async () => { - const { onHomePage } = await installSnap(); - - const response = await onHomePage(); - - const startScreen = response.getInterface(); - - expect(startScreen).toRender( - panel([ - heading('Interactive UI Example Snap'), - button({ value: 'Update UI', name: 'update' }), - ]), - ); - - await startScreen.clickElement('update'); - - const formScreen = response.getInterface(); - - expect(formScreen).toRender( - panel([ - heading('Interactive UI Example Snap'), - form({ - name: 'example-form', - children: [ - input({ - name: 'example-input', - placeholder: 'Enter something...', - }), - button('Submit', ButtonType.Submit, 'submit'), - ], - }), - ]), - ); - - await formScreen.typeInField('example-input', 'foobar'); - - await formScreen.clickElement('submit'); - - const resultScreen = response.getInterface(); - - expect(resultScreen).toRender( - panel([ - heading('Interactive UI Example Snap'), - text('The submitted value is:'), - copyable('foobar'), - ]), - ); - }); -}); - -describe('onTransaction', () => { - const FROM_ADDRESS = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045'; - const TO_ADDRESS = '0x4bbeeb066ed09b7aed07bf39eee0460dfa261520'; - it('returns custom UI', async () => { - const { onTransaction } = await installSnap(); - - const response = await onTransaction({ - from: FROM_ADDRESS, - to: TO_ADDRESS, - // This is not a valid ERC-20 transfer as all the values are zero, but it - // is enough to test the `onTransaction` handler. - data: '0xa9059cbb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', - }); - - const startScreen = response.getInterface(); - - expect(startScreen).toRender( - panel([ - row('From', address(FROM_ADDRESS)), - row('To', address(TO_ADDRESS)), - button({ value: 'See transaction type', name: 'transaction-type' }), - ]), - ); - - await startScreen.clickElement('transaction-type'); - - const txTypeScreen = response.getInterface(); - - expect(txTypeScreen).toRender( - panel([ - row('Transaction type', text('ERC-20')), - button({ value: 'Go back', name: 'go-back' }), - ]), - ); - }); -}); diff --git a/packages/examples/packages/interactive-ui/src/index.test.tsx b/packages/examples/packages/interactive-ui/src/index.test.tsx new file mode 100644 index 0000000000..779e70d88f --- /dev/null +++ b/packages/examples/packages/interactive-ui/src/index.test.tsx @@ -0,0 +1,132 @@ +import { expect } from '@jest/globals'; +import { installSnap } from '@metamask/snaps-jest'; +import { assert } from '@metamask/utils'; + +import { + Insight, + InteractiveForm, + Result, + TransactionType, +} from './components'; + +describe('onRpcRequest', () => { + it('throws an error if the requested method does not exist', async () => { + const { request } = await installSnap(); + + const response = await request({ + method: 'foo', + }); + + expect(response).toRespondWithError({ + code: -32601, + message: 'The method does not exist / is not available.', + stack: expect.any(String), + data: { + method: 'foo', + cause: null, + }, + }); + }); + + describe('dialog', () => { + it('creates a new Snap interface and use it in a confirmation dialog', async () => { + const { request } = await installSnap(); + + const response = request({ + method: 'dialog', + }); + + const formScreen = await response.getInterface(); + assert(formScreen.type === 'confirmation'); + + expect(formScreen).toRender(); + + await formScreen.typeInField('example-input', 'foobar'); + + await formScreen.clickElement('submit'); + + const resultScreen = await response.getInterface(); + + expect(resultScreen).toRender( + , + ); + await resultScreen.ok(); + + expect(await response).toRespondWith(true); + }); + + it('lets users input nothing', async () => { + const { request } = await installSnap(); + + const response = request({ + method: 'dialog', + }); + + const formScreen = await response.getInterface(); + assert(formScreen.type === 'confirmation'); + + expect(formScreen).toRender(); + + await formScreen.clickElement('submit'); + + const resultScreen = await response.getInterface(); + + expect(resultScreen).toRender( + , + ); + await resultScreen.ok(); + + expect(await response).toRespondWith(true); + }); + }); +}); + +describe('onHomePage', () => { + it('returns custom UI', async () => { + const { onHomePage } = await installSnap(); + + const response = await onHomePage(); + + const formScreen = response.getInterface(); + + expect(formScreen).toRender(); + + await formScreen.typeInField('example-input', 'foobar'); + + await formScreen.clickElement('submit'); + + const resultScreen = response.getInterface(); + + expect(resultScreen).toRender( + , + ); + }); +}); + +describe('onTransaction', () => { + const FROM_ADDRESS = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045'; + const TO_ADDRESS = '0x4bbeeb066ed09b7aed07bf39eee0460dfa261520'; + it('returns custom UI', async () => { + const { onTransaction } = await installSnap(); + + const response = await onTransaction({ + from: FROM_ADDRESS, + to: TO_ADDRESS, + // This is not a valid ERC-20 transfer as all the values are zero, but it + // is enough to test the `onTransaction` handler. + data: '0xa9059cbb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + }); + + const startScreen = response.getInterface(); + + expect(startScreen).toRender( + , + ); + + await startScreen.clickElement('transaction-type'); + + const txTypeScreen = response.getInterface(); + + expect(txTypeScreen).toRender(); + }); +}); diff --git a/packages/examples/packages/interactive-ui/src/index.ts b/packages/examples/packages/interactive-ui/src/index.tsx similarity index 73% rename from packages/examples/packages/interactive-ui/src/index.ts rename to packages/examples/packages/interactive-ui/src/index.tsx index cc92b710b0..672d080a33 100644 --- a/packages/examples/packages/interactive-ui/src/index.ts +++ b/packages/examples/packages/interactive-ui/src/index.tsx @@ -7,17 +7,11 @@ import type { import { UserInputEventType, ManageStateOperation, - assert, MethodNotFoundError, } from '@metamask/snaps-sdk'; -import { - createInterface, - displayTransactionType, - getInsightContent, - showForm, - showResult, -} from './ui'; +import { InteractiveForm, Result } from './components'; +import { displayTransactionType, getInsightContent } from './ui'; /** * Handle incoming JSON-RPC requests from the dapp, sent through the @@ -38,55 +32,13 @@ import { export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { switch (request.method) { case 'dialog': { - try { - const interfaceId = await createInterface(); - - await snap.request({ - method: 'snap_manageState', - params: { - operation: ManageStateOperation.UpdateState, - newState: { interfaceId }, - encrypted: false, - }, - }); - - return await snap.request({ - method: 'snap_dialog', - params: { - type: 'confirmation', - id: interfaceId, - }, - }); - } finally { - await snap.request({ - method: 'snap_manageState', - params: { - operation: ManageStateOperation.ClearState, - encrypted: false, - }, - }); - } - } - - case 'getState': { - const snapState = await snap.request({ - method: 'snap_manageState', - params: { - operation: ManageStateOperation.GetState, - encrypted: false, - }, - }); - - assert(snapState?.interfaceId, 'No interface ID found in state.'); - - const state = await snap.request({ - method: 'snap_getInterfaceState', + return await snap.request({ + method: 'snap_dialog', params: { - id: snapState.interfaceId as string, + type: 'confirmation', + content: , }, }); - - return state; } default: @@ -104,9 +56,7 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { * @see https://docs.metamask.io/snaps/reference/exports/#onhomepage */ export const onHomePage: OnHomePageHandler = async () => { - const interfaceId = await createInterface(); - - return { id: interfaceId }; + return { content: }; }; /** @@ -158,10 +108,6 @@ export const onTransaction: OnTransactionHandler = async ({ transaction }) => { export const onUserInput: OnUserInputHandler = async ({ id, event }) => { if (event.type === UserInputEventType.ButtonClickEvent) { switch (event.name) { - case 'update': - await showForm(id); - break; - case 'transaction-type': await displayTransactionType(id); break; @@ -176,6 +122,16 @@ export const onUserInput: OnUserInputHandler = async ({ id, event }) => { }); break; + case 'back': + await snap.request({ + method: 'snap_updateInterface', + params: { + id, + ui: , + }, + }); + break; + default: break; } @@ -185,7 +141,12 @@ export const onUserInput: OnUserInputHandler = async ({ id, event }) => { event.type === UserInputEventType.FormSubmitEvent && event.name === 'example-form' ) { - const inputValue = event.value['example-input']; - await showResult(id, inputValue ?? ''); + await snap.request({ + method: 'snap_updateInterface', + params: { + id, + ui: , + }, + }); } }; diff --git a/packages/examples/packages/interactive-ui/src/ui.ts b/packages/examples/packages/interactive-ui/src/ui.ts deleted file mode 100644 index 39af31a0ae..0000000000 --- a/packages/examples/packages/interactive-ui/src/ui.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { - ButtonType, - ManageStateOperation, - address, - button, - copyable, - form, - heading, - input, - panel, - row, - text, - assert, -} from '@metamask/snaps-sdk'; -import type { Component, Transaction } from '@metamask/snaps-sdk'; - -import { decodeData } from './utils'; - -/** - * Initiate a new interface with the starting screen. - * - * @returns The Snap interface ID. - */ -export async function createInterface(): Promise { - return await snap.request({ - method: 'snap_createInterface', - params: { - ui: panel([ - heading('Interactive UI Example Snap'), - button({ value: 'Update UI', name: 'update' }), - ]), - }, - }); -} - -/** - * Create the transaction insights components to display. - * - * @returns The transaction insight content. - */ -export async function getInsightContent(): Promise { - const snapState = await snap.request({ - method: 'snap_manageState', - params: { - operation: ManageStateOperation.GetState, - }, - }); - - assert(snapState?.transaction, 'No transaction found in Snap state.'); - - const { from, to } = snapState.transaction as Transaction; - - return panel([ - row('From', address(from)), - row('To', to ? address(to) : text('None')), - button({ value: 'See transaction type', name: 'transaction-type' }), - ]); -} - -/** - * Update a Snap interface to display the transaction type after fetching - * the transaction from state. - * - * @param id - The interface ID to update. - */ -export async function displayTransactionType(id: string) { - const snapState = await snap.request({ - method: 'snap_manageState', - params: { - operation: ManageStateOperation.GetState, - }, - }); - - assert(snapState?.transaction, 'No transaction found in Snap state.'); - - const transaction = snapState.transaction as Transaction; - - const type = decodeData(transaction.data); - - await snap.request({ - method: 'snap_updateInterface', - params: { - id, - ui: panel([ - row('Transaction type', text(type)), - button({ value: 'Go back', name: 'go-back' }), - ]), - }, - }); -} - -/** - * Update the interface with a simple form containing an input and a submit button. - * - * @param id - The Snap interface ID to update. - */ -export async function showForm(id: string) { - await snap.request({ - method: 'snap_updateInterface', - params: { - id, - ui: panel([ - heading('Interactive UI Example Snap'), - form({ - name: 'example-form', - children: [ - input({ - name: 'example-input', - placeholder: 'Enter something...', - }), - button('Submit', ButtonType.Submit, 'submit'), - ], - }), - ]), - }, - }); -} - -/** - * Update a Snap interface to show a given value. - * - * @param id - The Snap interface ID to update. - * @param value - The value to display in the UI. - */ -export async function showResult(id: string, value: string) { - await snap.request({ - method: 'snap_updateInterface', - params: { - id, - ui: panel([ - heading('Interactive UI Example Snap'), - text('The submitted value is:'), - copyable(value), - ]), - }, - }); -} diff --git a/packages/examples/packages/interactive-ui/src/ui.tsx b/packages/examples/packages/interactive-ui/src/ui.tsx new file mode 100644 index 0000000000..00db8969e0 --- /dev/null +++ b/packages/examples/packages/interactive-ui/src/ui.tsx @@ -0,0 +1,54 @@ +import { ManageStateOperation, assert } from '@metamask/snaps-sdk'; +import type { JSXElement, Transaction } from '@metamask/snaps-sdk'; + +import { Insight, TransactionType } from './components'; +import { decodeData } from './utils'; + +/** + * Create the transaction insights components to display. + * + * @returns The transaction insight content. + */ +export async function getInsightContent(): Promise { + const snapState = await snap.request({ + method: 'snap_manageState', + params: { + operation: ManageStateOperation.GetState, + }, + }); + + assert(snapState?.transaction, 'No transaction found in Snap state.'); + + const { from, to } = snapState.transaction as Transaction; + + return ; +} + +/** + * Update a Snap interface to display the transaction type after fetching + * the transaction from state. + * + * @param id - The interface ID to update. + */ +export async function displayTransactionType(id: string) { + const snapState = await snap.request({ + method: 'snap_manageState', + params: { + operation: ManageStateOperation.GetState, + }, + }); + + assert(snapState?.transaction, 'No transaction found in Snap state.'); + + const transaction = snapState.transaction as Transaction; + + const type = decodeData(transaction.data); + + await snap.request({ + method: 'snap_updateInterface', + params: { + id, + ui: , + }, + }); +} diff --git a/packages/examples/packages/interactive-ui/tsconfig.json b/packages/examples/packages/interactive-ui/tsconfig.json index 1cb4c3315f..32e2e013f3 100644 --- a/packages/examples/packages/interactive-ui/tsconfig.json +++ b/packages/examples/packages/interactive-ui/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"] } },