diff --git a/.env.example b/.env.example index 49d3c80..d8ae523 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,15 @@ # Defaults to 'devnet' if not specified VITE_ENVIRONMENT=devnet +# Optional client-visible backend URL override (for example, /api during local development) +VITE_BACKEND_URL= + +# Optional Vite-only proxy target used with VITE_BACKEND_URL=/api +BACKEND_PROXY_TARGET= + +# Set to false when a local browser cannot trust Vite's generated development certificate +DEV_HTTPS=true + # Application version (automatically set during build) VITE_APP_VERSION= diff --git a/.github/workflows/pr_lint_build.yml b/.github/workflows/pr_lint_build.yml index cfbef65..36a1d3a 100644 --- a/.github/workflows/pr_lint_build.yml +++ b/.github/workflows/pr_lint_build.yml @@ -22,6 +22,9 @@ jobs: - name: Lint run: npm run lint + - name: Test + run: npm test + build: runs-on: ubuntu-latest steps: diff --git a/package-lock.json b/package-lock.json index 6c1a4ed..f4cafbc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,7 +69,8 @@ "tailwindcss": "^4.1.13", "typescript": "^5.7.3", "typescript-eslint": "^8.18.1", - "vite": "^6.4.1" + "vite": "^6.4.1", + "vitest": "3.2.6" } }, "node_modules/@aave/account": { @@ -6858,6 +6859,64 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.8.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.8.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", @@ -7002,6 +7061,17 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -7020,6 +7090,13 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -7355,6 +7432,120 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, + "node_modules/@vitest/expect": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz", + "integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz", + "integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.6", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz", + "integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.6", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", + "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz", + "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz", + "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.6", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/@vitest/pretty-format": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", + "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@wagmi/connectors": { "version": "5.11.2", "resolved": "https://registry.npmjs.org/@wagmi/connectors/-/connectors-5.11.2.tgz", @@ -8370,6 +8561,16 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/async-mutex": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.2.6.tgz", @@ -8730,6 +8931,16 @@ "node": ">= 0.8" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -8852,6 +9063,23 @@ "node": ">=6" } }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -8894,6 +9122,16 @@ "node": "*" } }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -9423,6 +9661,16 @@ "node": ">=0.10" } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -9689,6 +9937,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -9971,6 +10226,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -10313,6 +10578,16 @@ "dev": true, "license": "ISC" }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/extension-port-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/extension-port-stream/-/extension-port-stream-3.0.0.tgz", @@ -11756,6 +12031,13 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -12290,6 +12572,23 @@ "dev": true, "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -13472,6 +13771,13 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -13577,6 +13883,20 @@ "node": ">= 10.x" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/stream-chain": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", @@ -13673,6 +13993,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/style-value-types": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.0.0.tgz", @@ -13818,6 +14158,20 @@ "real-require": "^0.1.0" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -13863,6 +14217,36 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-buffer": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", @@ -14495,6 +14879,29 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/vite/node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -14524,6 +14931,119 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/vitest": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz", + "integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.6", + "@vitest/mocker": "3.2.6", + "@vitest/pretty-format": "^3.2.6", + "@vitest/runner": "3.2.6", + "@vitest/snapshot": "3.2.6", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.6", + "@vitest/ui": "3.2.6", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz", + "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.6", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/wagmi": { "version": "2.19.5", "resolved": "https://registry.npmjs.org/wagmi/-/wagmi-2.19.5.tgz", @@ -14835,6 +15355,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/widest-line": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", diff --git a/package.json b/package.json index 2c39cd4..7ffe79a 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "dev:logs": "vite --debug", "build": "tsc -b && vite build", "lint": "eslint .", + "test": "vitest run", "serve": "serve -s dist", "preview": "vite preview" }, @@ -72,7 +73,8 @@ "tailwindcss": "^4.1.13", "typescript": "^5.7.3", "typescript-eslint": "^8.18.1", - "vite": "^6.4.1" + "vite": "^6.4.1", + "vitest": "3.2.6" }, "overrides": { "@babel/helpers": "7.28.4", diff --git a/src/components/Licenses/modals/LicenseBulkLinkModal.tsx b/src/components/Licenses/modals/LicenseBulkLinkModal.tsx index 32fafb1..a53f01f 100644 --- a/src/components/Licenses/modals/LicenseBulkLinkModal.tsx +++ b/src/components/Licenses/modals/LicenseBulkLinkModal.tsx @@ -6,7 +6,7 @@ import { Spinner } from '@heroui/spinner'; import { config, environment } from '@lib/config'; import { AuthenticationContextType, useAuthenticationContext } from '@lib/contexts/authentication'; import { routePath } from '@lib/routes/route-paths'; -import { fBI, getShortAddressOrHash } from '@lib/utils'; +import { fBI, getShortAddressOrHash, getVerificationTypeLabel, getVerificationTypeLongLabel } from '@lib/utils'; import { DetailedAlert } from '@shared/DetailedAlert'; import { ApplicationStatus } from '@typedefs/profile'; import { addDays } from 'date-fns'; @@ -667,12 +667,14 @@ const LicenseBulkLinkModal = forwardRef(({ licenses, li variant="red" icon={} title="Unavailable" - description={
KYC (Know Your Customer) must be completed before linking licenses.
} + description={ +
{getVerificationTypeLongLabel(account?.applicantType)} must be completed before linking licenses.
+ } />
diff --git a/src/components/Licenses/modals/LicenseLinkModal.tsx b/src/components/Licenses/modals/LicenseLinkModal.tsx index d662a7f..73af1c0 100644 --- a/src/components/Licenses/modals/LicenseLinkModal.tsx +++ b/src/components/Licenses/modals/LicenseLinkModal.tsx @@ -13,6 +13,7 @@ import { AuthenticationContextType, useAuthenticationContext } from '@lib/contex import { BlockchainContextType, useBlockchainContext } from '@lib/contexts/blockchain'; import { routePath } from '@lib/routes/route-paths'; import useAwait from '@lib/useAwait'; +import { getVerificationTypeLabel, getVerificationTypeLongLabel } from '@lib/utils'; import { DetailedAlert } from '@shared/DetailedAlert'; import { R1ValueWithLabel } from '@shared/R1ValueWithLabel'; import { TokenSvg } from '@shared/TokenSvg'; @@ -274,13 +275,17 @@ const LicenseLinkModal = forwardRef(({ nodeAddresses, onClaim, shouldTriggerGhos variant="red" icon={} title="Unavailable" - description={
KYC (Know Your Customer) must be completed before linking license.
} + description={ +
+ {getVerificationTypeLongLabel(account?.applicantType)} must be completed before linking a license. +
+ } />
diff --git a/src/components/Profile/sections/PersonalInformation.tsx b/src/components/Profile/sections/PersonalInformation.tsx index 1fac0ba..2d52dea 100644 --- a/src/components/Profile/sections/PersonalInformation.tsx +++ b/src/components/Profile/sections/PersonalInformation.tsx @@ -1,8 +1,7 @@ import { Button } from '@heroui/button'; import { Skeleton } from '@heroui/skeleton'; import { Switch } from '@heroui/switch'; -import { emailSubscribe, emailUnsubscribe, getKycInfo, initSumsubSession } from '@lib/api/backend'; -import { environment } from '@lib/config'; +import { emailSubscribe, emailUnsubscribe, getKycInfo } from '@lib/api/backend'; import { AuthenticationContextType, useAuthenticationContext } from '@lib/contexts/authentication'; import { routePath } from '@lib/routes/route-paths'; import { getApplicationStatusInfo } from '@lib/utils'; @@ -12,6 +11,7 @@ import ProfileRow from '@shared/ProfileRow'; import { ApiAccount } from '@typedefs/blockchain'; import { KycInfo } from '@typedefs/general'; import { ApplicationStatus } from '@typedefs/profile'; +import { VerificationApplicantType } from '@typedefs/verification'; import { useEffect, useState } from 'react'; import toast from 'react-hot-toast'; import { RiInformation2Line } from 'react-icons/ri'; @@ -29,8 +29,6 @@ export default function PersonalInformation() { const [isFetchingKycInfo, setFetchingKycInfo] = useState(false); const [kycInfo, setKycInfo] = useState(); - const [isLoading, setLoading] = useState(false); - const [applicationStatusInfo, setApplicationStatusInfo] = useState< { text: string; color: 'yellow' | 'green' | 'red' } | undefined >(); @@ -67,10 +65,6 @@ export default function PersonalInformation() { }; const toggleEmailSubscriptionPreference = async () => { - if (isLoading) { - return; - } - // We set the new value optimistically setSubscribed(!isSubscribed); @@ -87,25 +81,8 @@ export default function PersonalInformation() { } }; - const initializeApplication = async () => { - setLoading(true); - - const type: 'individual' | 'company' = isCompany ? 'company' : 'individual'; - - try { - const tokenResponse: string = await initSumsubSession(type); - - if (!tokenResponse) { - throw new Error('Unexpected error, please try again.'); - } - - navigate(`${routePath.kyc}?type=${type}&token=${tokenResponse}`); - } catch (error) { - console.error('Error', error); - toast.error('Unexpected error, please try again.'); - } finally { - setLoading(false); - } + const initializeApplication = (type: VerificationApplicantType) => { + navigate(`${routePath.kyc}?type=${type}`); }; const getKycInfoContent = () => { @@ -138,8 +115,7 @@ export default function PersonalInformation() {
initializeApplication(account.applicantType)} />
@@ -188,8 +164,7 @@ export default function PersonalInformation() {
initializeApplication(isCompany ? 'company' : 'individual')} />
@@ -232,43 +207,26 @@ export default function PersonalInformation() { {/* KYC/KYB */} - {environment === 'mainnet' && ( -
-
-
{!kycInfo ? 'KYC/KYB' : `${kycInfo.isCompany ? 'KYB' : 'KYC'}`}
- - {isApplicationInitiated && ( -
diff --git a/src/pages/EmailConfirmation.tsx b/src/pages/EmailConfirmation.tsx index e0240bb..1999255 100644 --- a/src/pages/EmailConfirmation.tsx +++ b/src/pages/EmailConfirmation.tsx @@ -73,7 +73,7 @@ function EmailConfirmation() {
Your account has been successfully activated.
-
You may now proceed with completing your KYC (Know Your Customer) verification.
+
You may now proceed with completing your KYC or KYB verification.
@@ -96,7 +96,7 @@ function EmailConfirmation() { -
Complete KYC
+
Complete KYC/KYB
@@ -135,7 +135,7 @@ function EmailConfirmation() {
- Complete KYC + Complete KYC/KYB
@@ -155,7 +155,7 @@ function EmailConfirmation() {
diff --git a/src/pages/KYC.tsx b/src/pages/KYC.tsx index 5f35351..097cb64 100644 --- a/src/pages/KYC.tsx +++ b/src/pages/KYC.tsx @@ -1,29 +1,193 @@ -import { initSumsubSession } from '@lib/api/backend'; +import { VerificationExperience } from '@components/Verification/VerificationExperience'; +import { Button } from '@heroui/button'; +import { createVerificationSession, getAccount } from '@lib/api/backend'; +import { AuthenticationContextType, useAuthenticationContext } from '@lib/contexts/authentication'; import { routePath } from '@lib/routes/route-paths'; -import { Button } from "@heroui/button"; -import { Spinner } from "@heroui/spinner"; -import { BigCard } from '@shared/BigCard'; +import { startVerificationStatusPolling } from '@lib/verification-status-polling'; +import { ApplicationStatus } from '@typedefs/profile'; +import { SumsubVerificationSession, VerificationApplicantType, VerificationSession } from '@typedefs/verification'; import SumsubWebSdk from '@sumsub/websdk-react'; -import { useEffect, useState } from 'react'; -import { RiArrowLeftLine } from 'react-icons/ri'; -import { Link, useNavigate, useSearchParams } from 'react-router-dom'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { RiArrowLeftLine, RiExternalLinkLine } from 'react-icons/ri'; +import { Link, Navigate, useSearchParams } from 'react-router-dom'; -function KYC() { - const navigate = useNavigate(); +const isApplicantType = (value: string | null): value is VerificationApplicantType => + value === 'individual' || value === 'company'; + +const isStatusOutcome = (status: ApplicationStatus) => + status === ApplicationStatus.Approved || + status === ApplicationStatus.OnHold || + status === ApplicationStatus.Rejected || + status === ApplicationStatus.FinalRejected; + +const getStatusMessage = (status: ApplicationStatus, applicantType: VerificationApplicantType) => { + const verificationType = applicantType === 'company' ? 'KYB' : 'KYC'; + + switch (status) { + case ApplicationStatus.Approved: + return `Your ${verificationType} has been approved.`; + case ApplicationStatus.OnHold: + return `Your ${verificationType} was submitted and is under review.`; + case ApplicationStatus.Rejected: + return `Your ${verificationType} needs additional information. You can continue it from your profile.`; + case ApplicationStatus.FinalRejected: + return `Your ${verificationType} could not be approved. Review the status from your profile.`; + default: + return `Your latest ${verificationType} status is available from your profile.`; + } +}; +function KYC() { const [searchParams] = useSearchParams(); - const type = searchParams.get('type'); - const token = searchParams.get('token'); + const typeParam = searchParams.get('type'); + const { account, setAccount } = useAuthenticationContext() as AuthenticationContextType; + + const [session, setSession] = useState(); + const [isConsentAccepted, setConsentAccepted] = useState(false); + const [isLoadingSession, setLoadingSession] = useState(false); + const [isLaunched, setLaunched] = useState(false); + const [errorMessage, setErrorMessage] = useState(); + const [confirmedStatus, setConfirmedStatus] = useState(); + const statusAtLaunch = useRef(); + const sessionRequestInFlight = useRef(false); + + const loadSession = useCallback(async (type: VerificationApplicantType) => { + if (sessionRequestInFlight.current) return; + sessionRequestInFlight.current = true; + setSession(undefined); + setErrorMessage(undefined); + setConfirmedStatus(undefined); + setLoadingSession(true); + setLaunched(false); + + try { + const response = await createVerificationSession(type); - const [accessToken, setAccessToken] = useState(); + if (response.applicantType !== type) { + throw new Error('Verification applicant type does not match the requested flow.'); + } + + setSession(response); + setLaunched(true); + } catch (error) { + console.error('Unable to prepare verification session:', error); + setErrorMessage('We could not securely prepare your verification session. Please try again.'); + } finally { + sessionRequestInFlight.current = false; + setLoadingSession(false); + } + }, []); useEffect(() => { - if (!token || !type) { - navigate(routePath.notFound); - } else { - setAccessToken(token); + if (!isLaunched || !isApplicantType(typeParam)) { + return; } - }, [type, token]); + + let isDisposed = false; + + const refreshStatus = async () => { + try { + const latestAccount = await getAccount(); + if (isDisposed) return false; + + setAccount(latestAccount); + + if (isStatusOutcome(latestAccount.kycStatus) && latestAccount.kycStatus !== statusAtLaunch.current) { + setConfirmedStatus(latestAccount.kycStatus); + return true; + } + } catch (error) { + console.error('Unable to refresh verification status:', error); + } + + return false; + }; + + const stopPolling = startVerificationStatusPolling(refreshStatus); + + return () => { + isDisposed = true; + stopPolling(); + }; + }, [isLaunched, setAccount, typeParam]); + + if (!isApplicantType(typeParam)) { + return ; + } + + const retry = () => loadSession(typeParam); + + const launch = async () => { + if (!isConsentAccepted) return; + statusAtLaunch.current = account?.kycStatus; + await loadSession(typeParam); + }; + + const refreshSumsubAccessToken = async (currentSession: SumsubVerificationSession) => { + const refreshedSession = await createVerificationSession(currentSession.applicantType); + + if (refreshedSession.provider !== 'sumsub') { + throw new Error('Verification provider changed. Reload the page to continue.'); + } + + setSession(refreshedSession); + return refreshedSession.accessToken; + }; + + const renderProvider = () => { + if (!session) return null; + + if (session.provider === 'didit') { + return ( +
+