From 4569503b7da27cdcc8ad3e93328dd518b3e1b37f Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Thu, 25 Jun 2026 12:17:40 +0200 Subject: [PATCH 01/18] refactor!: remove gatsby --- .github/workflows/publish-dapp.yml | 8 +- .github/workflows/publish-release.yml | 3 - README.md | 2 +- packages/site/.depcheckrc.json | 9 +- packages/site/.eslintrc.js | 2 +- packages/site/.prettierignore | 1 + packages/site/README.md | 48 +- packages/site/gatsby-browser.tsx | 17 - packages/site/gatsby-config.ts | 122 - packages/site/gatsby-node.js | 26 - packages/site/gatsby-ssr.tsx | 15 - packages/site/package.json | 44 +- packages/site/scripts/build.js | 53 + packages/site/scripts/start.js | 34 + packages/site/src/App.tsx | 769 +- packages/site/src/Root.tsx | 30 +- packages/site/src/components/Accordion.tsx | 77 - packages/site/src/components/Account.tsx | 83 - packages/site/src/components/AccountList.tsx | 31 - packages/site/src/components/AlertBanner.tsx | 79 - packages/site/src/components/Buttons.tsx | 181 - packages/site/src/components/Card.tsx | 67 - packages/site/src/components/CopyableItem.tsx | 29 - packages/site/src/components/Footer.tsx | 49 - packages/site/src/components/Header.tsx | 123 - packages/site/src/components/JSONViewer.tsx | 26 - packages/site/src/components/MetaMask.tsx | 44 - packages/site/src/components/Method.tsx | 198 - packages/site/src/components/PoweredBy.tsx | 16 - packages/site/src/components/SnapLogo.tsx | 16 - packages/site/src/components/Toggle.tsx | 159 - .../site/src/components/icons/CopyIcon.tsx | 22 - packages/site/src/components/index.ts | 11 - .../site/src/components/styledComponents.ts | 201 - packages/site/src/config/snap.ts | 2 +- packages/site/src/config/theme.ts | 187 - packages/site/src/hooks/MetamaskContext.tsx | 6 +- packages/site/src/index.html | 11 + packages/site/src/index.tsx | 19 + packages/site/src/pages/index.tsx | 391 - packages/site/src/types/custom.d.ts | 15 +- packages/site/src/types/react-dom-client.d.ts | 12 + packages/site/src/types/styled.d.ts | 19 - packages/site/src/utils/button.ts | 5 - packages/site/src/utils/index.ts | 3 - packages/site/src/utils/keyring-snap.ts | 10 +- packages/site/src/utils/localStorage.ts | 33 - packages/site/src/utils/snap.ts | 18 +- packages/site/src/utils/theme.ts | 27 - packages/site/tsconfig.json | 4 +- packages/site/webpack.config.js | 130 + packages/snap/snap.config.ts | 2 +- packages/snap/snap.manifest.json | 2 +- yarn.lock | 22130 ++++------------ 54 files changed, 5958 insertions(+), 19663 deletions(-) delete mode 100644 packages/site/gatsby-browser.tsx delete mode 100644 packages/site/gatsby-config.ts delete mode 100644 packages/site/gatsby-node.js delete mode 100644 packages/site/gatsby-ssr.tsx create mode 100644 packages/site/scripts/build.js create mode 100644 packages/site/scripts/start.js delete mode 100644 packages/site/src/components/Accordion.tsx delete mode 100644 packages/site/src/components/Account.tsx delete mode 100644 packages/site/src/components/AccountList.tsx delete mode 100644 packages/site/src/components/AlertBanner.tsx delete mode 100644 packages/site/src/components/Buttons.tsx delete mode 100644 packages/site/src/components/Card.tsx delete mode 100644 packages/site/src/components/CopyableItem.tsx delete mode 100644 packages/site/src/components/Footer.tsx delete mode 100644 packages/site/src/components/Header.tsx delete mode 100644 packages/site/src/components/JSONViewer.tsx delete mode 100644 packages/site/src/components/MetaMask.tsx delete mode 100644 packages/site/src/components/Method.tsx delete mode 100644 packages/site/src/components/PoweredBy.tsx delete mode 100644 packages/site/src/components/SnapLogo.tsx delete mode 100644 packages/site/src/components/Toggle.tsx delete mode 100644 packages/site/src/components/icons/CopyIcon.tsx delete mode 100644 packages/site/src/components/index.ts delete mode 100644 packages/site/src/components/styledComponents.ts delete mode 100644 packages/site/src/config/theme.ts create mode 100644 packages/site/src/index.html create mode 100644 packages/site/src/index.tsx delete mode 100644 packages/site/src/pages/index.tsx create mode 100644 packages/site/src/types/react-dom-client.d.ts delete mode 100644 packages/site/src/types/styled.d.ts delete mode 100644 packages/site/src/utils/button.ts delete mode 100644 packages/site/src/utils/localStorage.ts delete mode 100644 packages/site/src/utils/theme.ts create mode 100644 packages/site/webpack.config.js diff --git a/.github/workflows/publish-dapp.yml b/.github/workflows/publish-dapp.yml index bfa582e2..b81ef30c 100644 --- a/.github/workflows/publish-dapp.yml +++ b/.github/workflows/publish-dapp.yml @@ -30,18 +30,16 @@ jobs: cache: 'yarn' - name: Install npm dependencies run: yarn --immutable - - name: Set snap origin - run: | - echo GATSBY_SNAP_ORIGIN=npm:@metamask/${{ github.event.repository.name }}-snap >> .env.production - name: Run build script run: yarn build env: - GATSBY_PATH_PREFIX: ${{ github.event.repository.name }}/${{ inputs.destination_dir }}/ + SNAP_ORIGIN: npm:@metamask/${{ github.event.repository.name }}-snap + PATH_PREFIX: ${{ github.event.repository.name }}/${{ inputs.destination_dir }}/ - name: Deploy to `${{ inputs.destination_dir }}` directory of `gh-pages` branch uses: peaceiris/actions-gh-pages@de7ea6f8efb354206b205ef54722213d99067935 with: # This `PUBLISH_DOCS_TOKEN` needs to be manually set per-repository. # Look in the repository settings under "Environments", and set this token in the `github-pages` environment. personal_token: ${{ secrets.PUBLISH_DOCS_TOKEN }} - publish_dir: ./packages/site/public + publish_dir: ./packages/site/dist destination_dir: ${{ inputs.destination_dir }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 2514c248..0a4b7c35 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -35,7 +35,6 @@ jobs: with: path: | ./packages/*/dist - ./packages/*/public ./node_modules/.yarn-state.yml key: ${{ github.sha }} @@ -51,7 +50,6 @@ jobs: with: path: | ./packages/*/dist - ./packages/*/public ./node_modules/.yarn-state.yml key: ${{ github.sha }} - name: Dry Run Publish @@ -76,7 +74,6 @@ jobs: with: path: | ./packages/*/dist - ./packages/*/public ./node_modules/.yarn-state.yml key: ${{ github.sha }} - name: Publish diff --git a/README.md b/README.md index 85e8cab1..89abefc5 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ organization, you probably want to run `./scripts/cleanup.sh` to remove some files that will not work properly outside the MetaMask GitHub organization. Note that the `action-publish-release.yml` workflow contains a step that -publishes the frontend of this snap (contained in the `public/` directory) to +publishes the frontend of this snap (contained in the `dist/` directory) to GitHub pages. If you do not want to publish the frontend to GitHub pages, simply remove the step named "Publish to GitHub Pages" in that workflow. diff --git a/packages/site/.depcheckrc.json b/packages/site/.depcheckrc.json index de3d4d76..3e81aac7 100644 --- a/packages/site/.depcheckrc.json +++ b/packages/site/.depcheckrc.json @@ -1,9 +1,4 @@ { - "ignore-patterns": [".cache/"], - "ignores": [ - "@metamask/auto-changelog", - "@svgr/webpack", - "gatsby-plugin-*", - "react-scripts" - ] + "ignore-patterns": ["dist/", "public/", ".cache/"], + "ignores": ["@swc/core"] } diff --git a/packages/site/.eslintrc.js b/packages/site/.eslintrc.js index ca60fe59..73938b68 100644 --- a/packages/site/.eslintrc.js +++ b/packages/site/.eslintrc.js @@ -1,5 +1,5 @@ module.exports = { extends: ['../../.eslintrc.js'], - ignorePatterns: ['!.eslintrc.js', '.cache/', 'public/'], + ignorePatterns: ['!.eslintrc.js', 'dist/'], }; diff --git a/packages/site/.prettierignore b/packages/site/.prettierignore index 6f4cc146..7442d9d8 100644 --- a/packages/site/.prettierignore +++ b/packages/site/.prettierignore @@ -1,2 +1,3 @@ .cache/ public/ +dist/ diff --git a/packages/site/README.md b/packages/site/README.md index f6f0748b..62994b15 100644 --- a/packages/site/README.md +++ b/packages/site/README.md @@ -1,43 +1,19 @@ -# TypeScript Example Snap Front-end +# Simple Snap Keyring Site -This project was bootstrapped with [Gatsby](https://www.gatsbyjs.com/). +This package contains the browser UI used to install and test the Simple Snap +Keyring snap. -## Available Scripts +## Scripts -In the project directory, you can run: +`yarn start` runs the Webpack dev server at +[http://localhost:8000](http://localhost:8000). -### `yarn start` +`yarn build` creates a production build in `dist/`. -Runs the app in the development mode.\ -Open [http://localhost:8000](http://localhost:8000) to view it in the browser. +## Environment Variables -The page will reload if you make edits.\ -You will also see any lint errors in the console. +`SNAP_ORIGIN` defines the snap origin installed by the site. It defaults to +`local:http://localhost:8081`. -### `yarn build` - -Builds the app for production to the `public` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. - -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! - -See the section about [deployment](https://www.gatsbyjs.com/docs/how-to/previews-deploys-hosting/) for more information. - -## Environment variables - -Gatsby has built-in support for loading environment variables into the browser and Functions. Loading environment variables into Node.js requires a small code snippet. - -In development, Gatsby will load environment variables from a file named `.env.development`. For builds, it will load from `.env.production`. - -By default you can use the `SNAP_ORIGIN` variable (used in `src/config/snap.ts`) to define a production origin for you snap (eg. `npm:MyPackageName`). If not defined it will defaults to `local:http://localhost:8080`. - -A `.env` file template is available, to use it rename `.env.production.dist` to `.env.production` - -To learn more visit [Gatsby documentation](https://www.gatsbyjs.com/docs/how-to/local-development/environment-variables/) - -## Learn More - -You can learn more in the [Gatsby documentation](https://www.gatsbyjs.com/docs/). - -To learn React, check out the [React documentation](https://reactjs.org/). +`PATH_PREFIX` defines the public asset prefix for static deployments, such as +GitHub Pages subdirectories. diff --git a/packages/site/gatsby-browser.tsx b/packages/site/gatsby-browser.tsx deleted file mode 100644 index a9ca6862..00000000 --- a/packages/site/gatsby-browser.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { GatsbyBrowser } from 'gatsby'; -import React, { StrictMode } from 'react'; - -import { App } from './src/App'; -import { Root } from './src/Root'; - -export const wrapRootElement: GatsbyBrowser['wrapRootElement'] = ({ - element, -}) => ( - - {element} - -); - -export const wrapPageElement: GatsbyBrowser['wrapPageElement'] = ({ - element, -}) => {element}; diff --git a/packages/site/gatsby-config.ts b/packages/site/gatsby-config.ts deleted file mode 100644 index 3512039b..00000000 --- a/packages/site/gatsby-config.ts +++ /dev/null @@ -1,122 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import type { GatsbyConfig } from 'gatsby'; - -const config: GatsbyConfig = { - // This is required to make use of the React 17+ JSX transform. - jsxRuntime: 'automatic', - - pathPrefix: process.env.GATSBY_PATH_PREFIX ?? '/', - - plugins: [ - 'gatsby-plugin-svgr', - 'gatsby-plugin-styled-components', - { - resolve: 'gatsby-plugin-manifest', - options: { - name: 'Template Snap', - icon: 'src/assets/logo.svg', - theme_color: '#6F4CFF', - background_color: '#FFFFFF', - display: 'standalone', - }, - }, - { - // See: - resolve: `gatsby-plugin-webfonts`, - options: { - fonts: { - google: [ - { - family: 'Roboto Mono', - variants: ['400'], - }, - ], - selfHosted: [ - // Common weight name mapping - // - // 100 - Thin (Hairline) - // 200 - Extra Light (Ultra Light) - // 300 - Light - // 400 - Normal (Regular) - // 500 - Medium - // 600 - Semi Bold (Demi Bold) - // 700 - Bold - // 800 - Extra Bold (Ultra Bold) - // 900 - Black (Heavy) - // 950 - Extra Black (Ultra Black) - // - // See: - - // ---------------------------------------------------------------- - // Regular - { - family: 'Geist', - urls: { - woff2: `fonts/Geist-Regular.woff2`, - }, - fontStyle: 'normal', - fontWeight: 400, - }, - - // ---------------------------------------------------------------- - // Regular Italic - { - family: 'Geist', - urls: { - woff2: `fonts/Geist-RegularItalic.woff2`, - }, - fontStyle: 'italic', - fontWeight: 400, - }, - - // ---------------------------------------------------------------- - // Medium - { - family: 'Geist', - urls: { - woff2: `fonts/Geist-Medium.woff2`, - }, - fontStyle: 'normal', - fontWeight: 500, - }, - - // ---------------------------------------------------------------- - // Medium Italic - { - family: 'Geist', - urls: { - woff2: `fonts/Geist-MediumItalic.woff2`, - }, - fontStyle: 'italic', - fontWeight: 500, - }, - - // ---------------------------------------------------------------- - // Bold - { - family: 'Geist', - urls: { - woff2: `fonts/Geist-Bold.woff2`, - }, - fontStyle: 'normal', - fontWeight: 700, - }, - - // ---------------------------------------------------------------- - // Bold Italic - { - family: 'Geist', - urls: { - woff2: `fonts/Geist-BoldItalic.woff2`, - }, - fontStyle: 'italic', - fontWeight: 700, - }, - ], - }, - }, - }, - ], -}; - -export default config; diff --git a/packages/site/gatsby-node.js b/packages/site/gatsby-node.js deleted file mode 100644 index 839d292c..00000000 --- a/packages/site/gatsby-node.js +++ /dev/null @@ -1,26 +0,0 @@ -const webpack = require('webpack'); - -module.exports.onCreateWebpackConfig = ({ actions }) => { - actions.setWebpackConfig({ - plugins: [ - new webpack.NormalModuleReplacementPlugin(/node:/u, (resource) => { - const mod = resource.request.replace(/^node:/u, ''); - switch (mod) { - case 'crypto': - resource.request = 'crypto'; - break; - default: - throw new Error(`Not found ${mod}`); - } - }), - ], - resolve: { - alias: { - crypto: require.resolve('crypto-browserify'), - }, - fallback: { - crypto: require.resolve('crypto-browserify'), - }, - }, - }); -}; diff --git a/packages/site/gatsby-ssr.tsx b/packages/site/gatsby-ssr.tsx deleted file mode 100644 index f02da02f..00000000 --- a/packages/site/gatsby-ssr.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import type { GatsbySSR } from 'gatsby'; -import React, { StrictMode } from 'react'; - -import { App } from './src/App'; -import { Root } from './src/Root'; - -export const wrapRootElement: GatsbySSR['wrapRootElement'] = ({ element }) => ( - - {element} - -); - -export const wrapPageElement: GatsbySSR['wrapPageElement'] = ({ element }) => ( - {element} -); diff --git a/packages/site/package.json b/packages/site/package.json index b2afceb9..d54adcbc 100644 --- a/packages/site/package.json +++ b/packages/site/package.json @@ -4,18 +4,18 @@ "description": "A snap simple keyring dapp used in MetaMask e2e tests.", "license": "(MIT-0 OR Apache-2.0)", "scripts": { - "build": "rimraf .cache && cross-env GATSBY_TELEMETRY_DISABLED=1 gatsby build --prefix-paths", - "clean": "rimraf public .cache", + "build": "yarn clean && cross-env NODE_ENV=production node ./scripts/build.js", + "clean": "rimraf dist public .cache", "lint": "yarn lint:eslint && yarn lint:misc && yarn lint:deps && yarn lint:types", "lint:deps": "depcheck", "lint:eslint": "eslint . --cache --ext js,jsx,ts,tsx", "lint:fix": "yarn lint:eslint --fix && yarn lint:misc --write", - "lint:misc": "prettier '**/*.json' '**/*.md' '!CHANGELOG.md' --check", + "lint:misc": "prettier '**/*.html' '**/*.json' '**/*.md' '!CHANGELOG.md' --check", "lint:types": "tsc --noEmit", - "start": "rimraf .cache && cross-env GATSBY_TELEMETRY_DISABLED=1 gatsby develop" + "start": "cross-env NODE_ENV=development node ./scripts/start.js" }, "files": [ - "public/" + "dist/" ], "publishConfig": { "access": "public" @@ -33,36 +33,24 @@ ] }, "dependencies": { - "@emotion/react": "^11.11.1", - "@emotion/styled": "^11.11.0", "@metamask/keyring-api": "^23.1.0", "@metamask/keyring-snap-client": "^9.0.2", "@metamask/providers": "^19.0.0", - "@mui/icons-material": "^5.14.0", - "@mui/material": "^5.14.0", - "@types/react-helmet": "^6.1.6", - "crypto-browserify": "^3.12.1", + "bootstrap": "^5.2.2", "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-helmet": "^6.1.0", - "react-icons": "^4.8.0", - "react-is": "^18.2.0", - "semver": "^7.5.4", - "styled-components": "5.3.9", - "webpack": "^5.88.2" + "react-dom": "^18.2.0" }, "devDependencies": { - "@metamask/auto-changelog": "^3.3.0", "@metamask/eslint-config": "^12.2.0", "@metamask/eslint-config-jest": "^12.1.0", "@metamask/eslint-config-nodejs": "^12.1.0", "@metamask/eslint-config-typescript": "^12.1.0", - "@svgr/webpack": "^6.5.1", + "@swc/core": "1.3.78", "@types/react": "^18.0.28", - "@types/styled-components": "^5.1.26", "@typescript-eslint/eslint-plugin": "^5.55.0", "@typescript-eslint/parser": "^5.55.0", "cross-env": "^7.0.3", + "css-loader": "^5.2.7", "depcheck": "^1.4.6", "eslint": "^8.36.0", "eslint-config-prettier": "^8.7.0", @@ -72,14 +60,14 @@ "eslint-plugin-n": "^16.1.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-promise": "^6.1.1", - "gatsby": "^5.12.4", - "gatsby-plugin-manifest": "^5.12.0", - "gatsby-plugin-styled-components": "^6.12.0", - "gatsby-plugin-svgr": "^3.0.0-beta.0", - "gatsby-plugin-webfonts": "^2.3.2", + "html-webpack-plugin": "^5.5.0", "prettier": "^2.8.4", - "react-scripts": "^5.0.1", "rimraf": "^4.4.0", - "typescript": "^4.9.5" + "style-loader": "^2.0.0", + "swc-loader": "^0.2.3", + "terser-webpack-plugin": "^5.3.9", + "typescript": "^4.9.5", + "webpack": "^5.88.2", + "webpack-dev-server": "^4.6.0" } } diff --git a/packages/site/scripts/build.js b/packages/site/scripts/build.js new file mode 100644 index 00000000..cd22fef7 --- /dev/null +++ b/packages/site/scripts/build.js @@ -0,0 +1,53 @@ +const { cpSync, existsSync } = require('fs'); +const { resolve } = require('path'); +const webpack = require('webpack'); + +const config = require('../webpack.config'); + +const copyStaticFiles = () => { + const staticPath = resolve(__dirname, '..', 'static'); + const distPath = resolve(__dirname, '..', 'dist'); + + if (existsSync(staticPath)) { + cpSync(staticPath, distPath, { recursive: true }); + } +}; + +const compiler = webpack(config); + +compiler.run((error, stats) => { + compiler.close((closeError) => { + if (error || closeError) { + console.error(error || closeError); + process.exitCode = 1; + return; + } + + if (!stats) { + console.error('Webpack did not return build stats.'); + process.exitCode = 1; + return; + } + + if (stats.hasErrors()) { + console.error( + stats.toString({ + colors: true, + errors: true, + }), + ); + process.exitCode = 1; + return; + } + + copyStaticFiles(); + + console.log( + stats.toString({ + colors: true, + errors: true, + warnings: true, + }), + ); + }); +}); diff --git a/packages/site/scripts/start.js b/packages/site/scripts/start.js new file mode 100644 index 00000000..1f698e54 --- /dev/null +++ b/packages/site/scripts/start.js @@ -0,0 +1,34 @@ +const { resolve } = require('path'); +const webpack = require('webpack'); +const WebpackDevServer = require('webpack-dev-server'); + +const config = require('../webpack.config'); + +const compiler = webpack(config); +const server = new WebpackDevServer(config.devServer, compiler); + +const stopServer = async () => { + await server.stop(); +}; + +server.startCallback(() => { + const { host, port } = config.devServer; + console.log(`Site running at http://${host}:${port}`); + console.log( + `Serving static files from ${resolve(__dirname, '..', 'static')}`, + ); +}); + +process.on('SIGINT', () => { + stopServer().catch((error) => { + console.error(error); + process.exitCode = 1; + }); +}); + +process.on('SIGTERM', () => { + stopServer().catch((error) => { + console.error(error); + process.exitCode = 1; + }); +}); diff --git a/packages/site/src/App.tsx b/packages/site/src/App.tsx index befe6272..b7f1648d 100644 --- a/packages/site/src/App.tsx +++ b/packages/site/src/App.tsx @@ -1,55 +1,734 @@ -import type { FunctionComponent, ReactNode } from 'react'; -import React from 'react'; -import { Helmet } from 'react-helmet'; -import styled from 'styled-components'; - -import { Footer, Header, AlertBanner, AlertType } from './components'; -import { GlobalStyle } from './config/theme'; - -const Wrapper = styled.div` - display: flex; - flex-direction: column; - width: 100%; - min-height: 100vh; - max-width: 100vw; -`; - -const BannerWrapper = styled.div` - padding-top: 25px; - padding-left: 5%; - padding-right: 5%; -`; - -export type AppProps = { +import type { KeyringAccount, KeyringRequest } from '@metamask/keyring-api'; +import { KeyringSnapRpcClient } from '@metamask/keyring-snap-client'; +import type { + ChangeEvent, + FormEvent, + FunctionComponent, + ReactNode, +} from 'react'; +import { useCallback, useContext, useEffect, useState } from 'react'; + +import { defaultSnapOrigin } from './config'; +import { MetaMaskContext, MetamaskActions } from './hooks'; +import type { KeyringState } from './utils'; +import { + connectSnap, + getSnap, + isSynchronousMode, + toggleSynchronousApprovals, +} from './utils'; +import snapPackageInfo from '../../snap/package.json'; + +const snapId = defaultSnapOrigin; + +const initialKeyringState: KeyringState = { + pendingRequests: [], + accounts: [], + useSynchronousApprovals: true, +}; + +type MethodInput = { + id: string; + title: string; + type: 'text' | 'textarea'; + placeholder: string; + value: string; + options?: string[]; + onChange: (value: string) => void; +}; + +type MethodConfig = { + name: string; + description: string; + inputs?: MethodInput[]; + action: { + label: string; + disabled?: boolean; + callback: () => Promise; + }; +}; + +type SectionProps = { + name: string; + testId: string; children: ReactNode; }; -export const App: FunctionComponent = ({ children }) => { - // Make sure we are on a browser, otherwise we can't use window.ethereum. - if (typeof window === 'undefined') { - return null; +const valueBlockClassName = + 'overflow-auto text-break border rounded bg-light font-monospace small p-2 mb-0'; + +const getClient = () => { + if (!window.ethereum) { + throw new Error('MetaMask is not available.'); + } + + return new KeyringSnapRpcClient(snapId, window.ethereum); +}; + +const getErrorMessage = (error: unknown) => { + if (error instanceof Error) { + return error.message; + } + + if (typeof error === 'string') { + return error; + } + + return JSON.stringify(error, null, 2); +}; + +const formatResponse = (response: unknown) => { + if (typeof response === 'string') { + return response; + } + + return JSON.stringify(response, null, 2); +}; + +const Section: FunctionComponent = ({ + name, + testId, + children, +}) => ( +
+
+
+

{name}

+
+
{children}
+
+
+); + +const ValueBlock: FunctionComponent<{ value: string }> = ({ value }) => ( +
{value}
+); + +const Result: FunctionComponent<{ + value: string; + variant: 'primary' | 'danger'; +}> = ({ value, variant }) => ( +
+ +
+); + +const Method: FunctionComponent = ({ + description, + inputs = [], + action, +}) => { + const [response, setResponse] = useState(); + const [error, setError] = useState(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + setResponse(undefined); + setError(undefined); + setIsSubmitting(true); + + try { + const result = await action.callback(); + setResponse(result === undefined ? null : result); + } catch (caughtError) { + setError(getErrorMessage(caughtError)); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
{ + handleSubmit(event).catch(console.error); + }} + > +

{description}

+ {inputs.map((input) => ( +