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..392f032f 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", "webpack-cli", "webpack-dev-server"] } diff --git a/packages/site/.env.production b/packages/site/.env.production deleted file mode 100644 index e31e26dd..00000000 --- a/packages/site/.env.production +++ /dev/null @@ -1 +0,0 @@ -SNAP_ORIGIN=npm:@metamask/snap-simple-keyring-snap 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..849ddff3 100644 --- a/packages/site/.prettierignore +++ b/packages/site/.prettierignore @@ -1,2 +1 @@ -.cache/ -public/ +dist/ diff --git a/packages/site/README.md b/packages/site/README.md index f6f0748b..12d3d8a4 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:8080`. -### `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..09520e03 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": "cross-env NODE_ENV=production webpack", + "clean": "rimraf dist", "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 webpack serve" }, "files": [ - "public/" + "dist/" ], "publishConfig": { "access": "public" @@ -32,37 +32,22 @@ "last 1 safari version" ] }, - "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", - "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" - }, "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", + "@metamask/keyring-api": "^23.1.0", + "@metamask/keyring-snap-client": "^9.0.2", + "@metamask/providers": "^19.0.0", + "@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", + "bootstrap": "^5.2.2", + "copy-webpack-plugin": "^11.0.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 +57,17 @@ "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", + "react": "^18.2.0", + "react-dom": "^18.2.0", "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-cli": "^5.1.4", + "webpack-dev-server": "^4.6.0" } } diff --git a/packages/site/src/App.tsx b/packages/site/src/App.tsx index befe6272..fe18877e 100644 --- a/packages/site/src/App.tsx +++ b/packages/site/src/App.tsx @@ -1,55 +1,739 @@ -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 ResultTitle: FunctionComponent<{ variant: 'success' | 'danger' }> = ({ + variant, +}) =>

{`${variant === 'success' ? 'Successful' : 'Error'} request:`}

; + +const Result: FunctionComponent<{ + value: string; + variant: 'success' | '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) => ( +