diff --git a/packages/examples/packages/bip32/snap.manifest.json b/packages/examples/packages/bip32/snap.manifest.json index 472409d061..9744416b8e 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": "3cyrvn8TOcqu5AG1BfJ+bHNhlGzJNjD1WrvZnGzA9cM=", + "shasum": "xWXsw5LjFwQRaufki1t/iampdYMdJH4k3k7mu2t5egc=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/bip44/snap.manifest.json b/packages/examples/packages/bip44/snap.manifest.json index b80df34711..6f6fa830f7 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": "PvcD86EO/ZJWmK+OjX1S9Dx7JbLENispYZ0TlxqyBWg=", + "shasum": "O/w/gpIoCZLlBU7v8HlCelxUq+A47R+yhWHFzAP+5sU=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/browserify-plugin/snap.manifest.json b/packages/examples/packages/browserify-plugin/snap.manifest.json index d23963ce1e..7d5daa3039 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": "Z7bFULEf26ABMGWFe56MUwxdFqqk7wSXpEqBnVOc0QI=", + "shasum": "X2vOy1ncPOAfKxbSEM/AO0ubcSVqntFIIOtHLrQaOTM=", "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 cd71d2cbd6..f7af6c9b21 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": "gkiI/1xzz3QLou/HR+0p8nPGdrr6s3EoVKSIEhpHSEc=", + "shasum": "4Qn0ULfxP+10Fgc4owpRcjwPNk79L0iQDavQwfNyXFE=", "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 53ee5e927c..c6d2a26dfb 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": "QR/NJxBMFk0XrPJyuVgcFuR/YiqLKdj2JZjxkY4lZj0=", + "shasum": "DvdA5FUwoN/JNzeXBfont4//3v4mX20PN2eOqTwqGsI=", "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 9e7121c0e9..8bb1b1c88b 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": "+4Yl3+BXI3uaZfjSrQxuPMLQY2476cAO/FcR/a2jycU=", + "shasum": "qqZwJjSmhOoSLgFDJCLUqI4QYUQ+Ig/erSJKxOHsuuk=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/dialogs/snap.manifest.json b/packages/examples/packages/dialogs/snap.manifest.json index 60a934cbd7..38d3b32bb0 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": "g2p8BK9pJXrC9/hJw+n3BkBKDfS0Dng/oR1cJGLrnuc=", + "shasum": "layewHpnFhX6O3TE2XVOubS6LanUXgTxkchzQ2XRSxo=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/ethereum-provider/snap.manifest.json b/packages/examples/packages/ethereum-provider/snap.manifest.json index d6d24254af..be22cbc165 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": "8cRG0o0f7+IZWgMNYyAHRO3Nrht3779FDBiNclCYxYU=", + "shasum": "fYwoCqxiXH9CeaKasOHRuQHPZrky7FjclslIlEqcCok=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/ethers-js/snap.manifest.json b/packages/examples/packages/ethers-js/snap.manifest.json index 43f7629016..25a6fe6b14 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": "Zqdf6Sl/sfvJYl7A87/BB7e0nhdXqe+0KxIeGxnjr5g=", + "shasum": "SZGja3r8voeQPoASG8C/1kRbeLq0iJRu8eu5xIq9zaQ=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/file-upload/snap.manifest.json b/packages/examples/packages/file-upload/snap.manifest.json index c03fd8e5c2..013c04931f 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": "Ko0mJ92OTHqNdBu/CHrMImRa+Afyr8AsEyojCczLLsc=", + "shasum": "WivEfGs9i26XuaoXhzYyw74jagvrJ3aqSRzJYrabqXI=", "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 d548afcbfe..81f2dab17d 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": "Ii9gvF7VRzccakiuibaWqR5fcGUmn3bADL6vSxr5Lc8=", + "shasum": "3a26x4h/EN0xQdsEzMLFk/62vAcT2l1lX9k9pjk4Dd8=", "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 fab639f5e1..a53c8abab2 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": "d7GagnmYnwhrnLc+oQQV7IC9f4cPRM8BZUeQ50cuydA=", + "shasum": "sLPMU1UnLtRzV8c/5o9fOrowYDkgQnmOALauSDDAUOQ=", "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 377a5a4071..3bdf6b2759 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": "YMn4Z3+ddOce0OOYrk39M+kmlaIXfYaurjAOWpJhzp0=", + "shasum": "/+paPL+SWumPGtVX2jLLY+OFOWEeHT8LcHNmCeB1vx4=", "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 c10843f18f..ffe79146de 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": "roTBoTud+bVBTfv04q2wAIOzbV75XAIPTa+fbs+u4S0=", + "shasum": "hNZ6qo8t1RH8+As3gJ2mu4VwcfeqWKmhZ2HxsfAgQZI=", "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 3c51e8e2c5..0ff42a34b5 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": "6k3MXI/vf6lRt5uVk586yydm6LZaqS0NZ4TP2se6XBg=", + "shasum": "JR7eQdPo8TM5uLTyMIhpNLEDaiZW6PkE+54syXGX2Tw=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/invoke-snap/packages/consumer-signer/snap.manifest.json b/packages/examples/packages/invoke-snap/packages/consumer-signer/snap.manifest.json index 8cd98c0c8f..3cbca05151 100644 --- a/packages/examples/packages/invoke-snap/packages/consumer-signer/snap.manifest.json +++ b/packages/examples/packages/invoke-snap/packages/consumer-signer/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "wWbKF414M1/e1r9bG0Y+Ntbcn1XX7DIml4pwl041Uug=", + "shasum": "LxSwsGjDv2U4dz1Y58Z8kCNjErxQgsJkySfkr9cwt8k=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/invoke-snap/packages/core-signer/snap.manifest.json b/packages/examples/packages/invoke-snap/packages/core-signer/snap.manifest.json index 08e8ad7544..3e9312e76c 100644 --- a/packages/examples/packages/invoke-snap/packages/core-signer/snap.manifest.json +++ b/packages/examples/packages/invoke-snap/packages/core-signer/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "xvwZwBbmoekH92t7tYei5dZ5uKxtdvz85kesxMab8mo=", + "shasum": "WbIJsP+TbOyq1TaqyctQ5JTlyLoHta94tQaozw4Azr4=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/json-rpc/snap.manifest.json b/packages/examples/packages/json-rpc/snap.manifest.json index 54dfbdaa7c..f1838cc3aa 100644 --- a/packages/examples/packages/json-rpc/snap.manifest.json +++ b/packages/examples/packages/json-rpc/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "WxAHmnlgdoRw7w0u8B9N3C8aB52epM3JUy2TpFRu46g=", + "shasum": "qKiHk6XvwHtGV/bJOba3n4spGWPG76FiI9F8SD/A/3o=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/jsx/snap.manifest.json b/packages/examples/packages/jsx/snap.manifest.json index 6aa4c5e92a..ca355bdfa6 100644 --- a/packages/examples/packages/jsx/snap.manifest.json +++ b/packages/examples/packages/jsx/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "yP7SkzjBPN5/uvEnqGjTGGyq5GBHPm8py/VNJW0h//4=", + "shasum": "VOsd46ulxkzN4nGHhDJTov5UKM84YpvmYGZZaFRbFdU=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/lifecycle-hooks/snap.manifest.json b/packages/examples/packages/lifecycle-hooks/snap.manifest.json index 222bf95150..c3637b63c3 100644 --- a/packages/examples/packages/lifecycle-hooks/snap.manifest.json +++ b/packages/examples/packages/lifecycle-hooks/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "/WjyMJSHqfad0PNKZGei84ChfrZQiQpOCumQpDxSZ7A=", + "shasum": "zuKxAh/8so9bC0WwSsEoXeNCXdCPTWpUpTGS5cJL54c=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/localization/snap.manifest.json b/packages/examples/packages/localization/snap.manifest.json index ea759c8386..6739edf745 100644 --- a/packages/examples/packages/localization/snap.manifest.json +++ b/packages/examples/packages/localization/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "xnBCnKIYB1IJXLUx0iDC4U+QdGVDM72i4uQ9+vVupBo=", + "shasum": "2s0C8J6NK2q5XDJjmPABEdU9BEhWa79bhmBt4zUD+uU=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/manage-state/snap.manifest.json b/packages/examples/packages/manage-state/snap.manifest.json index 8372d8c033..423f50250f 100644 --- a/packages/examples/packages/manage-state/snap.manifest.json +++ b/packages/examples/packages/manage-state/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "SRpkwJ5zj0JuY112AOcufRVqqsAheKhEtv4D9WqVvZY=", + "shasum": "9zISfOgwECMw5QUInlwm9BYthXi8qIkPZZDvmnUWS/c=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/network-access/snap.manifest.json b/packages/examples/packages/network-access/snap.manifest.json index 75175e34f5..949e2ff98d 100644 --- a/packages/examples/packages/network-access/snap.manifest.json +++ b/packages/examples/packages/network-access/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "JZ2kwUDXE7cu77OVONa8GdxwjUSqMqgv3JKUPSArElg=", + "shasum": "hDslA6wk04fYto1u0qW5Lgge9kG0Gd8Nx67ZlvdN8m0=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/notifications/snap.manifest.json b/packages/examples/packages/notifications/snap.manifest.json index 92ac7fd2c3..49f64b4557 100644 --- a/packages/examples/packages/notifications/snap.manifest.json +++ b/packages/examples/packages/notifications/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "JR9gpxBsEizgvJLKe7cjMHnEUks/dlmK/kTq+OcdcIo=", + "shasum": "opFjKht+K7XFm8QczfBNKcqiafkfaMQ8CI0iGJyDhI4=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/rollup-plugin/snap.manifest.json b/packages/examples/packages/rollup-plugin/snap.manifest.json index 95a9e4846d..3ca4fdc796 100644 --- a/packages/examples/packages/rollup-plugin/snap.manifest.json +++ b/packages/examples/packages/rollup-plugin/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "NVGTP6xdPKOSoReiGTr18o8r08qTsQqAE4JP+f9aisU=", + "shasum": "cFvHPCVfRrSorz939WQ6KOhB1GVhfp4oGrMg00XZLss=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/signature-insights/snap.manifest.json b/packages/examples/packages/signature-insights/snap.manifest.json index 830aa27ef7..05f2f88a99 100644 --- a/packages/examples/packages/signature-insights/snap.manifest.json +++ b/packages/examples/packages/signature-insights/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "X8e8OA1+EUjGr0uVpwkvmKEFGdOE8Sif1ngaGEh5SmU=", + "shasum": "6CYAvvQA8tjddm13YblRAApORMK6yb5Ohmp6aLubvH4=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/transaction-insights/snap.manifest.json b/packages/examples/packages/transaction-insights/snap.manifest.json index 6d472637e1..f271b2496e 100644 --- a/packages/examples/packages/transaction-insights/snap.manifest.json +++ b/packages/examples/packages/transaction-insights/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "w7qMjYQa/4n/DMD33zMRIW6YGtp4vOiAC1GIzebGF6M=", + "shasum": "qz7638J0CvhehIGgkx9i+UzYWRgRajAoGoIH3o7DsNU=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/wasm/snap.manifest.json b/packages/examples/packages/wasm/snap.manifest.json index 5413372445..c00292d093 100644 --- a/packages/examples/packages/wasm/snap.manifest.json +++ b/packages/examples/packages/wasm/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "afWjohN+iHMlPf+cooYW1l9bkvdxHNu5coHTSIVixlI=", + "shasum": "qt4KP5CPWCc9OkOmA/dS74ESPlbxTdlHcCVGjHbTbKk=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/webpack-plugin/snap.manifest.json b/packages/examples/packages/webpack-plugin/snap.manifest.json index b52da23ea2..b9fd693816 100644 --- a/packages/examples/packages/webpack-plugin/snap.manifest.json +++ b/packages/examples/packages/webpack-plugin/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "Vqgei7k5/YlpTKwwRtQMltlGMZJF6AWR/2UY9UYjMAo=", + "shasum": "Nm2zazUdQaJe8m5O6dNc8UpSX7u/1nY3xKjEkI/qgQM=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/snaps-controllers/coverage.json b/packages/snaps-controllers/coverage.json index 263bcd6345..c46771d2a5 100644 --- a/packages/snaps-controllers/coverage.json +++ b/packages/snaps-controllers/coverage.json @@ -1,6 +1,6 @@ { "branches": 91.98, - "functions": 96.73, + "functions": 96.74, "lines": 97.95, "statements": 97.63 } diff --git a/packages/snaps-controllers/src/interface/SnapInterfaceController.test.tsx b/packages/snaps-controllers/src/interface/SnapInterfaceController.test.tsx index c5cead35a5..20decb4405 100644 --- a/packages/snaps-controllers/src/interface/SnapInterfaceController.test.tsx +++ b/packages/snaps-controllers/src/interface/SnapInterfaceController.test.tsx @@ -1,4 +1,4 @@ -import type { SnapId } from '@metamask/snaps-sdk'; +import type { InterfaceState, SnapId } from '@metamask/snaps-sdk'; import { form, image, input, panel, text } from '@metamask/snaps-sdk'; import { Box, @@ -63,7 +63,17 @@ describe('SnapInterfaceController', () => { ); expect(content).toStrictEqual(getJsxElementFromComponent(components)); - expect(state).toStrictEqual({ foo: { bar: null } }); + expect(state).toStrictEqual({ + foo: { + type: 'Form', + value: { + bar: { + type: 'Input', + value: null, + }, + }, + }, + }); }); it('can create a new interface from JSX', async () => { @@ -113,7 +123,17 @@ describe('SnapInterfaceController', () => { ); expect(content).toStrictEqual(element); - expect(state).toStrictEqual({ foo: { bar: null } }); + expect(state).toStrictEqual({ + foo: { + type: 'Form', + value: { + bar: { + type: 'Input', + value: null, + }, + }, + }, + }); }); it('supports providing interface context', async () => { @@ -474,7 +494,17 @@ describe('SnapInterfaceController', () => { ); expect(content).toStrictEqual(getJsxElementFromComponent(newContent)); - expect(state).toStrictEqual({ foo: { baz: null } }); + expect(state).toStrictEqual({ + foo: { + type: 'Form', + value: { + baz: { + type: 'Input', + value: null, + }, + }, + }, + }); }); it('can update an interface using JSX', async () => { @@ -523,7 +553,17 @@ describe('SnapInterfaceController', () => { ); expect(content).toStrictEqual(newElement); - expect(state).toStrictEqual({ foo: { baz: null } }); + expect(state).toStrictEqual({ + foo: { + type: 'Form', + value: { + baz: { + type: 'Input', + value: null, + }, + }, + }, + }); }); it('throws if a link is on the phishing list', async () => { @@ -828,7 +868,17 @@ describe('SnapInterfaceController', () => { const content = form({ name: 'foo', children: [input({ name: 'bar' })] }); - const newState = { foo: { bar: 'baz' } }; + const newState: InterfaceState = { + foo: { + type: 'Form', + value: { + bar: { + type: 'Input', + value: 'baz', + }, + }, + }, + }; const id = await rootMessenger.call( 'SnapInterfaceController:createInterface', @@ -869,13 +919,19 @@ describe('SnapInterfaceController', () => { ); - const newState = { + const newState: InterfaceState = { foo: { - bar: { - name: 'test.png', - size: 123, - contentType: 'image/png', - contents: 'foo', + type: 'Form', + value: { + bar: { + type: 'FileInput', + value: { + name: 'test.png', + size: 123, + contentType: 'image/png', + contents: 'foo', + }, + }, }, }, }; diff --git a/packages/snaps-controllers/src/interface/utils.test.tsx b/packages/snaps-controllers/src/interface/utils.test.tsx index 59f41f9ae9..4ac9311fe5 100644 --- a/packages/snaps-controllers/src/interface/utils.test.tsx +++ b/packages/snaps-controllers/src/interface/utils.test.tsx @@ -1,3 +1,4 @@ +import type { FormState, InterfaceState } from '@metamask/snaps-sdk'; import { Box, Button, @@ -10,11 +11,20 @@ import { FileInput, } from '@metamask/snaps-sdk/jsx'; -import { assertNameIsUnique, constructState } from './utils'; +import { + assertFormNameIsUnique, + assertNameIsUnique, + constructState, +} from './utils'; describe('assertNameIsUnique', () => { it('throws an error if a name is not unique', () => { - const state = { test: 'foo' }; + const state: InterfaceState = { + test: { + type: 'Input', + value: 'foo', + }, + }; expect(() => assertNameIsUnique(state, 'test')).toThrow( `Duplicate component names are not allowed, found multiple instances of: "test".`, @@ -22,12 +32,49 @@ describe('assertNameIsUnique', () => { }); it('passes if there is no duplicate name', () => { - const state = { test: 'foo' }; + const state: InterfaceState = { + test: { + type: 'Input', + value: 'foo', + }, + }; expect(() => assertNameIsUnique(state, 'bar')).not.toThrow(); }); }); +describe('assertFormNameIsUnique', () => { + it('throws an error if a name is not unique', () => { + const state: FormState = { + type: 'Form', + value: { + test: { + type: 'Input', + value: 'foo', + }, + }, + }; + + expect(() => assertFormNameIsUnique(state, 'test')).toThrow( + `Duplicate component names are not allowed, found multiple instances of: "test".`, + ); + }); + + it('passes if there is no duplicate name', () => { + const state: FormState = { + type: 'Form', + value: { + test: { + type: 'Input', + value: 'foo', + }, + }, + }; + + expect(() => assertFormNameIsUnique(state, 'bar')).not.toThrow(); + }); +}); + describe('constructState', () => { it('can construct a new component state', () => { const element = ( @@ -43,7 +90,17 @@ describe('constructState', () => { const result = constructState({}, element); - expect(result).toStrictEqual({ foo: { bar: null } }); + expect(result).toStrictEqual({ + foo: { + type: 'Form', + value: { + bar: { + type: 'Input', + value: null, + }, + }, + }, + }); }); it('can construct a new component state from a field with a button', () => { @@ -61,11 +118,31 @@ describe('constructState', () => { const result = constructState({}, element); - expect(result).toStrictEqual({ foo: { bar: null } }); + expect(result).toStrictEqual({ + foo: { + type: 'Form', + value: { + bar: { + type: 'Input', + value: null, + }, + }, + }, + }); }); it('merges two states', () => { - const state = { foo: { bar: 'test' } }; + const state: InterfaceState = { + foo: { + type: 'Form', + value: { + bar: { + type: 'Input', + value: 'test', + }, + }, + }, + }; const element = ( @@ -82,11 +159,39 @@ describe('constructState', () => { ); const result = constructState(state, element); - expect(result).toStrictEqual({ foo: { bar: 'test', baz: null } }); + expect(result).toStrictEqual({ + foo: { + type: 'Form', + value: { + bar: { + type: 'Input', + value: 'test', + }, + baz: { + type: 'Input', + value: null, + }, + }, + }, + }); }); it('deletes unused state', () => { - const state = { form: { foo: null, bar: 'test' } }; + const state: InterfaceState = { + form: { + type: 'Form', + value: { + foo: { + type: 'Input', + value: 'test', + }, + bar: { + type: 'Input', + value: 'test', + }, + }, + }, + }; const element = ( @@ -103,13 +208,51 @@ describe('constructState', () => { ); const result = constructState(state, element); - expect(result).toStrictEqual({ form: { bar: 'test', baz: null } }); + expect(result).toStrictEqual({ + form: { + type: 'Form', + value: { + bar: { + type: 'Input', + value: 'test', + }, + baz: { + type: 'Input', + value: null, + }, + }, + }, + }); }); it('handles multiple forms', () => { - const state = { - form1: { foo: null, bar: 'test' }, - form2: { foo: 'abc', bar: 'def' }, + const state: InterfaceState = { + form1: { + type: 'Form', + value: { + foo: { + type: 'Input', + value: 'test', + }, + bar: { + type: 'Input', + value: 'test', + }, + }, + }, + form2: { + type: 'Form', + value: { + foo: { + type: 'Input', + value: 'abc', + }, + bar: { + type: 'Input', + value: 'def', + }, + }, + }, }; const element = ( @@ -137,15 +280,51 @@ describe('constructState', () => { const result = constructState(state, element); expect(result).toStrictEqual({ - form1: { bar: 'test', baz: null }, - form2: { bar: 'def', baz: null }, + form1: { + type: 'Form', + value: { + bar: { type: 'Input', value: 'test' }, + baz: { type: 'Input', value: null }, + }, + }, + form2: { + type: 'Form', + value: { + bar: { type: 'Input', value: 'def' }, + baz: { type: 'Input', value: null }, + }, + }, }); }); it('deletes an unused form', () => { - const state = { - form1: { foo: null, bar: 'test' }, - form2: { foo: 'abc', bar: 'def' }, + const state: InterfaceState = { + form1: { + type: 'Form', + value: { + foo: { + type: 'Input', + value: 'test', + }, + bar: { + type: 'Input', + value: 'test', + }, + }, + }, + form2: { + type: 'Form', + value: { + foo: { + type: 'Input', + value: 'abc', + }, + bar: { + type: 'Input', + value: 'def', + }, + }, + }, }; const element = ( @@ -164,14 +343,44 @@ describe('constructState', () => { const result = constructState(state, element); expect(result).toStrictEqual({ - form1: { bar: 'test', baz: null }, + form1: { + type: 'Form', + value: { + bar: { type: 'Input', value: 'test' }, + baz: { type: 'Input', value: null }, + }, + }, }); }); it('handles nested forms', () => { - const state = { - form1: { foo: null, bar: 'test' }, - form2: { foo: 'abc', bar: 'def' }, + const state: InterfaceState = { + form1: { + type: 'Form', + value: { + foo: { + type: 'Input', + value: null, + }, + bar: { + type: 'Input', + value: 'test', + }, + }, + }, + form2: { + type: 'Form', + value: { + foo: { + type: 'Input', + value: 'abc', + }, + bar: { + type: 'Input', + value: 'def', + }, + }, + }, }; const element = ( @@ -202,8 +411,20 @@ describe('constructState', () => { const result = constructState(state, element); expect(result).toStrictEqual({ - form1: { bar: 'test', baz: null }, - form2: { bar: 'def', baz: null }, + form1: { + type: 'Form', + value: { + bar: { type: 'Input', value: 'test' }, + baz: { type: 'Input', value: null }, + }, + }, + form2: { + type: 'Form', + value: { + bar: { type: 'Input', value: 'def' }, + baz: { type: 'Input', value: null }, + }, + }, }); }); @@ -216,7 +437,10 @@ describe('constructState', () => { const result = constructState({}, element); expect(result).toStrictEqual({ - foo: 'bar', + foo: { + type: 'Input', + value: 'bar', + }, }); }); @@ -229,7 +453,10 @@ describe('constructState', () => { const result = constructState({}, element); expect(result).toStrictEqual({ - foo: null, + foo: { + type: 'Input', + value: null, + }, }); }); @@ -245,7 +472,10 @@ describe('constructState', () => { const result = constructState({}, element); expect(result).toStrictEqual({ - foo: 'option1', + foo: { + type: 'Dropdown', + value: 'option1', + }, }); }); @@ -261,7 +491,10 @@ describe('constructState', () => { const result = constructState({}, element); expect(result).toStrictEqual({ - foo: 'option2', + foo: { + type: 'Dropdown', + value: 'option2', + }, }); }); @@ -281,7 +514,15 @@ describe('constructState', () => { const result = constructState({}, element); expect(result).toStrictEqual({ - form: { foo: 'option1' }, + form: { + type: 'Form', + value: { + foo: { + type: 'Dropdown', + value: 'option1', + }, + }, + }, }); }); @@ -301,7 +542,15 @@ describe('constructState', () => { const result = constructState({}, element); expect(result).toStrictEqual({ - form: { foo: 'option2' }, + form: { + type: 'Form', + value: { + foo: { + type: 'Dropdown', + value: 'option2', + }, + }, + }, }); }); @@ -324,7 +573,15 @@ describe('constructState', () => { const result = constructState({}, element); expect(result).toStrictEqual({ - form: { bar: 'option2' }, + form: { + type: 'Form', + value: { + bar: { + type: 'Dropdown', + value: 'option2', + }, + }, + }, }); }); @@ -355,8 +612,21 @@ describe('constructState', () => { const result = constructState({}, element); expect(result).toStrictEqual({ - form: { baz: 'option4' }, - form2: { bar: 'option2' }, + form: { + type: 'Form', + value: { + baz: { + type: 'Dropdown', + value: 'option4', + }, + }, + }, + form2: { + type: 'Form', + value: { + bar: { type: 'Dropdown', value: 'option2' }, + }, + }, }); }); @@ -367,15 +637,32 @@ describe('constructState', () => { ); - const result = constructState({ foo: null, bar: null }, element); + const oldState: InterfaceState = { + foo: { + type: 'Input', + value: null, + }, + bar: { + type: 'Input', + value: null, + }, + }; + + const result = constructState(oldState, element); expect(result).toStrictEqual({ - foo: null, + foo: { + type: 'Input', + value: null, + }, }); }); it('merges root level inputs from old state', () => { - const state = { - foo: 'bar', + const state: InterfaceState = { + foo: { + type: 'Input', + value: 'bar', + }, }; const element = ( @@ -386,7 +673,10 @@ describe('constructState', () => { const result = constructState(state, element); expect(result).toStrictEqual({ - foo: 'bar', + foo: { + type: 'Input', + value: 'bar', + }, }); }); @@ -399,7 +689,10 @@ describe('constructState', () => { const result = constructState({}, element); expect(result).toStrictEqual({ - foo: null, + foo: { + type: 'FileInput', + value: null, + }, }); }); diff --git a/packages/snaps-controllers/src/interface/utils.ts b/packages/snaps-controllers/src/interface/utils.ts index f1965bbd08..2c393865b7 100644 --- a/packages/snaps-controllers/src/interface/utils.ts +++ b/packages/snaps-controllers/src/interface/utils.ts @@ -50,6 +50,19 @@ export function assertNameIsUnique(state: InterfaceState, name: string) { ); } +/** + * Assert that the component name is unique in form state. + * + * @param state - The interface state to verify against. + * @param name - The component name to verify. + */ +export function assertFormNameIsUnique(state: FormState, name: string) { + assert( + state.value[name] === undefined, + `Duplicate component names are not allowed, found multiple instances of: "${name}".`, + ); +} + /** * Construct default state for a component. * @@ -82,20 +95,29 @@ function constructInputState( oldState: InterfaceState, element: InputElement | DropdownElement | FileInputElement, form?: string, -) { - const oldStateUnwrapped = form ? (oldState[form] as FormState) : oldState; +): State { + const oldStateUnwrapped = form + ? (oldState[form] as FormState)?.value + : oldState; + const oldInputState = oldStateUnwrapped?.[element.props.name] as State; if (element.type === 'FileInput') { - return oldInputState ?? null; + return ( + oldInputState ?? { + value: null, + type: 'FileInput', + } + ); } - return ( - element.props.value ?? - oldInputState ?? - constructComponentSpecificDefaultState(element) ?? - null - ); + return { + value: (element.props.value ?? + oldInputState?.value ?? + constructComponentSpecificDefaultState(element) ?? + null) as string | null, + type: element.type, + }; } /** @@ -126,7 +148,12 @@ export function constructState( if (component.type === 'Form') { assertNameIsUnique(newState, component.props.name); formStack.push({ name: component.props.name, depth }); - newState[component.props.name] = {}; + + newState[component.props.name] = { + type: 'Form', + value: {}, + }; + return; } @@ -138,8 +165,8 @@ export function constructState( component.type === 'FileInput') ) { const formState = newState[currentForm.name] as FormState; - assertNameIsUnique(formState, component.props.name); - formState[component.props.name] = constructInputState( + assertFormNameIsUnique(formState, component.props.name); + formState.value[component.props.name] = constructInputState( oldState, component, currentForm.name, diff --git a/packages/snaps-jest/src/internals/simulation/interface.test.tsx b/packages/snaps-jest/src/internals/simulation/interface.test.tsx index d10e2da666..3d3368ac7c 100644 --- a/packages/snaps-jest/src/internals/simulation/interface.test.tsx +++ b/packages/snaps-jest/src/internals/simulation/interface.test.tsx @@ -1,4 +1,5 @@ import { SnapInterfaceController } from '@metamask/snaps-controllers'; +import type { InterfaceState } from '@metamask/snaps-sdk'; import { ButtonType, DialogType, @@ -37,6 +38,7 @@ import { getElement, getInterface, getInterfaceResponse, + getValue, mergeValue, selectInDropdown, typeInField, @@ -443,21 +445,88 @@ describe('clickElement', () => { }); }); +describe('getValue', () => { + it('returns the value for a file input', () => { + const result = getValue('FileInput', { + name: 'foo', + size: 123, + contentType: 'text/plain', + contents: 'base64', + }); + + expect(result).toStrictEqual({ + type: 'FileInput', + value: { + name: 'foo', + size: 123, + contentType: 'text/plain', + contents: 'base64', + }, + }); + }); + + it('returns the value for an input', () => { + const result = getValue('Input', 'foo'); + + expect(result).toStrictEqual({ + type: 'Input', + value: 'foo', + }); + }); + + it('returns the value for a dropdown', () => { + const result = getValue('Dropdown', 'foo'); + + expect(result).toStrictEqual({ + type: 'Dropdown', + value: 'foo', + }); + }); +}); + describe('mergeValue', () => { it('merges a value outside of a form', () => { - const state = { foo: 'bar' }; - - const result = mergeValue(state, 'foo', 'baz'); + const state: InterfaceState = { + foo: { + type: 'Input', + value: 'bar', + }, + }; - expect(result).toStrictEqual({ foo: 'baz' }); + const result = mergeValue(state, 'foo', 'Input', 'baz'); + expect(result).toStrictEqual({ + foo: { + type: 'Input', + value: 'baz', + }, + }); }); it('merges a value inside of a form', () => { - const state = { foo: { bar: 'baz' } }; - - const result = mergeValue(state, 'bar', 'test', 'foo'); + const state: InterfaceState = { + foo: { + type: 'Form', + value: { + bar: { + type: 'Input', + value: 'bar', + }, + }, + }, + }; - expect(result).toStrictEqual({ foo: { bar: 'test' } }); + const result = mergeValue(state, 'bar', 'Input', 'test', 'foo'); + expect(result).toStrictEqual({ + foo: { + type: 'Form', + value: { + bar: { + type: 'Input', + value: 'test', + }, + }, + }, + }); }); }); @@ -499,7 +568,12 @@ describe('typeInField', () => { expect(rootControllerMessenger.call).toHaveBeenCalledWith( 'SnapInterfaceController:updateInterfaceState', interfaceId, - { bar: 'baz' }, + { + bar: { + type: 'Input', + value: 'baz', + }, + }, ); expect(handleRpcRequestMock).toHaveBeenCalledWith(MOCK_SNAP_ID, { @@ -610,7 +684,12 @@ describe('selectInDropdown', () => { expect(rootControllerMessenger.call).toHaveBeenCalledWith( 'SnapInterfaceController:updateInterfaceState', interfaceId, - { foo: 'option2' }, + { + foo: { + type: 'Dropdown', + value: 'option2', + }, + }, ); expect(handleRpcRequestMock).toHaveBeenCalledWith(MOCK_SNAP_ID, { diff --git a/packages/snaps-jest/src/internals/simulation/interface.ts b/packages/snaps-jest/src/internals/simulation/interface.ts index 7247588867..b96226e81a 100644 --- a/packages/snaps-jest/src/internals/simulation/interface.ts +++ b/packages/snaps-jest/src/internals/simulation/interface.ts @@ -1,8 +1,10 @@ import type { + File, FormState, InterfaceContext, InterfaceState, SnapId, + State, UserInputEvent, } from '@metamask/snaps-sdk'; import { DialogType, UserInputEventType, assert } from '@metamask/snaps-sdk'; @@ -12,6 +14,7 @@ import { getJsxChildren, unwrapError, walkJsx, + getFormValues, } from '@metamask/snaps-utils'; import { hasProperty } from '@metamask/utils'; import type { PayloadAction } from '@reduxjs/toolkit'; @@ -322,8 +325,7 @@ export async function clickElement( { type: UserInputEventType.FormSubmitEvent, name: result.form, - value: state[result.form] as Record, - files: {}, + ...getFormValues(state[result.form] as FormState), }, context, ); @@ -331,10 +333,55 @@ export async function clickElement( } /** - * Merge a value in the interface state. + * Get the value of an interface element. + * + * @param type - The component type. + * @param value - The value. + * @returns The state. + */ +export function getValue( + type: 'Dropdown' | 'FileInput' | 'Input', + value: string | File | null, +): State { + if (type === 'FileInput') { + return { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + value: value as File | null, + type, + }; + } + + return { + value: value as string | null, + type, + }; +} + +/** + * Merge a file in the interface state. This is used for file inputs. + * + * @param state - The actual interface state. + * @param name - The component name that changed value. + * @param type - The component type. + * @param value - The new value. + * @param form - The form name if the element is in one. + * @returns The state with the merged value. + */ +export function mergeValue( + state: InterfaceState, + name: string, + type: 'FileInput', + value: File | null, + form?: string, +): InterfaceState; + +/** + * Merge a string value in the interface state. This is used for input and + * dropdown elements. * * @param state - The actual interface state. * @param name - The component name that changed value. + * @param type - The component type. * @param value - The new value. * @param form - The form name if the element is in one. * @returns The state with the merged value. @@ -342,20 +389,46 @@ export async function clickElement( export function mergeValue( state: InterfaceState, name: string, + type: 'Dropdown' | 'Input', value: string | null, form?: string, +): InterfaceState; + +/** + * Merge a value in the interface state. + * + * @param state - The actual interface state. + * @param name - The component name that changed value. + * @param type - The component type. + * @param value - The new value. + * @param form - The form name if the element is in one. + * @returns The state with the merged value. + */ +export function mergeValue( + state: InterfaceState, + name: string, + type: 'Dropdown' | 'FileInput' | 'Input', + value: string | File | null, + form?: string, ): InterfaceState { if (form) { + const formState = state[form] as FormState; return { ...state, [form]: { - ...(state[form] as FormState), - [name]: value, + ...formState, + value: { + ...formState.value, + [name]: getValue(type, value), + }, }, }; } - return { ...state, [name]: value }; + return { + ...state, + [name]: getValue(type, value), + }; } /** @@ -394,7 +467,13 @@ export async function typeInField( id, ); - const newState = mergeValue(state, name, value, result.form); + const newState = mergeValue( + state, + name, + result.element.type, + value, + result.form, + ); controllerMessenger.call( 'SnapInterfaceController:updateInterfaceState', @@ -468,7 +547,13 @@ export async function selectInDropdown( id, ); - const newState = mergeValue(state, name, value, result.form); + const newState = mergeValue( + state, + name, + result.element.type, + value, + result.form, + ); controllerMessenger.call( 'SnapInterfaceController:updateInterfaceState', diff --git a/packages/snaps-rpc-methods/src/permitted/getInterfaceState.test.ts b/packages/snaps-rpc-methods/src/permitted/getInterfaceState.test.ts index 9d336a55d3..96f2f19d54 100644 --- a/packages/snaps-rpc-methods/src/permitted/getInterfaceState.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/getInterfaceState.test.ts @@ -1,10 +1,127 @@ import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import type { InterfaceState } from '@metamask/snaps-sdk'; import { type GetInterfaceStateResult } from '@metamask/snaps-sdk'; import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; -import { getInterfaceStateHandler } from './getInterfaceState'; +import { + getInterfaceStateHandler, + getLegacyInterfaceState, +} from './getInterfaceState'; import type { UpdateInterfaceParameters } from './updateInterface'; +describe('getLegacyInterfaceState', () => { + it('returns the legacy state', () => { + const state: InterfaceState = { + foo: { + value: 'bar', + type: 'Input', + }, + baz: { + value: 'qux', + type: 'Dropdown', + }, + quux: { + value: { + name: 'file.txt', + contentType: 'text/plain', + size: 42, + contents: 'base64', + }, + type: 'FileInput', + }, + }; + + const legacyState = getLegacyInterfaceState(state); + expect(legacyState).toStrictEqual({ + foo: 'bar', + baz: 'qux', + quux: { + name: 'file.txt', + contentType: 'text/plain', + size: 42, + contents: 'base64', + }, + }); + }); + + it('returns the legacy state with form state', () => { + const state: InterfaceState = { + form: { + type: 'Form', + value: { + foo: { + value: 'bar', + type: 'Input', + }, + baz: { + value: 'qux', + type: 'Dropdown', + }, + quux: { + value: { + name: 'file.txt', + contentType: 'text/plain', + size: 42, + contents: 'base64', + }, + type: 'FileInput', + }, + }, + }, + input: { + value: 'input', + type: 'Input', + }, + }; + + const legacyState = getLegacyInterfaceState(state); + expect(legacyState).toStrictEqual({ + form: { + foo: 'bar', + baz: 'qux', + quux: { + name: 'file.txt', + contentType: 'text/plain', + size: 42, + contents: 'base64', + }, + }, + input: 'input', + }); + }); + + it('returns the legacy state with form state containing an input named type and value', () => { + const state: InterfaceState = { + form: { + type: 'Form', + value: { + type: { + value: 'bar', + type: 'Input', + }, + value: { + value: 'qux', + type: 'Dropdown', + }, + }, + }, + input: { + value: 'input', + type: 'Input', + }, + }; + + const legacyState = getLegacyInterfaceState(state); + expect(legacyState).toStrictEqual({ + form: { + type: 'bar', + value: 'qux', + }, + input: 'input', + }); + }); +}); + describe('snap_getInterfaceState', () => { describe('getInterfaceStateHandler', () => { it('has the expected shape', () => { @@ -22,7 +139,12 @@ describe('snap_getInterfaceState', () => { it('returns the result from the `getInterfaceState` hook', async () => { const { implementation } = getInterfaceStateHandler; - const getInterfaceState = jest.fn().mockReturnValue({ foo: 'bar' }); + const getInterfaceState = jest.fn().mockReturnValue({ + foo: { + type: 'Input', + value: 'bar', + }, + }); const hooks = { getInterfaceState, diff --git a/packages/snaps-rpc-methods/src/permitted/getInterfaceState.ts b/packages/snaps-rpc-methods/src/permitted/getInterfaceState.ts index 55a2fe22f5..22df303421 100644 --- a/packages/snaps-rpc-methods/src/permitted/getInterfaceState.ts +++ b/packages/snaps-rpc-methods/src/permitted/getInterfaceState.ts @@ -6,6 +6,8 @@ import type { GetInterfaceStateResult, InterfaceState, JsonRpcRequest, + LegacyState, + State, } from '@metamask/snaps-sdk'; import { type InferMatching } from '@metamask/snaps-utils'; import type { PendingJsonRpcResponse } from '@metamask/utils'; @@ -44,6 +46,60 @@ export type GetInterfaceStateParameters = InferMatching< GetInterfaceStateParams >; +/** + * Get the legacy interface state object from the current interface state. This + * exists for backwards compatibility when using the `snap_getInterfaceState` + * method. + * + * @param state - The interface state. + * @returns The legacy interface state object. + * @example + * const state: InterfaceState = { + * foo: { + * value: 'bar', + * type: 'Input', + * }, + * baz: { + * type: 'Form', + * value: { + * qux: { + * type: 'Dropdown', + * value: 'quux', + * }, + * }, + * }, + * }; + * + * const legacyState = getLegacyInterfaceState(state); + * // { + * // foo: 'bar', + * // baz: { + * // qux: 'quux', + * // }, + * // } + */ +export function getLegacyInterfaceState(state: InterfaceState): LegacyState { + return Object.entries(state).reduce( + (accumulator, [key, value]) => { + if (value.type === 'Form') { + return { + ...accumulator, + [key]: getLegacyInterfaceState(value.value) as Record< + string, + State['value'] + >, + }; + } + + return { + ...accumulator, + [key]: value.value, + }; + }, + {}, + ); +} + /** * The `snap_getInterfaceState` method implementation. * @@ -70,7 +126,7 @@ function getGetInterfaceStateImplementation( const { id } = validatedParams; - res.result = getInterfaceState(id); + res.result = getLegacyInterfaceState(getInterfaceState(id)); } catch (error) { return end(error); } diff --git a/packages/snaps-sdk/src/types/interface.test.ts b/packages/snaps-sdk/src/types/interface.test.ts index 1bce8da22b..d9c65591fe 100644 --- a/packages/snaps-sdk/src/types/interface.test.ts +++ b/packages/snaps-sdk/src/types/interface.test.ts @@ -4,14 +4,44 @@ import { FormStateStruct, InterfaceStateStruct } from './interface'; describe('FormStateStruct', () => { it('passes for a valid form state', () => { - expect(() => assert({ foo: 'bar' }, FormStateStruct)).not.toThrow(); + expect(() => + assert( + { + type: 'Form', + value: { + foo: { + type: 'Input', + value: 'bar', + }, + }, + }, + FormStateStruct, + ), + ).not.toThrow(); }); }); describe('InterfaceStateStruct', () => { it('passes for a valid form state', () => { expect(() => - assert({ test: { bar: 'baz' }, foo: 'bar' }, InterfaceStateStruct), + assert( + { + test: { + type: 'Form', + value: { + bar: { + type: 'Input', + value: 'baz', + }, + }, + }, + foo: { + type: 'Input', + value: 'bar', + }, + }, + InterfaceStateStruct, + ), ).not.toThrow(); }); }); diff --git a/packages/snaps-sdk/src/types/interface.ts b/packages/snaps-sdk/src/types/interface.ts index b144c74601..8a20fb5db2 100644 --- a/packages/snaps-sdk/src/types/interface.ts +++ b/packages/snaps-sdk/src/types/interface.ts @@ -1,6 +1,6 @@ import { JsonStruct } from '@metamask/utils'; import type { Infer } from 'superstruct'; -import { nullable, record, string, union } from 'superstruct'; +import { literal, object, nullable, record, string, union } from 'superstruct'; import type { JSXElement } from '../jsx'; import { RootJSXElementStruct } from '../jsx'; @@ -8,20 +8,30 @@ import type { Component } from '../ui'; import { ComponentStruct } from '../ui'; import { FileStruct } from './handlers'; -/** - * To avoid typing problems with the interface state when manipulating it we - * have to differentiate the state of a form (that will be contained inside the - * root state) and the root state since a key in the root stat can contain - * either the value of an input or a sub-state of a form. - */ - -export const StateStruct = union([FileStruct, string()]); +// To avoid typing problems with the interface state when manipulating it we +// have to differentiate the state of a form (that will be contained inside the +// root state) and the root state since a key in the root stat can contain +// either the value of an input or a sub-state of a form. + +export const StateStruct = union([ + object({ + value: nullable(string()), + type: union([literal('Dropdown'), literal('Input')]), + }), + object({ + value: nullable(FileStruct), + type: literal('FileInput'), + }), +]); -export const FormStateStruct = record(string(), nullable(StateStruct)); +export const FormStateStruct = object({ + type: literal('Form'), + value: record(string(), StateStruct), +}); export const InterfaceStateStruct = record( string(), - union([FormStateStruct, nullable(StateStruct)]), + union([FormStateStruct, StateStruct]), ); export type State = Infer; diff --git a/packages/snaps-sdk/src/types/methods/get-interface-state.ts b/packages/snaps-sdk/src/types/methods/get-interface-state.ts index 7c2253de06..2bdeef3af7 100644 --- a/packages/snaps-sdk/src/types/methods/get-interface-state.ts +++ b/packages/snaps-sdk/src/types/methods/get-interface-state.ts @@ -1,4 +1,4 @@ -import type { InterfaceState } from '../interface'; +import type { State } from '../interface'; /** * The request parameters for the `snap_getInterfaceState` method. @@ -10,6 +10,16 @@ export type GetInterfaceStateParams = { }; /** - * The result returned by the `snap_getInterfaceState` method, which is the state of the interface. + * The legacy interface state object. This does not include additional metadata + * about the state, such as the type of the state. */ -export type GetInterfaceStateResult = InterfaceState; +export type LegacyState = Record< + string, + State['value'] | Record +>; + +/** + * The result returned by the `snap_getInterfaceState` method, which is the + * state of the interface. + */ +export type GetInterfaceStateResult = LegacyState; diff --git a/packages/snaps-utils/coverage.json b/packages/snaps-utils/coverage.json index a5ffcef354..0e8741249b 100644 --- a/packages/snaps-utils/coverage.json +++ b/packages/snaps-utils/coverage.json @@ -1,6 +1,6 @@ { - "branches": 96.75, - "functions": 98.76, - "lines": 98.84, - "statements": 94.92 + "branches": 96.78, + "functions": 98.77, + "lines": 98.85, + "statements": 94.95 } diff --git a/packages/snaps-utils/src/ui.test.tsx b/packages/snaps-utils/src/ui.test.tsx index 8bd5c20f3d..561ba28bd2 100644 --- a/packages/snaps-utils/src/ui.test.tsx +++ b/packages/snaps-utils/src/ui.test.tsx @@ -1,3 +1,4 @@ +import type { FormState } from '@metamask/snaps-sdk'; import { panel, text, @@ -41,6 +42,7 @@ import { validateTextLinks, walkJsx, getJsxChildren, + getFormValues, } from './ui'; describe('getTextChildren', () => { @@ -919,3 +921,67 @@ describe('walkJsx', () => { expect(result).toBeUndefined(); }); }); + +describe('getFormValues', () => { + it('returns an empty object if the state is undefined', () => { + expect(getFormValues(undefined)).toStrictEqual({ + value: {}, + files: {}, + }); + }); + + it('returns the form values from the state', () => { + const state: FormState = { + type: 'Form', + value: { + foo: { + type: 'Input' as const, + value: 'bar', + }, + baz: { + type: 'Dropdown' as const, + value: null, + }, + qux: { + type: 'FileInput' as const, + value: { + name: 'quux', + size: 3, + contentType: 'image/svg+xml', + contents: 'base64', + }, + }, + }, + }; + + expect(getFormValues(state)).toStrictEqual({ + value: { foo: 'bar', baz: null }, + files: { qux: state.value.qux.value }, + }); + }); + + it('properly groups form values that are null', () => { + const state: FormState = { + type: 'Form', + value: { + foo: { + type: 'Input', + value: null, + }, + baz: { + type: 'Dropdown' as const, + value: null, + }, + qux: { + type: 'FileInput' as const, + value: null, + }, + }, + }; + + expect(getFormValues(state)).toStrictEqual({ + value: { foo: null, baz: null }, + files: { qux: null }, + }); + }); +}); diff --git a/packages/snaps-utils/src/ui.tsx b/packages/snaps-utils/src/ui.tsx index 6ba648315a..c90cd43eb6 100644 --- a/packages/snaps-utils/src/ui.tsx +++ b/packages/snaps-utils/src/ui.tsx @@ -1,4 +1,4 @@ -import type { Component } from '@metamask/snaps-sdk'; +import type { Component, FormState, File } from '@metamask/snaps-sdk'; import { NodeType } from '@metamask/snaps-sdk'; import type { BoldChildren, @@ -508,3 +508,43 @@ export function walkJsx( return undefined; } + +/** + * The values and files of a form. + * + * @property value - The values of the form fields, if any. + * @property files - The files of the form fields, if any. + */ +export type FormValues = { + value: Record; + files: Record; +}; + +/** + * Get the form values from the interface state. If the state is undefined, an + * object with empty values and files is returned. Otherwise, the form values + * are extracted from the state, and the values and files are returned + * separately. + * + * @param state - The interface state. + * @returns The form values. + */ +export function getFormValues(state?: FormState): FormValues { + if (!state?.value) { + return { value: {}, files: {} }; + } + + // TODO: Use `Object.groupBy` when it's available in our browser targets. + return Object.entries(state.value).reduce( + (accumulator, [key, value]) => { + if (value.type === 'FileInput') { + accumulator.files[key] = value.value; + } else { + accumulator.value[key] = value.value; + } + + return accumulator; + }, + { value: {}, files: {} }, + ); +}