diff --git a/.gitignore b/.gitignore index fe97056833b..c592bfb3724 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,5 @@ npm-debug.log* .vercel .envrc -.env \ No newline at end of file +.env +.integrationBuilderCache \ No newline at end of file diff --git a/docusaurus.config.js b/docusaurus.config.js index 4c67292f8fb..fe1c8335c39 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -12,23 +12,21 @@ const { MM_RPC_URL, } = require('./src/plugins/plugin-json-rpc') const codeTheme = themes.dracula -const helpDropdown = fs.readFileSync('./src/components/NavDropdown/DeveloperTools.html', 'utf-8') -const connectDropdown = fs.readFileSync( - './src/components/NavDropdown/ConnectMetaMask.html', +const productsDropdown = fs.readFileSync( + './src/components/NavDropdown/Products.html', 'utf-8' ) -const embedDropdown = fs.readFileSync('./src/components/NavDropdown/EmbedMetaMask.html', 'utf-8') -const extendDropdown = fs.readFileSync('./src/components/NavDropdown/ExtendScale.html', 'utf-8') +const baseUrl = process.env.DEST || '/'; +const siteUrl = 'https://docs.metamask.io'; + const npm2yarnPlugin = [require('@docusaurus/remark-plugin-npm2yarn'), { sync: true }] /** @type {import('@docusaurus/types').Config} */ -const siteUrl = 'https://docs.metamask.io' -const baseUrl = process.env.DEST || '/' const fullUrl = new URL(baseUrl, siteUrl).toString() const config = { title: 'MetaMask developer documentation', // tagline: '', url: 'https://docs.metamask.io', - baseUrl: process.env.DEST || '/', // overwritten in github action for staging / latest + baseUrl, // overwritten in github action for staging / latest onBrokenLinks: 'warn', onBrokenMarkdownLinks: 'warn', favicon: 'img/favicons/favicon-96x96.png', @@ -110,6 +108,16 @@ const config = { trailingSlash: true, scripts: [ + { + src: baseUrl + "js/fix-trailing-slash.js", + async: false, + defer: false, + }, + { + src: baseUrl + "js/code-focus.js", + async: false, + defer: true, + }, { src: 'https://cmp.osano.com/AzZMxHTbQDOQD8c1J/84e64bce-4a70-4dcc-85cb-7958f22b2371/osano.js', }, @@ -133,6 +141,20 @@ const config = { breadcrumbs: false, remarkPlugins: [npm2yarnPlugin], }, + pages: { + path: 'src/pages', + routeBasePath: '/', + include: ['**/**.{js,jsx,ts,tsx,md,mdx}'], + exclude: [ + '**/_*.{js,jsx,ts,tsx,md,mdx}', + '**/_*/**', + '**/*.test.{js,jsx,ts,tsx}', + '**/__tests__/**', + '**/quickstart/**', // Exclude quickstart directory from pages plugin + ], + mdxPageComponent: '@theme/MDXPage', + remarkPlugins: [npm2yarnPlugin], + }, theme: { customCss: require.resolve('./src/scss/custom.scss'), }, @@ -140,6 +162,8 @@ const config = { ], ], plugins: [ + ['./src/plugins/docusaurus-plugin-virtual-files', { rootDir: '.integrationBuilderCache' }], + './src/plugins/docusaurus-plugin-guides', 'docusaurus-plugin-sass', './src/plugins/mm-scss-utils', [ @@ -336,54 +360,39 @@ const config = { items: [ { type: 'dropdown', - label: 'Connect to MetaMask', + label: 'Products', items: [ { type: 'html', - value: connectDropdown, + value: productsDropdown, }, ], }, { - type: 'dropdown', - label: 'Embed MetaMask', - items: [ - { - type: 'html', - value: embedDropdown, - }, - ], + label: 'Quick Start', + to: '/quickstart', + position: 'left', }, { - type: 'dropdown', - label: 'Extend and scale', - items: [ - { - type: 'html', - value: extendDropdown, - }, - ], + label: "Guides", + to: "/guides", + position: "left", }, { - type: 'dropdown', - label: 'Developer tools', - items: [ - { - type: 'html', - value: helpDropdown, - }, - ], - }, - { - to: 'whats-new', - label: "What's new?", + to: 'developer-tools/faucet/', + label: "Faucet", position: 'right', }, { - type: 'custom-navbarWallet', + to: 'https://community.metamask.io/', + label: "Help ↗", position: 'right', - includeUrl: REF_ALLOW_LOGIN_PATH, }, + // { + // type: 'custom-navbarWallet', + // position: 'right', + // includeUrl: REF_ALLOW_LOGIN_PATH, + // }, /* Language drop down { type: "localeDropdown", @@ -524,6 +533,21 @@ const config = { theme: codeTheme, additionalLanguages: ['csharp', 'gradle', 'bash', 'json'], magicComments: [ + { + className: 'theme-code-block-highlighted-line', + line: 'highlight-next-line', + block: { start: 'highlight-start', end: 'highlight-end' }, + }, + { + className: "code-unfocus", + line: "unfocus-next-line", + block: { start: "unfocus-start", end: "unfocus-end" }, + }, + { + className: "code-focus", + line: "focus-next-line", + block: { start: "focus-start", end: "focus-end" }, + }, { className: 'git-diff-remove', line: 'remove-next-line', @@ -554,7 +578,7 @@ const config = { // Optional: Replace parts of the item URLs from Algolia. Useful when using the same search index for multiple deployments using a different baseUrl. You can use regexp or string in the `from` param. For example: localhost:3000 vs myCompany.com/docs replaceSearchResultPathname: { from: '/', - to: process.env.DEST || '/', + to: baseUrl, }, // Optional: Algolia search parameters diff --git a/package-lock.json b/package-lock.json index 75e976dbfc6..7c51c866fa9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,9 @@ "@rjsf/validator-ajv8": "^5.24.12", "@sentry/browser": "^8.51.0", "@types/react": "^18.3.3", + "classnames": "^2.5.1", "clsx": "^2.1.1", + "copy-to-clipboard": "^3.3.3", "docusaurus-plugin-sass": "^0.2.5", "dotenv": "^16.4.7", "ethers": "^6.13.5", @@ -41,14 +43,19 @@ "lodash.debounce": "^4.0.8", "lodash.isplainobject": "^4.0.6", "node-polyfill-webpack-plugin": "^2.0.1", + "playwright": "^1.54.2", "prettier": "^3.3.3", "prism-react-renderer": "^2.1.0", "react": "^18.0.0", "react-alert": "^7.0.3", + "react-bookmark": "^0.8.2", "react-dom": "^18.0.0", "react-dropdown-select": "^4.12.2", + "react-icons": "^5.5.0", "react-modal": "^3.16.3", "react-player": "^3.3.1", + "react-spinners": "^0.17.0", + "react-spring": "^10.0.1", "react-tippy": "^1.4.0", "remark-codesandbox": "^0.10.1", "remark-docusaurus-tabs": "^0.2.0", @@ -375,13 +382,15 @@ } }, "node_modules/@babel/generator": { - "version": "7.27.5", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.5", - "@babel/types": "^7.27.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -481,6 +490,16 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.27.1", "license": "MIT", @@ -621,10 +640,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.5", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.28.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -712,6 +733,61 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-dynamic-import": { "version": "7.8.3", "license": "MIT", @@ -748,6 +824,32 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-jsx": { "version": "7.27.1", "license": "MIT", @@ -761,6 +863,116 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-typescript": { "version": "7.27.1", "license": "MIT", @@ -1770,8 +1982,30 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/traverse--for-generate-function-map": { + "name": "@babel/traverse", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { - "version": "7.27.6", + "version": "7.28.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", + "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -5311,98 +5545,316 @@ "version": "0.0.14", "license": "MIT" }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, + "node_modules/@isaacs/ttlcache": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", + "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==", + "license": "ISC", + "peer": true, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=12" } }, - "node_modules/@jest/types": { - "version": "29.6.3", - "license": "MIT", + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "license": "ISC", + "peer": true, "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=8" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "license": "MIT", + "peer": true, "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" + "sprintf-js": "~1.0.2" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", + "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "license": "MIT", + "peer": true, "engines": { - "node": ">=6.0.0" + "node": ">=6" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "license": "MIT", + "peer": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, "engines": { - "node": ">=6.0.0" + "node": ">=8" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.6", + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "license": "MIT", + "peer": true, "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "license": "MIT", + "peer": true, "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@keystonehq/alias-sampling": { - "version": "0.1.2", + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "license": "MIT", - "peer": true + "peer": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/@keystonehq/base-eth-keyring": { - "version": "0.14.1", + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "license": "MIT", "peer": true, "dependencies": { - "@ethereumjs/tx": "^4.0.2", - "@ethereumjs/util": "^8.0.0", - "@keystonehq/bc-ur-registry-eth": "^0.19.1", - "hdkey": "^2.0.1", - "rlp": "^3.0.0", - "uuid": "^8.3.2" + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@keystonehq/base-eth-keyring/node_modules/rlp": { - "version": "3.0.0", - "license": "MPL-2.0", + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/create-cache-key-function": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", + "integrity": "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "license": "ISC", + "peer": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@keystonehq/alias-sampling": { + "version": "0.1.2", + "license": "MIT", + "peer": true + }, + "node_modules/@keystonehq/base-eth-keyring": { + "version": "0.14.1", + "license": "MIT", + "peer": true, + "dependencies": { + "@ethereumjs/tx": "^4.0.2", + "@ethereumjs/util": "^8.0.0", + "@keystonehq/bc-ur-registry-eth": "^0.19.1", + "hdkey": "^2.0.1", + "rlp": "^3.0.0", + "uuid": "^8.3.2" + } + }, + "node_modules/@keystonehq/base-eth-keyring/node_modules/rlp": { + "version": "3.0.0", + "license": "MPL-2.0", "peer": true, "bin": { "rlp": "bin/rlp" @@ -8172,109 +8624,397 @@ "version": "1.0.0-next.28", "license": "MIT" }, - "node_modules/@rjsf/core": { - "version": "5.24.10", - "license": "Apache-2.0", - "dependencies": { - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", - "markdown-to-jsx": "^7.4.1", - "nanoid": "^3.3.7", - "prop-types": "^15.8.1" - }, + "node_modules/@react-native/assets-registry": { + "version": "0.80.1", + "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.80.1.tgz", + "integrity": "sha512-T3C8OthBHfpFIjaGFa0q6rc58T2AsJ+jKAa+qPquMKBtYGJMc75WgNbk/ZbPBxeity6FxZsmg3bzoUaWQo4Mow==", + "license": "MIT", + "peer": true, "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@rjsf/utils": "^5.24.x", - "react": "^16.14.0 || >=17" + "node": ">=18" } }, - "node_modules/@rjsf/utils": { - "version": "5.24.12", - "resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.24.12.tgz", - "integrity": "sha512-fDwQB0XkjZjpdFUz5UAnuZj8nnbxDbX5tp+jTOjjJKw2TMQ9gFFYCQ12lSpdhezA2YgEGZfxyYTGW0DKDL5Drg==", - "license": "Apache-2.0", + "node_modules/@react-native/codegen": { + "version": "0.80.1", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.80.1.tgz", + "integrity": "sha512-CFhOYkXmExOeZDZnd0UJCK9A4AOSAyFBoVgmFZsf+fv8JqnwIx/SD6RxY1+Jzz9EWPQcH2v+WgwPP/4qVmjtKw==", + "license": "MIT", + "peer": true, "dependencies": { - "json-schema-merge-allof": "^0.8.1", - "jsonpointer": "^5.0.1", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", - "react-is": "^18.2.0" + "glob": "^7.1.1", + "hermes-parser": "0.28.1", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "yargs": "^17.6.2" }, "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { - "react": "^16.14.0 || >=17" + "@babel/core": "*" } }, - "node_modules/@rjsf/validator-ajv8": { - "version": "5.24.12", - "resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-5.24.12.tgz", - "integrity": "sha512-IMXdCjvDNdvb+mDgZC3AlAtr0pjYKq5s0GcLECjG5PuiX7Ib4JaDQHZY5ZJdKblMfgzhsn8AAOi573jXAt7BHQ==", + "node_modules/@react-native/community-cli-plugin": { + "version": "0.80.1", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.80.1.tgz", + "integrity": "sha512-M1lzLvZUz6zb6rn4Oyc3HUY72wye8mtdm1bJSYIBoK96ejMvQGoM+Lih/6k3c1xL7LSruNHfsEXXePLjCbhE8Q==", + "license": "MIT", + "peer": true, "dependencies": { - "ajv": "^8.12.0", - "ajv-formats": "^2.1.1", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21" + "@react-native/dev-middleware": "0.80.1", + "chalk": "^4.0.0", + "debug": "^4.4.0", + "invariant": "^2.2.4", + "metro": "^0.82.2", + "metro-config": "^0.82.2", + "metro-core": "^0.82.2", + "semver": "^7.1.3" }, "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { - "@rjsf/utils": "^5.24.x" + "@react-native-community/cli": "*" + }, + "peerDependenciesMeta": { + "@react-native-community/cli": { + "optional": true + } } }, - "node_modules/@rjsf/validator-ajv8/node_modules/ajv": { - "version": "8.17.1", + "node_modules/@react-native/community-cli-plugin/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", + "peer": true, "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" + "ms": "^2.1.3" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/@rjsf/validator-ajv8/node_modules/json-schema-traverse": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/@scure/base": { - "version": "1.1.9", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" + "node_modules/@react-native/debugger-frontend": { + "version": "0.80.1", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.80.1.tgz", + "integrity": "sha512-5dQJdX1ZS4dINNw51KNsDIL+A06sZQd2hqN2Pldq5SavxAwEJh5NxAx7K+lutKhwp1By5gxd6/9ruVt+9NCvKA==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=18" } }, - "node_modules/@scure/bip32": { - "version": "1.4.0", + "node_modules/@react-native/dev-middleware": { + "version": "0.80.1", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.80.1.tgz", + "integrity": "sha512-EBnZ3s6+hGAlUggDvo9uI37Xh0vG55H2rr3A6l6ww7+sgNuUz+wEJ63mGINiU6DwzQSgr6av7rjrVERxKH6vxg==", "license": "MIT", + "peer": true, "dependencies": { - "@noble/curves": "~1.4.0", - "@noble/hashes": "~1.4.0", - "@scure/base": "~1.1.6" + "@isaacs/ttlcache": "^1.4.1", + "@react-native/debugger-frontend": "0.80.1", + "chrome-launcher": "^0.15.2", + "chromium-edge-launcher": "^0.2.0", + "connect": "^3.6.5", + "debug": "^4.4.0", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "open": "^7.0.3", + "serve-static": "^1.16.2", + "ws": "^6.2.3" }, - "funding": { - "url": "https://paulmillr.com/funding/" + "engines": { + "node": ">=18" } }, - "node_modules/@scure/bip32/node_modules/@noble/curves": { - "version": "1.4.2", + "node_modules/@react-native/dev-middleware/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", + "peer": true, "dependencies": { - "@noble/hashes": "1.4.0" + "ms": "^2.1.3" }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32/node_modules/@noble/hashes": { - "version": "1.4.0", + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@react-native/dev-middleware/node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "license": "MIT", + "peer": true, + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/@react-native/gradle-plugin": { + "version": "0.80.1", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.80.1.tgz", + "integrity": "sha512-6B7bWUk27ne/g/wCgFF4MZFi5iy6hWOcBffqETJoab6WURMyZ6nU+EAMn+Vjhl5ishhUvTVSrJ/1uqrxxYQO2Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/js-polyfills": { + "version": "0.80.1", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.80.1.tgz", + "integrity": "sha512-cWd5Cd2kBMRM37dor8N9Ck4X0NzjYM3m8K6HtjodcOdOvzpXfrfhhM56jdseTl5Z4iB+pohzPJpSmFJctmuIpA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/normalize-colors": { + "version": "0.80.1", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.80.1.tgz", + "integrity": "sha512-YP12bjz0bzo2lFxZDOPkRJSOkcqAzXCQQIV1wd7lzCTXE0NJNwoaeNBobJvcPhiODEWUYCXPANrZveFhtFu5vw==", + "license": "MIT", + "peer": true + }, + "node_modules/@react-spring/animated": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-10.0.1.tgz", + "integrity": "sha512-BGL3hA66Y8Qm3KmRZUlfG/mFbDPYajgil2/jOP0VXf2+o2WPVmcDps/eEgdDqgf5Pv9eBbyj7LschLMuSjlW3Q==", + "license": "MIT", + "dependencies": { + "@react-spring/shared": "~10.0.1", + "@react-spring/types": "~10.0.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-10.0.1.tgz", + "integrity": "sha512-KaMMsN1qHuVTsFpg/5ajAVye7OEqhYbCq0g4aKM9bnSZlDBBYpO7Uf+9eixyXN8YEbF+YXaYj9eoWDs+npZ+sA==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~10.0.1", + "@react-spring/shared": "~10.0.1", + "@react-spring/types": "~10.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-spring/konva": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/konva/-/konva-10.0.1.tgz", + "integrity": "sha512-mxy9dmfq2gcY3fisWBqGkbks5EmNDvwU5ya8w44xjVcDm7fI7ANsoDjJSS6d51JElO5fL3LYaAFi1urqQ2BjrQ==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~10.0.1", + "@react-spring/core": "~10.0.1", + "@react-spring/shared": "~10.0.1", + "@react-spring/types": "~10.0.1" + }, + "peerDependencies": { + "konva": ">=2.6", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-konva": "^19" + } + }, + "node_modules/@react-spring/rafz": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-10.0.1.tgz", + "integrity": "sha512-UrzG/d6Is+9i0aCAjsjWRqIlFFiC4lFqFHrH63zK935z2YDU95TOFio4VKGISJ5SG0xq4ULy7c1V3KU+XvL+Yg==", + "license": "MIT" + }, + "node_modules/@react-spring/shared": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-10.0.1.tgz", + "integrity": "sha512-KR2tmjDShPruI/GGPfAZOOLvDgkhFseabjvxzZFFggJMPkyICLjO0J6mCIoGtdJSuHywZyc4Mmlgi+C88lS00g==", + "license": "MIT", + "dependencies": { + "@react-spring/rafz": "~10.0.1", + "@react-spring/types": "~10.0.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-spring/types": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-10.0.1.tgz", + "integrity": "sha512-Fk1wYVAKL+ZTYK+4YFDpHf3Slsy59pfFFvnnTfRjQQFGlyIo4VejPtDs3CbDiuBjM135YztRyZjIH2VbycB+ZQ==", + "license": "MIT" + }, + "node_modules/@react-spring/web": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-10.0.1.tgz", + "integrity": "sha512-FgQk02OqFrYyJBTTnBTWAU0WPzkHkKXauc6aeexcvATvLapUxwnfGuLlsLYF8BYjEVfkivPT04ziAue6zyRBtQ==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~10.0.1", + "@react-spring/core": "~10.0.1", + "@react-spring/shared": "~10.0.1", + "@react-spring/types": "~10.0.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-spring/zdog": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/zdog/-/zdog-10.0.1.tgz", + "integrity": "sha512-yEU2vf4C5FPxcnbYqnYtEMLElaoaepL5l9oAZI5/hK40EAQjo9uW6rtznOvbmL8z8RLM0PUCym0FtNHTlu0Ysw==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~10.0.1", + "@react-spring/core": "~10.0.1", + "@react-spring/shared": "~10.0.1", + "@react-spring/types": "~10.0.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-zdog": ">=1.0", + "zdog": ">=1.0" + } + }, + "node_modules/@rjsf/core": { + "version": "5.24.10", + "license": "Apache-2.0", + "dependencies": { + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "markdown-to-jsx": "^7.4.1", + "nanoid": "^3.3.7", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@rjsf/utils": "^5.24.x", + "react": "^16.14.0 || >=17" + } + }, + "node_modules/@rjsf/utils": { + "version": "5.24.12", + "resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.24.12.tgz", + "integrity": "sha512-fDwQB0XkjZjpdFUz5UAnuZj8nnbxDbX5tp+jTOjjJKw2TMQ9gFFYCQ12lSpdhezA2YgEGZfxyYTGW0DKDL5Drg==", + "license": "Apache-2.0", + "dependencies": { + "json-schema-merge-allof": "^0.8.1", + "jsonpointer": "^5.0.1", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "react-is": "^18.2.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.14.0 || >=17" + } + }, + "node_modules/@rjsf/validator-ajv8": { + "version": "5.24.12", + "resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-5.24.12.tgz", + "integrity": "sha512-IMXdCjvDNdvb+mDgZC3AlAtr0pjYKq5s0GcLECjG5PuiX7Ib4JaDQHZY5ZJdKblMfgzhsn8AAOi573jXAt7BHQ==", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^2.1.1", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@rjsf/utils": "^5.24.x" + } + }, + "node_modules/@rjsf/validator-ajv8/node_modules/ajv": { + "version": "8.17.1", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@rjsf/validator-ajv8/node_modules/json-schema-traverse": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/@scure/base": { + "version": "1.1.9", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.4.0", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.4.2", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.4.0", "license": "MIT", "engines": { "node": ">= 16" @@ -8396,6 +9136,26 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, "node_modules/@slorber/remark-comment": { "version": "1.0.0", "license": "MIT", @@ -8699,6 +9459,51 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, "node_modules/@types/bn.js": { "version": "5.1.6", "license": "MIT", @@ -8983,6 +9788,16 @@ "version": "7946.0.16", "license": "MIT" }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/gtag.js": { "version": "0.0.12", "license": "MIT" @@ -9139,6 +9954,16 @@ "@types/react": "*" } }, + "node_modules/@types/react-reconciler": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.32.0.tgz", + "integrity": "sha512-+WHarFkJevhH1s655qeeSEf/yxFST0dVRsmSqUgxG8mMOKqycgYBv2wVpyubBY7MX8KiX5FQ03rNIwrxfm7Bmw==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-router": { "version": "5.1.20", "license": "MIT", @@ -9220,6 +10045,20 @@ "@types/node": "*" } }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/stylis": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", + "license": "MIT", + "peer": true + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "license": "MIT", @@ -9234,6 +10073,13 @@ "license": "MIT", "peer": true }, + "node_modules/@types/webxr": { + "version": "0.5.22", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.22.tgz", + "integrity": "sha512-Vr6Stjv5jPRqH690f5I5GLjVk8GSsoQSYJ2FVd/3jJF7KaqfwPi3ehfBS96mlQ2kPCwZaX6U0rG2+NGHBKkA/A==", + "license": "MIT", + "peer": true + }, "node_modules/@types/ws": { "version": "8.5.12", "license": "MIT", @@ -9688,6 +10534,13 @@ "algoliasearch": ">= 3.1 < 6" } }, + "node_modules/anser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", + "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", + "license": "MIT", + "peer": true + }, "node_modules/ansi-align": { "version": "3.0.1", "license": "ISC", @@ -9923,6 +10776,13 @@ "node": ">=0.10.0" } }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT", + "peer": true + }, "node_modules/asn1.js": { "version": "4.10.1", "license": "MIT", @@ -9962,6 +10822,13 @@ "node": ">= 0.4" } }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "license": "MIT", + "peer": true + }, "node_modules/async-mutex": { "version": "0.5.0", "license": "MIT", @@ -10031,6 +10898,28 @@ "license": "Apache-2.0", "peer": true }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, "node_modules/babel-loader": { "version": "9.2.1", "license": "MIT", @@ -10053,6 +10942,39 @@ "object.assign": "^4.1.0" } }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/babel-plugin-macros": { "version": "3.1.0", "license": "MIT", @@ -10127,6 +11049,60 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.28.1.tgz", + "integrity": "sha512-meT17DOuUElMNsL5LZN56d+KBp22hb0EfxWfuPUeoSi54e40v1W4C2V36P75FpsH9fVEfDKpw5Nnkahc8haSsQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "hermes-parser": "0.28.1" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "license": "MIT", + "peer": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/bail": { "version": "2.0.2", "license": "MIT", @@ -10557,6 +11533,16 @@ "safe-buffer": "^5.1.2" } }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, "node_modules/buffer": { "version": "5.7.1", "funding": [ @@ -10743,6 +11729,42 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "callsites": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/caller-callsite/node_modules/callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==", + "license": "MIT", + "peer": true, + "dependencies": { + "caller-callsite": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/callsites": { "version": "3.1.0", "license": "MIT", @@ -10796,6 +11818,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-api": { "version": "3.0.0", "license": "MIT", @@ -11004,6 +12036,25 @@ "version": "1.1.4", "license": "ISC" }, + "node_modules/chrome-launcher": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", + "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, "node_modules/chrome-trace-event": { "version": "1.0.4", "license": "MIT", @@ -11011,6 +12062,34 @@ "node": ">=6.0" } }, + "node_modules/chromium-edge-launcher": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz", + "integrity": "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0", + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + } + }, + "node_modules/chromium-edge-launcher/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "peer": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ci-info": { "version": "3.9.0", "funding": [ @@ -11032,6 +12111,12 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/clean-css": { "version": "5.3.3", "license": "MIT", @@ -11106,6 +12191,71 @@ "version": "2.2.1", "license": "ISC" }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "peer": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "peer": true + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/clone-deep": { "version": "4.0.1", "license": "MIT", @@ -11870,6 +13020,22 @@ "typedarray-to-buffer": "^3.1.5" } }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/connect-history-api-fallback": { "version": "2.0.0", "license": "MIT", @@ -11877,6 +13043,23 @@ "node": ">=0.8" } }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT", + "peer": true + }, "node_modules/consola": { "version": "3.4.2", "license": "MIT", @@ -11952,6 +13135,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, "node_modules/copy-webpack-plugin": { "version": "11.0.0", "license": "MIT", @@ -12244,6 +13436,16 @@ "node": ">=4" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=4" + } + }, "node_modules/css-declaration-sorter": { "version": "7.2.0", "license": "ISC", @@ -12427,6 +13629,18 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/css-tree": { "version": "2.3.1", "license": "MIT", @@ -13822,6 +15036,16 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "stackframe": "^1.3.4" + } + }, "node_modules/es-abstract": { "version": "1.23.9", "dev": true, @@ -14722,6 +15946,13 @@ "node": ">=0.10.0" } }, + "node_modules/exponential-backoff": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", + "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==", + "license": "Apache-2.0", + "peer": true + }, "node_modules/express": { "version": "4.21.2", "license": "MIT", @@ -14982,6 +16213,16 @@ "node": ">=0.8.0" } }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "bser": "2.1.1" + } + }, "node_modules/feed": { "version": "4.2.2", "license": "MIT", @@ -15090,22 +16331,58 @@ "node": ">=8" } }, - "node_modules/find-cache-dir": { - "version": "4.0.0", + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", "license": "MIT", + "peer": true, "dependencies": { - "common-path-prefix": "^3.0.0", - "pkg-dir": "^7.0.0" + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.8" } }, - "node_modules/find-file-up": { - "version": "0.1.3", + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT", + "peer": true + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "license": "MIT", + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-file-up": { + "version": "0.1.3", "license": "MIT", "dependencies": { "fs-exists-sync": "^0.1.0", @@ -15169,6 +16446,13 @@ "dev": true, "license": "ISC" }, + "node_modules/flow-enums-runtime": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz", + "integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==", + "license": "MIT", + "peer": true + }, "node_modules/flush-write-stream": { "version": "1.1.1", "license": "MIT", @@ -15432,6 +16716,16 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "peer": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.2.7", "license": "MIT", @@ -15466,6 +16760,16 @@ "version": "3.0.2", "license": "ISC" }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/get-proto": { "version": "1.0.1", "license": "MIT", @@ -16105,6 +17409,23 @@ "he": "bin/he" } }, + "node_modules/hermes-estree": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.28.1.tgz", + "integrity": "sha512-w3nxl/RGM7LBae0v8LH2o36+8VqwOZGv9rX1wyoWT6YaKZLqpJZ0YQ5P0LVr3tuRpf7vCx0iIG4i/VmBJejxTQ==", + "license": "MIT", + "peer": true + }, + "node_modules/hermes-parser": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.28.1.tgz", + "integrity": "sha512-nf8o+hE8g7UJWParnccljHumE9Vlq8F7MqIdeahl+4x0tvCUJYRrT0L7h0MMg/X9YJmkNwsfbaNNrzPtFXOscg==", + "license": "MIT", + "peer": true, + "dependencies": { + "hermes-estree": "0.28.1" + } + }, "node_modules/history": { "version": "4.10.1", "license": "MIT", @@ -17066,6 +18387,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-docker": { "version": "2.2.1", "license": "MIT", @@ -17456,6 +18787,43 @@ "node": ">=0.10.0" } }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/istextorbinary": { "version": "2.6.0", "license": "MIT", @@ -17487,6 +18855,106 @@ "node": ">= 0.4" } }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/jest-util": { "version": "29.7.0", "license": "MIT", @@ -17502,6 +18970,24 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/jest-worker": { "version": "29.7.0", "license": "MIT", @@ -17576,6 +19062,13 @@ "license": "Apache-2.0", "peer": true }, + "node_modules/jsc-safe-url": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz", + "integrity": "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==", + "license": "0BSD", + "peer": true + }, "node_modules/jsesc": { "version": "3.1.0", "license": "MIT", @@ -17788,6 +19281,27 @@ "version": "1.8.0", "license": "MIT" }, + "node_modules/konva": { + "version": "9.3.22", + "resolved": "https://registry.npmjs.org/konva/-/konva-9.3.22.tgz", + "integrity": "sha512-yQI5d1bmELlD/fowuyfOp9ff+oamg26WOCkyqUyc+nczD/lhRa3EvD2MZOoc4c1293TAubW9n34fSQLgSeEgSw==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/lavrton" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/konva" + }, + { + "type": "github", + "url": "https://github.com/sponsors/lavrton" + } + ], + "license": "MIT", + "peer": true + }, "node_modules/langium": { "version": "3.3.1", "license": "MIT", @@ -17883,6 +19397,34 @@ "immediate": "~3.0.5" } }, + "node_modules/lighthouse-logger": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", + "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "debug": "^2.6.9", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-logger/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/lighthouse-logger/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT", + "peer": true + }, "node_modules/lilconfig": { "version": "3.1.3", "license": "MIT", @@ -18007,6 +19549,13 @@ "version": "4.1.1", "license": "MIT" }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "license": "MIT", + "peer": true + }, "node_modules/lodash.truncate": { "version": "4.4.2", "dev": true, @@ -18220,6 +19769,16 @@ "version": "2.1.2", "license": "ISC" }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, "node_modules/map-obj": { "version": "4.3.0", "dev": true, @@ -18269,6 +19828,13 @@ "node": ">= 18" } }, + "node_modules/marky": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", + "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", + "license": "Apache-2.0", + "peer": true + }, "node_modules/math-intrinsics": { "version": "1.1.0", "license": "MIT", @@ -18687,6 +20253,13 @@ "node": ">= 4.0.0" } }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "license": "MIT", + "peer": true + }, "node_modules/meow": { "version": "10.1.5", "dev": true, @@ -18836,44 +20409,574 @@ "node": ">= 0.6" } }, - "node_modules/micro-ftch": { - "version": "0.3.1", - "license": "MIT" - }, - "node_modules/micromark": { - "version": "4.0.2", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/metro": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro/-/metro-0.82.5.tgz", + "integrity": "sha512-8oAXxL7do8QckID/WZEKaIFuQJFUTLzfVcC48ghkHhNK2RGuQq8Xvf4AVd+TUA0SZtX0q8TGNXZ/eba1ckeGCg==", "license": "MIT", + "peer": true, "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" + "@babel/code-frame": "^7.24.7", + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.3", + "@babel/types": "^7.25.2", + "accepts": "^1.3.7", + "chalk": "^4.0.0", + "ci-info": "^2.0.0", + "connect": "^3.6.5", + "debug": "^4.4.0", + "error-stack-parser": "^2.0.6", + "flow-enums-runtime": "^0.0.6", + "graceful-fs": "^4.2.4", + "hermes-parser": "0.29.1", + "image-size": "^1.0.2", + "invariant": "^2.2.4", + "jest-worker": "^29.7.0", + "jsc-safe-url": "^0.2.2", + "lodash.throttle": "^4.1.1", + "metro-babel-transformer": "0.82.5", + "metro-cache": "0.82.5", + "metro-cache-key": "0.82.5", + "metro-config": "0.82.5", + "metro-core": "0.82.5", + "metro-file-map": "0.82.5", + "metro-resolver": "0.82.5", + "metro-runtime": "0.82.5", + "metro-source-map": "0.82.5", + "metro-symbolicate": "0.82.5", + "metro-transform-plugins": "0.82.5", + "metro-transform-worker": "0.82.5", + "mime-types": "^2.1.27", + "nullthrows": "^1.1.1", + "serialize-error": "^2.1.0", + "source-map": "^0.5.6", + "throat": "^5.0.0", + "ws": "^7.5.10", + "yargs": "^17.6.2" + }, + "bin": { + "metro": "src/cli.js" + }, + "engines": { + "node": ">=18.18" } }, - "node_modules/micromark-core-commonmark": { + "node_modules/metro-babel-transformer": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.82.5.tgz", + "integrity": "sha512-W/scFDnwJXSccJYnOFdGiYr9srhbHPdxX9TvvACOFsIXdLilh3XuxQl/wXW6jEJfgIb0jTvoTlwwrqvuwymr6Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "hermes-parser": "0.29.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-babel-transformer/node_modules/hermes-estree": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.29.1.tgz", + "integrity": "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ==", + "license": "MIT", + "peer": true + }, + "node_modules/metro-babel-transformer/node_modules/hermes-parser": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.29.1.tgz", + "integrity": "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA==", + "license": "MIT", + "peer": true, + "dependencies": { + "hermes-estree": "0.29.1" + } + }, + "node_modules/metro-cache": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.82.5.tgz", + "integrity": "sha512-AwHV9607xZpedu1NQcjUkua8v7HfOTKfftl6Vc9OGr/jbpiJX6Gpy8E/V9jo/U9UuVYX2PqSUcVNZmu+LTm71Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "exponential-backoff": "^3.1.1", + "flow-enums-runtime": "^0.0.6", + "https-proxy-agent": "^7.0.5", + "metro-core": "0.82.5" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-cache-key": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.82.5.tgz", + "integrity": "sha512-qpVmPbDJuRLrT4kcGlUouyqLGssJnbTllVtvIgXfR7ZuzMKf0mGS+8WzcqzNK8+kCyakombQWR0uDd8qhWGJcA==", + "license": "MIT", + "peer": true, + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-cache/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/metro-cache/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "peer": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/metro-config": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.82.5.tgz", + "integrity": "sha512-/r83VqE55l0WsBf8IhNmc/3z71y2zIPe5kRSuqA5tY/SL/ULzlHUJEMd1szztd0G45JozLwjvrhAzhDPJ/Qo/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "connect": "^3.6.5", + "cosmiconfig": "^5.0.5", + "flow-enums-runtime": "^0.0.6", + "jest-validate": "^29.7.0", + "metro": "0.82.5", + "metro-cache": "0.82.5", + "metro-core": "0.82.5", + "metro-runtime": "0.82.5" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "peer": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/metro-config/node_modules/cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "license": "MIT", + "peer": true, + "dependencies": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/metro-config/node_modules/import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", + "license": "MIT", + "peer": true, + "dependencies": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/metro-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", + "peer": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/metro-config/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "license": "MIT", + "peer": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/metro-config/node_modules/resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/metro-core": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.82.5.tgz", + "integrity": "sha512-OJL18VbSw2RgtBm1f2P3J5kb892LCVJqMvslXxuxjAPex8OH7Eb8RBfgEo7VZSjgb/LOf4jhC4UFk5l5tAOHHA==", + "license": "MIT", + "peer": true, + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "lodash.throttle": "^4.1.1", + "metro-resolver": "0.82.5" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-file-map": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.82.5.tgz", + "integrity": "sha512-vpMDxkGIB+MTN8Af5hvSAanc6zXQipsAUO+XUx3PCQieKUfLwdoa8qaZ1WAQYRpaU+CJ8vhBcxtzzo3d9IsCIQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "fb-watchman": "^2.0.0", + "flow-enums-runtime": "^0.0.6", + "graceful-fs": "^4.2.4", + "invariant": "^2.2.4", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "nullthrows": "^1.1.1", + "walker": "^1.0.7" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-file-map/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/metro-minify-terser": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.82.5.tgz", + "integrity": "sha512-v6Nx7A4We6PqPu/ta1oGTqJ4Usz0P7c+3XNeBxW9kp8zayS3lHUKR0sY0wsCHInxZlNAEICx791x+uXytFUuwg==", + "license": "MIT", + "peer": true, + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "terser": "^5.15.0" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-resolver": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.82.5.tgz", + "integrity": "sha512-kFowLnWACt3bEsuVsaRNgwplT8U7kETnaFHaZePlARz4Fg8tZtmRDUmjaD68CGAwc0rwdwNCkWizLYpnyVcs2g==", + "license": "MIT", + "peer": true, + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-runtime": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.82.5.tgz", + "integrity": "sha512-rQZDoCUf7k4Broyw3Ixxlq5ieIPiR1ULONdpcYpbJQ6yQ5GGEyYjtkztGD+OhHlw81LCR2SUAoPvtTus2WDK5g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.25.0", + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-source-map": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.82.5.tgz", + "integrity": "sha512-wH+awTOQJVkbhn2SKyaw+0cd+RVSCZ3sHVgyqJFQXIee/yLs3dZqKjjeKKhhVeudgjXo7aE/vSu/zVfcQEcUfw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/traverse": "^7.25.3", + "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", + "@babel/types": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "metro-symbolicate": "0.82.5", + "nullthrows": "^1.1.1", + "ob1": "0.82.5", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-source-map/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/metro-symbolicate": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.82.5.tgz", + "integrity": "sha512-1u+07gzrvYDJ/oNXuOG1EXSvXZka/0JSW1q2EYBWerVKMOhvv9JzDGyzmuV7hHbF2Hg3T3S2uiM36sLz1qKsiw==", + "license": "MIT", + "peer": true, + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "metro-source-map": "0.82.5", + "nullthrows": "^1.1.1", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "bin": { + "metro-symbolicate": "src/index.js" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-symbolicate/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/metro-transform-plugins": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.82.5.tgz", + "integrity": "sha512-57Bqf3rgq9nPqLrT2d9kf/2WVieTFqsQ6qWHpEng5naIUtc/Iiw9+0bfLLWSAw0GH40iJ4yMjFcFJDtNSYynMA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.3", + "flow-enums-runtime": "^0.0.6", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-transform-worker": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.82.5.tgz", + "integrity": "sha512-mx0grhAX7xe+XUQH6qoHHlWedI8fhSpDGsfga7CpkO9Lk9W+aPitNtJWNGrW8PfjKEWbT9Uz9O50dkI8bJqigw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/types": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "metro": "0.82.5", + "metro-babel-transformer": "0.82.5", + "metro-cache": "0.82.5", + "metro-cache-key": "0.82.5", + "metro-minify-terser": "0.82.5", + "metro-source-map": "0.82.5", + "metro-transform-plugins": "0.82.5", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "license": "MIT", + "peer": true + }, + "node_modules/metro/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/metro/node_modules/hermes-estree": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.29.1.tgz", + "integrity": "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ==", + "license": "MIT", + "peer": true + }, + "node_modules/metro/node_modules/hermes-parser": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.29.1.tgz", + "integrity": "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA==", + "license": "MIT", + "peer": true, + "dependencies": { + "hermes-estree": "0.29.1" + } + }, + "node_modules/metro/node_modules/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "license": "MIT", + "peer": true, + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/metro/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/metro/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/micro-ftch": { + "version": "0.3.1", + "license": "MIT" + }, + "node_modules/micromark": { + "version": "4.0.2", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { "version": "2.0.3", "funding": [ { @@ -20843,6 +22946,13 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "license": "MIT", + "peer": true + }, "node_modules/node-polyfill-webpack-plugin": { "version": "2.0.1", "license": "MIT", @@ -21070,6 +23180,26 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/nullthrows": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", + "license": "MIT", + "peer": true + }, + "node_modules/ob1": { + "version": "0.82.5", + "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.82.5.tgz", + "integrity": "sha512-QyQQ6e66f+Ut/qUVjEce0E/wux5nAGLXYZDn1jr15JWstHsCH3l6VVrg8NKDptW9NEiBXKOJeGF/ydxeSDF3IQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18.18" + } + }, "node_modules/obj-multiplex": { "version": "1.0.0", "license": "ISC", @@ -21214,6 +23344,19 @@ "version": "1.1.2", "license": "MIT" }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "peer": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/on-headers": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", @@ -21420,6 +23563,16 @@ "node": ">=8" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/package-json": { "version": "8.1.1", "license": "MIT", @@ -21770,7 +23923,6 @@ }, "node_modules/path-exists": { "version": "4.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -21889,6 +24041,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/pkg-dir": { "version": "7.0.0", "license": "MIT", @@ -21981,6 +24143,12 @@ "pathe": "^2.0.3" } }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT" + }, "node_modules/player.style": { "version": "0.1.9", "resolved": "https://registry.npmjs.org/player.style/-/player.style-0.1.9.tgz", @@ -21997,6 +24165,50 @@ "media-chrome": "~4.11.0" } }, + "node_modules/playwright": { + "version": "1.54.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.2.tgz", + "integrity": "sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.54.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.54.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.2.tgz", + "integrity": "sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/points-on-curve": { "version": "0.2.0", "license": "MIT" @@ -23748,6 +25960,34 @@ "renderkid": "^3.0.0" } }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/pretty-time": { "version": "1.1.0", "license": "MIT", @@ -23784,6 +26024,16 @@ "version": "2.0.1", "license": "MIT" }, + "node_modules/promise": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "license": "MIT", + "peer": true, + "dependencies": { + "asap": "~2.0.6" + } + }, "node_modules/promise-inflight": { "version": "1.0.1", "license": "ISC" @@ -23958,6 +26208,16 @@ "node": ">=0.4.x" } }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "peer": true, + "dependencies": { + "inherits": "~2.0.3" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "funding": [ @@ -24062,22 +26322,73 @@ "version": "18.3.1", "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-alert": { + "version": "7.0.3", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2", + "react-transition-group": "^4.4.1" + }, + "peerDependencies": { + "react": "^16.8.1 || ^17", + "react-dom": "^16.8.1 || ^17" + } + }, + "node_modules/react-bookmark": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/react-bookmark/-/react-bookmark-0.8.2.tgz", + "integrity": "sha512-79iXtvwrPBjdvpJGfIYHY2f8/+eA9Gn2bhTbuc0vx8fyElM1QaQoL9VVWsKSfOcMH9Fxfz8BZbXc9RaMRGJjAA==", + "license": "MIT", + "dependencies": { + "platform": "^1.3.4", + "prop-types": "^15.5.10" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "react": ">=15.0.0", + "react-dom": ">=15.0.0", + "styled-components": ">=1.0.0" + } + }, + "node_modules/react-devtools-core": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-6.1.5.tgz", + "integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==", + "license": "MIT", + "peer": true, + "dependencies": { + "shell-quote": "^1.6.1", + "ws": "^7" } }, - "node_modules/react-alert": { - "version": "7.0.3", + "node_modules/react-devtools-core/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "license": "MIT", - "dependencies": { - "prop-types": "^15.7.2", - "react-transition-group": "^4.4.1" + "peer": true, + "engines": { + "node": ">=8.3.0" }, "peerDependencies": { - "react": "^16.8.1 || ^17", - "react-dom": "^16.8.1 || ^17" + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, "node_modules/react-dom": { @@ -24126,6 +26437,15 @@ "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "18.3.1", "license": "MIT" @@ -24140,6 +26460,84 @@ "react": "^18.0.0 || ^19.0.0" } }, + "node_modules/react-konva": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/react-konva/-/react-konva-19.0.7.tgz", + "integrity": "sha512-uYWCpSv4ajLymTh8S8fV9396fHDX7eDTWiLGkYlBuawud5MoNiuGjapPhA5Avdy/Jfh9P2KaWuNf4i9PI1F9HQ==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/lavrton" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/konva" + }, + { + "type": "github", + "url": "https://github.com/sponsors/lavrton" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@types/react-reconciler": "^0.32.0", + "its-fine": "^2.0.0", + "react-reconciler": "0.32.0", + "scheduler": "0.26.0" + }, + "peerDependencies": { + "konva": "^8.0.1 || ^7.2.5 || ^9.0.0", + "react": "^18.3.1 || ^19.0.0", + "react-dom": "^18.3.1 || ^19.0.0" + } + }, + "node_modules/react-konva/node_modules/its-fine": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", + "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/react-reconciler": "^0.28.9" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/react-konva/node_modules/its-fine/node_modules/@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/react-konva/node_modules/react-reconciler": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.32.0.tgz", + "integrity": "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.26.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-konva/node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT", + "peer": true + }, "node_modules/react-lifecycles-compat": { "version": "3.0.4", "license": "MIT" @@ -24206,6 +26604,16 @@ "react-dom": "^17.0.2 || ^18 || ^19" } }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-router": { "version": "5.3.4", "license": "MIT", @@ -24255,6 +26663,316 @@ "version": "16.13.1", "license": "MIT" }, + "node_modules/react-spinners": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.17.0.tgz", + "integrity": "sha512-L/8HTylaBmIWwQzIjMq+0vyaRXuoAevzWoD35wKpNTxxtYXWZp+xtgkfD7Y4WItuX0YvdxMPU79+7VhhmbmuTQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-spring": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/react-spring/-/react-spring-10.0.1.tgz", + "integrity": "sha512-N4TGwmMYtqC6DX6AcMbldH0WCvZm3r7OuilNFSjeP6sijwKLjMFmHpdXOSnbKBdg1LQudLRmk9uqpguf2oa/sg==", + "license": "MIT", + "dependencies": { + "@react-spring/core": "~10.0.1", + "@react-spring/konva": "~10.0.1", + "@react-spring/native": "~10.0.1", + "@react-spring/three": "~10.0.1", + "@react-spring/web": "~10.0.1", + "@react-spring/zdog": "~10.0.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-spring/node_modules/@react-spring/native": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/native/-/native-10.0.1.tgz", + "integrity": "sha512-uslIO25XugK9SugvQRVsmXoSiApmQzV2JWWcbhPJUvArqPyZ1yuPQvgNqjvHubauu5kXWWOya9MIuVEo7EMKUw==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~10.0.1", + "@react-spring/core": "~10.0.1", + "@react-spring/shared": "~10.0.1", + "@react-spring/types": "~10.0.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-native": ">=0.78" + } + }, + "node_modules/react-spring/node_modules/@react-spring/three": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-10.0.1.tgz", + "integrity": "sha512-JAgA573EqG1WkDGameWv0HYlPL5KYwVCRhXroBq5Ed0Chc9xXuAZU8fyNg9/uup8Pc32iGSW0PHRt0msvPNg+w==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~10.0.1", + "@react-spring/core": "~10.0.1", + "@react-spring/shared": "~10.0.1", + "@react-spring/types": "~10.0.1" + }, + "peerDependencies": { + "@react-three/fiber": ">=6.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "three": ">=0.126" + } + }, + "node_modules/react-spring/node_modules/@react-three/fiber": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.2.0.tgz", + "integrity": "sha512-esZe+E9T/aYEM4HlBkirr/yRE8qWTp9WUsLISyHHMCHKlJv85uc5N4wwKw+Ay0QeTSITw6T9Q3Svpu383Q+CSQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.17.8", + "@types/react-reconciler": "^0.28.9", + "@types/webxr": "*", + "base64-js": "^1.5.1", + "buffer": "^6.0.3", + "its-fine": "^2.0.0", + "react-reconciler": "^0.31.0", + "react-use-measure": "^2.1.7", + "scheduler": "^0.25.0", + "suspend-react": "^0.1.3", + "use-sync-external-store": "^1.4.0", + "zustand": "^5.0.3" + }, + "peerDependencies": { + "expo": ">=43.0", + "expo-asset": ">=8.4", + "expo-file-system": ">=11.0", + "expo-gl": ">=11.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-native": ">=0.78", + "three": ">=0.156" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "expo-asset": { + "optional": true + }, + "expo-file-system": { + "optional": true + }, + "expo-gl": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-spring/node_modules/@react-three/fiber/node_modules/its-fine": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", + "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/react-reconciler": "^0.28.9" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/react-spring/node_modules/@react-three/fiber/node_modules/react-reconciler": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.31.0.tgz", + "integrity": "sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.25.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/react-spring/node_modules/@types/react": { + "version": "19.1.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", + "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/react-spring/node_modules/@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/react-spring/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/react-spring/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/react-spring/node_modules/react-native": { + "version": "0.80.1", + "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.80.1.tgz", + "integrity": "sha512-cIiJiPItdC2+Z9n30FmE2ef1y4522kgmOjMIoDtlD16jrOMNTUdB2u+CylLTy3REkWkWTS6w8Ub7skUthkeo5w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/create-cache-key-function": "^29.7.0", + "@react-native/assets-registry": "0.80.1", + "@react-native/codegen": "0.80.1", + "@react-native/community-cli-plugin": "0.80.1", + "@react-native/gradle-plugin": "0.80.1", + "@react-native/js-polyfills": "0.80.1", + "@react-native/normalize-colors": "0.80.1", + "@react-native/virtualized-lists": "0.80.1", + "abort-controller": "^3.0.0", + "anser": "^1.4.9", + "ansi-regex": "^5.0.0", + "babel-jest": "^29.7.0", + "babel-plugin-syntax-hermes-parser": "0.28.1", + "base64-js": "^1.5.1", + "chalk": "^4.0.0", + "commander": "^12.0.0", + "flow-enums-runtime": "^0.0.6", + "glob": "^7.1.1", + "invariant": "^2.2.4", + "jest-environment-node": "^29.7.0", + "memoize-one": "^5.0.0", + "metro-runtime": "^0.82.2", + "metro-source-map": "^0.82.2", + "nullthrows": "^1.1.1", + "pretty-format": "^29.7.0", + "promise": "^8.3.0", + "react-devtools-core": "^6.1.1", + "react-refresh": "^0.14.0", + "regenerator-runtime": "^0.13.2", + "scheduler": "0.26.0", + "semver": "^7.1.3", + "stacktrace-parser": "^0.1.10", + "whatwg-fetch": "^3.0.0", + "ws": "^6.2.3", + "yargs": "^17.6.2" + }, + "bin": { + "react-native": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": "^19.1.0", + "react": "^19.1.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-spring/node_modules/react-native/node_modules/@react-native/virtualized-lists": { + "version": "0.80.1", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.80.1.tgz", + "integrity": "sha512-nqQAeHheSNZBV+syhLVMgKBZv+FhCANfxAWVvfEXZa4rm5jGHsj3yA9vqrh2lcJL3pjd7PW5nMX7TcuJThEAgQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "invariant": "^2.2.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": "^19.0.0", + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-spring/node_modules/react-native/node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-spring/node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "peer": true + }, + "node_modules/react-spring/node_modules/scheduler": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-spring/node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "license": "MIT", + "peer": true, + "dependencies": { + "async-limiter": "~1.0.0" + } + }, "node_modules/react-tippy": { "version": "1.4.0", "license": "MIT", @@ -24280,8 +26998,36 @@ "version": "5.2.1", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/react-use-measure": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", + "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-zdog": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/react-zdog/-/react-zdog-1.2.2.tgz", + "integrity": "sha512-Ix7ALha91aOEwiHuxumCeYbARS5XNpc/w0v145oGkM6poF/CvhKJwzLhM5sEZbtrghMA+psAhOJkCTzJoseicA==", + "license": "MIT", + "peer": true, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "resize-observer-polyfill": "^1.5.1" } }, "node_modules/read-pkg": { @@ -25079,6 +27825,16 @@ "node": ">=0.10" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "license": "MIT", @@ -25101,6 +27857,13 @@ "license": "MIT", "peer": true }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT", + "peer": true + }, "node_modules/resolve": { "version": "1.22.8", "license": "MIT", @@ -25707,6 +28470,16 @@ "node": ">= 0.8" } }, + "node_modules/serialize-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", + "integrity": "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/serialize-javascript": { "version": "6.0.2", "license": "BSD-3-Clause", @@ -26295,6 +29068,59 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "license": "MIT", + "peer": true + }, + "node_modules/stacktrace-parser": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", + "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", + "license": "MIT", + "peer": true, + "dependencies": { + "type-fest": "^0.7.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/stacktrace-parser/node_modules/type-fest": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", + "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", + "license": "(MIT OR CC0-1.0)", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/statuses": { "version": "1.5.0", "license": "MIT", @@ -26600,6 +29426,102 @@ "inline-style-parser": "0.2.4" } }, + "node_modules/styled-components": { + "version": "6.1.19", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.19.tgz", + "integrity": "sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", + "css-to-react-native": "3.2.0", + "csstype": "3.1.3", + "postcss": "8.4.49", + "shallowequal": "1.1.0", + "stylis": "4.3.2", + "tslib": "2.6.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/styled-components/node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/styled-components/node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT", + "peer": true + }, + "node_modules/styled-components/node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "license": "MIT", + "peer": true + }, + "node_modules/styled-components/node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/styled-components/node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", + "license": "MIT", + "peer": true + }, + "node_modules/styled-components/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD", + "peer": true + }, "node_modules/stylehacks": { "version": "6.1.1", "license": "MIT", @@ -26822,6 +29744,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/suspend-react": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", + "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "react": ">=17.0" + } + }, "node_modules/svg-parser": { "version": "2.0.4", "license": "MIT" @@ -27230,6 +30162,45 @@ "version": "2.20.3", "license": "MIT" }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "license": "ISC", + "peer": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/text-decoder": { "version": "1.2.3", "license": "Apache-2.0", @@ -27248,6 +30219,20 @@ "url": "https://bevry.me/fund" } }, + "node_modules/three": { + "version": "0.178.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.178.0.tgz", + "integrity": "sha512-ybFIB0+x8mz0wnZgSGy2MO/WCO6xZhQSZnmfytSPyNpM0sBafGRVhdaj+erYh5U+RhQOAg/eXqw5uVDiM2BjhQ==", + "license": "MIT", + "peer": true + }, + "node_modules/throat": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", + "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", + "license": "MIT", + "peer": true + }, "node_modules/through": { "version": "2.3.8", "license": "MIT" @@ -27344,6 +30329,13 @@ "node": ">=0.6.0" } }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "license": "BSD-3-Clause", + "peer": true + }, "node_modules/to-buffer": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", @@ -27434,6 +30426,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" + }, "node_modules/toidentifier": { "version": "1.0.1", "license": "MIT", @@ -27556,6 +30554,16 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "2.19.0", "license": "(MIT OR CC0-1.0)", @@ -28078,6 +31086,16 @@ "version": "1.4.1", "license": "MIT" }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/utf-8-validate": { "version": "5.0.10", "hasInstallScript": true, @@ -28231,6 +31249,13 @@ "@vimeo/player": "2.29.0" } }, + "node_modules/vlq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", + "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==", + "license": "MIT", + "peer": true + }, "node_modules/vm-browserify": { "version": "1.1.2", "license": "MIT" @@ -28272,6 +31297,16 @@ "version": "3.0.8", "license": "MIT" }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, "node_modules/warning": { "version": "4.0.3", "license": "MIT", @@ -28940,6 +31975,25 @@ "version": "3.1.1", "license": "ISC" }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "peer": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/yargs-parser": { "version": "20.2.9", "dev": true, @@ -28948,6 +32002,58 @@ "node": ">=10" } }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "peer": true + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "dev": true, @@ -28965,6 +32071,43 @@ "integrity": "sha512-FDRgXlPxpe1bh6HlhL6GfJVcvVNaZKCcLEZ90X1G3Iu+z2g2cIhm2OWj9abPZq1Zqit6SY7Gwh13H9g7acoBnQ==", "license": "MIT" }, + "node_modules/zdog": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/zdog/-/zdog-1.1.3.tgz", + "integrity": "sha512-raRj6r0gPzopFm5XWBJZr/NuV4EEnT4iE+U3dp5FV5pCb588Gmm3zLIp/j9yqqcMiHH8VNQlerLTgOqL7krh6w==", + "license": "MIT", + "peer": true + }, + "node_modules/zustand": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.6.tgz", + "integrity": "sha512-ihAqNeUVhe0MAD+X8M5UzqyZ9k3FFZLBTtqo6JLPwV53cbRB/mJwBI0PxcIgqhBBHlEs8G45OTDTMq3gNcLq3A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, "node_modules/zwitch": { "version": "2.0.4", "license": "MIT", diff --git a/package.json b/package.json index be1d1bc34ce..c12f81b9091 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,9 @@ "@rjsf/validator-ajv8": "^5.24.12", "@sentry/browser": "^8.51.0", "@types/react": "^18.3.3", + "classnames": "^2.5.1", "clsx": "^2.1.1", + "copy-to-clipboard": "^3.3.3", "docusaurus-plugin-sass": "^0.2.5", "dotenv": "^16.4.7", "ethers": "^6.13.5", @@ -60,14 +62,19 @@ "lodash.debounce": "^4.0.8", "lodash.isplainobject": "^4.0.6", "node-polyfill-webpack-plugin": "^2.0.1", + "playwright": "^1.54.2", "prettier": "^3.3.3", "prism-react-renderer": "^2.1.0", "react": "^18.0.0", "react-alert": "^7.0.3", + "react-bookmark": "^0.8.2", "react-dom": "^18.0.0", "react-dropdown-select": "^4.12.2", + "react-icons": "^5.5.0", "react-modal": "^3.16.3", "react-player": "^3.3.1", + "react-spinners": "^0.17.0", + "react-spring": "^10.0.1", "react-tippy": "^1.4.0", "remark-codesandbox": "^0.10.1", "remark-docusaurus-tabs": "^0.2.0", diff --git a/src/components/Button/button.module.scss b/src/components/Button/button.module.scss index de71208ae00..5aed34a667f 100644 --- a/src/components/Button/button.module.scss +++ b/src/components/Button/button.module.scss @@ -1,4 +1,4 @@ -:root[data-theme="dark"] { +:root[data-theme='dark'] { --button-primary-background-color: #1098fc; --button-secondary-background-color: transparent; --button-background-color: #1098fc; @@ -6,10 +6,10 @@ --button-hover-background-color: #036ab5; --button-hover-shadow: 0px 2px 8px 0px rgba(16, 152, 252, 0.4); --button-active-background-color: #3baafd; - --button-danger: #e88f97 + --button-danger: #e88f97; } -:root[data-theme="light"] { +:root[data-theme='light'] { --button-primary-background-color: #0376c9; --button-secondary-background-color: transparent; --button-background-color: #0376c9; @@ -17,7 +17,7 @@ --button-hover-background-color: #036ab5; --button-hover-shadow: 0px 2px 8px 0px rgba(3, 118, 201, 0.2); --button-active-background-color: #025ea1; - --button-danger: #D73847 + --button-danger: #d73847; } :root { @@ -50,7 +50,7 @@ a.button { justify-content: center; align-items: center; min-width: 145px; - transition-property: "box-shadow", "background-color"; + transition-property: 'box-shadow', 'background-color'; transition-duration: 0.2s; transition-timing-function: ease; @@ -61,12 +61,18 @@ a.button { --button-hover-background-color: #26a2fc; } + /* Dark mode: Ensure primary buttons use proper dark theme colors */ + [data-theme='dark'] &.primary { + background-color: var(--button-primary-background-color); + color: var(--button-color); + } + &.secondary { border: 1px solid rgba(3, 118, 201, 1); --button-color: rgba(3, 118, 201, 1); background-color: var(--button-secondary-background-color); &:hover { - --button-color: #141618 + --button-color: #141618; } } diff --git a/src/components/CardSection.module.scss b/src/components/CardSection.module.scss index ddb32ab5e51..a07d470d84c 100644 --- a/src/components/CardSection.module.scss +++ b/src/components/CardSection.module.scss @@ -46,6 +46,13 @@ gap: 4rem; align-items: start; } + + // When there's no title or description, use full width for cards + &.cards-only { + @include bp('desktop') { + grid-template-columns: 1fr; + } + } } .content-column { @@ -61,6 +68,10 @@ @include bp('tablet') { grid-template-columns: repeat(2, 1fr); } + + @include bp('desktop') { + grid-template-columns: repeat(3, 1fr); + } } .card-column { diff --git a/src/components/CardSection.tsx b/src/components/CardSection.tsx index 00f71ca0977..bdda1f32fea 100644 --- a/src/components/CardSection.tsx +++ b/src/components/CardSection.tsx @@ -30,13 +30,16 @@ export default function CardSection({
-
diff --git a/src/components/DiscourseComment/index.jsx b/src/components/DiscourseComment/index.jsx new file mode 100644 index 00000000000..884da416393 --- /dev/null +++ b/src/components/DiscourseComment/index.jsx @@ -0,0 +1,32 @@ +import { useEffect } from 'react' + +export default function DiscourseComment(props) { + // eslint-disable-next-line react/prop-types + const { postUrl } = props + useEffect(() => { + const url = window.location.href + if (!url.includes('https://web3auth.io/')) { + return + } else { + window.DiscourseEmbed = { + discourseUrl: 'https://web3auth.io/community/', + discourseEmbedUrl: postUrl, + } + + const d = document.createElement('script') + d.type = 'text/javascript' + d.async = true + d.src = `${window.DiscourseEmbed.discourseUrl}javascripts/embed.js` + ;( + document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0] + ).appendChild(d) + } + }, []) + + return ( + <> + +
+ + ) +} diff --git a/src/components/GuidesPage/CustomSelect.module.css b/src/components/GuidesPage/CustomSelect.module.css new file mode 100644 index 00000000000..0dae0445fc4 --- /dev/null +++ b/src/components/GuidesPage/CustomSelect.module.css @@ -0,0 +1,270 @@ +.customSelect { + position: relative; + min-width: 200px; + max-width: 300px; + width: 100%; + font-weight: 400; + font-size: 14px; + line-height: 125%; +} + +.control { + background: var(--ifm-background-surface-color); + color: var(--ifm-font-color-base); + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 10px; + min-height: 48px; + height: 48px; + max-height: none; + box-shadow: none; + margin: 0; + align-self: center; + display: flex; + align-items: center; + cursor: pointer; + transition: all 0.15s ease; + box-sizing: border-box; +} + +.control:hover { + border-color: var(--ifm-color-emphasis-400); +} + +.controlFocused { + box-shadow: 0 0 0 2px rgba(var(--ifm-color-primary-r), var(--ifm-color-primary-g), var(--ifm-color-primary-b), 0.2); + border-color: var(--ifm-color-primary); +} + +.controlFocused:hover { + border-color: var(--ifm-color-primary); +} + +.controlExpanded { + height: auto; + align-items: flex-start; +} + +.valueContainer { + padding: 8px 12px; + height: 30px; + overflow: hidden; + box-sizing: border-box; + flex: 1; + display: flex; + align-items: center; + flex-wrap: nowrap; + gap: 4px; +} + +.valueContainerExpanded { + height: auto; + overflow: visible; + align-items: flex-start; + flex-wrap: wrap; +} + +.multiValueContainer { + display: flex; + flex-wrap: wrap; + gap: 4px; + align-items: flex-start; + width: 100%; + overflow: visible; +} + +.multiValue { + color: var(--ifm-color-primary); + background-color: var(--ifm-color-primary-lightest); + font-weight: 500; + font-size: 14px; + line-height: 1.2; + border: none; + border-radius: 6px; + margin: 2px; + display: flex; + align-items: center; + max-width: 100%; +} + +.multiValueLabel { + color: var(--ifm-color-primary); + font-weight: 500; + font-size: 14px; + padding: 4px 8px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.multiValueRemove { + color: var(--ifm-color-primary); + border-radius: 0 6px 6px 0; + background: none; + border: none; + padding: 4px 8px; + cursor: pointer; + font-size: 16px; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s ease; +} + +.multiValueRemove:hover { + background-color: var(--ifm-color-primary-lighter); + color: var(--ifm-color-primary-dark); +} + +.placeholder { + color: var(--ifm-color-emphasis-600); + font-size: 14px; + padding: 4px 8px; + line-height: 30px; +} + +.indicatorsContainer { + height: 46px; + padding: 0 8px; + box-sizing: border-box; + display: flex; + align-items: center; + gap: 4px; +} + +.indicatorsContainerExpanded { + align-items: flex-start; + padding: 8px; + height: auto; +} + +.clearIndicator { + color: var(--ifm-color-emphasis-600); + background: none; + border: none; + cursor: pointer; + font-size: 16px; + font-weight: bold; + padding: 4px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s ease; +} + +.clearIndicator:hover { + color: var(--ifm-color-danger); +} + +.dropdownIndicator { + color: var(--ifm-color-emphasis-600); + font-size: 12px; + transition: transform 0.15s ease, color 0.15s ease; + user-select: none; +} + +.dropdownIndicatorRotated { + transform: rotate(180deg); +} + +.dropdownIndicator:hover { + color: var(--ifm-color-primary); +} + +.menu { + background: var(--ifm-background-surface-color); + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 10px; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + z-index: 1000; + position: absolute; + top: 100%; + left: 0; + right: 0; + margin-top: 4px; + max-height: 300px; + overflow-y: auto; +} + +.menuList { + padding: 4px 0; +} + +.option { + padding: 8px 12px; + cursor: pointer; + background-color: transparent; + color: var(--ifm-font-color-base); + font-size: 14px; + transition: background-color 0.15s ease; + display: flex; + align-items: center; + justify-content: space-between; +} + +.option:hover { + background-color: var(--ifm-color-primary-lightest); +} + +.optionSelected { + background-color: var(--ifm-color-primary); + color: white; +} + +.optionSelected:hover { + background-color: var(--ifm-color-primary-dark); +} + +.optionLabel { + flex: 1; + text-align: left; +} + +.optionRemove { + background: none; + border: none; + color: white; + cursor: pointer; + font-size: 16px; + font-weight: bold; + padding: 2px 6px; + margin-left: 8px; + border-radius: 3px; + transition: all 0.15s ease; + display: flex; + align-items: center; + justify-content: center; + min-width: 20px; + height: 20px; +} + +.optionRemove:hover { + background-color: rgba(255, 255, 255, 0.2); +} + +/* Mobile responsive styles */ +@media (max-width: 768px) { + .customSelect { + min-width: 100%; + max-width: 100%; + width: 100%; + } + + .control { + width: 100%; + min-width: 100%; + max-width: 100%; + min-height: 48px; + height: 48px; + max-height: none; + } + + .controlExpanded { + height: auto; + } + + .menu { + width: 100%; + min-width: 100%; + } +} \ No newline at end of file diff --git a/src/components/GuidesPage/CustomSelect.tsx b/src/components/GuidesPage/CustomSelect.tsx new file mode 100644 index 00000000000..9fe3a80d933 --- /dev/null +++ b/src/components/GuidesPage/CustomSelect.tsx @@ -0,0 +1,163 @@ +import React, { useState, useRef, useEffect } from 'react'; +import styles from './CustomSelect.module.css'; + +export interface OptionType { + label: string; + value: string; +} + +interface CustomSelectProps { + options: OptionType[]; + placeholder: string; + onChange: (selectedOptions: OptionType[]) => void; + value?: OptionType[]; + isMulti?: boolean; +} + +const CustomSelect: React.FC = ({ + options, + placeholder, + onChange, + value = [], + isMulti = true +}) => { + const [isOpen, setIsOpen] = useState(false); + const [selectedOptions, setSelectedOptions] = useState(value); + const dropdownRef = useRef(null); + + useEffect(() => { + setSelectedOptions(value); + }, [value]); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + const handleOptionClick = (option: OptionType) => { + let newSelectedOptions: OptionType[]; + + if (isMulti) { + const isSelected = selectedOptions.some(selected => selected.value === option.value); + if (isSelected) { + newSelectedOptions = selectedOptions.filter(selected => selected.value !== option.value); + } else { + newSelectedOptions = [...selectedOptions, option]; + } + } else { + newSelectedOptions = [option]; + setIsOpen(false); + } + + setSelectedOptions(newSelectedOptions); + onChange(newSelectedOptions); + }; + + const handleRemoveOption = (optionToRemove: OptionType, event: React.MouseEvent) => { + event.stopPropagation(); + const newSelectedOptions = selectedOptions.filter(selected => selected.value !== optionToRemove.value); + setSelectedOptions(newSelectedOptions); + onChange(newSelectedOptions); + }; + + const handleClearAll = (event: React.MouseEvent) => { + event.stopPropagation(); + setSelectedOptions([]); + onChange([]); + }; + + const toggleDropdown = () => { + setIsOpen(!isOpen); + }; + + const isOptionSelected = (option: OptionType) => { + return selectedOptions.some(selected => selected.value === option.value); + }; + + const hasMultipleSelections = selectedOptions.length >= 2; + + return ( +
+
+
+ {selectedOptions.length === 0 ? ( + {placeholder} + ) : ( +
+ {selectedOptions.map(option => ( +
+ {option.label} + +
+ ))} +
+ )} +
+
+ {selectedOptions.length > 0 && ( + + )} + + ▼ + +
+
+ + {isOpen && ( +
+
+ {options.map(option => ( +
handleOptionClick(option)} + > + {option.label} + {isOptionSelected(option) && ( + + )} +
+ ))} +
+
+ )} +
+ ); +}; + +export default CustomSelect; diff --git a/src/components/GuidesPage/GuideCard.module.css b/src/components/GuidesPage/GuideCard.module.css new file mode 100644 index 00000000000..f40fd7147f4 --- /dev/null +++ b/src/components/GuidesPage/GuideCard.module.css @@ -0,0 +1,137 @@ +:root { + --card-border-radius: 12px; + --card-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + --card-shadow-hover: 0 4px 16px rgba(0, 0, 0, 0.1); + --card-padding: 24px; + --card-gap: 16px; +} + +.card { + background-color: var(--ifm-background-surface-color); + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: var(--card-border-radius); + box-shadow: var(--card-shadow); + overflow: hidden; + transition: all 0.2s ease-in-out; + height: 100%; + display: flex; + flex-direction: column; +} + +.card:hover { + box-shadow: var(--card-shadow-hover); + transform: translateY(-2px); + border-color: var(--ifm-color-emphasis-300); +} + +.cardLink { + text-decoration: none; + color: inherit; + display: flex; + flex-direction: column; + flex: 1; +} + +.cardLink:hover { + text-decoration: none; + color: inherit; +} + +.imageContainer { + width: 100%; + height: 200px; + overflow: hidden; + background-color: var(--ifm-color-emphasis-100); +} + +.image { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.2s ease-in-out; +} + +.card:hover .image { + transform: scale(1.02); +} + +.content { + padding: var(--card-padding); + flex: 1; + display: flex; + flex-direction: column; + gap: var(--card-gap); +} + +.typeContainer { + margin-bottom: 8px; +} + +.title { + font-size: 20px; + font-weight: 700; + line-height: 1.4; + color: var(--ifm-font-color-base); + margin: 0; + margin-bottom: 12px; +} + +.description { + font-size: 16px; + line-height: 1.5; + color: var(--ifm-color-emphasis-700); + margin: 0; + flex: 1; +} + +.tagsContainer { + padding: 0 var(--card-padding) var(--card-padding); + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.meta { + padding: 0 var(--card-padding) var(--card-padding); + font-size: 14px; + color: var(--ifm-color-emphasis-600); + border-top: 1px solid var(--ifm-color-emphasis-200); + padding-top: 16px; +} + +/* Search highlight styling */ +mark { + background-color: #fef3c7; + color: #92400e; + padding: 2px 4px; + border-radius: 4px; + font-weight: 500; +} + +/* Dark theme adjustments */ +html[data-theme='dark'] { + --card-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + --card-shadow-hover: 0 4px 16px rgba(0, 0, 0, 0.3); +} + +html[data-theme='dark'] .card { + background-color: var(--ifm-background-surface-color); + border-color: var(--ifm-color-emphasis-300); +} + +html[data-theme='dark'] .card:hover { + border-color: var(--ifm-color-emphasis-400); +} + +html[data-theme='dark'] .imageContainer { + background-color: var(--ifm-color-emphasis-200); +} + +html[data-theme='dark'] .meta { + border-top-color: var(--ifm-color-emphasis-300); +} + +html[data-theme='dark'] mark { + background-color: #451a03; + color: #fbbf24; +} \ No newline at end of file diff --git a/src/components/GuidesPage/GuideCard.tsx b/src/components/GuidesPage/GuideCard.tsx new file mode 100644 index 00000000000..44a9d789560 --- /dev/null +++ b/src/components/GuidesPage/GuideCard.tsx @@ -0,0 +1,119 @@ +import React from 'react'; +import Link from '@docusaurus/Link'; +import Badge from '@site/src/components/Badge'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import styles from './GuideCard.module.css'; + +interface GuideCardProps { + title: string; + description: string; + link: string; + image?: string; + tags?: string[]; + author?: string; + date?: string; + type?: string; + searchInput?: string; + activeTags?: string[]; +} + +export default function GuideCard({ + title, + description, + link, + image, + tags = [], + author, + date, + type, + searchInput = '', + activeTags = [] +}: GuideCardProps) { + const { siteConfig } = useDocusaurusContext(); + const { baseUrl } = siteConfig; + + function highlightSearchText(text: string) { + if (!searchInput.trim()) { + return text; + } + + const searchTerms = searchInput.trim().split(/\s+/); + const regex = new RegExp(`(${searchTerms.join("|")})`, "gi"); + + // Use replace to find matches and build result + let lastIndex = 0; + const elements: React.ReactNode[] = []; + let match; + + // Reset regex lastIndex to avoid stateful issues + regex.lastIndex = 0; + + while ((match = regex.exec(text)) !== null) { + // Add text before the match + if (match.index > lastIndex) { + elements.push(text.slice(lastIndex, match.index)); + } + + // Add the highlighted match + elements.push({match[0]}); + + lastIndex = match.index + match[0].length; + + // Prevent infinite loop with zero-length matches + if (match.index === regex.lastIndex) { + regex.lastIndex++; + } + } + + // Add remaining text after last match + if (lastIndex < text.length) { + elements.push(text.slice(lastIndex)); + } + + return {elements}; + } + + return ( +
+ + {image && ( +
+ {title} +
+ )} + +
+ {type && ( +
+ +
+ )} + +

{highlightSearchText(title)}

+ +

+ {highlightSearchText(description)} +

+
+ + + {tags.length > 0 && ( +
+ {tags.map((tag) => ( + + ))} +
+ )} + + {(author || date) && ( +
+ {author && date && `${author} | ${date}`} +
+ )} +
+ ); +} diff --git a/src/components/GuidesPage/index.tsx b/src/components/GuidesPage/index.tsx new file mode 100644 index 00000000000..efc772c34aa --- /dev/null +++ b/src/components/GuidesPage/index.tsx @@ -0,0 +1,211 @@ +/* eslint-disable no-restricted-globals */ +/* eslint-disable react/no-array-index-key */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; +import Layout from "@theme/Layout"; +import { GuidesInterface, platformMap, productMap } from "../../utils/guides-map"; + + +import { useState, useEffect } from "react"; +import SEO from "../../components/SEO"; +import Hero from "@site/src/components/Hero/Hero"; +import Input from "@site/src/components/Input"; +import GuideCard from "./GuideCard"; +import CustomSelect, { OptionType } from "./CustomSelect"; +import styles from "./styles.module.css"; + +export default function Guides({ content = {} }: GuidesInterface) { + const safeContent = content || {}; + + const completeGuides = Object.entries(safeContent) + .map(([key, value]) => { + if (value && value.type === "guide") return { ...value, link: `/guides/${key}` }; + return null; + }) + .filter(Boolean) + .sort((a: any, b: any) => { + if (a.pinned && !b.pinned) return -1; + if (!a.pinned && b.pinned) return 1; + const aDate = new Date(a.date); + const bDate = new Date(b.date); + return +bDate - +aDate; + }); + + const [searchInput, setSearchInput] = useState(""); + const [tags, setTags] = useState([]); + const [productFilter, setProductFilter] = useState([]); + const [platformFilter, setPlatformFilter] = useState([]); + const [selectedProducts, setSelectedProducts] = useState([]); + const [selectedPlatforms, setSelectedPlatforms] = useState([]); + const [filteredGuides, setFilteredGuides] = useState(completeGuides); + const { siteConfig } = useDocusaurusContext(); + const { baseUrl } = siteConfig; + + // Apply tag filters first + useEffect(() => { + let filtered = completeGuides; + + if (productFilter.length > 0 || platformFilter.length > 0) { + filtered = completeGuides.filter((item) => { + if (!item || !item.tags || !Array.isArray(item.tags)) return false; + + const prodFil = + productFilter.length === 0 || productFilter.some((tag) => item.tags.includes(tag)); + const platFil = + platformFilter.length === 0 || platformFilter.some((tag) => item.tags.includes(tag)); + + return prodFil && platFil; + }); + } + + setFilteredGuides(filtered); + }, [productFilter, platformFilter, completeGuides]); + + const onChangeProduct = (selectedOptions: OptionType[]) => { + const filterValue = selectedOptions ? selectedOptions.map((item) => item.value) : []; + setSelectedProducts(selectedOptions); + setProductFilter(filterValue); + setTags([...platformFilter, ...filterValue]); + }; + + const onChangePlatform = (selectedOptions: OptionType[]) => { + const filterValue = selectedOptions ? selectedOptions.map((item) => item.value) : []; + setSelectedPlatforms(selectedOptions); + setPlatformFilter(filterValue); + setTags([...productFilter, ...filterValue]); + }; + + function highlightSearchText(text) { + if (!searchInput.trim()) { + return text; + } + + const searchTerms = searchInput.trim().split(/\s+/); + const regex = new RegExp(`(${searchTerms.join("|")})`, "gi"); + + // Use replace to find matches and build result + let lastIndex = 0; + const elements = []; + let match; + + // Reset regex lastIndex to avoid stateful issues + regex.lastIndex = 0; + + while ((match = regex.exec(text)) !== null) { + // Add text before the match + if (match.index > lastIndex) { + elements.push(text.slice(lastIndex, match.index)); + } + + // Add the highlighted match + elements.push({match[0]}); + + lastIndex = match.index + match[0].length; + + // Prevent infinite loop with zero-length matches + if (match.index === regex.lastIndex) { + regex.lastIndex++; + } + } + + // Add remaining text after last match + if (lastIndex < text.length) { + elements.push(text.slice(lastIndex)); + } + + return {elements}; + } + + function onChangeSearch(input) { + setSearchInput(input); + } + + // Filter the already filtered guides based on search + const displayedGuides = filteredGuides.filter((item) => { + if (!item) return false; // Skip null items + if (!searchInput.trim()) return true; + + const searchTerms = searchInput.toLowerCase().trim().split(/\s+/); + return searchTerms.every( + (term) => + (item.title && item.title.toLowerCase().includes(term)) || + (item.description && item.description.toLowerCase().includes(term)) || + (item.tags && Array.isArray(item.tags) && item.tags.some((tag) => tag.toLowerCase().includes(term))), + ); + }); + + // No transformation needed - we'll render GuideCard directly + + return ( + + + + + +
+
+
+ +
+ +
+
+ +
+
+
+ + {displayedGuides.length > 0 ? ( +
+ {displayedGuides.map((item: any) => ( + + ))} +
+ ) : ( +
+

No results found

+
+ )} +
+
+ ); +} + + diff --git a/src/components/GuidesPage/styles.module.css b/src/components/GuidesPage/styles.module.css new file mode 100644 index 00000000000..ae00b8759ee --- /dev/null +++ b/src/components/GuidesPage/styles.module.css @@ -0,0 +1,670 @@ +.header { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: space-between; + padding-top: 5%; + padding-right: 5%; + padding-bottom: 16px; + padding-left: 5%; +} + +.header h1 { + font-weight: 700; + font-size: 48px; + line-height: 150%; +} + +.header h1:after { + display: none; +} + +.header p { + color: var(--w3a-color-icon-gray); + font-weight: 500; + font-size: 14px; + line-height: 150%; +} + +.container { + width: 90%; + display: flex; + flex-flow: row wrap; + margin: 5%; + margin-top: 20px; + margin-bottom: 60px; + justify-content: space-between; +} + +.article { + width: 90%; + color: var(--ifm-font-color-base); + text-decoration: none; + padding: 0 8px; + margin-top: 20px; + margin-bottom: 20px; + max-width: 550px; +} + +.article img { + top: 0px; + left: 0px; + opacity: 1; + border-radius: 20px; + margin-bottom: 8px; +} + +.article:hover { + text-decoration: none; +} + +.articleContent:hover img { + opacity: 0.7; + cursor: pointer; +} + +.article .contentContainer { + padding: 0 8px; +} + +.article h3 { + font-family: var(--ifm-font-family-base); + font-size: 20px; + font-weight: 700; + line-height: 30px; + color: var(--ifm-font-color-base); +} + +.article p { + font-family: var(--ifm-font-family-base); + font-size: 16px; + line-height: 20px; + color: var(--ifm-font-color-base); +} + +.date { + color: var(--ifm-color-emphasis-500); + font-size: 14px; + padding: 2px; +} + +.tagContainer { + width: 100%; + max-width: 550px; + display: flex; + align-items: center; + justify-content: flex-start; + flex-direction: row; + overflow-x: scroll; + overflow-y: hidden; + gap: 5px; +} + +/* Hide scrollbar for Chrome, Safari and Opera */ +.tagContainer::-webkit-scrollbar { + display: none; +} + +/* Hide scrollbar for IE, Edge and Firefox */ +.tagContainer { + -ms-overflow-style: none; + /* IE and Edge */ + scrollbar-width: none; + /* Firefox */ +} + +.tag { + font-weight: 500; + font-size: 12px; + line-height: 150%; + padding: 2px 10px; + height: 22px; + background-color: var(--ifm-color-emphasis-200); + color: var(--ifm-color-emphasis-600); + border-radius: 30px; + display: flex; + align-items: center; + justify-content: center; + overflow: visible; + white-space: nowrap; +} + +.tagActive { + font-weight: 500; + font-size: 12px; + line-height: 150%; + padding: 2px 10px; + height: 22px; + background-color: var(--w3a-color-indigo-background); + color: var(--w3a-color-indigo); + border-radius: 30px; + display: flex; + align-items: center; + justify-content: center; + overflow: visible; + white-space: nowrap; +} + +.headerInteractionArea { + display: flex; + flex-direction: column; + width: 100%; + margin-top: 32px; + margin-bottom: 48px; + align-items: flex-start; + gap: 24px; + padding: 0 8px; +} + +.searchArea { + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + gap: 16px; + flex-wrap: wrap; +} + +.searchInput { + flex: 1; + min-width: 280px; + max-width: 400px; + align-self: center; + display: flex; + align-items: center; + margin: 0; + padding: 0; +} + +/* Target the Input component wrapper specifically */ +.searchInput>label { + margin: 0 !important; + width: 100%; + align-self: center; + display: flex; + align-items: center; +} + +/* Ensure input component has same baseline as selects */ +.searchInput input { + margin: 0 !important; + vertical-align: top !important; + border-color: var(--ifm-color-emphasis-300) !important; + background-color: var(--ifm-background-surface-color) !important; + box-sizing: border-box !important; + flex: 1; +} + +/* Ensure consistent minimum heights for all form elements */ +.searchArea input { + height: 48px; + min-height: 48px; + max-height: 48px; +} + +.searchArea>div { + min-height: 48px; + height: auto; + max-height: none; +} + +/* Style the custom select components to match */ +.searchArea .customSelect { + min-width: 200px; + max-width: 300px; + flex-shrink: 0; + margin: 0; + align-self: center; +} + +/* Add focus states for better accessibility */ +.searchArea input:focus { + border-color: var(--ifm-color-primary) !important; + box-shadow: 0 0 0 2px rgba(var(--ifm-color-primary-r), var(--ifm-color-primary-g), var(--ifm-color-primary-b), 0.2) !important; + outline: none !important; +} + + + +@media (max-width: 768px) { + .searchArea { + flex-direction: column; + align-items: stretch; + gap: 12px; + } + + .searchInput { + min-width: 100%; + max-width: 100%; + width: 100%; + } + + .searchInput input { + width: 100% !important; + } + + .searchArea .customSelect { + min-width: 100% !important; + max-width: 100% !important; + width: 100% !important; + } +} + +mark { + background-color: #fef3c7; + color: #92400e; + padding: 2px 4px; + border-radius: 4px; + font-weight: 500; +} + +html[data-theme='dark'] mark { + background-color: #451a03; + color: #fbbf24; +} + +.filterButton { + width: 100%; + max-width: 175px; + height: 42px; + padding: 10px; + background-color: var(--ifm-background-surface-color); + align-items: center; + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 20.5px; + padding: 10px; + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 125%; + color: var(--w3a-color-icon-gray); +} + +.filterButton:hover { + background-color: var(--ifm-footer-background-color); + cursor: pointer; +} + +.filterButton:active { + border-width: 2px; + padding: 9px; +} + +.searchBox { + width: 100%; + max-width: 276px; + height: 42px; + display: flex; + flex-direction: row; + align-items: center; + border-style: solid; + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 8px; + padding: 10px; + background-color: var(--ifm-background-surface-color); + color: var(--ifm-font-color-base); +} + +.searchIcon { + height: 18px; + width: 18px; + color: var(--w3a-color-icon-gray); +} + +.searchBox:focus-within { + border-width: 2px; + padding: 9px; +} + +.searchTerm { + width: 100%; + padding: 5px; + padding-left: 12px; + height: 24px; + border: 0; + outline: none; + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 125%; + background-color: var(--ifm-background-surface-color); + color: var(--ifm-font-color-base); +} + +.searchClearButton { + border: 0; + background-color: var(--ifm-background-surface-color); + padding: 5px; + align-items: center; + display: flex; + height: 24px; + width: 24px; +} + +.cardsGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); + gap: 24px; + margin-top: 0; + padding: 0px 8px 16px 8px; +} + +@media (max-width: 768px) { + .cardsGrid { + grid-template-columns: 1fr; + gap: 20px; + padding: 0; + } +} + +@media (min-width: 1200px) { + .cardsGrid { + grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); + gap: 32px; + } +} + +.noResults { + width: 100%; + text-align: center; + font-size: 20px; + font-weight: 700; + color: var(--ifm-font-color-base); + margin-top: 40px; + padding: 40px 20px; + background-color: var(--ifm-color-emphasis-100); + border-radius: 12px; +} + +.modalHeader { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 10px; + padding-bottom: 0; + border-width: 0; + border-color: var(--ifm-footer-background-color); + border-style: solid; + border-bottom-width: 2px; + height: 60px; +} + +.modalHeader h2 { + font-family: var(--ifm-font-family-base); + font-weight: 900; + font-size: 24px; + text-align: center; +} + +.modalBody { + width: 100%; + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: space-between; + padding: 20px; + padding-top: 5px; + overflow: scroll; + height: calc(80vh - 120px); +} + +.modalTagList { + width: 300px; +} + +.modalTagList h3 { + font-family: var(--ifm-font-family-base); + font-weight: 500; + font-size: 20px; + text-align: left; + padding: 5px; + padding-top: 20px; +} + +.checkBoxContainer { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + padding: 7px; + padding-left: 0; + border-radius: 5px; +} + +.checkBoxContainer:hover { + cursor: pointer; + background-color: rgba(var(--ifm-color-primary-r), var(--ifm-color-primary-g), var(--ifm-color-primary-b), 0.3); +} + +.checkBoxInputContainer { + width: 40px; + display: flex; + align-items: center; + justify-content: center; +} + +.checkBox { + width: 20px; + height: 20px; + border-color: var(--ifm-footer-background-color); + background-color: var(--ifm-background-surface-color); +} + +.checkBoxLabelContainer { + color: var(--ifm-font-color-base); + font-size: 17px; + font-weight: 400; + text-align: left; + display: flex; + align-items: center; + justify-content: center; +} + +.modalFooter { + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 10px; + height: 60px; + position: fixed; +} + +.modalClearButton { + background-color: var(--ifm-background-surface-color); + font-size: 17px; + border: none; + color: var(--ifm-color-primary); + padding: 10px; +} + +.modalClearButton:hover { + color: var(--ifm-font-color-base); + cursor: pointer; +} + +.modalClearButton:active { + color: var(--ifm-color-primary-dark); +} + +.modalSaveButton { + padding: 10px; + padding-left: 20px; + padding-right: 20px; + background-color: var(--ifm-color-primary-dark); + align-items: center; + border-style: none; + border-radius: 100px; + font-size: 17px; + line-height: 24px; + font-weight: 500; + color: #ffffff; +} + +.modalSaveButton:hover { + background-color: var(--ifm-color-primary-darker); + cursor: pointer; +} + +.modalSaveButton:active { + background-color: var(--ifm-color-primary-darkest); +} + +.buttonGroup { + display: flex; + width: 100%; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 500; + gap: 6px; +} + +.tab { + display: flex; + width: fit-content; + align-items: flex-start; + text-align: center; + justify-content: center; + border-width: 0; + border-bottom-width: 2px; + border-color: transparent; + border-style: solid; + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 125%; + padding: 0px 16px 30px; + gap: 6px; + color: var(--w3a-color-icon-gray); + cursor: pointer; +} + +.tab:hover { + border-width: 0; + border-bottom-width: 2px; + border-color: var(--ifm-color-primary); + color: var(--ifm-color-primary); + border-style: solid; +} + +.activeTab { + display: flex; + align-items: flex-start; + text-align: center; + justify-content: center; + border-width: 0; + border-bottom-width: 2px; + border-color: var(--ifm-color-primary); + color: var(--ifm-color-primary); + border-style: solid; + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 125%; + padding: 0px 16px 30px; + gap: 6px; + cursor: pointer; +} + +.tabIconContainer { + height: 18px; + width: 18px; + padding: 1px; +} + +.pillContainer { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + /* white */ + background: var(--w3a-color-indigo-background); + border-radius: 8px; + text-decoration: none; + padding: 0; + margin: 0; + margin-bottom: 7px; + width: max-content; + overflow: hidden; + flex-wrap: wrap; +} + +.pill { + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; + align-items: center; + width: max-content; + padding-top: 2px; + padding-bottom: 2px; + padding-left: 12px; + padding-right: 12px; + margin: 0; + font-weight: 500 !important; + font-size: 12px; + text-transform: uppercase; + line-height: 150%; + color: var(--w3a-color-indigo); + flex-wrap: wrap; +} + +@media only screen and (max-width: 1046px) { + .headerInteractionArea { + flex-direction: column; + gap: 50px; + } + + .searchArea { + justify-content: flex-start; + } +} + +@media only screen and (max-width: 889px) { + .header { + text-align: center; + align-items: center; + justify-content: center; + } + + .headerInteractionArea { + align-items: center; + justify-content: center; + } + + .buttonGroup { + width: 100%; + overflow: scroll; + } + + .searchArea { + flex-direction: column; + } + + .container { + justify-content: center; + } +} + +@media only screen and (min-width: 889px) { + .article { + width: 45%; + } +} + +@media only screen and (min-width: 1334px) { + .article { + width: 30%; + } +} + +@media only screen and (max-width: 750px) { + .modalBody { + flex-direction: column; + } +} \ No newline at end of file diff --git a/src/components/NavDropdown/ConnectMetaMask.html b/src/components/NavDropdown/ConnectMetaMask.html deleted file mode 100644 index 0476aba2472..00000000000 --- a/src/components/NavDropdown/ConnectMetaMask.html +++ /dev/null @@ -1,35 +0,0 @@ - - - diff --git a/src/components/NavDropdown/DeveloperTools.html b/src/components/NavDropdown/DeveloperTools.html deleted file mode 100644 index 18c9c77a15d..00000000000 --- a/src/components/NavDropdown/DeveloperTools.html +++ /dev/null @@ -1,33 +0,0 @@ - - - diff --git a/src/components/NavDropdown/EmbedMetaMask.html b/src/components/NavDropdown/EmbedMetaMask.html deleted file mode 100644 index a3115126a66..00000000000 --- a/src/components/NavDropdown/EmbedMetaMask.html +++ /dev/null @@ -1,38 +0,0 @@ - - - diff --git a/src/components/NavDropdown/ExtendScale.html b/src/components/NavDropdown/ExtendScale.html deleted file mode 100644 index 29e4596148c..00000000000 --- a/src/components/NavDropdown/ExtendScale.html +++ /dev/null @@ -1,38 +0,0 @@ - - - diff --git a/src/components/NavDropdown/Products.html b/src/components/NavDropdown/Products.html new file mode 100644 index 00000000000..a89e905a1b0 --- /dev/null +++ b/src/components/NavDropdown/Products.html @@ -0,0 +1,335 @@ + \ No newline at end of file diff --git a/src/components/NavbarWallet/index.tsx b/src/components/NavbarWallet/index.tsx index c6eaf559676..195494c18b5 100644 --- a/src/components/NavbarWallet/index.tsx +++ b/src/components/NavbarWallet/index.tsx @@ -118,17 +118,17 @@ const NavbarWalletComponent: FC = ({ includeUrl = [] }: INavbarWalletComponent) style={ colorMode === 'dark' ? { - '--button-color': 'var(--consumer-orange)', - '--button-text-color': 'var(--general-black)', - '--button-color-hover': 'var(--general-white)', - '--button-text-color-hover': 'var(--general-black)', - } + '--button-color': 'var(--consumer-orange)', + '--button-text-color': 'var(--general-black)', + '--button-color-hover': 'var(--general-white)', + '--button-text-color-hover': 'var(--general-black)', + } : { - '--button-color': 'var(--consumer-orange)', - '--button-text-color': 'var(--general-black)', - '--button-color-hover': 'var(--general-black)', - '--button-text-color-hover': 'var(--general-white)', - } + '--button-color': 'var(--consumer-orange)', + '--button-text-color': 'var(--general-black)', + '--button-color-hover': 'var(--general-black)', + '--button-text-color-hover': 'var(--general-white)', + } } /> ) : ( @@ -200,6 +200,14 @@ const NavbarWallet = props => { const [loginEnabled, setLoginEnabled] = useState(false) useEffect(() => { + // Handle case where ldClient is null (when LaunchDarkly isn't initialized) + if (!ldClient) { + console.warn('LaunchDarkly client not available, disabling login feature'); + setLdReady(true); + setLoginEnabled(false); + return; + } + ldClient.waitUntilReady().then(() => { setLoginEnabled(ldClient.variation(LOGIN_FF, false)) setLdReady(true) diff --git a/src/components/SEO/index.tsx b/src/components/SEO/index.tsx new file mode 100644 index 00000000000..9ef34af7212 --- /dev/null +++ b/src/components/SEO/index.tsx @@ -0,0 +1,90 @@ +import Head from "@docusaurus/Head"; + +export default function SEO(props) { + // eslint-disable-next-line react/prop-types + const { title, description, image, slug, keywords } = props; + + return ( + + {title ? {title} | Web3Auth : Documentation | Web3Auth} + {description ? ( + + ) : ( + + )} + + {/* Add keywords to meta from an array of keywords */} + {keywords ? ( + keywords.length > 0 && ( + + ) + ) : ( + + )} + + {/* Open Graph Meta Tags */} + + + {title ? : } + {description ? ( + + ) : ( + + )} + {slug ? : } + + {image ? ( + + ) : ( + + )} + + {/* Twitter Meta Tags */} + + + + + {/* {title ? : } + {description ? ( + + ) : ( + + )} */} + {image ? ( + + ) : ( + + )} + + {/* Google / Search Engine Tags */} + {title ? : } + {description ? ( + + ) : ( + + )} + {image ? ( + + ) : ( + + )} + + + + ); +} diff --git a/src/pages/guides/android-wallet.mdx b/src/pages/guides/android-wallet.mdx new file mode 100644 index 00000000000..74a836bc5ee --- /dev/null +++ b/src/pages/guides/android-wallet.mdx @@ -0,0 +1,1091 @@ +--- +title: Create an Ethereum Web3 wallet in Android +image: 'img/guides/guides-banners/android-wallet.png' +description: Empower your Android app with a Ethereum Web3 wallet using the Web3Auth PnP SDK. +type: guide +tags: [embedded wallets, android, evm, kotlin, secp256k1, web3auth] +date: May 27, 2024 +author: Web3Auth Team +--- + +import SEO from '@site/src/components/SEO' +import TabItem from '@theme/TabItem' +import Tabs from '@theme/Tabs' +import WalletPreview from '@site/static/img/guides/android-wallet-preview.png' + + + +In this guide, we'll talk about how we can use Web3Auth to build your Ethereum Web3 wallet in Android. The wallet will only support the Ethereum ecosystem, but functionality can be extended with any blockchain ecosystem. + +As an overview, the app is quite simple, with functionality to log in, display user details, and perform blockchain interactions. The signing of the blockchain transactions is done through the Web3Auth embedded wallet. You can check out the infrastructure docs, ["Web3Auth Wallet Management Infrastructure"](/docs/infrastructure) for a high-level overview of the Web3Auth architecture and implementation. For those who want to skip straight to the code, you can find it on [GitHub](https://github.com/Web3Auth/web3auth-android-examples/tree/main/android-playground). + +Here are a few screenshots of the application. + +Android Wallet Screenshots + +## How to set up Web3Auth Dashboard + +If you haven't already, sign up on the Web3Auth platform. It is free and gives you access to the Web3Auth's base plan. After the basic setup, explore other features and functionalities offered by the Web3Auth Dashboard. It includes custom verifiers, whitelabeling, analytics, and more. Head to [Web3Auth's documentation](/docs/dashboard) page for detailed instructions on setting up the Web3Auth Dashboard. + +## Integrating Web3Auth in Android + +Once, you have set up the Web3Auth Dashboard, and created a new project, it's time to integrate Web3Auth in your Android application. For the implementation, we'll use the ["web3auth-android-sdk"](https://github.com/Web3Auth/web3auth-android-sdk) SDK. This SDK facilitates integration with Web3Auth. This way you can easily manage an embedded wallet in your Android application. + +### Installation + +To install the web3auth-android-sdk SDK,in your module-level `build.gradle` or `settings.gradle` file, add JitPack repository. + +```groovy +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + // focus-next-line + maven { url "https://jitpack.io" } // <-- Add this line + } +} +``` + +Once, you have added the JitPack repository, then in your app-level `build.gradle` dependencies section, add the `web3auth-android-sdk`. + +```groovy +dependencies { + // ... + // focus-next-line + implementation 'com.github.web3auth:web3auth-android-sdk:7.4.0' +} +``` + +For the prerequisites, and other mandatory configuration of the SDK, please head to our [installation documentation](/docs/sdk/mobile/pnp/android/install). + +### Initialization + +After successfully installing the package, the next step is to initialize Web3Auth in your Android app. This sets up the necessary configurations using Client Id and prepares Web3Auth. [Learn more about Web3Auth Initialization](/docs/sdk/mobile/pnp/android/initialize). + +Since we are using the MVVM architecture for the wallet, along with dependency injection, we have defined a `Web3AuthHelper` to interact with a `Web3Auth` instance, which also makes it easier to write mocks for unit testing. + +```kotlin +class Web3AuthHelperImpl( + private val web3Auth: Web3Auth +): Web3AuthHelper { + + // Performs the login to authenticate the user with Web3Auth netowrk. + override suspend fun login(loginParams: LoginParams): CompletableFuture { + return web3Auth.login(loginParams) + } + + // Logout of the current active session. + override suspend fun logOut(): CompletableFuture { + return web3Auth.logout() + } + + // Returns the Ethereum compatible private key. + override fun getPrivateKey(): String { + return web3Auth.getPrivkey() + } + + // Returns the user information such as name, email, profile image, and etc. + // For more details, please checkout UserInfo. + override fun getUserInfo(): UserInfo { + try { + return web3Auth.getUserInfo()!! + } catch (e: Exception) { + throw e + } + } + + override suspend fun initialize(): CompletableFuture { + return web3Auth.initialize() + } + + override suspend fun setResultUrl(uri: Uri?) { + return web3Auth.setResultUrl(uri) + } + + override suspend fun isUserAuthenticated(): Boolean { + return web3Auth.getPrivkey().isNotEmpty() + } +} +``` + +Once we have the created `Web3AuthHelper`, the next is to initialize the `Web3Auth` instance in the Koin module and make it a singleton component. + +```kotlin +val appModule = module { + // focus-start + single { + getWeb3AuthHelper(get()) + } + // focus-end + + // Additional code + + viewModel { MainViewModel(get()) } +} + +private fun getWeb3AuthHelper(context: Context): Web3AuthHelper { + // focus-start + val web3Auth = Web3Auth( + Web3AuthOptions( + clientId = "WEB3AUTH_CLIENT_ID", + context = context, + network = Network.SAPPHIRE_MAINNET, + redirectUrl = Uri.parse("w3a://com.example.android_playground/auth") + ) + ) + // focus-end + + return Web3AuthHelperImpl(web3Auth) +} +``` + +### Session Management + +To check whether the user is authenticated, you can use the `getPrivateKey` or `getEd25519PrivKey` method. For a user already authenticated, the result would be a non-empty `String`. You can navigate to different views based on the result. If the user is already authenticated, we'll generate and prepare the `Credentials`, important to interact with the blockchain. Along with that, we'll retrieve user info, and navigate them to `HomeScreen`. In case of no active session, we'll navigate to `LoginScreen` to authenticate again. [Learn more about Web3Auth session management](/docs/features/session-management). + +Since we are using the MVVM architecture, we'll create a `ViewModel` class to encapsulate the business logic for Web3Auth and Ethereum chain interaction. + +```kotlin +class MainViewModel(private val web3AuthHelper: Web3AuthHelper) : ViewModel() { + + // _isLoggedIn can be used in the UI to know whether the user is logged. + private val _isLoggedIn: MutableStateFlow = MutableStateFlow(false) + val isLoggedIn: StateFlow = _isLoggedIn + + lateinit var credentials: Credentials + lateinit var userInfo: UserInfo + + // Additional code + + // Function to retrieve private key. + private fun privateKey(): String { + // focus-next-line + return web3AuthHelper.getPrivateKey() + } + + // prepareCredentials uses the private key to create Ethereum credentials which + // can be used to retrieve the EOA address, and sign the transactions. + private fun prepareCredentials() { + // focus-next-line + credentials = Credentials.create(privateKey()) + } + + private fun prepareUserInfo() { + // focus-next-line + userInfo = web3AuthHelper.getUserInfo() + } + + // Additional code + + fun initialise() { + viewModelScope.launch { + web3AuthHelper.initialize().await() + isUserLoggedIn() + } + } + + private fun isUserLoggedIn() { + viewModelScope.launch { + try { + // focus-start + val isLoggedIn = web3AuthHelper.isUserAuthenticated() + if (isLoggedIn) { + prepareCredentials() + prepareUserInfo() + } + _isLoggedIn.emit(isLoggedIn) + // focus-end + } catch (e: Exception) { + _isLoggedIn.emit(false) + } + } + } +} +``` + +### Authentication + +If the user is not authenticated, we can utilize the `login` method to authenticate the user. For the Wallet, we will add an Email Passwordless login. We'll create a helper function, `login` inside `MainViewModel`. The login method is pretty straightforward in Web3Auth and takes `LoginParams` as input. After successfully logging in, we'll generate and prepare the `Credentials`, important to interact with the blockchain. Along with that, we'll retrieve user info, and navigate them to `HomeScreen`. + +Learn more about [Web3Auth LoginParams](/docs/sdk/mobile/pnp/android/usage#parameters). + +```kotlin +class MainViewModel(private val web3AuthHelper: Web3AuthHelper) : ViewModel() { + // Additional code + + fun login(email: String) { + // focus-start + val loginParams = LoginParams( + loginProvider = Provider.EMAIL_PASSWORDLESS, + extraLoginOptions = ExtraLoginOptions(login_hint = email) + ) + // focus-end + viewModelScope.launch { + try { + // focus-next-line + web3AuthHelper.login(loginParams = loginParams).await() + // Functions from Session Management code snippets + prepareCredentials() + prepareUserInfo() + + // Emit true to navigate to HomeScreen + _isLoggedIn.emit(true) + } catch (error: Exception) { + _isLoggedIn.emit(false) + throw error + } + } + } +} +``` + +## Set up Blockchain Providers + +Once we have successfully authenticated the user, the next step would be to fetch the user details, retrieve the wallet address, and prepare blockchain providers for interactions. For this guide, we are supporting only the Ethereum ecosystem, but the general idea can be used for any blockchain ecosystem. + +Given that the project follows MVVM architecture pattern, we'll want to create a UseCase to interact with the Blockchain. This UseCase will help us easily expand the blockchain support while isolating it from the rest of the application. + +For interacting with Ethereum chains, we'll use the [web3j](https://github.com/hyperledger/web3j) SDK. + +To install the web3j SDK, in your module-level `build.gradle` or `settings.gradle` file, add `web3j` in your app-level dependencies. + +```groovy +dependencies { + // ... + // focus-next-line + implementation 'org.web3j:core:4.8.7-android' +} +``` + +After successfully installing the SDK, it's time to set up our Ethereum UseCase. First, we'll create a new class, `EthereumUseCase` interface, which will used as a base class for `EthereumUseCaseImpl`. If you wish to support any additional ecosystem, you can create the chain-agnostic UseCase and implement the methods. + +If you want to learn, how you can integrate different blockchains with Web3Auth, you can check out our [Connect Blockchain resources](/docs/connect-blockchain/). + +```kotlin +interface EthereumUseCase { + suspend fun getBalance(publicKey: String): String + suspend fun signMessage(message: String, sender: Credentials): String + suspend fun sendETH(amount: String, recipientAddress: String, sender: Credentials): String + + suspend fun getBalanceOf(contractAddress: String, address: String, credentials: Credentials): String + suspend fun approve(contractAddress: String, spenderAddress: String, credentials: Credentials): String +} +``` + +Generally, for any blockchain provider, you'll only require the `getBalance`, `sendTransaction`, and `signMessage`. The `getBalance` and `approve` can be used to interact with smart contracts. To interact with smart contracts, we'll be required to generate smart contract function wrappers in Java from Solidity ABI files. + +### Smart Contract Wrappers + +For generating the wrappers, we'll use the [web3j command line tools](https://docs.web3j.io/4.11.0/command_line_tools/). + +To install the web3j cli, you can use the below command. Read more about [web3j cli installation](https://docs.web3j.io/4.11.0/command_line_tools/#installation). + +```bash +curl -L get.web3j.io | sh && source ~/.web3j/source.sh +``` + +Once, we have installed the cli, the next step is to create `Token.sol` file, which has the smart contract interface for the ERC-20 token. [Learn more about ERC-20 token standard](https://docs.web3j.io/4.11.0/getting_started/deploy_interact_smart_contracts/#eip-20-ethereum-token-standard-smart-contract). + +```sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IERC20 { + function totalSupply() external view returns (uint256); + function balanceOf(address account) external view returns (uint256); + function transfer(address recipient, uint256 amount) + external + returns (bool); + function allowance(address owner, address spender) + external + view + returns (uint256); + function approve(address spender, uint256 amount) external returns (bool); + function transferFrom(address sender, address recipient, uint256 amount) + external + returns (bool); +} +``` + +After creating the interface for the ERC-20 token, the next step is to compile the solidity file and generate the abi and bin files to generate the wrappers. To compile the solidity file, we'll use the [solc](https://docs.soliditylang.org/en/latest/installing-solidity.html). + +To install the solc we'll require the npm or yarn. If you have the npm already installed, you can use the below command to install the solc package globally. + +```bash +npm install -g solc +``` + +Once, we have the solc package installed, we'll compile the smart contract. The bin and abi options will generate the abi and bin files. Feel free to choose the output directory of your choice. + +```bash +solc Token.sol --bin --abi --optimize -o / +``` + +Once, we have compiled the smart contract, the next step is to use the web3j cli to generat the wrpapers. + +```bash +web3j generate solidity -b /path/to/Tokne.bin -a /path/to/Token..abi -o /path/to/src/main/java -p com.your.organisation.name +``` + +Once you run the command, it'll create a wrapper `Token.java` which extends the `Contract`. You can use this class to interact with the smart contracts. Please make sure to compile and regenerate wrappers if you make any changes in the smart contract. + +### Ethereum UseCase Implementation + +Once we have generated `Token` wrapper, we'll create `EthereumUseCaseImpl` and implement the methods. To create the `Web3j` instance, you'll require the rpcTarget URL. If you are using public RPCs, you can face some network congestion. It's ideal to use paid RPCs for production. + +The `getBalance`, and `approve` methods are used to interact with smart contracts in the Ethereum ecosystem. The `getBalance` is used to read the balance from the ERC-20 smart contracts, whereas the `approve` is used to change the approval to zero for the ERC-20. For the `getBalance` and `approve` we'll be using the `Token` wrapper. + +```kotlin +class EthereumUseCaseImpl( + private val web3: Web3j +) : EthereumUseCase { + override suspend fun getBalance(publicKey: String): String = withContext(Dispatchers.IO) { + try { + val balanceResponse = web3.ethGetBalance(publicKey, DefaultBlockParameterName.LATEST).send() + val ethBalance = BigDecimal.valueOf(balanceResponse.balance.toDouble()).divide(BigDecimal.TEN.pow(18)) + DecimalFormat("#,##0.00000").format(ethBalance) + } catch (e: Exception) { + throw e + } + } + + override suspend fun signMessage(message: String, sender: Credentials): String { + try { + val signature = Sign.signPrefixedMessage(message.toByteArray(), sender.ecKeyPair) + val r = Numeric.toHexString(signature.r) + val s = Numeric.toHexString(signature.s).substring(2) + val v = Numeric.toHexString(signature.v).substring(2) + + return StringBuilder(r).append(s).append(v).toString() + } catch (e: Exception) { + throw e + } + } + + override suspend fun sendETH(amount: String, recipientAddress: String, sender: Credentials): String { + try { + + + val ethGetTransactionCount: EthGetTransactionCount = + web3.ethGetTransactionCount(sender.address, DefaultBlockParameterName.LATEST) + .sendAsync().get() + val nonce: BigInteger = ethGetTransactionCount.transactionCount + val value: BigInteger = Convert.toWei(amount, Convert.Unit.ETHER).toBigInteger() + val gasLimit: BigInteger = BigInteger.valueOf(21000) + val gasPrice = web3.ethGasPrice().sendAsync().get() + + + val rawTransaction: RawTransaction = RawTransaction.createEtherTransaction( + nonce, + gasPrice.gasPrice, + gasLimit, + recipientAddress, + value + ) + + val signedMessage: ByteArray = TransactionEncoder.signMessage(rawTransaction, sender) + val hexValue: String = Numeric.toHexString(signedMessage) + val ethSendTransaction: EthSendTransaction = + web3.ethSendRawTransaction(hexValue).sendAsync().get() + + if (ethSendTransaction.error != null) { + throw Exception(ethSendTransaction.error.message) + } else { + return ethSendTransaction.transactionHash + } + } catch (e: Exception) { + throw e + } + } + + override suspend fun getBalanceOf(contractAddress: String, address: String, credentials: Credentials): String = withContext(Dispatchers.IO) { + val token = Token.load(contractAddress, web3, credentials, DefaultGasProvider()) + val balanceResponse = token.balanceOf(address).sendAsync().get() + BigDecimal.valueOf(balanceResponse.toDouble()).divide(BigDecimal.TEN.pow(18)).toString() + } + + override suspend fun approve( + contractAddress: String, + spenderAddress: String, + credentials: Credentials + ): String = withContext(Dispatchers.IO) { + val token = Token.load(contractAddress, web3, credentials, DefaultGasProvider()) + val hash = token.approve(spenderAddress, BigInteger.ZERO).sendAsync().get() + hash.transactionHash + } +} +``` + +Once we have the created `EthereumUseCaseImpl`, next is to initialize the `EthereumUseCaseImpl` instance in the Koin module. + +```kotlin +val appModule = module { + // Additional code + + // focus-next-line + factory { EthereumUseCaseImpl(Web3j.build(HttpService(chainConfigList.first().rpcTarget))) } + + // Additonal code +``` + +## Set up Supported Chains + +After having our blockchain UseCase in place, the next step on the list is to define the supported chains. To keep things simple, we'll simply create a new file `ChainConfigList` with an array of ChainConfig to define the supported chains. + +For the guide, we have added the support for Ethereum Sepolia, and Arbitrum Sepolia. If you wish to support more chains in your wallet, you can simply add the config with the required details in the list below. Along with that, you can also add the desired chain using the add custom chain feature in the app. + +```kotlin +var chainConfigList = arrayOf( + ChainConfig( + chainNamespace = ChainNamespace.EIP155, + decimals = 18, + blockExplorerUrl = "https://sepolia.etherscan.io/", + chainId = "11155111", + displayName = "Ethereum Sepolia", + rpcTarget = "https://1rpc.io/sepolia", + ticker = "ETH", + tickerName = "Ethereum" + ), + ChainConfig( + chainNamespace = ChainNamespace.EIP155, + decimals = 18, + blockExplorerUrl = "https://sepolia.etherscan.io/", + chainId = "421614", + displayName = "Arbitrum Sepolia", + rpcTarget = "https://endpoints.omniatech.io/v1/arbitrum/sepolia/public", + ticker = "ETH", + tickerName = "Ethereum" + ) +) +``` + +## Wallet Implementation + +Once, we have set up the EthereumUseCase, and supported chains, it's time to integrate and plug them into the wallet. Since we have already created `MainViewModel` before, we'll add the other features inside it. + +This will help us to separate business logic from UI. + +### Set up MainViewModel + +Once we have set up supported chains, the next on the list is to add more functionality in `MinaViewModel` to help us manage the state & functionality of the wallet. It will help us manage the state of the currently selected chain, fetch balance, sign transactions, and access other functionalities of Web3Auth. + +```kotlin +class MainViewModel(private val web3AuthHelper: Web3AuthHelper) : ViewModel() { + // _isLoggedIn can be used in the UI to know whether the user is logged. + private val _isLoggedIn: MutableStateFlow = MutableStateFlow(false) + val isLoggedIn: StateFlow = _isLoggedIn + + // _isAccountLoaded can be used in the UI to know whether the user's account is loaded. + // If it's false, we'll show the loading indictor. + private val _isAccountLoaded: MutableStateFlow = MutableStateFlow(false) + val isAccountLoaded: StateFlow = _isAccountLoaded + + // _balance holds the user's balance for the selected ChainConfig. + private val _balance: MutableStateFlow = MutableStateFlow("0.0") + val balance: StateFlow = _balance + + + // Currently selected ChainConfig by the user. By default, it would be the first ChainConfig + // in the list. + private val _selectedChain: MutableStateFlow = MutableStateFlow(chainConfigList[0]) + val selectedChain: StateFlow = _selectedChain + + // Credentials will be used to retrive user's EOA address, and sign the transactions. + lateinit var credentials: Credentials + lateinit var userInfo: UserInfo + + // EthereumUseCaseImpl to interact with the selected Ethereum ChainConfig. + private var ethereumUseCase: EthereumUseCase = EthereumUseCaseImpl( + Web3j.build( + HttpService( + chainConfigList.first().rpcTarget + ) + ) + ) + + // User's Ethereum compatible private key. + private fun privateKey(): String { + return web3AuthHelper.getPrivateKey() + } + + private fun prepareCredentials() { + credentials = Credentials.create(privateKey()) + } + + private fun prepareUserInfo() { + userInfo = web3AuthHelper.getUserInfo() + } + + fun login(email: String) { + // Defined previously + } + + fun initialise() { + // Defined previously + } + + private fun isUserLoggedIn() { + // Defined previously + } + + // Retrieves user's balance for the currently selected ChainConfig. + fun getBalance() { + viewModelScope.launch { + _isAccountLoaded.emit(false) + try { + Log.d("Address", credentials.address) + _balance.emit(ethereumUseCase.getBalance(credentials.address)) + _isAccountLoaded.emit(true) + } catch (e: Exception) { + _isAccountLoaded.emit(false) + throw e + } + } + } + + // Logouts out user, and deletes the currently active session. + fun logOut() { + viewModelScope.launch { + try { + web3AuthHelper.logOut().await() + _isLoggedIn.emit(true) + } catch (e: Exception) { + _isLoggedIn.emit(false) + } + } + } + + // Signs and broadcast a trasnfer transaction. + fun sendTransaction(value: String, recipient: String, onSign: (hash: String?, error: String?) -> Unit) { + viewModelScope.launch { + try { + val hash = ethereumUseCase.sendETH(value, recipient, credentials) + onSign(hash, null) + } catch (e: Exception) { + e.localizedMessage?.let { onSign(null, it) } + } + } + } + + // Signs a personal message. + fun signMessage(message: String, onSign: (hash: String?, error: String?) -> Unit) { + viewModelScope.launch { + try { + val signature = ethereumUseCase.signMessage(message, credentials) + Log.d("Signature", signature) + onSign(signature, null) + } catch (e: Exception) { + e.localizedMessage?.let { onSign(null, it) } + } + } + } + + // Changes the currently selected ChainConfig. + fun changeChainConfig(config: ChainConfig) { + _selectedChain.value = config + ethereumUseCase = EthereumUseCaseImpl( + Web3j.build( + HttpService( + config.rpcTarget + ) + ) + ) + getBalance() + } + + // Retreives the ERC-20 token balance using the getBalanceOf method. + fun getTokenBalance(contractAddress: String, onSuccess: (balance: String?, error: String?) -> Unit) { + viewModelScope.launch { + try { + val balance = ethereumUseCase.getBalanceOf(contractAddress, credentials.address, credentials) + Log.d("Token Balance:",balance) + onSuccess(balance, null) + } catch (e: Exception) { + onSuccess(null, e.localizedMessage) + } + } + } + + // Revokes the approval for the ERC-20 token using the approve function. + fun revokeApproval(contractAddress: String, spenderAddress: String, onRevoke: (hash: String?, error: String?) -> Unit) { + viewModelScope.launch { + try { + val hash = ethereumUseCase.approve(contractAddress, spenderAddress, credentials) + Log.d("Revoke Hash:", hash) + onRevoke(hash, null) + } catch (e: Exception) { + onRevoke(null, e.localizedMessage) + } + } + } + + fun userInfo(onAvailable: (userInfo: UserInfo?, error: String?) -> Unit) { + try { + val info = web3AuthHelper.getUserInfo() + onAvailable(info, null) + } catch (e: Exception) { + e.localizedMessage?.let { onAvailable(null, it) } + } + } +} +``` + +### Set up Home screen + +Once, we have our view model ready, we create a new `HomeScreen` to show user details as email address, wallet address, user's balance for selectedChain, and blockchain interaction methods. + +To get the user's balance, we'll use `getBalance` method from the `MainViewModel`. The method internally uses `EthereumUseCaseImpl` to retrieve the user's wallet address and fetch the wallet balance for the address. Check out `EthereumUseCaseImpl` implementation for more details. + +For the bottom navigation, we have created `TabBarView`, please check TabBarView.kt file for more details on UI implementation. + +```kotlin +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomeScreen(viewModel: MainViewModel) { + val homeTab = TabBarItem( + title = "Home", + selectedIcon = Icons.Filled.Home, + unselectedIcon = Icons.Outlined.Home + ) + val alertsTab = TabBarItem( + title = "Sign & Send", + selectedIcon = Icons.Filled.Create, + unselectedIcon = Icons.Outlined.Create + ) + val settingsTab = TabBarItem( + title = "Smart Contracts", + selectedIcon = Icons.Filled.Receipt, + unselectedIcon = Icons.Outlined.Receipt + ) + + val tabBarItems = listOf(homeTab, alertsTab, settingsTab) + + val navController = rememberNavController() + + // Show the UI if the account is loaded, otherwise show the + // progress indictor. + if (viewModel.isAccountLoaded.collectAsState().value) { + Scaffold( + topBar = { + TopAppBar( + title = { + Text(text = "Android Playground") + }, + + actions = { + Row { + // Logs out user + IconButton(onClick = { viewModel.logOut() }) { + Icon(Icons.Outlined.ExitToApp, contentDescription = "Logout") + } + } + } + ) + }, + bottomBar = { + TabView(tabBarItems = tabBarItems, navController = navController) + } + ) { innerPadding -> + // Different Views which will be shown upon user selection. By default, it'll be AccountView. + NavHost(navController = navController, startDestination = "Home", modifier = Modifier.padding(innerPadding)) { + composable(homeTab.title) { + AccountView(viewModel = viewModel) + } + composable(alertsTab.title) { + TransactionScreen(viewModel = viewModel) + } + composable(settingsTab.title) { + SmartContractsScreen(viewModel = viewModel) + } + } + } + } else { + // Shows CircularProgressIndicator + Box(modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) +@Composable +fun AccountView(viewModel: MainViewModel) { + // Used for ExposedDropdownMenuBox state management + var expanded by remember { mutableStateOf(false) } + + // Defines whether to showcase user info dialog. By default, + // it's false. + val openUserInfoDialog = remember { + mutableStateOf(false) + } + + var balance = viewModel.balance.collectAsState().value + val clipboardManager: ClipboardManager = LocalClipboardManager.current + val refreshing by viewModel.isAccountLoaded.collectAsState() + + val pullRefreshState = rememberPullRefreshState(!refreshing, { viewModel.getBalance() }) + + // Displays UserInfoDialog when openUserInfoDialog is true. + if(openUserInfoDialog.value) { + UserInfoDialog(onDismissRequest = { + openUserInfoDialog.value = false + }, userInfo = viewModel.userInfo.toString()) + } + + Box(Modifier.pullRefresh(pullRefreshState)) { + LazyColumn( + modifier = Modifier + .padding(PaddingValues(horizontal = 16.dp, vertical = 8.dp)) + ) { + item { + // Additional UI + Box( + modifier = Modifier + .fillMaxWidth() + ) { + // Dropdown for chain selection + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { + expanded = !expanded + } + ) { + OutlinedTextField( + value = viewModel.selectedChain.collectAsState().value.displayName!!, + onValueChange = {}, + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + modifier = Modifier + .menuAnchor() + .fillMaxWidth() + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + chainConfigList.forEach { item -> + DropdownMenuItem( + text = { Text(text = item.displayName!!) }, + onClick = { + expanded = false + viewModel.changeChainConfig(item) + } + ) + } + } + } + } + // Additonal UI code + + // Display User info + Row { + Box( + modifier = Modifier + .height(120.dp) + .width(120.dp) + .background(color = MaterialTheme.colorScheme.primary), + contentAlignment = Alignment.Center + ) { + Text( + text = viewModel.userInfo.name.first().uppercase(), + style = Typography.headlineLarge.copy(color = Color.White) + ) + } + + Box(modifier = Modifier.width(16.dp)) + Column { + Text(text = viewModel.userInfo.name, style = Typography.titleLarge) + Box(modifier = Modifier.height(12.dp)) + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + // Displays user's EOA address + Text( + text = viewModel.credentials.address.addressAbbreviation(), + style = Typography.titleMedium + ) + IconButton(onClick = { + clipboardManager.setText(AnnotatedString(viewModel.credentials.address)) + }) { + Icon(Icons.Outlined.ContentCopy, contentDescription = "Copy") + } + } + } + } + // Additional UI code + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column { + // Displays user's wallet balance for selected ChainConfig. + Text(text = "Wallet Balance", style = Typography.titleMedium) + Spacer(modifier = Modifier.height(8.dp)) + Text(text = balance, style = Typography.headlineSmall) + } + Column(horizontalAlignment = Alignment.End) { + // Displays the chainId for selected ChainConfig + Text(text = "Chain id", style = Typography.titleMedium) + Spacer(modifier = Modifier.height(8.dp)) + Text(text = viewModel.selectedChain.collectAsState().value.chainId, style = Typography.headlineSmall) + } + } + } + } + + // Adds additional pull to refresh functionality + PullRefreshIndicator(!refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) + } +} +``` + +### Chain Interactions + +Once we have set up `HomeScreen` and `AccountView`, the next step is to set up chain interactions for signing messages, signing transactions, reading from contracts, and writing on contracts. For signing messages and transaction, we'll create a new `TransactionScreen` widget and utilize `signMessage` and `sendTransaction` from `MainViewModel` for the respective functionality. + +```kotlin +@OptIn(ExperimentalPagerApi::class) +@Composable +fun TransactionScreen(viewModel: MainViewModel) { + val pagerState = rememberPagerState( 0) + val tabItems = listOf( + "Sign Message", + "Send Transaction", + ) + + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "Signing/Transaction", style = Typography.headlineLarge, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(16.dp)) + Tabs(pagerState = pagerState, tabItems) + Spacer(modifier = Modifier.height(16.dp)) + TabsContent(pagerState = pagerState, viewModel) + } +} + +@OptIn(ExperimentalPagerApi::class) +@Composable +fun TabsContent(pagerState: PagerState, viewModel: MainViewModel) { + + HorizontalPager(state = pagerState, count = 2) { + page -> + when (page) { + 0 -> SigningView(viewModel = viewModel) + 1 -> TransactionView(viewModel = viewModel) + } + } +} + +@Composable +fun SigningView(viewModel: MainViewModel) { + // Default signing message + var messageText by remember { mutableStateOf("Welcome to Web3Auth") } + val openAlertDialog = remember { mutableStateOf(false) } + var dialogText by remember { mutableStateOf("") } + + when { + openAlertDialog.value -> MinimalDialog(dialogText) { + openAlertDialog.value = false + } + } + + Column(modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp)) { + // Additional UI code + Button(onClick = { + // focus-start + // Signs the message and show the signature + viewModel.signMessage(messageText, onSign = { + signature, error -> + if(signature != null) { + dialogText = "Signature:\n$signature" + openAlertDialog.value = true + } else { + dialogText = "Error:\n$error" + openAlertDialog.value = true + + } + }) + // focus-end + }, shape = RoundedCornerShape(4.dp), modifier = Modifier.fillMaxWidth()) { + Text("Sign Message") + } + + } +} + + +@Composable +fun TransactionView(viewModel: MainViewModel) { + var valueText by remember { mutableStateOf("") } + var addressText by remember { mutableStateOf("") } + val openAlertDialog = remember { mutableStateOf(false) } + var dialogText by remember { mutableStateOf("") } + + when { + openAlertDialog.value -> MinimalDialog(dialogText) { + openAlertDialog.value = false + } + } + + Column(modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp)) { + // Additional UI code + Button(onClick = { + // focus-start + // Performs transfer transaction and displays the hash for the transaction + viewModel.sendTransaction(valueText, addressText, onSign = { + hash, error -> + if(hash != null) { + dialogText = "Hash:\n$hash" + openAlertDialog.value = true + } else { + dialogText = "Error:\n$error" + openAlertDialog.value = true + } + }) + // focus-end + }, shape = RoundedCornerShape(4.dp), modifier = Modifier.fillMaxWidth()) { + Text("Send transaction") + } + } +} +``` + +Once we have set up `TransactionScreen`, the next is to create `SmartContractsScreen` for fetching ERC-20 token balance, and revoking approval. We'll utilize the `getTokenBalance` and `revokeApproval` methods from `MainViewModel` for the above functionality. + +```kotlin +@OptIn(ExperimentalPagerApi::class) +@Composable +fun SmartContractsScreen(viewModel: MainViewModel) { + val pagerState = rememberPagerState( 0) + val tabItems = listOf( + "Read from Contract", + "Write from Contract", + ) + + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "Smart Contract Interactions", style = Typography.headlineLarge, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(16.dp)) + Tabs(pagerState = pagerState, tabItems) + Spacer(modifier = Modifier.height(16.dp)) + ContractTabsContent(pagerState = pagerState, viewModel) + } +} + + +@Composable +@OptIn(ExperimentalPagerApi::class) +fun ContractTabsContent(pagerState: PagerState, viewModel: MainViewModel) { + HorizontalPager(state = pagerState, count = 2) { + page -> + when (page) { + 0 -> ReadContractView(viewModel = viewModel) + 1 -> WriteContractView(viewModel = viewModel) + } + } +} + +@Composable +fun ReadContractView(viewModel: MainViewModel) { + var contractAddressText by remember { mutableStateOf("0x10279e6333f9d0EE103F4715b8aaEA75BE61464C") } + val openAlertDialog = remember { mutableStateOf(false) } + var dialogText by remember { mutableStateOf("") } + + when { + openAlertDialog.value -> MinimalDialog(dialogText) { + openAlertDialog.value = false + } + } + + Column(modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp)) { + // Additional code + Button(onClick = { + // focus-start + // Retrieves ERC-20 token balance for the user's EOA address + viewModel.getTokenBalance(contractAddressText, onSuccess = { + balance, error -> + if(balance != null) { + dialogText = "Balance:\n$balance" + openAlertDialog.value = true + } else { + dialogText = "Error:\n$error" + openAlertDialog.value = true + } + }) + // focus-end + }, shape = RoundedCornerShape(4.dp), modifier = Modifier.fillMaxWidth()) { + Text("Fetch Balance") + } + } +} + +@Composable +fun WriteContractView(viewModel: MainViewModel) { + var contractAddressText by remember { mutableStateOf("0x10279e6333f9d0EE103F4715b8aaEA75BE61464C") } + var spenderAddressText by remember { mutableStateOf("") } + val openAlertDialog = remember { mutableStateOf(false) } + var dialogText by remember { mutableStateOf("") } + + when { + openAlertDialog.value -> MinimalDialog(dialogText) { + openAlertDialog.value = false + } + } + + Column(modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp)) { + // Additional code + Button(onClick = { + // focus-start + // Revokes the approval of ERC-20 token for respective spenderAddress. + viewModel.revokeApproval(contractAddressText, spenderAddressText, onRevoke = { + hash, error -> + if(hash != null) { + dialogText = "Hash:\n$hash" + openAlertDialog.value = true + } else { + dialogText = "Error:\n$error" + openAlertDialog.value = true + } + }) + // focus-end + }, shape = RoundedCornerShape(4.dp), modifier = Modifier.fillMaxWidth()) { + Text("Revoke Approval") + } + + } +} +``` + +## Conclusion + +Voila, you have build a Ethereum Web3 wallet. This guide only gives you an overview of how to create your wallet with Ethereum ecosystem support. The general idea of the guide can be used for any of the blockchain ecosystem. + +If you are interested in learning more about Web3Auth, please checkout our [documentation for Android](/docs/sdk/mobile/pnp/android). You can find the code used for the guide on our [examples repo](https://github.com/Web3Auth/web3auth-android-examples/tree/main/android-playground). diff --git a/src/pages/guides/erc20-paymaster.mdx b/src/pages/guides/erc20-paymaster.mdx new file mode 100644 index 00000000000..9c45ac1a35c --- /dev/null +++ b/src/pages/guides/erc20-paymaster.mdx @@ -0,0 +1,231 @@ +--- +title: Send your first transaction with ERC-20 Paymaster +image: 'img/guides/guides-banners/erc20-paymaster.png' +description: Learn how to use ERC-20 paymaster with Web3Auth Native Account Abstraction. +type: guide +tags: [embedded wallets, account abstraction, web, paymaster, erc4337, web3auth] +date: October 29, 2024 +author: Web3Auth Team +--- + +import SEO from '@site/src/components/SEO' +import TabItem from '@theme/TabItem' +import Tabs from '@theme/Tabs' + + + +A paymaster is a vital component in the ERC-4337 standard, responsible for covering transaction costs on behalf of the user. There are various types of paymasters, such as gasless paymasters, ERC-20 paymasters, and more. + +In this guide, we'll talk about how you can use the Pimlico's ERC-20 Paymaster with Web3Auth Account Abstraction Provider to allow your users to pay for their transactions using ERC-20 tokens. + +If you are looking to use sponsored paymaster, you can refer to the [sponsored paymaster guide](/docs/guides/sending-gasless-transaction). + +## Prerequisites + +- Pimlico Account: Since we'll be using the Pimlico paymaster, you'll need to have an API key from Pimlico. You can get a free API key from [Pimlico Dashboard](https://dashboard.pimlico.io/). +- Web3Auth Dashboard: If you haven't already, sign up on the Web3Auth platform. It is free and gives you access to the Web3Auth's base plan. Head to Web3Auth's documentation page for detailed [instructions on setting up the Web3Auth Dashboard](/docs/dashboard). +- Web3Auth PnP Web SDK: This guide assumes that you already know how to integrate the PnP Web SDK in your project. If not, you can learn how to [integrate Web3Auth in your Web app](/docs/sdk/web/react/). + +## Integrate AccountAbstractionProvider + +Once, you have set up the Web3Auth Dashboard, and created a new project, it's time to integrate Web3Auth Account Abstraction Provider in your Web application. For the implementation, we'll use the [@web3auth/account-abstraction-provider](https://www.npmjs.com/package/@web3auth/account-abstraction-provider). The provider simplifies the entire process by managing the complex logic behind configuring the account abstraction provider, bundler, and preparing user operations. + +If you are already using the Web3Auth Pnp SDK in your project, you just need to configure the AccountAbstractionProvider with the paymaster details, and pass it to the Web3Auth instance. No other changes are required. + +### Installation + +```bash +npm install --save @web3auth/account-abstraction-provider +``` + +### Configure ERC-20 Paymaster + +The `AccountAbstractionProvider` requires specific configurations to function correctly. One key configuration is the paymaster. Web3Auth supports custom paymaster configurations, allowing you to deploy your own paymaster and integrate it with the provider. + +You can choose from a variety of paymaster services available in the ecosystem. In this guide, we'll be configuring the Pimlico's ERC-20 Paymaster. However, it's important to note that paymaster support is not limited to the Pimlico, giving you the flexibility to integrate any compatible paymaster service that suits your requirements. + +To configure the ERC-20 Paymaster, you need to pass the `token` in the `paymasterContext` which allows you to specify the ERC-20 token that will be used to pay for the transaction. For this guide, we'll use the USDC token. [Find the USDC token address for your desired network](https://developers.circle.com/stablecoins/usdc-on-test-networks). + +For the simplicity, we have only use `SafeSmartAccount`, but you choose your favorite smart account provider from the available ones. [Learn how to configure the smart account](/docs/sdk/web/react/advanced/smart-accounts). + +```ts +// focus-start +import { + AccountAbstractionProvider, + SafeSmartAccount, +} from '@web3auth/account-abstraction-provider' +// focus-end + +const chainConfig = { + chainNamespace: CHAIN_NAMESPACES.EIP155, + chainId: '0xaa36a7', + rpcTarget: 'https://rpc.sepolia.org', + displayName: 'Ethereum Sepolia Testnet', + blockExplorerUrl: 'https://sepolia.etherscan.io', + ticker: 'ETH', + tickerName: 'Ethereum', + logo: 'https://cryptologos.cc/logos/ethereum-eth-logo.png', +} + +// focus-start +const accountAbstractionProvider = new AccountAbstractionProvider({ + config: { + chainConfig, + bundlerConfig: { + // Get the pimlico API Key from dashboard.pimlico.io + url: `https://api.pimlico.io/v2/${chainId}/rpc?apikey=${pimlicoAPIKey}`, + paymasterContext: { + // USDC address on Ethereum Sepolia + token: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238', + }, + }, + smartAccountInit: new SafeSmartAccount(), + paymasterConfig: { + // Get the pimlico API Key from dashboard.pimlico.io + url: `https://api.pimlico.io/v2/${chainId}/rpc?apikey=${pimlicoAPIKey}`, + }, + }, +}) +// focus-end +``` + +## Configure Web3Auth + +Once you have configured your `AccountAbstractionProvider`, you can now plug it in your Web3Auth Modal/No Modal instance. If you are using the external wallets like MetaMask, Coinbase, etc, you can define whether you want to use the AccountAbstractionProvider, or EthereumPrivateKeyProvider by setting the `useAAWithExternalWallet` in `IWeb3AuthCoreOptions`. + +If you are setting `useAAWithExternalWallet` to `true`, it'll create a new Smart Account for your user, where the signer/creator of the Smart Account would be the external wallet. + +If you are setting `useAAWithExternalWallet` to `false`, it'll skip creating a new Smart Account, and directly use the external wallet to sign the transactions. + + + + + +```ts +import { EthereumPrivateKeyProvider } from "@web3auth/ethereum-provider"; +import { Web3Auth } from "@web3auth/modal"; + +const privateKeyProvider = new EthereumPrivateKeyProvider({ + // Use the chain config we declared earlier + config: { chainConfig }, +}); + +const web3auth = new Web3Auth({ + clientId: "YOUR_WEB3AUTH_CLIENT_ID", + web3AuthNetwork: WEB3AUTH_NETWORK.SAPPHIRE_MAINNET, + privateKeyProvider, + // Use the account abstraction provider we configured earlier + accountAbstractionProvider + // This will allow you to use EthereumPrivateKeyProvider for + // external wallets, while use the AccountAbstractionProvider + // for Web3Auth embedded wallets. + useAAWithExternalWallet: false, +}); +``` + + + + + +```ts +import { Web3AuthNoModal } from "@web3auth/no-modal"; +import { EthereumPrivateKeyProvider } from "@web3auth/ethereum-provider"; +import { AuthAdapter } from "@web3auth/auth-adapter"; + +const privateKeyProvider = new EthereumPrivateKeyProvider({ + config: { chainConfig }, +}); + +const web3auth = new Web3AuthNoModal({ + clientId: "YOUR_WEB3AUTH_CLIENT_ID", + web3AuthNetwork: WEB3AUTH_NETWORK.SAPPHIRE_MAINNET, + privateKeyProvider, + // Use the account abstraction provider we configured earlier + accountAbstractionProvider + // This will allow you to use EthereumPrivateKeyProvider for + // external wallets, while use the AccountAbstractionProvider + // for Web3Auth embedded wallets. + useAAWithExternalWallet: false, +}); + +const authadapter = new AuthAdapter(); +web3auth.configureAdapter(authadapter); +``` + + + + +## Send a transaction + +To submit the transaction using ERC-20 paymaster, we'll require to first need to approve ERC-20 token to be used by the paymaster. Ideally, we should first check the token allowance, and only provide approve allowance to be used by the paymaster. + +To modify the token allowance, you'll need to perform a write operation on the ERC-20 contract. In the example below, we're using Pimlico, but be sure to update the paymaster and ERC-20 token addresses according to your specific case. + +Since, we want to perform the approval transaction, and send transaction in a single call, we'll use batch transaction feature of the `AccountAbstractionProvider`. Performing a batch transaction differs slightly from the normal flow. + +To execute a batch transaction, you'll need to use the `BundlerClient` generated by the `AccountAbstractionProvider`. The Web3Auth instance provider can't be used for this, as it's a proxy provider designed to ensure compatibility with your preferred signer package for basic operations. + +Please make sure you have enough USDC balance in the wallet to pay the transaction fees. [Request faucet for USDC tokens](https://faucet.circle.com/). + +```ts +// Use the same accountAbstractionProvider we created earlier. +const bundlerClient = accountAbstractionProvider.bundlerClient! +const smartAccount = accountAbstractionProvider.smartAccount! + +// Pimlico's ERC-20 Paymaster address +const pimlicoPaymasterAddress = '0x0000000000000039cd5e8aE05257CE51C473ddd1' + +// USDC address on Ethereum Sepolia +const usdcAddress = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238' + +// 0.00001 ETH in WEI format +const amount = 10000000000000n + +// 10 USDC in WEI format. Since USDC has 6 decimals, 10 * 10^6 +const approvalAmount = 10000000n + +const userOpHash = await bundlerClient.sendUserOperation({ + account: smartAccount, + calls: [ + // Approve USDC on Sepolia chain for Pimlico's ERC 20 Paymaster + { + to: usdcAddress, + abi: parseAbi(['function approve(address,uint)']), + functionName: 'approve', + args: [pimlicoPaymasterAddress, approvalAmount], + }, + { + to: 'DESTINATION_ADDRESS', + value: amount, + data: '0x', + }, + { + to: 'DESTINATION_ADDRESS', + value: amount, + data: '0x', + }, + ], +}) + +// Retrieve user operation receipt +const receipt = await bundlerClient.waitForUserOperationReceipt({ + hash: userOpHash, +}) + +const transactionHash = receipt.receipt.transactionHash +``` + +## Conclusion + +Voila, you have successfully sent your first transaction using the Pimlico's ERC-20 paymaster with Web3Auth Account Abstraction Provider. To learn more about advance features of the Account Abstraction Provider like performing batch transactions, using sponsored paymaster you can refer to the [Account Abstraction Provider](/docs/sdk/web/react) documentation. diff --git a/src/pages/guides/flutter-wallet.mdx b/src/pages/guides/flutter-wallet.mdx new file mode 100644 index 00000000000..97a3bb4514d --- /dev/null +++ b/src/pages/guides/flutter-wallet.mdx @@ -0,0 +1,925 @@ +--- +title: Create a Chain Agnostic Web3 wallet in Flutter +image: 'img/guides/guides-banners/flutter-wallet.png' +description: Empower your Flutter app with a Chain Agnostic Web3 wallet using the Web3Auth PnP SDK. +type: guide +tags: [embedded wallets, flutter, andriod, ios, evm, solana, web3auth] +date: April 22, 2024 +author: Web3Auth Team +--- + +import SEO from '@site/src/components/SEO' +import TabItem from '@theme/TabItem' +import Tabs from '@theme/Tabs' +import WalletPreview from '@site/static/img/guides/flutter-wallet-preview.png' + + + +In this guide, we'll talk about how we can use Web3Auth to build your chain-agnostic Web3 wallet in Flutter. The wallet will support the Ethereum and Solana ecosystem. + +As an overview, the app is quite simple, with functionality to log in, display user details, and perform blockchain interactions. The signing of the blockchain transactions is done through the Web3Auth embedded wallet. You can check out the infrastructure docs, ["Web3Auth Wallet Management Infrastructure"](/docs/infrastructure) for a high-level overview of the Web3Auth architecture and implementation. For those who want to skip straight to the code, you can find it on [GitHub](https://github.com/Web3Auth/web3auth-flutter-examples/tree/main/flutter-playground). + +Here are few screenshots of the application. + +Flutter Wallet Screenshots + +## How to set up Web3Auth Dashboard + +If you haven't already, sign up on the Web3Auth platform. It is free and gives you access to the Web3Auth's base plan. After the basic setup, explore other features and functionalities offered by the Web3Auth Dashboard. It includes custom verifiers, whitelabeling, analytics, and more. Head to [Web3Auth's documentation](/docs/dashboard) page for detailed instructions on setting up the Web3Auth Dashboard. + +## Integrating Web3Auth in Flutter + +Once, you have set up the Web3Auth Dashboard, and created a new project, it's time to integrate Web3Auth in your Flutter application. For the implementation, we'll use the "web3auth_flutter package". This package facilitates integration with Web3Auth. This way you can easily manage embedded wallet in your Flutter application. + +### Installation + +To install the web3auth_flutter package, you have two options. You can either manually add the package in the `pubspec.yaml` file, or you can use the `flutter pub add` command. + + + + +Add `web3auth_flutter` using `flutter pub add` command. + +```sh +flutter pub add web3auth_flutter +``` + + + + +Add `web3auth_flutter` as a dependency to your `pubspec.yaml`. + +```yaml +dependencies: + web3auth_flutter: ^6.1.2 +``` + + + + +### Initialization + +After successfully installing the package, the next step is to initialize Web3Auth in your Flutter app. This sets up the necessary configurations using Client Id and prepares Web3Auth. [Learn more about Web3Auth Initialization](/docs/sdk/mobile/pnp/flutter/initialize). + +```dart +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Addditional code + + final Uri redirectUrl; + if (Platform.isAndroid) { + redirectUrl = + Uri.parse('w3aexample://com.example.flutter_solana_example/auth'); + } else { + redirectUrl = Uri.parse('com.web3auth.fluttersolanasample://auth'); + } + + await Web3AuthFlutter.init( + Web3AuthOptions( + clientId: "YOUR_WEB3AUTH_CLIENT_ID", + network: Network.sapphire_mainnet, + redirectUrl: redirectUrl, + ), + ); + + await Web3AuthFlutter.initialize(); + + runApp(const MainApp()); +} +``` + +### Session Management + +To check whether the user is authenticated, you can use the `getPrivateKey` or `getEd25519PrivKey` method. For a user already authenticated, the result would be a non-empty `String`. You can navigate to different views based on the result. If the user is already authenticated, we'll navigate them to `HomeScreen`. In case of no active session, we'll navigate to `LoginScreen` to authenticate again. [Learn more about Web3Auth session management](/docs/features/session-management). + +```dart +class MainApp extends StatefulWidget { + const MainApp({super.key}); + + @override + State createState() => _MainAppState(); +} + +class _MainAppState extends State { + late final Future privateKeyFuture; + @override + void initState() { + super.initState(); + privateKeyFuture = Web3AuthFlutter.getEd25519PrivKey(); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: FutureBuilder( + future: privateKeyFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.hasData) { + if (snapshot.data!.isNotEmpty) { + return const HomeScreen(); + } + } + return const LoginScreen(); + } + return const Center( + child: CircularProgressIndicator.adaptive(), + ); + }, + ), + ); + } +} +``` + +### Authentication + +If the user is not authenticated, you should utilize the `login` method. For the Wallet, we will add two login options, Google, and Email Passwordless login. In Web3Auth, you can choose between a Single Page Authentication flow or a Regular Web Application flow. For this guide, we'll be using a Single Page Authentication flow. We'll create a helper function, `_login` inside `LoginScreen`. The login method is pretty straightforward in Web3Auth and takes `LoginParams` as input. After successfully logging in, we'll navigate the user to `HomeScreen`. + +Learn more about [Web3Auth LoginParams](/docs/sdk/mobile/pnp/flutter/usage#arguments). + +```dart +class _LoginScreenState extends State with WidgetsBindingObserver { + // Additional Code + + @override + void didChangeAppLifecycleState(final AppLifecycleState state) { + // This is important to trigger the user cancellation on Android. + if (state == AppLifecycleState.resumed) { + Web3AuthFlutter.setCustomTabsClosed(); + } + } + + @override + Widget build(BuildContext context) { + // Login View + } + + Future _login(BuildContext context) async { + try { + // Validate the form, and TextField. In case of invalide + // form state, return back. + if (!formKey.currentState!.validate()) { + return; + } + + // It can be used to set the OAuth login options for corresponding + // loginProvider. For instance, you'll need to pass user's email address as + // login_hint when the Provider is email_passwordless. + await Web3AuthFlutter.login( + LoginParams( + loginProvider: Provider.email_passwordless, + mfaLevel: MFALevel.DEFAULT, + extraLoginOptions: ExtraLoginOptions( + login_hint: emailController.text, + ), + ), + ); + + // If login is successful, navigate user to HomeScreen. + if (context.mounted) { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (context) { + return const HomeScreen(); + }), + ); + } + } catch (e, _) { + if (context.mounted) { + showInfoDialog(context, e.toString()); + } + } + } +} +``` + +## Set up Blockchain Providers + +Once we have successfully authenticated the user, the next step would be to fetch the user details, retrieve wallet address and prepare blockchain providers for interactions. For this guide, we are supporting only Ethereum and Solana ecosystem, but the general idea can be used for any blockchain ecosystem. + +Given that the project follows clean architecture and Test-Driven Development (TDD) principles, we'll want to create an abstract layer to interact with the Blockchain providers. This abstraction will help us easily expand the blockchain support while isolate it from the rest of the application. + +For interacting ethereum chains, we'll use the [web3dart](https://pub.dev/packages/web3dart) package. Similary for solana, we'll use the [solana](https://pub.dev/packages/solana) package. To install the packages, you have two options. You can either manually add the packages in the `pubspec.yaml` file, or you can use the `flutter pub add` command. + + + + +Add `web3dart` and `solana` using `flutter pub add` command. + +```sh +flutter pub add web3dart +flutter pub add solana +``` + + + + +Add `web3dart` and `solana` as a dependency to your `pubspec.yaml`. + +```yaml +dependencies: + web3dart: ^2.7.3 + solana: ^0.30.4 +``` + + + + +After successfully installing both packages, it's time to set up our Blockchain provider. First, we'll create a new class, `ChainProvider`, which will used as a base class for `EthereumProvider` and `SolanaProvider`. If you wish to support any additional ecosystem, you can extend the `ChainProvider` and implement the methods. + +If you want to learn, how you can integrate different blockchain with Web3Auth, you can checkout our [Connect Blockchain resources](/docs/connect-blockchain/). + +```dart +abstract class ChainProvider { + Future getBalance(String address); + Future sendTransaction(String to, double amount); + Future signMessage(String messsage); + Future readContract( + String address, + String function, + List params, + ); + + Future writeContract( + String address, + String function, + List params, + ); +} +``` + +Generally, for any blockchain provider, you'll only require the `getBalance`, `sendTransaction`, and `signMessage`. The `readContract` and `writeContract` can be used to interact with SmartContract. + +### Ethereum Provider + +Once we have our base class, we'll create `EthereumProvider` and implement the methods. To create the `Web3Client` instance, you'll require the rpcTarget URL. If you are using public RPCs, you can face some network congestion. It's ideal to use paid RPCs for production. + +The `readContract`, and `writeContract` methods are used to interact with smart contracts on Ethereum ecosystem. The `readContract` is used to read the data from the smart contracts, where as the `writeContract` is used to write data on smart contract. + +```dart +class EthereumProvider extends ChainProvider { + final Web3Client web3client; + + EthereumProvider({required String rpcTarget}) + : web3client = Web3Client( + rpcTarget, + Client(), + ); + + @override + Future getBalance(String address) async { + final balance = await web3client.getBalance( + EthereumAddress.fromHex(address), + ); + + // The result we from Web3Client is in wei, the smallest value. To convert + // the value to ether, you can divide it with 10^18, where 18 denotes the + // decimals for wei. + // + // For the sample, we'll use a helper function from web3dart package which + // has the same implementation. + return balance.getValueInUnit(EtherUnit.ether).toStringAsFixed(4); + } + + @override + Future sendTransaction(String to, double amount) async { + final Credentials credentials = await _prepareCredentials(); + final amountInWei = amount * pow(10, 18); + final Transaction transaction = Transaction( + to: EthereumAddress.fromHex(to), + value: EtherAmount.fromBigInt( + EtherUnit.wei, + BigInt.from(amountInWei), + ), + ); + + final hash = await web3client.sendTransaction( + credentials, + transaction, + chainId: null, + fetchChainIdFromNetworkId: true, + ); + return hash; + } + + @override + Future signMessage(String messsage) async { + final Credentials credentials = await _prepareCredentials(); + final signBytes = credentials.signPersonalMessageToUint8List( + Uint8List.fromList(messsage.codeUnits), + ); + + return bytesToHex(signBytes); + } + + // Prepares the Credentials used for signing the message, + // and transaction on EVM chains. EVM ecosystem uses the + // scep2561k curve. You can use the Web3AuthFlutter.getPrivKey + // to retrieve the scep2561K compatible private key. + Future _prepareCredentials() async { + final privateKey = await Web3AuthFlutter.getPrivKey(); + final Credentials credentials = EthPrivateKey.fromHex(privateKey); + return credentials; + } + + @override + Future readContract( + String address, + String function, + List params, + ) async { + // For this sample, we are using the ERC 20 Contract. The same can be + // used for any of the EVM smart contract. + final contract = DeployedContract( + ContractAbi.fromJson(erc20Abi, 'Contract'), + EthereumAddress.fromHex(address), + ); + + final readFunction = contract.function(function); + final result = await web3client.call( + contract: contract, + function: readFunction, + params: params, + ); + + return result; + } + + @override + Future writeContract(String address, String function, List params) async { + // For this sample, we are using the ERC 20 Contract. The same can be + // used for any of the EVM smart contract. + final contract = DeployedContract( + ContractAbi.fromJson(erc20Abi, 'Contract'), + EthereumAddress.fromHex(address), + ); + + final writeFunction = contract.function(function); + final Credentials credentials = await _prepareCredentials(); + final result = await web3client.sendTransaction( + credentials, + Transaction.callContract( + contract: contract, + function: writeFunction, + parameters: params, + ), + chainId: null, + fetchChainIdFromNetworkId: true, + ); + + return result; + } +} +``` + +### Solana Provider + +After `EthereumProvider`, it's time to extend `ChainProvider` and create `SolanaProvider`. For `SolanaProvider`, we'll only implement the `getBalance`, `sendTransaction`, and `signMessage`. We'll also add `_generateKeyPair()`, a helper method to create `Ed25519HDKeyPair`. It's used to sign the transactions and messages on Solana ecosystem. Since, Solana uses `ed25519` curve, we can utilize the `Web3AuthFlutter.getEd25519PrivKey`. + +```dart +class SolanaProvider extends ChainProvider { + final SolanaClient solanaClient; + + SolanaProvider({required String rpcTarget, required String wss}) + : solanaClient = SolanaClient( + rpcUrl: Uri.parse(rpcTarget), + websocketUrl: Uri.parse(wss), + ); + + @override + Future getBalance(String address) async { + final balanceResponse = await solanaClient.rpcClient.getBalance( + address, + ); + + /// We are dividing the balance by 10^9, because Solana's + /// token decimals is set to be 9; + return (balanceResponse.value / pow(10, 9)).toString(); + } + + @override + Future sendTransaction(String to, double amount) async { + final Ed25519HDKeyPair ed25519hdKeyPair = await _generateKeyPair(); + + /// Converting user input to the lamports, which are smallest value + /// in Solana. + final num lamports = amount * pow(10, 9); + final transactionHash = await solanaClient.transferLamports( + source: ed25519hdKeyPair, + destination: Ed25519HDPublicKey.fromBase58(to), + lamports: lamports.toInt(), + ); + + return transactionHash; + } + + @override + Future signMessage(String messsage) async { + final Ed25519HDKeyPair ed25519hdKeyPair = await _generateKeyPair(); + + final signatrure = await ed25519hdKeyPair.sign( + ByteArray.fromString(messsage), + ); + return signatrure.toBase58(); + } + + Future _generateKeyPair() async { + final privateKey = await Web3AuthFlutter.getEd25519PrivKey(); + return await Ed25519HDKeyPair.fromPrivateKeyBytes( + privateKey: privateKey.hexToBytes.take(32).toList(), + ); + } + + @override + Future readContract( + String address, + String function, + List params, + ) { + // TODO: implement readContract + throw UnimplementedError(); + } + + @override + Future writeContract(String address, String function, List params) { + // TODO: implement writeContract + throw UnimplementedError(); + } +} +``` + +## Set up Supported Chains + +After having our blockchain proivders in place, the next step on the list to define the supported chains. To keep things simple, we'll simply a create a new file `chain_configs` with list of Map to define the supported chains. + +For the guide, we have added the support for Ethereum Sepolia, Ethereum Mainnet, Polygon Mainnet, Polygon Amoy, and Solana devnet. If you wish to support more chains in your wallet, you can simply add the config with the required details in the list below. + +```dart +import 'package:web3auth_flutter/enums.dart'; + +final chainConfigs = [ + { + "chainNamespace": ChainNamespace.eip155.name, + "chainId": "0xaa36a7", + "displayName": "Ethereum Sepolia", + "ticker": "ETH", + "rpcTarget": "https://rpc.sepolia.org", + "blockExplorerUrl": "https://sepolia.etherscan.io", + "logo": "https://web3auth.io/images/web3authlog.png", + "wss": '', + }, + { + "chainNamespace": ChainNamespace.eip155.name, + "chainId": "0x1", + "displayName": "Ethereum Mainnet", + "rpcTarget": "https://rpc.ethereum.org", + "blockExplorerUrl": "https://etherscan.io", + "ticker": "ETH", + "logo": "https://web3auth.io/images/web3authlog.png", + "wss": '', + }, + { + "chainNamespace": ChainNamespace.eip155.name, + "chainId": "0x89", + "rpcTarget": "https://polygon-rpc.com", + "displayName": "Polygon Mainnet", + "blockExplorerUrl": "https://polygonscan.com", + "ticker": "POL", + "logo": "https://web3auth.io/images/web3authlog.png", + "wss": '', + }, + { + "chainNamespace": ChainNamespace.eip155.name, + "chainId": "80002", + "rpcTarget": "https://rpc-amoy.polygon.technology", + "displayName": "Polygon Amoy Testnet", + "blockExplorerUrl": "https://www.oklink.com/amoy", + "ticker": "POL", + "logo": "https://web3auth.io/images/web3authlog.png", + "wss": '', + }, + { + "chainNamespace": ChainNamespace.solana.name, + "chainId": "devnet", + "rpcTarget": "https://api.devnet.solana.com", + "displayName": "Solana Devnet", + "blockExplorerUrl": "https://explorer.solana.com/?cluster=devnet/", + "ticker": "SOL", + "logo": "https://web3auth.io/images/web3authlog.png", + "wss": "ws://api.devnet.solana.com" + }, +]; +``` + +Once, we have defined the supported chains, create a new model `ChainConfig`, to represent the Dart object for the above chain config map. We'll use the `ChainConfig` model for UI purposes and chain interaction. + +In the `ChainConfig,` we'll also add a `isEVM` parameter to help us differentiate the selected chain ecosystem. If `isEVM` is true for the selected chain, we can use `EthereumProvider` for chain interactions, or else we can use the `SolanaProvider`. + +```dart +import 'package:flutter_playground/features/home/domain/entities/chain_config.dart'; +import 'package:web3auth_flutter/enums.dart'; + +class ChainConfigModel extends ChainConfig { + ChainConfigModel({ + required super.chainNamespace, + required super.displayName, + required super.ticker, + required super.rpcTarget, + required super.logo, + required super.blockExplorerUrl, + required super.chainId, + required super.isEVMChain, + required super.wss, + }); + + factory ChainConfigModel.fromJson(Map json) { + final nameSpace = ChainNamespace.values.byName(json['chainNamespace']!); + final isEVM = nameSpace == ChainNamespace.eip155; + return ChainConfigModel( + isEVMChain: isEVM, + chainNamespace: nameSpace, + displayName: json['displayName']!, + ticker: json['ticker']!, + rpcTarget: json['rpcTarget']!, + logo: json['logo'], + blockExplorerUrl: json['blockExplorerUrl']!, + chainId: json['chainId']!, + wss: json['wss']!, + ); + } +} +``` + +## Wallet Implementation + +Once, we have set up the providers, and supported chains, it's time to integrate and plug them into the wallet. For this guide, we are using the `get_it` package for service locator abilities. It will help us with the dependency injection. + +### Service Locator + +Let's create a new `ServiceLocator` class, and set up the `ChainConfigDataSource` and `ChainConfigRepository`. The `ChainConfigRepository` is responsible for converting the list of chain configs map we defined earlier into a list of `ChainConfig` models and inject into UI. As said earlier, for simplicity we are maintaining the list of chain configs on the frontend, but using `ChainConfigRepository` you can get the list from the server as well. + +Checkout the implementation of `ChainConfigDataSource` and `ChainConfigRepository` for more details. + +```dart +class ServiceLocator { + ServiceLocator._(); + + static GetIt get getIt => GetIt.instance; + + static void setUp() { + getIt.registerLazySingleton( + () => ChainConfigDataSourceImpl(chainConfigs: chainConfigs), + ); + + getIt.registerLazySingleton( + () => ChainConfigRepositoryImp(getIt()), + ); + } +} +``` + +After successfully setting up the `ServiceLocator`, initialize it in the `main` function above `Web3AuthFlutter` initiliation. + +```dart +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + ServiceLocator.setUp(); + + // Additional Web3AuthFlutter initiliation code. +} +``` + +### Set up Home Provider + +Once we have set up service locator, the next on list is to create a `ChangeNotifier` to help us maange the state of the wallet. The notifier will help us manage the state of currently selected chain, and access the respective chain provider. For the state management, we will be using the `provider` package, so make sure to add `provider` as a dependency. + +```dart +class HomeProvider with ChangeNotifier { + late ChainConfig _selectedChain; + late List _chains; + late String _chainAddress; + + ChainConfig get selectedChain => _selectedChain; + List get chains => _chains; + String get chainAddress => _chainAddress; + + HomeProvider(List chains) { + _selectedChain = chains.first; + _chains = List.from(chains); + } + + /// Update the selected chain + void updateSelectedChain(ChainConfig chain) { + _selectedChain = chain; + notifyListeners(); + } + + /// Update the chain address for corresponding + /// selected chain. + void updateChainAddress(String address) { + _chainAddress = address; + } + + /// Add a new custom EVM chain on runtime. + void addNewChain(ChainConfig newChain) { + _chains.add(newChain); + notifyListeners(); + } +} +``` + +To access the blockchain provider for currently selected chain, we will create a new extension on `ChainConfig`. + +```dart +extension ChainConfigExtension on ChainConfig { + ChainProvider prepareChainProvider() { + if (isEVMChain) { + return EthereumProvider(rpcTarget: rpcTarget); + } else { + return SolanaProvider(rpcTarget: rpcTarget, wss: wss); + } + } +} +``` + +### Setting up Home screen + +Once, we have our provider ready, we create a new `HomeScreen` widget to show user details as email address, wallet address, user's balance for selectedChain, and blockchain interaction methods. We'll retrieve the `ChainConfigRepository` using `ServiceLocator`, and initialize our `HomeProvider`. + +To get the user's balance, we'll use `prepareAccount` method from the `ChainConfigRepository`. The method internally uses `ChainProvider` to retrieve user's wallet address, and fetch the wallet balance for the address. Checkout `ChainConfigRepository` implementation for more details. The methods returns `Account` object which has the above details. + +Checkout `Account` data model below. + +```dart +class Account { + final Ed25519HDKeyPair? solanaKeyPair; + final Credentials? ethereumKeyPair; + final String balance; + final String publicAddress; + + Account({ + this.solanaKeyPair, + this.ethereumKeyPair, + required this.balance, + required this.publicAddress, + }); +} +``` + +Once, we have retrieve the `ChainConfigRepository` in `init` method of `HomeScren`, we'll invoke the `prepareAccount`, and pass the `Account` instance to `StreamController` which is used for data flow in the application. + +```dart +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + late final ChainConfigRepository chainConfigRepository; + late final TorusUserInfo userInfo; + + late final StreamController streamController; + late final HomeProvider homeProvider; + + @override + void initState() { + super.initState(); + chainConfigRepository = ServiceLocator.getIt(); + + streamController = StreamController(); + homeProvider = Provider.of( + context, + listen: false, + ); + loadAccount(false); + } + + @override + void dispose() { + super.dispose(); + } + + // loadAccount function is used to fetch the account + // details such as balance, user address, and private key + // for currently selected chain. + Future loadAccount(bool isReload) async { + if (!isReload) { + userInfo = await Web3AuthFlutter.getUserInfo(); + } + + final account = await chainConfigRepository.prepareAccount( + homeProvider.selectedChain, + ); + + homeProvider.updateChainAddress(account.publicAddress); + // We streamController to control data flow in the application. + streamController.add(account); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text(StringConstants.appBarTitle), + ), + drawer: const SideDrawer(), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16), + child: StreamBuilder( + stream: streamController.stream, + builder: (context, snapShot) { + // Check if the AsyncSnapshot is in active connection, + // and if it's true, build the UI. + if (snapShot.connectionState == ConnectionState.active) { + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 24), + const HomeHeader(), + const SizedBox(height: 12), + // Helps users to switch chain in the wallet. + ChainSwitchTile( + onSelect: (chainConfig) { + homeProvider.updateSelectedChain(chainConfig); + loadAccount(true); + }, + ), + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 16), + // Displays user details, such as email, + // user name, and logo. + AccountDetails( + userInfo: userInfo, + account: snapShot.requireData, + ), + const SizedBox(height: 24), + Consumer(builder: ( + _, + homeProvider, + __, + ) { + final chain = homeProvider.selectedChain; + // Displays user balance. + return BalanceWidget( + balance: snapShot.data!.balance, + ticker: chain.ticker, + chainId: chain.chainId, + ); + }), + const SizedBox(height: 16), + Consumer(builder: (_, __, ___) { + return Column( + children: [ + CustomTextButton( + onTap: () { + _navigationToScreen( + context, + const TransactionsScreen(), + ); + }, + text: 'Transaction', + ), + + // Disable the SmartContractInteractionScreen for + // non evm chains. + if (homeProvider.selectedChain.isEVMChain) ...[ + const SizedBox(height: 16), + CustomTextButton( + onTap: () { + _navigationToScreen( + context, + const SmartContractInteractionScreen(), + ); + }, + text: + StringConstants.smartContractInteractionsText, + ), + ] + ], + ); + }), + ], + ), + ); + } + return const Center(child: CircularProgressIndicator.adaptive()); + }, + ), + ), + ); + } + + // Helper function to navigate to different screens. + void _navigationToScreen(BuildContext context, Widget screen) { + Navigator.of(context).push(MaterialPageRoute(builder: (_) { + return screen; + })); + } +} +``` + +In `HomeScreen` we'll also give users an option to logout from the wallet in navigation drawer. To do so, we'll utilize the `Web3AuthFlutter.logout`. Upon success, we'll navigate users back to `LoginScreen`. Checkout `SideDrawer` widget for navigation drawer implementation. + +### Chain Interactions + +Once we have setup `HomeScreen`, the next step is to setup chain interactions for signing message, signing transaction, reading from contracts, and writing on contracts. For signing message and transaction, we'll create a new `TransactionsScreen` widget and utilize `signMessage` and `sendTransaction` from `ChainProvider` for respective functionality. + +To retrieve currently selected chain, and respective provider we'll use the `HomeProvider`. + +```dart +class TransactionsScreen extends StatefulWidget { + const TransactionsScreen({super.key}); + + @override + State createState() => _TransactionsScreenState(); +} + +class _TransactionsScreenState extends State { + // Additional variable initiliation + + @override + void initState() { + super.initState(); + selectedChain = context.read().selectedChain; + chainProvider = selectedChain.prepareChainProvider(); + // Additional code + } + + @override + Widget build(BuildContext context) { + // Additiona UI code. + // Checkout GitHub repo for full code. + } + + Future _signMessage(BuildContext context) async { + try { + showLoader(context); + final signature = await chainProvider.signMessage( + signMessageTextController.text, + ); + if (context.mounted) { + removeDialog(context); + showInfoDialog(context, signature); + } + } catch (e, _) { + log(e.toString(), stackTrace: _); + if (context.mounted) { + removeDialog(context); + showInfoDialog(context, e.toString()); + } + } + } + + Future _sendTransaction(BuildContext context) async { + try { + showLoader(context); + final amount = double.parse(amountTextController.text); + final hash = await chainProvider.sendTransaction( + destinationTextController.text, + amount, + ); + + if (context.mounted) { + removeDialog(context); + showInfoDialog(context, hash); + } + } catch (e, _) { + log(e.toString(), stackTrace: _); + if (context.mounted) { + removeDialog(context); + showInfoDialog(context, e.toString()); + } + } + } +} +``` + +## Conclusion + +Voila, you have build a chain agnostic Web3 wallet. This guide only gives you an overview of how to create your wallet with EVM and Solana ecosystem support. The general idea of the guide can be used for any of the blockchain ecosystem. + +If you are interested in learning more about Web3Auth, please checkout our [documentation for Flutter](/docs/sdk/mobile/pnp/flutter). You can find the code used for the guide on our [examples repo](https://github.com/Web3Auth/web3auth-flutter-examples/tree/main/flutter-playground). diff --git a/src/pages/guides/pnp-no-modal-multichain.mdx b/src/pages/guides/pnp-no-modal-multichain.mdx new file mode 100644 index 00000000000..b0f250e4be7 --- /dev/null +++ b/src/pages/guides/pnp-no-modal-multichain.mdx @@ -0,0 +1,606 @@ +--- +title: Create a Chain Agnostic Web3 Wallet with Web3Auth +image: 'img/guides/guides-banners/multi.png' +description: Learn how to create a chain-agnostic Web3 wallet using Web3Auth. +type: guide +tags: [embedded wallets, web, multi chain, polkadot, evm, cosmos, web3auth] +date: February 9, 2024 +author: Web3Auth Team +communityPortalTopicId: +--- + +import SEO from '@site/src/components/SEO' +import TabItem from '@theme/TabItem' +import Tabs from '@theme/Tabs' + + + +This guide will cover the basics of how to integrate Web3Auth with different blockchains at the same time. In this demo, you will be able to authenticate with different social logins and get different addresses from each blockchain. Of course, you can interact and sign transactions with any of them. + +You will be able to make calls like get the user's `account`, fetch `balance`, `sign message`, `send transaction`, `read` from and `write` to smart contracts, etc. + +**Web3Auth is designed to support any blockchain that follows the `secp256k1` & `ed25519` curves**. This means it works seamlessly with all EVMs such as Ethereum, Polygon, Binance Smart Chain, and others. Additionally, it supports non-EVM blockchains like Aptos, Cosmos, Polkadot, Solana, Tezos, Bitcoin, among many others. Web3Auth is not limited to these examples and is capable of integrating with any blockchain that adheres to these cryptographic standards, offering a wide range of compatibility to suit various needs and preferences in the blockchain ecosystem. + +## Quick Start + +You can run the following command or you can check the [full example](https://github.com/Web3Auth/web3auth-pnp-examples/tree/main/custom-authentication/multi-chain-example) in our Github. + +```bash +npx degit Web3Auth/web3auth-pnp-examples/custom-authentication/multi-chain-example/ w3a-multi-chain-demo && cd w3a-multi-chain-demo && npm install && npm run dev +``` + +## Prerequisites + +- For Web Apps: A basic knowledge of JavaScript is required to use Web3Auth SDK. + +- For Mobile Apps: For the Web3Auth Mobile SDKs, you have a choice between iOS, Android, React Native & Flutter. Please refer to the [Web3Auth SDK Reference](/sdk) for more information. + +- Create a Web3Auth account on the [Web3Auth Dashboard](https://dashboard.web3auth.io) + +## How to set up Web3Auth Dashboard + +If you haven't already, sign up on the Web3Auth platform. It is free and gives you access to the Web3Auth's base plan. After the basic setup, explore other features and functionalities offered by the Web3Auth Dashboard. It includes custom verifiers, whitelabeling, analytics, and more. Head to [Web3Auth's documentation](/docs/dashboard) page for detailed instructions on setting up the Web3Auth Dashboard. + +## Using Web3Auth with Multiple Blockchains + +To use Web3Auth with multiple blockchains, you need to set up your React application with the Web3Auth SDK. This guide will walk you through the setup and implementation. + +#### Setting up your base project for using Web3 libraries: + +If you are starting from scratch, to set up this project locally, you will need to create a base Web application, where you can install the required dependencies. However, while working with Web3, there are a few base libraries, which need additional configuration. This is because certain packages are not available in the browser environment, and we need to polyfill them manually. You can follow [this documentation](/troubleshooting/webpack-issues) where we have mentioned the configuration changes for some popular frameworks for your reference. + +### Installation + +We need several dependencies to make this work with multiple blockchains: + +```bash npm2yarn +npm install @web3auth/modal @solana/web3.js ethers @taquito/signer @taquito/taquito @taquito/utils @polkadot/api @polkadot/util-crypto tweetnacl +``` + +### Setting up Web3Auth Provider + +First, create a configuration file for Web3Auth (e.g., `web3authContext.tsx`): + +```tsx +import { WEB3AUTH_NETWORK, type Web3AuthOptions } from '@web3auth/modal' + +// Get your client ID from https://dashboard.web3auth.io +const clientId = import.meta.env.VITE_WEB3AUTH_CLIENT_ID || '' + +// Instantiate SDK +const web3AuthOptions: Web3AuthOptions = { + clientId, + web3AuthNetwork: WEB3AUTH_NETWORK.SAPPHIRE_MAINNET, +} + +const web3AuthContextConfig = { + web3AuthOptions, +} + +export default web3AuthContextConfig +``` + +Then, set up the Web3Auth provider in your application entry point (`index.tsx` or similar): + +```tsx +import ReactDOM from 'react-dom/client' +// Setup Web3Auth Provider +import { Web3AuthProvider } from '@web3auth/modal/react' +import web3AuthContextConfig from './web3authContext' + +import App from './App' + +const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement) +root.render( + + + +) +``` + +### Implementing the Multi-Chain Wallet + +Now, let's implement the main application with support for multiple blockchains. We'll create RPC modules for each blockchain and use them in our app. + +#### Setting up the Main App Component + +```tsx +import './App.css' +import { + useWeb3AuthConnect, + useWeb3AuthDisconnect, + useWeb3AuthUser, + useWeb3Auth, +} from '@web3auth/modal/react' + +// Import RPC functions +import { + getEthereumAccounts, + getEthereumBalance, + signEthereumMessage, + sendEthereumTransaction, +} from './RPC/ethersRPC' +import { + getSolanaAccount, + getSolanaBalance, + signSolanaMessage, + sendSolanaTransaction, +} from './RPC/solanaRPC' +import { + getTezosAccount, + getTezosBalance, + signTezosMessage, + signAndSendTezosTransaction, +} from './RPC/tezosRPC' +import { + getPolkadotAccounts, + getPolkadotBalance, + signAndSendPolkadotTransaction, +} from './RPC/polkadotRPC' + +function App() { + const { + connect, + isConnected, + loading: connectLoading, + error: connectError, + } = useWeb3AuthConnect() + const { disconnect, loading: disconnectLoading, error: disconnectError } = useWeb3AuthDisconnect() + const { userInfo } = useWeb3AuthUser() + const { provider } = useWeb3Auth() + + const getAllAccounts = async () => { + if (!provider) { + uiConsole('provider not initialized yet') + return + } + const eth_address = await getEthereumAccounts(provider) + const solana_address = await getSolanaAccount(provider) + const tezos_address = await getTezosAccount(provider) + const polkadot_address = await getPolkadotAccounts(provider) + + uiConsole( + 'Ethereum Address: ' + eth_address, + 'Solana Address: ' + solana_address, + 'Tezos Address: ' + tezos_address, + 'Polkadot Address: ' + polkadot_address + ) + } + + const getAllBalances = async () => { + if (!provider) { + uiConsole('provider not initialized yet') + return + } + + const eth_balance = await getEthereumBalance(provider) + const solana_balance = await getSolanaBalance(provider) + const tezos_balance = await getTezosBalance(provider) + const polkadot_balance = await getPolkadotBalance(provider) + + uiConsole( + 'Ethereum Balance: ' + eth_balance, + 'Solana Balance: ' + solana_balance, + 'Tezos Balance: ' + tezos_balance, + 'Polkadot Balance: ' + polkadot_balance + ) + } + + function uiConsole(...args: any[]): void { + const el = document.querySelector('#console>p') + if (el) { + el.innerHTML = JSON.stringify(args || {}, null, 2) + } + } + + const loggedInView = ( +
+
+
+ +
+
+ + {disconnectLoading &&
Disconnecting...
} + {disconnectError &&
{disconnectError.message}
} +
+
+

Account Operations

+
+
+ +
+
+ +
+ {/* Additional blockchain operations buttons */} +
+
+

+
+
+ ) + + const unloggedInView = ( +
+ + {connectLoading &&
Connecting...
} + {connectError &&
{connectError.message}
} +
+ ) + + return ( +
+

+ + Web3Auth{' '} + + & React Multi-chain Example +

+ +
{isConnected ? loggedInView : unloggedInView}
+ + +
+ ) +} + +export default App +``` + +### Implementing Blockchain RPC Modules + +Let's implement the RPC modules for each blockchain. These modules will interact with the respective blockchain networks. + +## EVM (Ethereum) + +Here's how to implement the Ethereum RPC module: + +```tsx +// ethersRPC.ts +import type { IProvider } from '@web3auth/modal' +import { ethers } from 'ethers' + +export async function getEthereumAccounts(provider: IProvider): Promise { + try { + const ethersProvider = new ethers.BrowserProvider(provider) + const signer = await ethersProvider.getSigner() + // Get user's Ethereum public address + const address = signer.getAddress() + return await address + } catch (error) { + return error + } +} + +export async function getEthereumBalance(provider: IProvider): Promise { + try { + const ethersProvider = new ethers.BrowserProvider(provider) + const signer = await ethersProvider.getSigner() + // Get user's Ethereum public address + const address = signer.getAddress() + // Get user's balance in ether + const balance = ethers.formatEther( + await ethersProvider.getBalance(address) // Balance is in wei + ) + return balance + } catch (error) { + return error as string + } +} + +export async function signEthereumMessage(provider: IProvider): Promise { + try { + const ethersProvider = new ethers.BrowserProvider(provider) + const signer = await ethersProvider.getSigner() + const originalMessage = 'YOUR_MESSAGE' + // Sign the message + const signedMessage = await signer.signMessage(originalMessage) + return signedMessage + } catch (error) { + return error as string + } +} + +export async function sendEthereumTransaction(provider: IProvider): Promise { + try { + const ethersProvider = new ethers.BrowserProvider(provider) + const signer = await ethersProvider.getSigner() + const destination = '0x40e1c367Eca34250cAF1bc8330E9EddfD403fC56' + // Convert 1 ether to wei + const amount = ethers.parseEther('0.001') + // Submit transaction to the blockchain + const tx = await signer.sendTransaction({ + to: destination, + value: amount, + maxPriorityFeePerGas: '5000000000', // Max priority fee per gas + maxFeePerGas: '6000000000000', // Max fee per gas + }) + // Wait for transaction to be mined + const receipt = await tx.wait() + return receipt + } catch (error) { + return error as string + } +} +``` + +## Solana + +Here's how to implement the Solana RPC module: + +```tsx +// solanaRPC.ts +import { Keypair, Connection } from '@solana/web3.js' +import { IProvider, getED25519Key } from '@web3auth/modal' +import nacl from 'tweetnacl' + +export async function getSolanaAccount(ethProvider: IProvider): Promise { + const ethPrivateKey = await ethProvider.request({ + method: 'private_key', + }) + + const privateKey = getED25519Key(ethPrivateKey as string).sk.toString('hex') + const secretKey = new Uint8Array(Buffer.from(privateKey, 'hex')) + const keypair = Keypair.fromSecretKey(secretKey) + return keypair.publicKey.toBase58() +} + +export async function getSolanaBalance(ethProvider: IProvider): Promise { + const ethPrivateKey = await ethProvider.request({ + method: 'private_key', + }) + const privateKey = getED25519Key(ethPrivateKey as string).sk.toString('hex') + const secretKey = new Uint8Array(Buffer.from(privateKey, 'hex')) + const keypair = Keypair.fromSecretKey(secretKey) + const connection = new Connection('https://api.devnet.solana.com') + const balance = await connection.getBalance(keypair.publicKey) + return balance.toString() +} + +export async function signSolanaMessage(ethProvider: IProvider): Promise { + try { + const ethPrivateKey = await ethProvider.request({ + method: 'private_key', + }) + const privateKey = getED25519Key(ethPrivateKey as string).sk.toString('hex') + const secretKey = new Uint8Array(Buffer.from(privateKey, 'hex')) + const keypair = Keypair.fromSecretKey(secretKey) + + // Convert message to Uint8Array + const messageBytes = new TextEncoder().encode('Hello Solana') + + // Sign the message + const signature = nacl.sign.detached(messageBytes, keypair.secretKey) + + return Buffer.from(signature).toString('base64') + } catch (error) { + console.error('Error signing Solana message:', error) + throw error + } +} + +export async function sendSolanaTransaction(ethProvider: IProvider): Promise { + try { + const ethPrivateKey = await ethProvider.request({ + method: 'private_key', + }) + const privateKey = getED25519Key(ethPrivateKey as string).sk.toString('hex') + const secretKey = new Uint8Array(Buffer.from(privateKey, 'hex')) + const keypair = Keypair.fromSecretKey(secretKey) + + const connection = new Connection('https://api.devnet.solana.com') + + // Import required modules for transaction + const { SystemProgram, Transaction, PublicKey, sendAndConfirmTransaction } = await import( + '@solana/web3.js' + ) + + // Create a test recipient address + const toAccount = new PublicKey('7C4jsPZpht1JHMWmwDF5ZEVfGSBViXCKbQEcm2GKHtKQ') + + // Create a transfer instruction + const transferInstruction = SystemProgram.transfer({ + fromPubkey: keypair.publicKey, + toPubkey: toAccount, + lamports: 100000, // 0.0001 SOL + }) + + // Create a transaction and add the instruction + const transaction = new Transaction().add(transferInstruction) + + // Set a recent blockhash + transaction.recentBlockhash = (await connection.getRecentBlockhash()).blockhash + transaction.feePayer = keypair.publicKey + + // Sign and send the transaction + const signature = await sendAndConfirmTransaction(connection, transaction, [keypair]) + + return signature + } catch (error) { + console.error('Error sending Solana transaction:', error) + throw error + } +} +``` + +## Tezos + +Here's how to implement the Tezos RPC module: + +```tsx +// tezosRPC.ts +import { InMemorySigner } from '@taquito/signer' +import { TezosToolkit } from '@taquito/taquito' +import { hex2buf } from '@taquito/utils' +// @ts-ignore +import * as tezosCrypto from '@tezos-core-tools/crypto-utils' +import type { IProvider } from '@web3auth/modal' + +const tezos = new TezosToolkit('https://rpc.tzbeta.net/') + +export async function getTezosKeyPair(provider: IProvider): Promise { + try { + const privateKey = (await provider.request({ method: 'private_key' })) as string + const keyPair = tezosCrypto.utils.seedToKeyPair(hex2buf(privateKey)) + return keyPair + } catch (error) { + console.error(error) + return null + } +} + +export async function setProvider(provider: IProvider): Promise { + const keyPair = await getTezosKeyPair(provider) + tezos.setSignerProvider(await InMemorySigner.fromSecretKey(keyPair?.sk as string)) +} + +export async function getTezosAccount(provider: IProvider): Promise { + try { + const keyPair = await getTezosKeyPair(provider) + return keyPair?.pkh + } catch (error) { + console.error('Error', error) + return error + } +} + +export async function getTezosBalance(provider: IProvider): Promise { + try { + const keyPair = await getTezosKeyPair(provider) + const balance = await tezos.tz.getBalance(keyPair?.pkh as string) + return balance.toString() + } catch (error) { + console.error('Error', error) + return error + } +} + +export async function signTezosMessage(provider: IProvider): Promise { + try { + const keyPair = await getTezosKeyPair(provider) + const signer = new InMemorySigner(keyPair.sk) + const message = '0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad' + const signature = await signer.sign(message) + return signature + } catch (error) { + return error + } +} + +export async function signAndSendTezosTransaction(provider: IProvider): Promise { + try { + await setProvider(provider) + // example address + const address = 'tz1dHzQTA4PGBk2igZ3kBrDsVXuvHdN8kvTQ' + + const op = await tezos.wallet + .transfer({ + to: address, + amount: 0.00005, + }) + .send() + + const txRes = await op.confirmation() + return txRes + } catch (error) { + return error + } +} +``` + +## Polkadot + +Here's how to implement the Polkadot RPC module: + +```tsx +// polkadotRPC.ts +import { ApiPromise, Keyring, WsProvider } from '@polkadot/api' +import { cryptoWaitReady } from '@polkadot/util-crypto' +import type { IProvider } from '@web3auth/modal' + +export async function makeClient(): Promise { + console.log('Establishing connection to Rococo Relay Chain RPC...') + const provider = new WsProvider('wss://rococo-rpc.polkadot.io') // roccoco testnet relay chain + const api = await ApiPromise.create({ provider }) + const resp = await api.isReady + console.log('Polkadot RPC is ready', resp) + return api +} + +export async function getPolkadotKeyPair(provider: IProvider): Promise { + await cryptoWaitReady() + const privateKey = (await provider.request({ + method: 'private_key', + })) as string + const keyring = new Keyring({ ss58Format: 42, type: 'sr25519' }) + const keyPair = keyring.addFromUri(`0x${privateKey}`) + return keyPair +} + +export async function getPolkadotAccounts(provider: IProvider): Promise { + const keyPair = await getPolkadotKeyPair(provider) + return keyPair.address +} + +export async function getPolkadotBalance(provider: IProvider): Promise { + const keyPair = await getPolkadotKeyPair(provider) + const api = await makeClient() + const data = await api.query.system.account(keyPair.address) + const accountData = data.toHuman() + return accountData.data.free +} + +export async function signAndSendPolkadotTransaction(provider: IProvider): Promise { + try { + const keyPair = await getPolkadotKeyPair(provider) + const api = await makeClient() + const txHash = await api.tx.balances + .transferKeepAlive('5Gzhnn1MsDUjMi7S4cN41CfggEVzSyM58LkTYPFJY3wt7o3d', 12345) + .signAndSend(keyPair) + return txHash.toHuman() + } catch (err: any) { + return err.toString() + } +} +``` + +## Conclusion + +This guide has shown you how to create a multi-chain wallet application using Web3Auth. With this setup, you can: + +1. Authenticate users with their social accounts +2. Generate blockchain addresses for multiple chains (Ethereum, Solana, Tezos, Polkadot) +3. Check balances across different blockchains +4. Sign messages and send transactions on various networks + +The power of Web3Auth lies in its ability to derive keys for multiple blockchains from a single authentication session, providing users with a seamless experience across the web3 ecosystem. + +If you want to integrate with a specific blockchain and you're having trouble with the code, please contact us in our [community portal](https://web3auth.io/community/). + +## References + +- [Web3Auth Documentation](https://web3auth.io/docs/) +- [Web3Auth Examples](https://github.com/Web3Auth/web3auth-pnp-examples/) diff --git a/src/pages/guides/sending-gasless-transaction.mdx b/src/pages/guides/sending-gasless-transaction.mdx new file mode 100644 index 00000000000..5567bc7f052 --- /dev/null +++ b/src/pages/guides/sending-gasless-transaction.mdx @@ -0,0 +1,294 @@ +--- +title: Send your first gasless transaction +image: 'img/guides/guides-banners/gasless-transaction.png' +description: Learn how to use gasless paymaster with Web3Auth Native Account Abstraction. +type: guide +tags: [embedded wallets, account abstraction, web, paymaster, erc4337, web3auth] +date: October 22, 2024 +author: Web3Auth Team +--- + +import SEO from '@site/src/components/SEO' +import TabItem from '@theme/TabItem' +import Tabs from '@theme/Tabs' + + + +A paymaster is a vital component in the ERC-4337 standard, responsible for covering transaction costs on behalf of the user. There are various types of paymasters, such as gasless paymasters, ERC-20 paymasters, and more. + +In this guide, we'll talk about how you can use the Pimlico gasless Paymaster with Web3Auth Account Abstraction Provider to sponsor the transaction for your users without requiring the user to pay gas fees. + +For those who want to skip straight to the code, you can find it on [GitHub](https://github.com/Web3Auth/web3auth-examples/tree/main/other/smart-account-example). + +## Prerequisites + +- Pimlico Account: Since we'll be using the Pimlico paymaster, you'll need to have an API key from Pimlico. You can get a free API key from [Pimlico Dashboard](https://dashboard.pimlico.io/). +- Web3Auth Dashboard: If you haven't already, sign up on the Web3Auth platform. It is free and gives you access to the Web3Auth's base plan. Head to Web3Auth's documentation page for detailed [instructions on setting up the Web3Auth Dashboard](/docs/dashboard). +- Web3Auth PnP Web SDK: This guide assumes that you already know how to integrate the PnP Web SDK in your project. If not, you can learn how to [integrate Web3Auth in your Web app](/docs/sdk/web/react/). + +## Integrate AccountAbstractionProvider + +Once, you have set up the Web3Auth Dashboard, and created a new project, it's time to integrate Web3Auth Account Abstraction Provider in your Web application. For the implementation, we'll use the [@web3auth/account-abstraction-provider](https://www.npmjs.com/package/@web3auth/account-abstraction-provider). The provider simplifies the entire process by managing the complex logic behind configuring the account abstraction provider, bundler, and preparing user operations. + +If you are already using the Web3Auth Pnp SDK in your project, you just need to configure the AccountAbstractionProvider with the paymaster details, and pass it to the Web3Auth instance. No other changes are required. + +### Installation + +```bash +npm install --save @web3auth/account-abstraction-provider +``` + +### Configure Paymaster + +The AccountAbstractionProvider requires specific configurations to function correctly. One key configuration is the paymaster. Web3Auth supports custom paymaster configurations, allowing you to deploy your own paymaster and integrate it with the provider. + +You can choose from a variety of paymaster services available in the ecosystem. In this guide, we'll be configuring the Pimlico's paymaster. However, it's important to note that paymaster support is not limited to the Pimlico, giving you the flexibility to integrate any compatible paymaster service that suits your requirements. + +For the simplicity, we have only use `SafeSmartAccount`, but you choose your favorite smart account provider from the available ones. [Learn how to configure the smart account](/docs/sdk/web/react/advanced/smart-accounts). + +```ts +// focus-start +import { + AccountAbstractionProvider, + SafeSmartAccount, +} from '@web3auth/account-abstraction-provider' +// focus-end + +const chainConfig = { + chainNamespace: CHAIN_NAMESPACES.EIP155, + chainId: '0xaa36a7', + rpcTarget: 'https://rpc.sepolia.org', + displayName: 'Ethereum Sepolia Testnet', + blockExplorerUrl: 'https://sepolia.etherscan.io', + ticker: 'ETH', + tickerName: 'Ethereum', + logo: 'https://cryptologos.cc/logos/ethereum-eth-logo.png', +} + +// focus-start +const accountAbstractionProvider = new AccountAbstractionProvider({ + config: { + chainConfig, + bundlerConfig: { + // Get the pimlico API Key from dashboard.pimlico.io + url: `https://api.pimlico.io/v2/${chainId}/rpc?apikey=${pimlicoAPIKey}`, + }, + smartAccountInit: new SafeSmartAccount(), + paymasterConfig: { + // Get the pimlico API Key from dashboard.pimlico.io + url: `https://api.pimlico.io/v2/${chainId}/rpc?apikey=${pimlicoAPIKey}`, + }, + }, +}) +// focus-end +``` + +## Configure Web3Auth + +Once you have configured your `AccountAbstractionProvider`, you can now plug it in your Web3Auth Modal/No Modal instance. If you are using the external wallets like MetaMask, Coinbase, etc, you can define whether you want to use the AccountAbstractionProvider, or EthereumPrivateKeyProvider by setting the `useAAWithExternalWallet` in `IWeb3AuthCoreOptions`. + +If you are setting `useAAWithExternalWallet` to `true`, it'll create a new Smart Account for your user, where the signer/creator of the Smart Account would be the external wallet. + +If you are setting `useAAWithExternalWallet` to `false`, it'll skip creating a new Smart Account, and directly use the external wallet to sign the transactions. + + + + + +```ts +import { EthereumPrivateKeyProvider } from "@web3auth/ethereum-provider"; +import { Web3Auth } from "@web3auth/modal"; + +const privateKeyProvider = new EthereumPrivateKeyProvider({ + // Use the chain config we declared earlier + config: { chainConfig }, +}); + +const web3auth = new Web3Auth({ + clientId: "YOUR_WEB3AUTH_CLIENT_ID", + web3AuthNetwork: WEB3AUTH_NETWORK.SAPPHIRE_MAINNET, + privateKeyProvider, + // Use the account abstraction provider we configured earlier + accountAbstractionProvider + // This will allow you to use EthereumPrivateKeyProvider for + // external wallets, while use the AccountAbstractionProvider + // for Web3Auth embedded wallets. + useAAWithExternalWallet: false, +}); +``` + + + + + +```ts +import { Web3AuthNoModal } from "@web3auth/no-modal"; +import { EthereumPrivateKeyProvider } from "@web3auth/ethereum-provider"; +import { AuthAdapter } from "@web3auth/auth-adapter"; + +const privateKeyProvider = new EthereumPrivateKeyProvider({ + config: { chainConfig }, +}); + +const web3auth = new Web3AuthNoModal({ + clientId: "YOUR_WEB3AUTH_CLIENT_ID", + web3AuthNetwork: WEB3AUTH_NETWORK.SAPPHIRE_MAINNET, + privateKeyProvider, + // Use the account abstraction provider we configured earlier + accountAbstractionProvider + // This will allow you to use EthereumPrivateKeyProvider for + // external wallets, while use the AccountAbstractionProvider + // for Web3Auth embedded wallets. + useAAWithExternalWallet: false, +}); + +const authadapter = new AuthAdapter(); +web3auth.configureAdapter(authadapter); +``` + + + + +## Configure Signer + +The Web3Auth Smart Account feature is compatible with popular signer SDKs, including wagmi, ethers, and viem. You can choose your preferred package to configure the signer. + +You can retreive the provider to configure the signer from Web3Auth instance. + +:::info Wagmi + +Wagmi does not require any special configuration to use the signer with smart accounts. Once you have set up your Web3Auth provider and connected your wallet, Wagmi's hooks (such as useSigner or useAccount) will automatically use the smart account as the signer. You can interact with smart accounts using Wagmi just like you would with a regular EOA (Externally Owned Account) signer—no additional setup is needed. + +::: + + + + + +```tsx +import { createWalletClient } from 'viem' + +// Use your Web3Auth instance to retreive the provider. +const provider = web3auth.provider + +const walletClient = createWalletClient({ + transport: custom(provider), +}) +``` + + + + + +```tsx +import { ethers } from 'ethers' + +// Use your Web3Auth instance to retreive the provider. +const provider = web3auth.provider + +const ethersProvider = new ethers.providers.Web3Provider(provider) +const signer = await ethersProvider.getSigner() +``` + + + + +## Send a transaction + +Developers can use their preferred signer or Wagmi hooks to initiate on-chain transactions, while Web3Auth manages the creation and submission of the UserOperation. Only the `to`, `data`, and `value` fields need to be provided. Any additional parameters will be ignored and automatically overridden. + +To ensure reliable execution, the bundler client sets maxFeePerGas and maxPriorityFeePerGas values. If custom values are required, developers can use the [Viem's BundlerClient](https://viem.sh/account-abstraction/clients/bundler#bundler-client) to manually construct and send the user operation. + +Since Smart Accounts are deployed smart contracts, the user's first transaction also triggers the on-chain deployment of their wallet. + + + + + +```tsx +import { useSendTransaction } from 'wagmi' + +const { data: hash, sendTransaction } = useSendTransaction() + +// Convert 1 ether to WEI format +const value = web3.utils.toWei(1) + +sendTransaction({ to: 'DESTINATION_ADDRESS', value, data: '0x' }) + +const { + data: receipt, + isLoading: isConfirming, + isSuccess: isConfirmed, +} = useWaitForTransactionReceipt({ + hash, +}) +``` + + + + +```tsx +// Convert 1 ether to WEI format +const amount = ethers.parseEther('1.0') + +// Submits a user operation to the blockchain +const transaction = await signer.sendTransaction({ + to: 'DESTINATION_ADDRESS', + value: amount, + // This will perform the transfer of ETH + data: '0x', +}) + +// Wait for the transaction to be mined +const receipt = await transaction.wait() +``` + + + + +```tsx +// Convert 1 ether to WEI format +const amount = parseEther('1') + +// Submits a user operation to the blockchain +const hash = await walletClient.sendTransaction({ + to: 'DESTINATION_ADDRESS', + value: amount, + // This will perform the transfer of ETH + data: '0x', +}) + +// Wait for the transaction to be mined +const receipt = await publicClient.waitForTransactionReceipt({ hash }) +``` + + + + +## Conclusion + +Voila, you have successfully sent your first gasless transaction using the Pimlico paymaster with Web3Auth Account Abstraction Provider. To learn more about advance features of the Account Abstraction Provider like performing batch transactions, using ERC-20 paymaster you can refer to the [Account Abstraction Provider](/docs/sdk/web/react) documentation. diff --git a/src/pages/guides/telegram-miniapp-client.mdx b/src/pages/guides/telegram-miniapp-client.mdx new file mode 100644 index 00000000000..17f99dc4fd7 --- /dev/null +++ b/src/pages/guides/telegram-miniapp-client.mdx @@ -0,0 +1,474 @@ +--- +title: 'Client-Side Setup for Telegram Mini App with Web3Auth' +image: 'img/guides/guides-banners/telegram-miniapp-client.png' +description: | + Learn how to set up the client-side of a Telegram Mini App using Web3Auth to authenticate users and retrieve wallet details. This guide is the second part of the Telegram Mini App series. +type: guide +tags: [embedded wallets, telegram, authentication, web3auth, ton] +date: October 24, 2024 +author: Web3Auth Team +--- + +import SEO from '@site/src/components/SEO' +import TelegramMiniAppDiagram from '@site/static/img/guides/telegram-mini-app-flow-diagram.png' + + + +:::tip Live Demo + +Before diving into development, experience Web3Auth in action! Check out our demo Telegram Mini App: + +👉 **[Launch Demo Mini App](https://t.me/w3a_tg_mini_app_bot)** + +::: + +:::info Source Code + +You can find the full working code for this client + server example here: [**Web3Auth Telegram Mini App Example**](https://github.com/Web3Auth/web3auth-core-kit-examples/tree/main/single-factor-auth-web/sfa-web-ton-telegram-example) + +::: + +### **Objectives** + +In this guide, we will focus on setting up the client-side of a Telegram Mini App using Web3Auth for authentication. By the end of this guide, you will: + +1. Implement Web3Auth in the **client-side app** to authenticate Telegram users using the JWT tokens generated in [Part 1](/guides/telegram-miniapp-server). +2. Retrieve wallet details (e.g., TON blockchain addresses) via Web3Auth and display them in the app. + +:::tip + +**Prerequisite**: If you haven't completed [Part 1](/guides/telegram-miniapp-server), where we set up the backend server and handled Telegram authentication, it's recommended to start from there before proceeding with this guide. + +::: + +--- + +### **Guide Breakdown** + +1. **Part 1**: Set up the server-side logic to validate Telegram login data and generate JWT tokens. +2. **Part 2 (Current Guide)**: Focuses on integrating Web3Auth into the client-side app to authenticate users and retrieve their wallet details. + +--- + +### Overview + +In this guide, we will implement the client-side part of the Telegram Mini App. The client will handle user interaction, manage the login flow using Web3Auth, and retrieve wallet details from the TON blockchain. + +We'll be using the JWT token generated in [Part 1](/guides/telegram-miniapp-server) of this guide to authenticate users on the client-side and establish a session with Web3Auth to retrieve decentralized wallet information. + +The flow is as follows: + +Telegram Mini App Flow Diagram + +1. The user logs into the Telegram Mini App. +2. The JWT token (generated from the backend) is passed to Web3Auth for authentication. +3. Web3Auth authenticates the user and retrieves wallet details (e.g., the user's TON blockchain address). +4. The client displays the wallet details to the user. + +--- + +### **What You Will Learn:** + +1. Integrate Web3Auth into a client-side app to handle user authentication. +2. Retrieve wallet details (TON blockchain) from Web3Auth. +3. Mock Telegram environments for local development and testing. + +--- + +[Follow these steps to set up your Custom Verifier with Web3Auth](/docs/authentication/custom-connections/custom-jwt#set-up-custom-jwt-connection). + +--- + +### Step 1: Install Required Packages + +Before starting, install the required dependencies for the client-side app. + +```bash +npm install @web3auth/single-factor-auth @telegram-apps/sdk-react dotenv @orbs-network/ton-access +``` + +#### **Brief Overview of Dependencies:** + +- **@web3auth/single-factor-auth**: This is the core package for using Web3Auth's Single Factor Authentication (SFA) flow, which will allow us to authenticate Telegram users and retrieve blockchain data. +- **@telegram-apps/sdk-react**: This package is used to handle Telegram's launch parameters and mock Telegram environments for testing during development. +- **dotenv**: Allows loading environment variables from a `.env` file, ensuring secure storage of sensitive data such as server URLs. +- **@orbs-network/ton-access**: Provides access to the TON blockchain RPC endpoint for fetching account information and signing messages on the TON network. + +--- + +### Step 2: Set Up Environment Variables + +Ensure that the `.env` file contains the following value: + +```env +VITE_SERVER_URL=https://your-server-url.com +``` + +This value will be used to connect the client-side app to your backend server. + +--- + +### Step 3: Import Necessary Packages and Set Up States + +In this step, we will import all the necessary packages for our client-side application and set up the states that will manage the login flow, user data, and TON blockchain interactions. + +#### Import and State Setup + +```tsx +import { useEffect, useState } from "react"; +import { Web3Auth, decodeToken } from "@web3auth/single-factor-auth"; +import { CHAIN_NAMESPACES, WEB3AUTH_NETWORK } from "@web3auth/base"; +import { CommonPrivateKeyProvider } from "@web3auth/base-provider"; +import { useLaunchParams } from "@telegram-apps/sdk-react"; +import { useTelegramMock } from "./hooks/useMockTelegramInitData"; +import { getHttpEndpoint } from "@orbs-network/ton-access"; +import "./App.css"; + +const verifier = "w3a-telegram-demo"; +const clientId = "BPi5PB_UiIZ-cPz1GtV5i1I2iOSOHuimiXBI0e-Oe_u6X3oVAbCiAZOTEBtTXw4tsluTITPqA8zMsfxIKMjiqNQ"; + +function App() { + const [isLoggingIn, setIsLoggingIn] = useState(false); + const [web3authSfa, setWeb3authSfa] = useState(null); + const [web3AuthInitialized, setWeb3AuthInitialized] = useState(false); + const [userData, setUserData] = useState(null); + const [tonAccountAddress, setTonAccountAddress] = useState(null); + const [signedMessage, setSignedMessage] = useState(null); + const [isLoggedIn, setIsLoggedIn] = useState(false); + + const { initDataRaw, initData, themeParams } = useLaunchParams() || {}; + + useTelegramMock(); // Initialize the Telegram mock data +``` + +- **Explanation**: In this step, we import the necessary libraries and set up states like `isLoggingIn`, `web3authSfa`, `web3AuthInitialized`, `userData`, `tonAccountAddress`, and `signedMessage` that will manage our app's flow. + +--- + +### Step 4: Initialize Web3Auth and Fetch the ID Token + +Now that the states are set up, let's move on to initializing **Web3Auth** and setting up the connection to our backend server to fetch the ID token generated in **[Part 1](/guides/telegram-miniapp-server)**. + +#### Web3Auth Initialization and Configuration + +```tsx +useEffect(() => { + const initializeWeb3Auth = async () => { + try { + console.log('Fetching TON Testnet RPC endpoint...') + const testnetRpc = await getHttpEndpoint({ + network: 'testnet', + protocol: 'json-rpc', + }) + + const chainConfig = { + chainNamespace: CHAIN_NAMESPACES.OTHER, + chainId: 'testnet', + rpcTarget: testnetRpc, + displayName: 'TON Testnet', + blockExplorerUrl: 'https://testnet.tonscan.org', + ticker: 'TON', + tickerName: 'Toncoin', + } + + const privateKeyProvider = new CommonPrivateKeyProvider({ + config: { chainConfig }, + }) + + // Initialize Web3Auth + const web3authInstance = new Web3Auth({ + clientId, + web3AuthNetwork: WEB3AUTH_NETWORK.SAPPHIRE_MAINNET, + usePnPKey: false, + privateKeyProvider, + }) + + setWeb3authSfa(web3authInstance) + + console.log('Initializing Web3Auth...') + await web3authInstance.init() + console.log('Web3Auth initialized.') + + setWeb3AuthInitialized(true) + } catch (error) { + console.error('Error fetching TON Testnet RPC endpoint: ', error) + } + } + + initializeWeb3Auth() +}, []) +``` + +- **Explanation**: This block initializes Web3Auth using the **Single Factor Authentication** method. We also fetch the TON blockchain's testnet RPC endpoint, which will allow us to connect to the network and retrieve blockchain data such as the wallet address. + +--- + +### Step 5: Mocking Telegram Environments for Local Development + +Before we proceed with connecting to Web3Auth, we need to simulate the Telegram environment in local development. We achieve this by using the `useTelegramMock()` hook. + +#### Mocking Telegram Environment with `useTelegramMock.ts` + +```tsx +/* eslint-disable camelcase */ +import { mockTelegramEnv, parseInitData, retrieveLaunchParams } from '@telegram-apps/sdk-react' + +/** + * Mocks + + Telegram environment in development mode. + */ +export function useTelegramMock(): void { + if (process.env.NODE_ENV !== 'development') return + + let shouldMock: boolean + + try { + retrieveLaunchParams() + shouldMock = !!sessionStorage.getItem('____mocked') + } catch (e) { + shouldMock = true + } + + if (shouldMock) { + const randomId = Math.floor(Math.random() * 1000000000) + + const initDataRaw = new URLSearchParams([ + [ + 'user', + JSON.stringify({ + id: randomId, + first_name: 'Andrew', + last_name: 'Rogue', + username: 'rogue', + language_code: 'en', + is_premium: true, + allows_write_to_pm: true, + }), + ], + ['hash', '89d6079ad6762351f38c6dbbc41bb53048019256a9443988af7a48bcad16ba31'], + ['auth_date', '1716922846'], + ['start_param', 'debug'], + ['chat_type', 'sender'], + ['chat_instance', '8428209589180549439'], + ]).toString() + + mockTelegramEnv({ + themeParams: { + accentTextColor: '#6ab2f2', + bgColor: '#17212b', + buttonColor: '#5288c1', + buttonTextColor: '#ffffff', + destructiveTextColor: '#ec3942', + headerBgColor: '#17212b', + hintColor: '#708499', + linkColor: '#6ab3f3', + secondaryBgColor: '#232e3c', + sectionBgColor: '#17212b', + sectionHeaderTextColor: '#6ab3f3', + subtitleTextColor: '#708499', + textColor: '#f5f5f5', + }, + initData: parseInitData(initDataRaw), + initDataRaw, + version: '7.7', + platform: 'tdesktop', + }) + + sessionStorage.setItem('____mocked', '1') + } +} +``` + +- **Explanation**: This hook mocks the Telegram environment by generating mock `initData` that mimics Telegram's launch parameters, ensuring that the app behaves as if it were running in an actual Telegram Mini App environment. + +:::tip **Why is Mocking Necessary?** + +Mocking the environment during local development ensures smooth testing. Without mocking, the Telegram SDK might throw errors or behave unexpectedly when not running within Telegram. + +::: + +--- + +### Step 6: Connecting to Web3Auth and Fetching the ID Token + +Now, let's connect to Web3Auth and retrieve the ID token from our backend server, as configured in **[Part 1](/guides/telegram-miniapp-server)**. + +#### Connecting Web3Auth + +```tsx +useEffect(() => { + const connectWeb3Auth = async () => { + if (web3authSfa && web3AuthInitialized && initDataRaw) { + setIsLoggingIn(true) + try { + if (web3authSfa.status === 'connected') { + await web3authSfa.logout() + } + + const idToken = await getIdTokenFromServer(initDataRaw, initData.user.photoUrl) // Fetch ID token + const { payload } = decodeToken(idToken) + + await web3authSfa.connect({ + verifier, + verifierId: payload.sub, + idToken, + }) + + setUserData(payload) + setIsLoggedIn(true) + + const tonRpc = new TonRPC(web3authSfa.provider) + const tonAddress = await tonRpc.getAccounts() + setTonAccountAddress(tonAddress) + + const signedMsg = await tonRpc.signMessage('Hello, TON!') + setSignedMessage(signedMsg) + } catch (error) { + console.error('Error during Web3Auth connection:', error) + } finally { + setIsLoggingIn(false) + } + } + } + + if (web3AuthInitialized && initDataRaw) { + connectWeb3Auth() + } +}, [initDataRaw, web3authSfa, web3AuthInitialized]) +``` + +- **Explanation**: This effect establishes a connection to Web3Auth using the ID token fetched from the backend. Once connected, we fetch the TON blockchain account details, including the account address and a signed message. + +--- + +### Step 7: Display User and TON Account Information + +Finally, we display the user's Telegram profile details and their TON account address along with the signed message. + +```tsx +const userInfoBox = ( +
+ User avatar +
+

+ ID: {userData?.telegram_id} +

+

+ Username: {userData?.username} +

+

+ Name: {userData?.name} +

+
+
+) + +const tonAccountBox = ( +
+

+ TON Account: {tonAccountAddress} +

+
+) + +const signedMessageBox = ( +
+

+ Signed Message: {signedMessage} +

+
+) + +return ( +
+

Web3Auth TON Telegram MiniApp

+ {isLoggedIn ? ( + <> + {userInfoBox} + {tonAccountBox} + {signedMessageBox} + + ) : ( + + )} +
+) +``` + +--- + +### Step 8: Running and Testing the App + +To run the app locally: + +```bash +npm run start +``` + +Ensure that the backend server from **[Part 1](/guides/telegram-miniapp-server)** is running and accessible via the `VITE_SERVER_URL`. + +For detailed steps on debugging your Telegram Mini App, refer to this [official guide](https://docs.ton.org/develop/dapps/telegram-apps/testing-apps). + +--- + +### Step 9: Deploy Your App + +Deploy this app to GitHub Pages, Vercel, or any other hosting service. For Vercel: + +- Push your code to GitHub. +- Sign in to [Vercel](https://vercel.com/). +- Import your GitHub repository. +- Deploy your project. + +After deployment, you will get a domain link. Use this link when setting up your Telegram bot. + +:::info + +Remember to whitelist the deployed client app's URL in the allowed origins array in the backend server CORS configuration that we set up in **[Part 1](/guides/telegram-miniapp-server)**. + +::: + +--- + +### Step 10: Setting Up a Telegram Bot + +To connect your Mini App to Telegram, you need to **create a bot** and set up a Mini App for it. + +Follow these steps: + +1. **Start a Chat with BotFather** + + - Open the Telegram app or web version. + - Search for `@BotFather` or follow the [link](https://t.me/BotFather). + - Start a chat with BotFather by clicking on the `START` button. + +2. **Create a New Bot** + + - Send the `/newbot` command to BotFather. + - BotFather will ask you to choose a name for your bot (this is a display name and can contain spaces). + - Next, choose a unique username for your bot, which must end in "bot" (e.g., `sample_bot`). + +3. **Set Up Bot Mini App** + + - Send the `/mybots` command to BotFather. + - Choose your bot from the list and select the "Bot Settings" option. + - Choose the "Menu Button" option. + - Choose "Edit Menu Button URL" and send the URL to your Telegram Mini App, for example, the link from GitHub Pages or Vercel deploy. + +4. **Accessing the Bot** + - You can now search for your bot using its username in Telegram's search bar. + - Press the button next to the attach picker to launch your Telegram Mini App in the messenger. + +--- + +This completes the client-side setup for the Telegram Mini App. By following these steps, you can authenticate Telegram users, retrieve their TON blockchain account details, and allow them to interact with decentralized applications via Web3Auth. diff --git a/src/pages/guides/telegram-miniapp-server.mdx b/src/pages/guides/telegram-miniapp-server.mdx new file mode 100644 index 00000000000..e50a2df9289 --- /dev/null +++ b/src/pages/guides/telegram-miniapp-server.mdx @@ -0,0 +1,341 @@ +--- +title: 'Server-Side Setup for Telegram Mini App with Web3Auth' +image: 'img/guides/guides-banners/telegram-miniapp-server.png' +description: | + Learn how to set up a server for a Telegram Mini App using Web3Auth and validate Telegram login + data to generate JWT tokens. This guide is the first step in creating a Telegram Mini App with Web3Auth. +type: guide +tags: [embedded wallets, telegram, authentication, jwt, web3auth] +date: October 24, 2024 +author: Web3Auth Team +--- + +import SEO from '@site/src/components/SEO' +import TelegramMiniAppFlowDiagram from '@site/static/img/guides/telegram-mini-app-flow-diagram.png' + + + +:::tip Live Demo + +Before diving into development, experience Web3Auth in action! Check out our demo Telegram Mini App: + +👉 **[Launch Demo Mini App](https://t.me/w3a_tg_mini_app_bot)** + +::: + +:::info Source Code + +The backend example featured in this guide is part of a full working demo: [**Web3Auth Telegram Mini App Example**](https://github.com/Web3Auth/web3auth-core-kit-examples/tree/main/single-factor-auth-web/sfa-web-ton-telegram-example) + +::: + +### **Objectives** + +This guide is part of a two-part series aimed at helping you build a fully functioning Telegram Mini App that uses Web3Auth for decentralized authentication. By the end of this series, you will have: + +1. Set up a **backend server** that can handle Telegram authentication, validate the `initData`, and generate JWT tokens. +2. Integrated the **Web3Auth** system into the **client-side app** to authenticate users and retrieve wallet details (e.g., TON blockchain addresses). + +By following these two guides, you will be able to securely authenticate Telegram users and allow them to interact with decentralized applications via their wallet on the TON blockchain. + +--- + +### **Guide Breakdown** + +1. **Part 1 (Current Guide)**: Focuses on setting up the server-side logic to validate Telegram login data and generate JWT tokens. + + - This will include setting up an Express server, handling Telegram login requests, validating `initData`, and generating JWT tokens for users. + - You will also see how to mock data during development for easy testing and debugging. + +2. **[Part 2](/guides/telegram-miniapp-client)**: Focuses on the client-side integration with Web3Auth, where you will use the JWT tokens generated in Part 1 to authenticate users and retrieve their wallet information from Web3Auth. + + - You will implement the Web3Auth SDK and handle interactions between the Telegram Mini App and Web3Auth to access TON wallet details. + +--- + +:::info Already completed this guide? + +You can continue to Part 2, where we implement the client-side app and integrate Web3Auth for authentication. [Go to Part 2](/guides/telegram-miniapp-client) to continue your journey in creating a working Telegram Mini App. + +::: + +--- + +### Overview + +In this guide, we will set up a backend server for a Telegram Mini App using Telegram OAuth and Web3Auth for authentication. The server will validate the Telegram-provided `initData` and generate JWT tokens to securely authenticate users. + +Telegram Mini App Flow Diagram + +Above is a visual flow diagram that shows how this process works from start to finish. You can visualize the interaction between the user, the Telegram client, the server, and Web3Auth. + +- The process starts when a user opens the Telegram Mini App. +- Telegram provides `initData`, which includes user data and a signature. +- The client forwards this data to the backend server for validation. +- Once the server validates the `initData`, it generates a JWT token and returns it to the client. +- Finally, the client uses the JWT token with Web3Auth to authenticate the user and obtain wallet details. + +By following this flow, you will enable users to authenticate seamlessly within the Telegram Mini App environment while allowing them access to their decentralized wallet information. + +--- + +### **What You Will Learn:** + +1. Set up an Express server to handle Telegram Mini App authentication. +2. Validate Telegram login data and generate JWT tokens. +3. Use the `isMocked` parameter to bypass validation for local testing. + +--- + +### Prerequisites + +- Basic knowledge of Node.js and Express. +- A Telegram bot token (learn how to get one from the [Telegram Bot documentation](https://core.telegram.org/bots)). +- Understanding of JWT authentication. + +--- + +### Step 1: Install Required Packages + +```bash +npm install express jsonwebtoken @telegram-apps/init-data-node dotenv cors +``` + +#### **Package Usage:** + +- **express**: Used to create and manage the server. +- **jsonwebtoken**: Required for generating and verifying JWT tokens, which are essential for authenticating users. +- **@telegram-apps/init-data-node**: Provides the utility to validate Telegram's `initData` to ensure data integrity and prevent tampering. +- **dotenv**: Allows you to securely manage environment variables like your bot token and JWT secret. +- **cors**: Used to configure Cross-Origin Resource Sharing (CORS), which ensures that your app can securely communicate with other domains like your front-end app. + +--- + +### Step 2: Environment Variables Setup + +Create a `.env` file to store your sensitive data. + +```env +NODE_ENV=development +APP_URL="https://your-app-url.com" +TELEGRAM_BOT_TOKEN=your_bot_token_here +JWT_SECRET=your_secret_key +JWT_KEY_ID=your_key_id +``` + +- The `NODE_ENV` is the environment in which the app is running. +- The `APP_URL` is the URL of your deployed app. +- The `JWT_SECRET` is the private key used to sign the JWT. +- The `TELEGRAM_BOT_TOKEN` is the Telegram bot token. +- The `JWT_KEY_ID` is a unique identifier in for the key used to sign the JWT. This ID helps Web3Auth locate the correct public key from your JWKS endpoint for token verification. It must match the `kid` value in your JWKS configuration. + +--- + +### Step 3: Set Up the Express Server + +Let’s break down the code into smaller chunks to better understand each part: + +#### **Part 1: Basic Setup** + +```javascript +const jwt = require('jsonwebtoken') +const fs = require('fs') +const express = require('express') +const dotenv = require('dotenv') +const path = require('path') +const { validate } = require('@telegram-apps/init-data-node') + +dotenv.config() + +const app = express() +app.use(express.json()) // Middleware to parse JSON requests +const { TELEGRAM_BOT_TOKEN, JWT_KEY_ID, APP_URL } = process.env +const privateKey = fs.readFileSync(path.resolve(__dirname, 'privateKey.pem'), 'utf8') +``` + +:::tip Where Do I Get the PEM File? + +The PEM file is a private key used for signing JSON Web Tokens (JWTs). If you're wondering how to generate or obtain this PEM file, follow the **BYO JWT guide** from Web3Auth. It explains how to create a custom JWT verifier and generate the necessary keys. You can find the guide here: [Custom JWT Providers](/docs/authentication/custom-connections/custom-jwt). + +This step is crucial if you're using your own custom login providers, as you'll need to register this verifier on the Web3Auth dashboard, which we will use in **[Part 2](/guides/telegram-miniapp-client)** when setting up the client-side app. + +::: + +This is the basic setup for your Express server. It reads your environment variables, sets up the required middleware for parsing JSON, and loads the private key that will be used for signing the JWT. + +--- + +#### **Part 2: CORS Configuration** + +```javascript +// Define allowed origins +const allowedOrigins = [APP_URL] + +// CORS configuration +app.use((req, res, next) => { + const origin = req.headers.origin + if (allowedOrigins.includes(origin)) { + res.setHeader('Access-Control-Allow-Origin', origin) + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') + res.setHeader( + 'Access-Control-Allow-Headers', + 'Content-Type, Authorization, X-Requested-With, Accept' + ) + res.setHeader('Access-Control-Allow-Credentials', 'true') + } + + if (req.method === 'OPTIONS') { + res.setHeader( + 'Access-Control-Allow-Headers', + 'Content-Type, Authorization, X-Requested-With, Accept' + ) + return res.sendStatus(204) + } + next() +}) +``` + +Here, we are configuring CORS (Cross-Origin Resource Sharing) to only allow requests from specific origins (your app’s URL). Be sure to add your app’s URL to the `allowedOrigins` array. + +:::tip + +After deploying your client-side app in **[Part 2](/guides/telegram-miniapp-client)**, don't forget to add its URL to the `allowedOrigins` array to ensure proper communication between the client and the server. + +::: + +--- + +#### **Part 3: Rate Limiting** + +```javascript +const RateLimit = require('express-rate-limit') + +const limiter = RateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // Max 100 requests per IP + message: 'Too many requests from this IP, please try again later.', +}) + +app.use(limiter) +``` + +This section sets up a rate limiter to prevent abuse by limiting the number of requests from the same IP address. + +:::tip Why Use a Rate Limiter and CORS Configuration? + +- **Rate Limiter**: Protects your server from being overwhelmed by too many requests in a short period, preventing abuse or DoS attacks. In this example, it limits each IP address to 100 requests every 15 minutes. + +- **CORS**: The Cross-Origin Resource Sharing (CORS) settings ensure only requests from your allowed origins (like your app's domain) can access the server. Don't forget to whitelist your app's origin by adding its address to the `allowedOrigins` array, ensuring the right domain is permitted. + +::: + +--- + +#### **Part 4: Helper Function to Generate JWT Token** + +```javascript +const generateJwtToken = userData => { + const payload = { + telegram_id: userData.id, + username: userData.username, + avatar_url: userData.photo_url || 'https://www.gravatar.com/avatar', + sub: userData.id.toString(), + name: userData.first_name, + iss: 'https://api.telegram.org', + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 * 60, // Token valid for 1 hour + } + return jwt.sign(payload, privateKey, { algorithm: 'RS256', keyid: JWT_KEY_ID }) +} +``` + +This function creates a JWT using the `userData` and signs it with your private key. This JWT will be used for user authentication. + +--- + +#### **Part 5: Routes** + +```javascript +// Route 1: Test route to verify server is running +app.get('/test', (req, res) => { + res.json({ message: 'Connection successful. Server is running!' }) +}) +``` + +This route is just for testing whether the server is running successfully. + +```javascript +// Route 2: Telegram authentication route +app.post('/auth/telegram', async (req, res) => { + const { initDataRaw, isMocked, photoUrl } = req.body // Extract photoUrl from request body + + if (!initDataRaw) { + return res.status(400).json({ error: 'initDataRaw is required' }) + } + + if (isMocked) { + // Handle mock data parsing + const data = new URLSearchParams(initDataRaw) + const user = JSON.parse(decodeURIComponent(data.get('user'))) + const mockUser = { + id: user.id, + username: user.username, + photo_url: photoUrl || user.photo_url || 'https://www.gravatar.com/avatar', + first_name: user.first_name, + } + const JWTtoken = generateJwtToken(mockUser) + return res.json({ token: JWTtoken }) + } + + try { + // Validate the real initDataRaw using @telegram-apps/init-data-node + validate(initDataRaw, TELEGRAM_BOT_TOKEN) + + // If validation is successful, parse the data + const data = new URLSearchParams(initDataRaw) + const user = JSON.parse(decodeURIComponent(data.get('user'))) + const validatedUser = { + ...user, + photo_url: photoUrl || user.photo_url || 'https://www.gravatar.com/avatar', + } + + // Generate the JWT token + const JWTtoken = generateJwtToken(validatedUser) + res.json({ token: JWTtoken }) + } catch (error) { + console.error('Error validating Telegram data:', error) + res.status(400).json({ error: 'Invalid Telegram data' }) + } +}) +``` + +This route handles the Telegram login process. If the data is mocked, it creates a mock user and generates a JWT. If not, it validates the `initData` using the `@telegram-apps/init-data-node` package. + +--- + +### Step 4: Run the Server + +```bash +node index.js +``` + +You can now run your server and verify its operation by sending requests to the `/test` or `/auth/telegram` routes. + +--- + +Once you deploy your app (which you'll build in **[Part 2](/guides/telegram-miniapp-client)**), you can replace the `APP_URL` with your deployed app’s URL. + +For custom JWT login (getting the JWKS and more), follow the guide here: [Custom JWT Providers](https://web3auth.io/docs/authentication/custom-connections/custom-jwt). This will also help you create a verifier on the Web3Auth dashboard, which you'll use in **[Part 2](/guides/telegram-miniapp-client)**. + +--- + +This guide sets up the server-side logic for validating Telegram logins and generating JWT tokens. To continue the journey, head over to [Part 2](/guides/telegram-miniapp-client), where we integrate Web3Auth into the client-side app for a complete solution. diff --git a/src/pages/guides/telegram.mdx b/src/pages/guides/telegram.mdx new file mode 100644 index 00000000000..787af9da4d2 --- /dev/null +++ b/src/pages/guides/telegram.mdx @@ -0,0 +1,496 @@ +--- +title: How to use Telegram OAuth with Web3Auth +image: 'img/guides/guides-banners/telegram-oauth.png' +description: Learn how to seamlessly integrate Telegram Login with Web3Auth to enhance your app's authentication capabilities. +type: guide +tags: [embedded wallets, telegram, oauth, telegram login widget, authentication, telegram login, telegram oauth, web3auth] +date: August 15, 2024 +author: Web3Auth Team +--- + +import SEO from '@site/src/components/SEO' +import Tabs from '@theme/Tabs' +import TabItem from '@theme/TabItem' + + + +This guide will explain the basic steps of integrating the [Telegram Login Widget](https://core.telegram.org/widgets/login) _aka_ Telegram OAuth with Web3Auth for authentication. In this guide, you will learn how to create a Telegram bot, generate a JWKS file, and establish a custom JWT verifier on Web3Auth. Finally, we will demonstrate how to implement the Telegram OAuth flow for user authentication and generate Ethereum/Solana keys using [Web3Auth Plug and Play](https://web3auth.io/docs/sdk/web/install) and [Single Factor Auth](https://web3auth.io/docs/sdk/sfa/sfa-js/install) SDKs. + +:::note TLDR; + +1. [Create a Telegram bot](https://core.telegram.org/bots/features#creating-a-new-bot) and [generate an API Token](https://core.telegram.org/bots/features#generating-an-authentication-token). +2. [Create a JWKS file for your public key used for JWT signing](/docs/authentication/custom-connections/custom-jwt/#how-to-convert-pem-to-jwks) and host it on a public endpoint. +3. [Set up a Telegram custom JWT verifier](/docs/authentication/social-logins/oauth) for Telegram on the Web3Auth Dashboard. +4. Implement the Telegram OAuth flow for user authentication. +5. Use user data from Telegram to generate the JWT token. +6. Pass the JWT token to Web3Auth for generating Ethereum/Solana keys using the Web3Auth Plug and Play and Single Factor Auth Mobile SDKs. + +::: + +:::info Important Links + +- Telegram Login example with Web3Auth PnP SDK + - [GitHub Repository](https://github.com/Web3Auth/web3auth-pnp-examples/tree/main/web-no-modal-sdk/custom-authentication/single-verifier-examples/telegram-no-modal-example) + - [Live Demo](https://telegram-no-modal-example.vercel.app) +- Telegram Login example with Web3Auth SFA Node SDK + - [GitHub Repository](https://github.com/Web3Auth/web3auth-core-kit-examples/tree/main/single-factor-auth-node/sfa-telegram-oauth-server) +- Telegram Login example with Web3Auth SFA JS SDK + - [GitHub Repository](https://github.com/Web3Auth/web3auth-core-kit-examples/tree/main/single-factor-auth-web/sfa-web-telegram-example) + +::: + +## How it works? + +When a user clicks on the Telegram Login Widget, it initiates the Telegram OAuth flow. The widget will prompt the user to log in to their Telegram account and grant permission for the application to access their Telegram data. Once the user logs in, the Telegram API will send the user data to the specified callback URL in the Telegram Login Widget. The callback URL, with the help of the backend server, will then create a JWT token using the user data and send it to the application's frontend. The frontend will then send the JWT token to the Web3Auth SDK, which will use it to generate Ethereum or Solana keys. + +## Prerequisites + +1. [Telegram bot](https://core.telegram.org/bots/features#creating-a-new-bot) with [an API Token](https://core.telegram.org/bots/features#generating-an-authentication-token). +2. [Custom JWT verifier for Telegram](https://web3auth.io/docs/authentication/social-logins/oauth) + +## Express server for Telegram OAuth + +To set up an Express server to manage the Telegram OAuth flow, you will need to create three routes: + +1. `/` - This route will simply check if the server is running. + +2. `/login` - Use this route to display the login page with the Telegram Login Widget. + +3. `/callback` - This route will manage the Telegram OAuth callback and generate the JWT. + +In order to build the server, you will use the `express`, `jsonwebtoken`, and `@telegram-auth/server` packages. The `generateJwtToken` function will be used to create a JWT token based on the user data received from Telegram. The `/callback` route will validate the Telegram data using the `AuthDataValidator` class and generate the JWT token, which will then be sent to the frontend. + +**What each packages do**: + +- `express` - A Node.js web application framework that provides a robust set of features for web and mobile applications. + +- `jsonwebtoken` - An implementation of JSON Web Tokens (JWT) to generate and verify JWT tokens. + +- `@telegram-auth/server` - A package that provides a utility class to validate the Telegram data received from the Telegram Login Widget. + +The server will utilize the `login.html` file to display the Telegram Login Widget. This HTML file will dynamically create the Telegram Login Widget using the Telegram bot username and the callback URL. + +A few important points: + +- Ensure that the private key matches the one generated during the JWKS setup phase. + +- The `JWT_KEY_ID` should be identical to the one specified during the JWKS setup phase and should be present in the JWKS file. + + + + + +```js title="server.js" +const express = require('express') +const app = express() +const { resolve } = require('path') +const jwt = require('jsonwebtoken') +const fs = require('fs') +const { AuthDataValidator } = require('@telegram-auth/server') +const { objectToAuthDataMap } = require('@telegram-auth/server/utils') + +// Read the private key for JWT signing (Ensure you specify the correct path to the private key file) +const privateKey = fs.readFileSync('/path/to/privateKey.pem', 'utf8') + +const generateJwtToken = userData => { + const payload = { + telegram_id: userData.id, + username: userData.username, + avatar_url: userData.photo_url, + sub: userData.id.toString(), + name: userData.first_name, + iss: 'https://api.telegram.org', // Issuer + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 * 60, // 1 hour expiration, can lower or increase as needed + } + + return jwt.sign(payload, privateKey, { algorithm: 'RS256', keyid: 'JWT_KEY_ID' }) +} + +app.get('/', (req, res) => res.send('Express Server for Telegram Login to be used with Web3Auth')) + +app.get('/login', (req, res) => { + res.sendFile(resolve(__dirname, 'login.html')) +}) + +app.get('/callback', async (req, res) => { + const validator = new AuthDataValidator({ botToken: process.env.TELEGRAM_BOT_API_TOKEN }) // Use environment variable for bot token + const data = objectToAuthDataMap(req.query || {}) + + try { + const user = await validator.validate(data) + const JWTtoken = generateJwtToken(user) + + const redirectUrl = `${process.env.FRONTEND_URL}?token=${JWTtoken}` // Use environment variable for frontend URL + res.redirect(redirectUrl) + } catch (error) { + console.error('Error validating Telegram data:', error) + res.status(400).send('Invalid Telegram data') + } +}) + +// Port 3000 or environment variable for port if deployed on a different service +const PORT = process.env.PORT || 3000 +app.listen(PORT, () => console.log(`Server ready on port ${PORT}.`)) + +module.exports = app +``` + + + + + +```html title="login.html" + + + + Telegram OAuth App with Web3Auth + + + + + + + +``` + + + + + +**_Please note that the Telegram OAuth flow will not work in localhost_**. To test the Telegram OAuth flow, you'll need to deploy the server to a public endpoint. Alternatively, you can use a service like [ngrok](https://ngrok.com/) to create a public URL for your localhost. + +For your convenience, we have deployed the server to [Vercel](https://vercel.com/guides/using-express-with-vercel). You can find the server code [here](https://github.com/Web3Auth/web3auth-pnp-examples/tree/main/web-no-modal-sdk/custom-authentication/single-verifier-examples/telegram-no-modal-example/server) and the live demo [here](https://telegram-no-modal-server.vercel.app/login). + +## Edit Telegram Bot to set the domain + +To ensure that the Telegram Login Widget works correctly, you will need to set the domain of your server. + +Check this guide on editing the Telegram Bot at https://core.telegram.org/bots/features#edit-bots to set the domain. + +1. Simply select your Bot from the BotFather and click on the `Bot Settings` option. +2. Then, click on the `Domain` option, and click on `Set Domain` to enter the domain of your server. +3. You will see a message saying `Success! Domain updated.` + + ![Setting Domain in a Telegram Bot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fjamlitgos1c95tgw86e.gif) + +## Using Telegram Login with Web3Auth + +The Telegram OAuth server mentioned above will authenticate users and generate a JWT token. This token can be utilized with Web3Auth's Plug and Play SDK or Single Factor Auth SDK. In the upcoming sections, we will illustrate how to use the Web3Auth SDKs with the JWT token. + +### Web3Auth Plug and Play SDK + +To use the Web3Auth Plug and Play SDK with the Telegram OAuth flow, you'll need to first configure Web3Auth based on your project and Telegram verifier. Then, initiate the login by redirecting the user to the `/login` route of your Express server. After the user logs in and is redirected back to the frontend, you can extract the JWT token from the URL and pass it to the Web3Auth SDK to initiate the login process with Web3Auth. + +#### Set up the Web3Auth PnP SDK + +[Install the PnP packages](https://web3auth.io/docs/sdk/web/install) and then import it and configure the Web3Auth PnP No Modal SDK with the Telegram verifier. + +```js +import { Web3AuthNoModal } from '@web3auth/no-modal' +import { EthereumPrivateKeyProvider } from '@web3auth/ethereum-provider' +import { + WALLET_ADAPTERS, + CHAIN_NAMESPACES, + IProvider, + UX_MODE, + WEB3AUTH_NETWORK, +} from '@web3auth/base' +import { AuthAdapter } from '@web3auth/auth-adapter' + +const clientId = + 'BPi5PB_UiIZ-cPz1GtV5i1I2iOSOHuimiXBI0e-Oe_u6X3oVAbCiAZOTEBtTXw4tsluTITPqA8zMsfxIKMjiqNQ' // get from https://dashboard.web3auth.io + +const chainConfig = { + chainNamespace: CHAIN_NAMESPACES.EIP155, + chainId: '0x1', // Please use 0x1 for Mainnet + rpcTarget: 'https://rpc.ethereum.org', + displayName: 'Ethereum Mainnet', + blockExplorerUrl: 'https://etherscan.io/', + ticker: 'ETH', + tickerName: 'Ethereum', + logo: 'https://cryptologos.cc/logos/ethereum-eth-logo.png', +} + +const privateKeyProvider = new EthereumPrivateKeyProvider({ config: { chainConfig } }) + +const web3auth = new Web3AuthNoModal({ + clientId, + web3AuthNetwork: WEB3AUTH_NETWORK.SAPPHIRE_MAINNET, + privateKeyProvider, +}) + +const authAdapter = new AuthAdapter({ + adapterSettings: { + uxMode: UX_MODE.REDIRECT, + loginConfig: { + jwt: { + // focus-next-line + verifier: 'w3a-telegram-oauth-demo', // Replace with your verifier name + typeOfLogin: 'jwt', + clientId, + }, + }, + }, +}) +web3auth.configureAdapter(authAdapter) + +await web3auth?.init() +``` + +#### Initiate the Telegram OAuth flow + +Redirect the user to the `/login` route of your Express server to initiate the Telegram OAuth flow. + +```js +const login = async () => { + window.location.href = `${SERVER_URL}/login` + // e.g https://w3a-telegram-server.vercel.app/login +} +``` + +#### Extract the JWT token from the URL + +Extract the JWT token from the URL and pass it to the Web3Auth SDK to initiate the login process and then reset the URL state to remove the token. + +```js +useEffect(() => { + const params = new URLSearchParams(window.location.search) + const jwtToken = params.get('token') + if (jwtToken) { + // focus-next-line + loginWithWeb3Auth(jwtToken) + window.history.replaceState({}, document.title, window.location.pathname) + } +}, []) +``` + +#### Login with Web3Auth using the JWT token + +Use the JWT token in the `connectTo()` method to initiate the login process with Web3Auth with the verifierIdField set to `sub` or any other field based on the verifier's configuration. + +```js +const loginWithWeb3Auth = async (token: string) => { + await web3auth?.init(); + const web3authProvider = await web3auth?.connectTo(WALLET_ADAPTERS.AUTH, { + loginProvider: "jwt", + extraLoginOptions: { + // focus-next-line + id_token: token, + verifierIdField: "sub", // Based on the verifier's verifierIdField + }, + }); +}; +``` + +The above code snippets demonstrate how to use the Web3Auth Plug and Play SDK with the Telegram OAuth flow. You can find the complete example code in [this GitHub repository](https://github.com/Web3Auth/web3auth-pnp-examples/tree/main/web-no-modal-sdk/custom-authentication/single-verifier-examples/telegram-no-modal-example). + +### Web3Auth Single Factor Auth SDK + +To use the Web3Auth Single Factor Auth SDK with the Telegram OAuth flow, there could be two ways to use it: + +1. Using the Web3Auth Single Factor Auth SDK in the Node environment. +2. Using the Web3Auth Single Factor Auth SDK in the browser environment. + +#### Using the Web3Auth Single Factor Auth SDK in the Node environment + +To use the Web3Auth Single Factor Auth SDK in the Node environment, you'll need to first configure the SFA Node SDK with the Web3Auth project and Telegram verifier details. Then, initiate the login by calling the `connect` method with the verifier details along with the JWT token. + +#### Set up the Web3Auth SFA Node SDK + +[Install the SFA Node packages](https://web3auth.io/docs/sdk/sfa/sfa-js/install) and then import it and configure the Web3Auth SFA Node SDK with the Telegram verifier. + +Note: For this, you can continue to use the Express server setup from the previous section. Simply update with the below code snippet. + +```js +const { Web3Auth } = require('@web3auth/node-sdk') +const { EthereumPrivateKeyProvider } = require('@web3auth/ethereum-provider') + +const privateKeyProvider = new EthereumPrivateKeyProvider({ + config: { + chainConfig: { + chainId: '0x1', + rpcTarget: 'https://rpc.ethereum.org', + displayName: 'Ethereum Mainnet', + blockExplorer: 'https://etherscan.io', + ticker: 'ETH', + tickerName: 'Ethereum', + }, + }, +}) + +const web3auth = new Web3Auth({ + clientId: + 'BPi5PB_UiIZ-cPz1GtV5i1I2iOSOHuimiXBI0e-Oe_u6X3oVAbCiAZOTEBtTXw4tsluTITPqA8zMsfxIKMjiqNQ', // Get your Client ID from the Web3Auth Dashboard + web3AuthNetwork: 'sapphire_mainnet', + usePnPKey: false, // Setting this to true returns the same key as PnP Web SDK, By default, this SDK returns CoreKitKey. +}) +web3auth.init({ provider: privateKeyProvider }) +``` + +#### Initiate the Telegram OAuth flow + +The "/login" route will start the Telegram OAuth flow. The code snippet remains the same for this part. When handling the "/callback" route, update the function to utilize the "getPrivateKey()" function for generating the Ethereum private key. "getPrivateKey()" is a helper function that uses the JWT token and Telegram user ID to produce the Ethereum private key. This is necessary because we are working in a server environment and don't have access to web3 libraries at the server level. Therefore, we are using a provider to obtain the Ethereum private and public keys. For simplicity, we are logging the Ethereum private key and public address to the express server, but you can customize this according to your needs. + +Here's the updated code snippet: + +```js +app.get('/login', (req, res) => { + res.sendFile(resolve(__dirname, 'login.html')) +}) + +app.get('/callback', async (req, res) => { + const validator = new AuthDataValidator({ botToken: process.env.TELEGRAM_BOT_API_TOKEN }) // Use environment variable for bot token + const data = objectToAuthDataMap(req.query || {}) + + try { + const user = await validator.validate(data) + const JWTtoken = generateJwtToken(user) + // getPriavteKey function is defined below + // focus-next-line + const ethData = await getPrivateKey(JWTtoken, user.id.toString()) + // Use ethData as per your requirement + console.log('Ethereum Data:', ethData) + res.json('Ethereum Data is generated on your server.') + } catch (error) { + console.error('Error validating Telegram data:', error) + res.status(400).send('Invalid Telegram data') + } +}) +``` + +#### Generate Ethereum private key + +Use the provider to generate the Ethereum private key using the JWT token and the Telegram user ID. + +```js +const getPrivateKey = async (idToken, verifierId) => { + const web3authNodeprovider = await web3auth.connect({ + verifier: 'WEB3AUTH_VERIFIER_NAME', // Replace with your verifier name + verifierId, + idToken, + }) + + const ethPrivateKey = await web3authNodeprovider.request({ method: 'eth_private_key' }) + const ethPublicAddress = await web3authNodeprovider.request({ method: 'eth_accounts' }) + const ethData = { + ethPrivateKey, + ethPublicAddress, + } + return ethData +} +``` + +The above code snippets demonstrate how to use the Web3Auth SFA Node SDK with the Telegram OAuth flow. You can find the complete example code in [this GitHub repository](https://github.com/Web3Auth/web3auth-pnp-examples/tree/main/web-no-modal-sdk/custom-authentication/single-verifier-examples/telegram-no-modal-example). + +#### Using the Web3Auth Single Factor Auth SDK in the browser environment + +To use the Web3Auth Single Factor Auth SDK in the browser environment, you'll need to first configure the SFA JS SDK with the Web3Auth project and Telegram verifier details. Then, initiate the login by calling the `connect` method with the verifier details along with the JWT token. + +#### Set up the Web3Auth SFA JS SDK + +[Install the SFA JS packages](https://web3auth.io/docs/sdk/sfa/sfa-js/install) and then import it and configure the Web3Auth SFA JS SDK with the Telegram verifier in your React or any other frontend project. + +```js +import { CHAIN_NAMESPACES, WEB3AUTH_NETWORK } from '@web3auth/base' +import { Web3Auth, decodeToken } from '@web3auth/single-factor-auth' +import { EthereumPrivateKeyProvider } from '@web3auth/ethereum-provider' + +const chainConfig = { + chainId: '0x1', + displayName: 'Ethereum Mainnet', + chainNamespace: CHAIN_NAMESPACES.EIP155, + tickerName: 'Ethereum', + ticker: 'ETH', + decimals: 18, + rpcTarget: 'https://rpc.ethereum.org', + blockExplorerUrl: 'https://etherscan.io', +} + +const ethereumPrivateKeyProvider = new EthereumPrivateKeyProvider({ + config: { chainConfig }, +}) + +const web3auth = new Web3Auth({ + clientId: + 'BPi5PB_UiIZ-cPz1GtV5i1I2iOSOHuimiXBI0e-Oe_u6X3oVAbCiAZOTEBtTXw4tsluTITPqA8zMsfxIKMjiqNQ', // Get your Client ID from the Web3Auth Dashboard + web3AuthNetwork: WEB3AUTH_NETWORK.SAPPHIRE_MAINNET, + usePnPKey: false, // Setting this to true returns the same key as PnP Web SDK, By default, this SDK returns CoreKitKey. + privateKeyProvider: ethereumPrivateKeyProvider, +}) + +await web3auth.init() +``` + +#### Initiate the Telegram OAuth flow + +The `/login` route will initiate the Telegram OAuth flow. The `/callback` will handle the Telegram OAuth callback and generate the JWT token. The code snippet remains the same for both routes from the [Express server setup section](#express-server-for-telegram-oauth). + +#### Extract the JWT token from the URL + +Extract the JWT token from the URL and pass it to the Web3Auth SDK to initiate the login process and then reset the URL state to remove the token. + +```js +useEffect(() => { + const params = new URLSearchParams(window.location.search) + const jwtToken = params.get('token') + if (jwtToken) { + // focus-next-line + loginWithWeb3Auth(jwtToken) + window.history.replaceState({}, document.title, window.location.pathname) + } +}, []) +``` + +#### Login with Web3Auth SFA JS using the JWT token + +Use the JWT token in the `connect()` method to initiate the login process with Web3Auth SFA JS SDK. + +```js +const loginWithWeb3Auth = async (token: string) => { + const decodedToken = decodeToken(token); + const verifierId = decodedToken.sub; + // focus-next-line + const web3authProvider = await web3auth.connect({ + verifier: "WEB3AUTH_VERIFIER_NAME", // Replace with your verifier name + verifierId, + idToken: token, + }); +}; +``` + +The above code snippets demonstrate how to use the Web3Auth SFA JS SDK with the Telegram OAuth flow. You can find the complete example code in [this GitHub repository](https://github.com/Web3Auth/web3auth-pnp-examples/tree/main/web-no-modal-sdk/custom-authentication/single-verifier-examples/telegram-no-modal-example). diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 0570b0e8229..d66f3c0d184 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -15,73 +15,71 @@ export default function Home(): JSX.Element { description="Build with the world's leading self-custodial crypto wallet." button={{ label: 'Get Started', - href: '/sdk', + href: '/quickstart', icon: 'arrow-right', }} /> - + + + + + - - diff --git a/src/pages/quickstart/MediaStep/MediaStep.module.css b/src/pages/quickstart/MediaStep/MediaStep.module.css new file mode 100644 index 00000000000..0296fcfb47d --- /dev/null +++ b/src/pages/quickstart/MediaStep/MediaStep.module.css @@ -0,0 +1,269 @@ +/* Media Step Container */ +.mediaContainer { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + background: var(--ifm-background-surface-color); + border-radius: 12px; + overflow: hidden; + position: relative; +} + +/* Media Content */ +.mediaContent { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + position: relative; + transition: opacity 0.3s ease; +} + +.mediaContent.hidden { + opacity: 0; + pointer-events: none; +} + +/* YouTube Player */ +.youtubePlayer { + width: 100%; + height: 400px; + min-height: 300px; + border-radius: 8px; + border: none; + background: #000; + aspect-ratio: 16/9; +} + +/* Video Player */ +.videoPlayer { + width: 100%; + height: auto; + max-height: 100%; + object-fit: contain; + background: #000; + border-radius: 8px; +} + +/* Image Display */ +.imageDisplay { + width: 100%; + height: auto; + max-height: 100%; + object-fit: contain; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +/* Media Caption */ +.mediaCaption { + padding: 1rem; + background: var(--ifm-color-emphasis-50); + border-top: 1px solid var(--ifm-color-emphasis-200); + margin-top: auto; +} + +.mediaCaption p { + margin: 0; + font-size: 14px; + line-height: 1.5; + color: var(--ifm-color-content-secondary); + text-align: center; +} + +/* Placeholder State */ +.mediaPlaceholder { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 300px; + gap: 1rem; + color: var(--ifm-color-content-secondary); + border: 2px dashed var(--ifm-color-emphasis-300); + border-radius: 8px; + margin: 1rem; +} + +.placeholderIcon { + font-size: 48px; +} + +.loadButton { + padding: 0.75rem 1.5rem; + background: var(--ifm-color-primary); + color: white; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: all 0.2s ease; +} + +.loadButton:hover { + background: var(--ifm-color-primary-dark); + transform: translateY(-1px); +} + +.mediaPlaceholder p { + margin: 0; + font-size: 16px; + color: var(--ifm-color-content-secondary); +} + +/* Loading State */ +.mediaLoading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 300px; + gap: 1rem; + color: var(--ifm-color-content-secondary); +} + +.loadingSpinner { + width: 32px; + height: 32px; + border: 3px solid var(--ifm-color-emphasis-300); + border-top: 3px solid var(--ifm-color-primary); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +.mediaLoading p { + margin: 0; + font-size: 14px; + color: var(--ifm-color-content-secondary); +} + +/* Error State */ +.mediaError { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 300px; + gap: 1rem; + padding: 2rem; + text-align: center; + color: var(--ifm-color-content-secondary); +} + +.errorIcon { + font-size: 48px; +} + +.mediaError p { + margin: 0; + font-size: 16px; + color: var(--ifm-color-content-secondary); +} + +.retryButton { + padding: 0.5rem 1rem; + background: var(--ifm-color-primary); + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + transition: background-color 0.2s ease; +} + +.retryButton:hover { + background: var(--ifm-color-primary-dark); +} + +/* Play Status Indicator */ +.playStatus { + position: absolute; + top: 1rem; + right: 1rem; + opacity: 0; + transform: translateY(-10px); + transition: all 0.3s ease; + pointer-events: none; +} + +.playStatus.visible { + opacity: 1; + transform: translateY(0); +} + +.playIndicator { + background: rgba(0, 0, 0, 0.7); + color: white; + padding: 0.5rem 1rem; + border-radius: 20px; + font-size: 12px; + display: flex; + align-items: center; + gap: 0.5rem; + backdrop-filter: blur(4px); +} + +/* Playing state effects */ +.mediaContent.playing .videoPlayer { + box-shadow: 0 0 20px rgba(var(--ifm-color-primary-rgb), 0.3); +} + +/* Responsive Design */ +@media (max-width: 768px) { + .mediaContainer { + border-radius: 8px; + } + + .youtubePlayer { + height: 250px; + min-height: 200px; + } + + .mediaCaption { + padding: 0.75rem; + } + + .mediaCaption p { + font-size: 13px; + } + + .mediaLoading, + .mediaError, + .mediaPlaceholder { + height: 200px; + padding: 1rem; + } + + .errorIcon, + .placeholderIcon { + font-size: 36px; + } +} + +/* Dark Mode Adjustments */ +[data-theme='dark'] .mediaCaption { + background: var(--ifm-color-emphasis-100); + border-top-color: var(--ifm-color-emphasis-300); +} + +[data-theme='dark'] .loadingSpinner { + border-color: var(--ifm-color-emphasis-400); + border-top-color: var(--ifm-color-primary); +} + +[data-theme='dark'] .playIndicator { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(8px); +} \ No newline at end of file diff --git a/src/pages/quickstart/MediaStep/index.tsx b/src/pages/quickstart/MediaStep/index.tsx new file mode 100644 index 00000000000..aabb67186b2 --- /dev/null +++ b/src/pages/quickstart/MediaStep/index.tsx @@ -0,0 +1,180 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import classNames from 'classnames'; +import { IntegrationStep } from '../interfaces'; +import styles from './MediaStep.module.css'; + +interface MediaStepProps { + step: IntegrationStep; + className?: string; + isVisible?: boolean; // Add visibility prop for lazy loading +} + +const MediaStep: React.FC = ({ step, className, isVisible = true }) => { + const [isLoading, setIsLoading] = useState(true); // Start loading immediately + const [hasError, setHasError] = useState(false); + const [isPlaying, setIsPlaying] = useState(false); + const videoRef = useRef(null); + const imageRef = useRef(null); + + if (!step.mediaContent) { + return ( +
+
+

No media content available

+
+
+ ); + } + + const { type, url, youtubeId, alt, caption, poster, autoplay = false, loop = false, muted = true } = step.mediaContent; + + const handleLoad = useCallback(() => { + setIsLoading(false); + setHasError(false); + }, []); + + const handleError = useCallback(() => { + setIsLoading(false); + setHasError(true); + }, []); + + const handlePlay = useCallback(() => { + setIsPlaying(true); + }, []); + + const handlePause = useCallback(() => { + setIsPlaying(false); + }, []); + + + + + + // Reset when URL changes + useEffect(() => { + setIsLoading(true); + setHasError(false); + setIsPlaying(false); + }, [url]); + + const renderMedia = () => { + + if (type === 'youtube') { + const youtubeUrl = `https://www.youtube.com/embed/${youtubeId}?rel=0&modestbranding=1&showinfo=0${autoplay ? '&autoplay=1' : ''}${muted ? '&mute=1' : ''}`; + + return ( +