diff --git a/.github/workflows/build-rc.yml b/.github/workflows/build-rc.yml index d0d35344a..dff30f980 100644 --- a/.github/workflows/build-rc.yml +++ b/.github/workflows/build-rc.yml @@ -83,7 +83,7 @@ jobs: env: SEED_WORDS1: ${{ secrets.SEED_WORDS1 }} SEED_WORDS2: ${{ secrets.SEED_WORDS2 }} - run: xvfb-run --auto-servernum --server-args="-screen 0 360x360x24" npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + run: xvfb-run --auto-servernum --server-args="-screen 0 360x360x24" npx playwright test --grep-invert "#localexecution" --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - name: Upload Playwright report if: ${{ !cancelled() }} uses: actions/upload-artifact@v3 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 67aec51a8..f10252803 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ on: - develop jobs: build: - if: ${{ !startsWith(github.head_ref, 'release/') && !startsWith(github.head_ref, 'e2etest/')}} + if: ${{ !startsWith(github.head_ref, 'release/') && !startsWith(github.head_ref, 'e2etest/') && !github.event.pull_request.draft == true }} runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 @@ -44,7 +44,7 @@ jobs: env: SEED_WORDS1: ${{ secrets.SEED_WORDS1 }} SEED_WORDS2: ${{ secrets.SEED_WORDS2 }} - run: xvfb-run --auto-servernum --server-args="-screen 0 360x360x24" npx playwright test --grep "#smoketest" --reporter=html + run: xvfb-run --auto-servernum --server-args="-screen 0 360x360x24" npm run e2etest:smoketest --reporter=html - name: Upload Playwright report if: always() uses: actions/upload-artifact@v3 diff --git a/.github/workflows/create-release-pr.yml b/.github/workflows/create-release-pr.yml index b0d85aed2..187ed9642 100644 --- a/.github/workflows/create-release-pr.yml +++ b/.github/workflows/create-release-pr.yml @@ -32,6 +32,7 @@ jobs: - id: run-create-release-pr-sh env: BUMP: ${{ inputs.bump }} + SOURCE_BRANCH: ${{ github.ref_name }} GH_TOKEN: ${{ github.token }} run: | # git config diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 23d54de8b..c4b1853b2 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -7,7 +7,7 @@ on: jobs: build: - if: ${{ startsWith(github.head_ref, 'e2etest/') || github.event_name == 'workflow_dispatch' }} + if: ${{ (startsWith(github.head_ref, 'e2etest/') && github.event.pull_request && github.event.pull_request.draft == false) || github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -77,7 +77,7 @@ jobs: env: SEED_WORDS1: ${{ secrets.SEED_WORDS1 }} SEED_WORDS2: ${{ secrets.SEED_WORDS2 }} - run: xvfb-run --auto-servernum --server-args="-screen 0 360x360x24" npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + run: xvfb-run --auto-servernum --server-args="-screen 0 360x360x24" npx playwright test --grep-invert "#localexecution" --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - name: Upload Playwright report if: ${{ !cancelled() }} uses: actions/upload-artifact@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a062aa7d6..1527eac78 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,7 +60,7 @@ jobs: PR_ID: ${{ github.event.pull_request.number }} run: | # update PR description - cat release.json | jq -r .body > body.md + cat scripts/release.json | jq -r .body > body.md echo -e "\n\nPublished latest release: $(cat release.json | jq -r .html_url)" >> body.md gh api \ --method PATCH \ @@ -71,7 +71,7 @@ jobs: - id: download-latest-asset name: Download latest asset from rc run: | - ASSET_ID=$(cat releases.json | jq -r ".[] | select(.tag_name==\"$TAG_RC\") | .assets[0].id") + ASSET_ID=$(cat scripts/releases.json | jq -r ".[] | select(.tag_name==\"$TAG_RC\") | .assets[0].id") gh api \ -H "Accept: application/octet-stream" \ -H "X-GitHub-Api-Version: 2022-11-28" \ @@ -96,6 +96,7 @@ jobs: git config user.name "GitHub Actions Bot" git config user.email "<>" # run shell script + cd scripts ./merge-to-remote.sh - id: copy-release-to-public name: Copy release to public remote @@ -103,7 +104,7 @@ jobs: REMOTE_REPO: xverse-web-extension run: | # publish the latest release on remote - cat release.json | jq -r .body > public-body.md + cat scripts/release.json | jq -r .body > public-body.md gh api \ --method POST \ -H "Accept: application/vnd.github+json" \ diff --git a/README.md b/README.md index b1aef1bb4..1bca9fcc6 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ npm run e2etest If you only want to run the smoke test suite, run ``` -npm run smoketest +npm run e2etest:smoketest ``` If you want to run the e2e test in UI Mode: diff --git a/package-lock.json b/package-lock.json index 0243b27d2..8f2aadcdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,21 @@ { "name": "xverse-web-extension", - "version": "0.36.1", + "version": "0.38.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "xverse-web-extension", - "version": "0.36.1", + "version": "0.38.1", "dependencies": { + "@aryzing/superqs": "0.0.6", "@ledgerhq/hw-transport-webusb": "^6.27.13", "@phosphor-icons/react": "^2.0.10", "@playwright/test": "^1.43.1", "@react-spring/web": "^9.6.1", - "@sats-connect/core": "0.0.9", + "@sats-connect/core": "0.0.14", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "13.7.1", + "@secretkeylabs/xverse-core": "17.1.1", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.13.1", @@ -27,6 +28,7 @@ "@testing-library/user-event": "^13.5.0", "alex-sdk": "0.1.26", "argon2-browser": "^1.18.0", + "async-mutex": "^0.5.0", "axios": "1.7.0", "bignumber.js": "^9.1.0", "bip39": "^3.0.3", @@ -74,6 +76,7 @@ "superjson": "2.2.1", "swiper": "11.0.6", "ts-transformer-keys": "0.4.4", + "valibot": "0.33.2", "valid-url": "^1.0.9", "webextension-polyfill": "^0.10.0", "zod": "3.22.4", @@ -108,7 +111,7 @@ "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-no-inline-styles": "^1.0.5", - "eslint-plugin-playwright": "^1.5.4", + "eslint-plugin-playwright": "^1.6.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.31.8", "eslint-plugin-react-hooks": "^4.6.0", @@ -134,6 +137,7 @@ "type-fest": "^2.19.0", "typescript": "^5.0.0", "typescript-plugin-styled-components": "^3.0.0", + "vite-tsconfig-paths": "4.3.2", "vitest": "^0.34.6", "webpack": "^5.89.0", "webpack-cli": "^4.0.0", @@ -249,6 +253,14 @@ "node": ">=6.0.0" } }, + "node_modules/@aryzing/superqs": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@aryzing/superqs/-/superqs-0.0.6.tgz", + "integrity": "sha512-MQR7BysZv1BqEmiFohs967UvMJMnWBipn2EFTG9AotyYXGgY67g7iofVOq8K83LAiOLVImr5kLhlzsmMTKLjFA==", + "dependencies": { + "superjson": "2.2.1" + } + }, "node_modules/@babel/code-frame": { "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz", @@ -1312,15 +1324,18 @@ ] }, "node_modules/@sats-connect/core": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.0.9.tgz", - "integrity": "sha512-7/hY7VF9OqNDYcXlADluMKCTDu9f4OENWkWWO8K1LqanuyZhepu3fBRQKSCT6Xr/3KorvSH/e8sSTcJCzuVOAA==", + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.0.14.tgz", + "integrity": "sha512-wWV7EajoyADV/8zRsC0ThD5/VAe4LYPhY07W4KtJ75iFehc56ZbfPIKvqc0LLUTvEv7EO/M4ntTBrgorEI8ggQ==", "dependencies": { "axios": "1.6.8", "bitcoin-address-validation": "2.2.3", "buffer": "6.0.3", "jsontokens": "4.0.1", "lodash.omit": "4.5.0" + }, + "peerDependencies": { + "valibot": "0.33.2" } }, "node_modules/@sats-connect/core/node_modules/axios": { @@ -1334,9 +1349,9 @@ } }, "node_modules/@scure/base": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", - "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.7.tgz", + "integrity": "sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g==", "funding": { "url": "https://paulmillr.com/funding/" } @@ -1409,15 +1424,16 @@ } }, "node_modules/@secretkeylabs/xverse-core": { - "version": "13.7.1", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/13.7.1/a0d1c4ea750be9bfa448e206b246388c422254e8", - "integrity": "sha512-t+H6kOFBANnD0xrOg8P+bc9VA1WP6W1xc0A7qeS3Yb3MALg4wsiGQ8bEWPuEut+bInMvF98BiUM8rSotyjc2vA==", - "license": "ISC", + "version": "17.1.1", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/17.1.1/8824e8522bcad7fc01e0cf36d8f323ddcecc77fa", + "integrity": "sha512-RwLeyPvlPfr08DCuSpc5O/YLphkz00mMfiDzpgvLGF7KzSJ6QDLJhZcMl90hC2yRQN3egXC3a9rNkXOJlVcxeQ==", "dependencies": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/curves": "^1.2.0", "@noble/secp256k1": "^1.7.1", "@scure/base": "^1.1.1", + "@scure/bip32": "^1.4.0", + "@scure/bip39": "^1.3.0", "@scure/btc-signer": "1.2.1", "@stacks/auth": "6.13.1", "@stacks/connect": "7.7.1", @@ -1462,6 +1478,53 @@ "react-dom": ">18.0.0" } }, + "node_modules/@secretkeylabs/xverse-core/node_modules/@noble/curves": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@secretkeylabs/xverse-core/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@secretkeylabs/xverse-core/node_modules/@scure/bip32": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", + "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "dependencies": { + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@secretkeylabs/xverse-core/node_modules/@scure/bip39": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", + "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", + "dependencies": { + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@secretkeylabs/xverse-core/node_modules/@stacks/connect": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/@stacks/connect/-/connect-7.7.1.tgz", @@ -1488,6 +1551,14 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==" }, + "node_modules/@secretkeylabs/xverse-core/node_modules/async-mutex": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", + "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@secretkeylabs/xverse-core/node_modules/bip39": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.3.tgz", @@ -3278,9 +3349,9 @@ } }, "node_modules/@zondax/ledger-stacks/node_modules/@types/node": { - "version": "18.19.31", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.31.tgz", - "integrity": "sha512-ArgCD39YpyyrtFKIqMDvjz79jto5fcI/SVUs2HwB+f0dAzq68yqOdyaSivLiLugSziTpNXLQrVb7RZFmdZzbhA==", + "version": "18.19.34", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.34.tgz", + "integrity": "sha512-eXF4pfBNV5DAMKGbI02NnDtWrQ40hAN558/2vvS4gMpMIxaf6JmD7YjnZbq0Q9TDSSkKBamime8ewRoomHdt4g==", "dependencies": { "undici-types": "~5.26.4" } @@ -3738,9 +3809,9 @@ } }, "node_modules/async-mutex": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", - "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", "dependencies": { "tslib": "^2.4.0" } @@ -3998,9 +4069,9 @@ } }, "node_modules/bitcoinjs-lib": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.5.tgz", - "integrity": "sha512-yuf6xs9QX/E8LWE2aMJPNd0IxGofwfuVOiYdNUESkc+2bHHVKjhJd8qewqapeoolh9fihzHGoDCB5Vkr57RZCQ==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.6.tgz", + "integrity": "sha512-Fk8+Vc+e2rMoDU5gXkW9tD+313rhkm5h6N9HfZxXvYU9LedttVvmXKTgd9k5rsQJjkSfsv6XRM8uhJv94SrvcA==", "dependencies": { "@noble/hashes": "^1.2.0", "bech32": "^2.0.0", @@ -6346,9 +6417,9 @@ } }, "node_modules/eslint-plugin-playwright": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-1.5.4.tgz", - "integrity": "sha512-J38Wy3Vc2f9y73J+KRmgXgbYI8TZ3zbz6qBbTj3PhpFndUS572jZ7kqQ3rJ9si5BaMHT7lmZzraO+3UjwIDV4Q==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-1.6.0.tgz", + "integrity": "sha512-tI1E/EDbHT4Fx5KvukUG3RTIT0gk44gvTP8bNwxLCFsUXVM98ZJG5zWU6Om5JOzH9FrmN4AhMu/UKyEsu0ZoDA==", "dev": true, "dependencies": { "globals": "^13.23.0" @@ -7458,6 +7529,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, "node_modules/goober": { "version": "2.1.13", "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.13.tgz", @@ -15539,6 +15616,26 @@ "typescript": ">=3" } }, + "node_modules/tsconfck": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.0.tgz", + "integrity": "sha512-CMjc5zMnyAjcS9sPLytrbFmj89st2g+JYtY/c02ug4Q+CZaAtCgbyviI0n1YvjZE/pzoc6FbNsINS13DOL1B9w==", + "dev": true, + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -15849,9 +15946,9 @@ "dev": true }, "node_modules/uglify-js": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", - "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.18.0.tgz", + "integrity": "sha512-SyVVbcNBCk0dzr9XL/R/ySrmYf0s372K6/hFklzgcp2lBFyXtw4I7BOdDjlLhE1aVqaI/SHWXWmYdlZxuyF38A==", "bin": { "uglifyjs": "bin/uglifyjs" }, @@ -16014,6 +16111,11 @@ "uuid": "8.3.2" } }, + "node_modules/valibot": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.33.2.tgz", + "integrity": "sha512-ZpFWuI+bs5+PP66q4zVFn4e4t/s5jmMw5iPBZmGUoi8iQqXyU9YY/BLCAyk62Z/bNS8qdUNBEyx52952qdqW3w==" + }, "node_modules/valid-url": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", @@ -16114,6 +16216,25 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite-tsconfig-paths": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz", + "integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, "node_modules/vitest": { "version": "0.34.6", "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", @@ -16921,6 +17042,14 @@ "@jridgewell/trace-mapping": "^0.3.9" } }, + "@aryzing/superqs": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@aryzing/superqs/-/superqs-0.0.6.tgz", + "integrity": "sha512-MQR7BysZv1BqEmiFohs967UvMJMnWBipn2EFTG9AotyYXGgY67g7iofVOq8K83LAiOLVImr5kLhlzsmMTKLjFA==", + "requires": { + "superjson": "2.2.1" + } + }, "@babel/code-frame": { "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz", @@ -17698,9 +17827,9 @@ "optional": true }, "@sats-connect/core": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.0.9.tgz", - "integrity": "sha512-7/hY7VF9OqNDYcXlADluMKCTDu9f4OENWkWWO8K1LqanuyZhepu3fBRQKSCT6Xr/3KorvSH/e8sSTcJCzuVOAA==", + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.0.14.tgz", + "integrity": "sha512-wWV7EajoyADV/8zRsC0ThD5/VAe4LYPhY07W4KtJ75iFehc56ZbfPIKvqc0LLUTvEv7EO/M4ntTBrgorEI8ggQ==", "requires": { "axios": "1.6.8", "bitcoin-address-validation": "2.2.3", @@ -17722,9 +17851,9 @@ } }, "@scure/base": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", - "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==" + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.7.tgz", + "integrity": "sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g==" }, "@scure/bip32": { "version": "1.1.3", @@ -17771,14 +17900,16 @@ } }, "@secretkeylabs/xverse-core": { - "version": "13.7.1", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/13.7.1/a0d1c4ea750be9bfa448e206b246388c422254e8", - "integrity": "sha512-t+H6kOFBANnD0xrOg8P+bc9VA1WP6W1xc0A7qeS3Yb3MALg4wsiGQ8bEWPuEut+bInMvF98BiUM8rSotyjc2vA==", + "version": "17.1.1", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/17.1.1/8824e8522bcad7fc01e0cf36d8f323ddcecc77fa", + "integrity": "sha512-RwLeyPvlPfr08DCuSpc5O/YLphkz00mMfiDzpgvLGF7KzSJ6QDLJhZcMl90hC2yRQN3egXC3a9rNkXOJlVcxeQ==", "requires": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/curves": "^1.2.0", "@noble/secp256k1": "^1.7.1", "@scure/base": "^1.1.1", + "@scure/bip32": "^1.4.0", + "@scure/bip39": "^1.3.0", "@scure/btc-signer": "1.2.1", "@stacks/auth": "6.13.1", "@stacks/connect": "7.7.1", @@ -17814,6 +17945,38 @@ "varuint-bitcoin": "^1.1.2" }, "dependencies": { + "@noble/curves": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", + "requires": { + "@noble/hashes": "1.4.0" + } + }, + "@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==" + }, + "@scure/bip32": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", + "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "requires": { + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + } + }, + "@scure/bip39": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", + "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", + "requires": { + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + } + }, "@stacks/connect": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/@stacks/connect/-/connect-7.7.1.tgz", @@ -17840,6 +18003,14 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==" }, + "async-mutex": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", + "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==", + "requires": { + "tslib": "^2.4.0" + } + }, "bip39": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.3.tgz", @@ -19308,9 +19479,9 @@ } }, "@types/node": { - "version": "18.19.31", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.31.tgz", - "integrity": "sha512-ArgCD39YpyyrtFKIqMDvjz79jto5fcI/SVUs2HwB+f0dAzq68yqOdyaSivLiLugSziTpNXLQrVb7RZFmdZzbhA==", + "version": "18.19.34", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.34.tgz", + "integrity": "sha512-eXF4pfBNV5DAMKGbI02NnDtWrQ40hAN558/2vvS4gMpMIxaf6JmD7YjnZbq0Q9TDSSkKBamime8ewRoomHdt4g==", "requires": { "undici-types": "~5.26.4" } @@ -19657,9 +19828,9 @@ } }, "async-mutex": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", - "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", "requires": { "tslib": "^2.4.0" } @@ -19862,9 +20033,9 @@ } }, "bitcoinjs-lib": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.5.tgz", - "integrity": "sha512-yuf6xs9QX/E8LWE2aMJPNd0IxGofwfuVOiYdNUESkc+2bHHVKjhJd8qewqapeoolh9fihzHGoDCB5Vkr57RZCQ==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.6.tgz", + "integrity": "sha512-Fk8+Vc+e2rMoDU5gXkW9tD+313rhkm5h6N9HfZxXvYU9LedttVvmXKTgd9k5rsQJjkSfsv6XRM8uhJv94SrvcA==", "requires": { "@noble/hashes": "^1.2.0", "bech32": "^2.0.0", @@ -21802,9 +21973,9 @@ } }, "eslint-plugin-playwright": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-1.5.4.tgz", - "integrity": "sha512-J38Wy3Vc2f9y73J+KRmgXgbYI8TZ3zbz6qBbTj3PhpFndUS572jZ7kqQ3rJ9si5BaMHT7lmZzraO+3UjwIDV4Q==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-1.6.0.tgz", + "integrity": "sha512-tI1E/EDbHT4Fx5KvukUG3RTIT0gk44gvTP8bNwxLCFsUXVM98ZJG5zWU6Om5JOzH9FrmN4AhMu/UKyEsu0ZoDA==", "dev": true, "requires": { "globals": "^13.23.0" @@ -22516,6 +22687,12 @@ "slash": "^3.0.0" } }, + "globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, "goober": { "version": "2.1.13", "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.13.tgz", @@ -28186,6 +28363,13 @@ "dev": true, "requires": {} }, + "tsconfck": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.0.tgz", + "integrity": "sha512-CMjc5zMnyAjcS9sPLytrbFmj89st2g+JYtY/c02ug4Q+CZaAtCgbyviI0n1YvjZE/pzoc6FbNsINS13DOL1B9w==", + "dev": true, + "requires": {} + }, "tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -28418,9 +28602,9 @@ "dev": true }, "uglify-js": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", - "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==" + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.18.0.tgz", + "integrity": "sha512-SyVVbcNBCk0dzr9XL/R/ySrmYf0s372K6/hFklzgcp2lBFyXtw4I7BOdDjlLhE1aVqaI/SHWXWmYdlZxuyF38A==" }, "unbox-primitive": { "version": "1.0.2", @@ -28533,6 +28717,11 @@ "uuid": "8.3.2" } }, + "valibot": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.33.2.tgz", + "integrity": "sha512-ZpFWuI+bs5+PP66q4zVFn4e4t/s5jmMw5iPBZmGUoi8iQqXyU9YY/BLCAyk62Z/bNS8qdUNBEyx52952qdqW3w==" + }, "valid-url": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", @@ -28578,6 +28767,17 @@ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" } }, + "vite-tsconfig-paths": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz", + "integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + } + }, "vitest": { "version": "0.34.6", "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", diff --git a/package.json b/package.json index eba710883..be98cf018 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xverse-web-extension", "description": "A Bitcoin wallet for Web3", - "version": "0.36.1", + "version": "0.38.1", "private": true, "engines": { "node": "^18.18.2" @@ -14,11 +14,14 @@ "test": "vitest ./src", "style": "prettier --write \"src/**/*.{ts,tsx}\"", "prepare": "husky install", - "e2etest": "npx playwright test", + "e2etest": "npx playwright test -g \"\" --grep-invert \"#localexecution\"", "e2etest:ui": "npx playwright test --ui", - "smoketest": "npx playwright test --grep \"#smoketest\"", + "e2etest:smoketest": "npx playwright test --grep \"#smoketest\"", + "e2etest:skipped": "npx playwright test --grep \"#localexecution\"", "e2etest:report": "playwright show-report", - "find-deadcode": "ts-prune" + "find-deadcode": "ts-prune -p tsconfig.tsPrune.json", + "ts-check": "tsc --noEmit", + "lint": "eslint . --ext .js,.jsx,.ts,.tsx --quiet" }, "overrides": { "buffer": "6.0.3" @@ -35,13 +38,14 @@ ] }, "dependencies": { + "@aryzing/superqs": "0.0.6", "@ledgerhq/hw-transport-webusb": "^6.27.13", "@phosphor-icons/react": "^2.0.10", "@playwright/test": "^1.43.1", "@react-spring/web": "^9.6.1", - "@sats-connect/core": "0.0.9", + "@sats-connect/core": "0.0.14", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "13.7.1", + "@secretkeylabs/xverse-core": "17.1.1", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.13.1", @@ -54,6 +58,7 @@ "@testing-library/user-event": "^13.5.0", "alex-sdk": "0.1.26", "argon2-browser": "^1.18.0", + "async-mutex": "^0.5.0", "axios": "1.7.0", "bignumber.js": "^9.1.0", "bip39": "^3.0.3", @@ -101,6 +106,7 @@ "superjson": "2.2.1", "swiper": "11.0.6", "ts-transformer-keys": "0.4.4", + "valibot": "0.33.2", "valid-url": "^1.0.9", "webextension-polyfill": "^0.10.0", "zod": "3.22.4", @@ -135,7 +141,7 @@ "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-no-inline-styles": "^1.0.5", - "eslint-plugin-playwright": "^1.5.4", + "eslint-plugin-playwright": "^1.6.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.31.8", "eslint-plugin-react-hooks": "^4.6.0", @@ -161,6 +167,7 @@ "type-fest": "^2.19.0", "typescript": "^5.0.0", "typescript-plugin-styled-components": "^3.0.0", + "vite-tsconfig-paths": "4.3.2", "vitest": "^0.34.6", "webpack": "^5.89.0", "webpack-cli": "^4.0.0", diff --git a/playwright.config.ts b/playwright.config.ts index 36533c558..288391240 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -18,8 +18,11 @@ export default defineConfig({ forbidOnly: !!process.env.CI, /* Retry on CI only */ retries: process.env.CI ? 1 : 0, - /* Opt out of 2 tests parallel on CI. */ - workers: process.env.CI ? 2 : 1, + /* Opt to only 2 tests parallel on CI. + Note that you may want to change the non-ci amount to a lower number depending on + the specs of your computer or if the number of tests increases over time. + */ + workers: process.env.CI ? 2 : 5, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: process.env.CI ? 'blob' : 'html', snapshotDir: './playwright-snapshots', diff --git a/scripts/create-release-pr.sh b/scripts/create-release-pr.sh index c9d7a9b07..6daa86c50 100755 --- a/scripts/create-release-pr.sh +++ b/scripts/create-release-pr.sh @@ -14,10 +14,13 @@ if [[ -z "$BUMP" ]]; then exit 1 fi -echo -e "\n--- Prepare for $BUMP release branch ---" +# Check for an optional SOURCE_BRANCH variable, default to 'develop' if not provided +SOURCE_BRANCH="${SOURCE_BRANCH:-develop}" + +echo -e "\n--- Prepare for $BUMP release branch from $SOURCE_BRANCH ---" git fetch --all -git checkout develop +git checkout $SOURCE_BRANCH git pull npm version $BUMP --git-tag-version=false diff --git a/src/app/App.tsx b/src/app/App.tsx index 3b2a0d7ad..a4b9336c3 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,4 +1,5 @@ -import LoadingScreen from '@components/loadingScreen'; +import { PermissionsProvider } from '@components/permissionsManager'; +import StartupLoadingScreen from '@components/startupLoadingScreen'; import { CheckCircle, XCircle } from '@phosphor-icons/react'; import { setXClientVersion } from '@secretkeylabs/xverse-core'; import rootStore from '@stores/index'; @@ -28,66 +29,68 @@ const StyledIcon = styled.div` justify-content: center; `; -function App(): JSX.Element { +function App(): React.ReactNode { return ( <> - - - - }> - - - - - - - ), - style: { - ...Theme.typography.body_medium_m, - backgroundColor: Theme.colors.success_medium, - borderRadius: Theme.radius(2), - padding: Theme.space.s, - color: Theme.colors.elevation0, + + + + + }> + + + + + + + ), + style: { + ...Theme.typography.body_medium_m, + backgroundColor: Theme.colors.success_medium, + borderRadius: Theme.radius(2), + padding: Theme.space.s, + color: Theme.colors.elevation0, + }, }, - }, - error: { - icon: ( - - - - ), - style: { - ...Theme.typography.body_medium_m, - backgroundColor: Theme.colors.danger_dark, - borderRadius: Theme.radius(2), - padding: Theme.space.s, - color: Theme.colors.white_0, + error: { + icon: ( + + + + ), + style: { + ...Theme.typography.body_medium_m, + backgroundColor: Theme.colors.danger_dark, + borderRadius: Theme.radius(2), + padding: Theme.space.s, + color: Theme.colors.white_0, + }, }, - }, - blank: { - style: { - ...Theme.typography.body_medium_m, - backgroundColor: Theme.colors.white_0, - borderRadius: Theme.radius(2), - padding: Theme.space.s, - color: Theme.colors.elevation0, + blank: { + style: { + ...Theme.typography.body_medium_m, + backgroundColor: Theme.colors.white_0, + borderRadius: Theme.radius(2), + padding: Theme.space.s, + color: Theme.colors.elevation0, + }, }, - }, - }} - /> - - - - - + }} + /> + + + + + + ); } diff --git a/src/app/components/accountHeader/index.tsx b/src/app/components/accountHeader/index.tsx index bf1179e2b..477418fee 100644 --- a/src/app/components/accountHeader/index.tsx +++ b/src/app/components/accountHeader/index.tsx @@ -9,7 +9,7 @@ import styled from 'styled-components'; import OptionsDialog from '@components/optionsDialog/optionsDialog'; import useSeedVault from '@hooks/useSeedVault'; -import useWalletSelector from '@hooks/useWalletSelector'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import { DotsThreeVertical } from '@phosphor-icons/react'; import { OPTIONS_DIALOG_WIDTH } from '@utils/constants'; @@ -71,19 +71,19 @@ const WarningButton = styled(ButtonRow)` color: ${(props) => props.theme.colors.feedback.error}; `; -interface AccountHeaderComponentProps { +type Props = { disableMenuOption?: boolean; disableAccountSwitch?: boolean; showBorderBottom?: boolean; -} +}; function AccountHeaderComponent({ disableMenuOption = false, disableAccountSwitch = false, showBorderBottom = true, -}: AccountHeaderComponentProps) { +}: Props) { const navigate = useNavigate(); - const { selectedAccount } = useWalletSelector(); + const selectedAccount = useSelectedAccount(); const { t } = useTranslation('translation', { keyPrefix: 'SETTING_SCREEN' }); const { t: optionsDialogTranslation } = useTranslation('translation', { diff --git a/src/app/components/accountRow/index.tsx b/src/app/components/accountRow/index.tsx index 09c93ce39..a24fd49d8 100644 --- a/src/app/components/accountRow/index.tsx +++ b/src/app/components/accountRow/index.tsx @@ -1,6 +1,5 @@ import LedgerBadge from '@assets/img/ledger/ledger_badge.svg'; import BarLoader from '@components/barLoader'; -import BottomModal from '@components/bottomModal'; import OptionsDialog from '@components/optionsDialog/optionsDialog'; import useWalletReducer from '@hooks/useWalletReducer'; import useWalletSelector from '@hooks/useWalletSelector'; @@ -8,13 +7,9 @@ import { CaretDown, DotsThreeVertical } from '@phosphor-icons/react'; import { Account, currencySymbolMap } from '@secretkeylabs/xverse-core'; import Button from '@ui-library/button'; import Input from '@ui-library/input'; +import Sheet from '@ui-library/sheet'; import Spinner from '@ui-library/spinner'; -import { - EMPTY_LABEL, - LoaderSize, - MAX_ACC_NAME_LENGTH, - OPTIONS_DIALOG_WIDTH, -} from '@utils/constants'; +import { EMPTY_LABEL, LoaderSize, OPTIONS_DIALOG_WIDTH } from '@utils/constants'; import { getAccountGradient } from '@utils/gradient'; import { isLedgerAccount, validateAccountName } from '@utils/helper'; import { useEffect, useRef, useState } from 'react'; @@ -63,10 +58,14 @@ const CurrentAccountTextContainer = styled.div((props) => ({ gap: props.theme.space.xs, })); -const AccountName = styled.h1<{ isSelected: boolean }>((props) => ({ +const AccountName = styled.span<{ isSelected: boolean }>((props) => ({ ...props.theme.typography.body_bold_m, color: props.isSelected ? props.theme.colors.white_0 : props.theme.colors.white_400, textAlign: 'start', + maxWidth: 160, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', })); const BarLoaderContainer = styled.div((props) => ({ @@ -87,8 +86,6 @@ const OptionsButton = styled.button({ }); const ModalContent = styled.form((props) => ({ - padding: props.theme.space.m, - paddingTop: props.theme.space.m, paddingBottom: props.theme.space.xxl, })); @@ -217,15 +214,6 @@ function AccountRow({ } }, [accountName]); - const getName = () => { - const name = - account?.accountName ?? - account?.bnsName ?? - `${t('ACCOUNT_NAME')} ${`${(account?.id ?? 0) + 1}`}`; - - return name.length > MAX_ACC_NAME_LENGTH ? `${name.slice(0, MAX_ACC_NAME_LENGTH)}...` : name; - }; - const handleClick = () => { onAccountSelected(account!); }; @@ -341,7 +329,9 @@ function AccountRow({ - {getName()} + {account?.accountName ?? + account?.bnsName ?? + `${t('ACCOUNT_NAME')} ${`${(account?.id ?? 0) + 1}`}`} {isLedgerAccount(account) && Ledger icon} {isSelected && !disabledAccountSelect && !isAccountListView && ( @@ -399,9 +389,9 @@ function AccountRow({ )} {showRemoveAccountModal && ( - @@ -423,13 +413,13 @@ function AccountRow({ - + )} {showRenameAccountModal && ( - @@ -467,7 +457,7 @@ function AccountRow({ - + )} ); diff --git a/src/app/components/alertMessage/index.tsx b/src/app/components/alertMessage/index.tsx index 6715936e9..6287416aa 100644 --- a/src/app/components/alertMessage/index.tsx +++ b/src/app/components/alertMessage/index.tsx @@ -86,6 +86,7 @@ interface Props { onButtonClick?: () => void; onSecondButtonClick?: () => void; tickMarkButtonClick?: (e: React.ChangeEvent) => void; + tickMarkButtonChecked?: boolean; } function AlertMessage({ @@ -99,6 +100,7 @@ function AlertMessage({ onButtonClick, onSecondButtonClick, tickMarkButtonClick, + tickMarkButtonChecked, }: Props) { return ( <> @@ -134,6 +136,7 @@ function AlertMessage({ checkboxId={`${title}-ticker`} text={tickMarkButtonText} onChange={tickMarkButtonClick} + checked={tickMarkButtonChecked} /> )} diff --git a/src/app/components/bottomModal/index.tsx b/src/app/components/bottomModal/index.tsx index c022155d2..4e27454c1 100644 --- a/src/app/components/bottomModal/index.tsx +++ b/src/app/components/bottomModal/index.tsx @@ -28,7 +28,7 @@ const CustomisedModal = styled(Modal)` position: absolute; `; -interface Props { +type Props = { header: string; visible: boolean; children: React.ReactNode; @@ -36,7 +36,7 @@ interface Props { overlayStylesOverriding?: {}; contentStylesOverriding?: {}; className?: string; -} +}; function BottomModal({ header, diff --git a/src/app/components/collectibleCollectionGridItem/index.tsx b/src/app/components/collectibleCollectionGridItem/index.tsx index d2a876aa1..7adbf116d 100644 --- a/src/app/components/collectibleCollectionGridItem/index.tsx +++ b/src/app/components/collectibleCollectionGridItem/index.tsx @@ -48,15 +48,16 @@ const GridItemContainer = styled.button` width: 100%; `; -interface Props { +type Props = { item?: Inscription | NonFungibleToken; itemId?: string; itemSubText?: string; itemSubTextColor?: Color; children: ReactNode; onClick?: (item: Inscription | NonFungibleToken) => void; -} -export function CollectibleCollectionGridItem({ +}; + +function CollectibleCollectionGridItem({ item, itemId, itemSubText, @@ -72,7 +73,7 @@ export function CollectibleCollectionGridItem({ : undefined; return ( - + {children} diff --git a/src/app/screens/createInscription/ContentLabel/common.ts b/src/app/components/confirmBtcTransaction/ContentLabel/common.ts similarity index 100% rename from src/app/screens/createInscription/ContentLabel/common.ts rename to src/app/components/confirmBtcTransaction/ContentLabel/common.ts diff --git a/src/app/screens/createInscription/ContentLabel/index.tsx b/src/app/components/confirmBtcTransaction/ContentLabel/index.tsx similarity index 97% rename from src/app/screens/createInscription/ContentLabel/index.tsx rename to src/app/components/confirmBtcTransaction/ContentLabel/index.tsx index b2fe5d1f2..2cd79c82d 100644 --- a/src/app/screens/createInscription/ContentLabel/index.tsx +++ b/src/app/components/confirmBtcTransaction/ContentLabel/index.tsx @@ -101,9 +101,16 @@ type Props = { contentType: string; content: string; repeat?: number; + inscriptionId?: string; }; -function ContentIcon({ type, content, contentType: inputContentType, repeat = 1 }: Props) { +function ContentIcon({ + type, + content, + contentType: inputContentType, + repeat = 1, + inscriptionId, +}: Props) { const { t } = useTranslation('translation', { keyPrefix: 'INSCRIPTION_REQUEST.PREVIEW' }); const [showPreview, setShowPreview] = useState(false); const [showMenu, setShowMenu] = useState(false); @@ -245,6 +252,7 @@ function ContentIcon({ type, content, contentType: inputContentType, repeat = 1 contentTypeRaw={inputContentType} type={type} visible={showPreview} + inscriptionId={inscriptionId} onClick={() => setShowPreview(false)} /> diff --git a/src/app/screens/createInscription/ContentLabel/preview.tsx b/src/app/components/confirmBtcTransaction/ContentLabel/preview.tsx similarity index 84% rename from src/app/screens/createInscription/ContentLabel/preview.tsx rename to src/app/components/confirmBtcTransaction/ContentLabel/preview.tsx index cedd7e592..ca6847dd0 100644 --- a/src/app/screens/createInscription/ContentLabel/preview.tsx +++ b/src/app/components/confirmBtcTransaction/ContentLabel/preview.tsx @@ -2,6 +2,8 @@ import { X } from '@phosphor-icons/react'; import { useLayoutEffect, useRef, useState } from 'react'; import styled from 'styled-components'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { XVERSE_ORDIVIEW_URL } from '@utils/constants'; import { ContentType } from './common'; const MIN_FONT_SIZE = 9; @@ -73,12 +75,22 @@ type Props = { contentType: ContentType; contentTypeRaw: string; visible: boolean; + inscriptionId?: string; }; -function Preview({ onClick, type, content, contentType, contentTypeRaw, visible }: Props) { +function Preview({ + onClick, + type, + content, + contentType, + contentTypeRaw, + visible, + inscriptionId, +}: Props) { const containerRef = useRef(null); const textRef = useRef(null); const [fontSize, setFontSize] = useState(24); + const { network } = useWalletSelector(); useLayoutEffect(() => { // this decreases the font size until the preview text fits in the container @@ -121,7 +133,17 @@ function Preview({ onClick, type, content, contentType, contentTypeRaw, visible ); } else if (contentType === ContentType.IMAGE) { - preview = ; + // workaround to show the inscription preview for an already inscribed inscription + if (inscriptionId) { + preview = ( + + ); + } else { + preview = ; + } } else if (contentType === ContentType.VIDEO) { preview = ( diff --git a/src/app/screens/createInscription/ContentLabel/utils.ts b/src/app/components/confirmBtcTransaction/ContentLabel/utils.ts similarity index 100% rename from src/app/screens/createInscription/ContentLabel/utils.ts rename to src/app/components/confirmBtcTransaction/ContentLabel/utils.ts diff --git a/src/app/components/confirmBtcTransaction/etchSection.tsx b/src/app/components/confirmBtcTransaction/etchSection.tsx new file mode 100644 index 000000000..0b1f25230 --- /dev/null +++ b/src/app/components/confirmBtcTransaction/etchSection.tsx @@ -0,0 +1,236 @@ +import useOrdinalsApi from '@hooks/apiClients/useOrdinalsApi'; +import useSelectedAccount from '@hooks/useSelectedAccount'; +import { ArrowRight } from '@phosphor-icons/react'; +import { EtchActionDetails, Inscription, RUNE_DISPLAY_DEFAULTS } from '@secretkeylabs/xverse-core'; +import { StyledP } from '@ui-library/common.styled'; +import { ftDecimals, getShortTruncatedAddress } from '@utils/helper'; +import { useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { NumericFormat } from 'react-number-format'; +import Theme from '../../../theme'; +import InscribeSection from './inscribeSection'; +import { + AddressLabel, + Container, + Header, + RowCenter, + RuneAmount, + RuneData, + RuneImage, + RuneSymbol, + RuneValue, +} from './runes'; + +type Props = { + etch?: EtchActionDetails; +}; + +/** + * only used for ordinals service etches + */ +function EtchSection({ etch }: Props) { + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + const { ordinalsAddress } = useSelectedAccount(); + const [delegateInscriptionDetails, setDelegateInscriptionDetails] = useState( + null, + ); + const ordinalsApi = useOrdinalsApi(); + + const fetchInscriptionDetails = useCallback(async (inscriptionId: string) => { + const inscriptionDetails = await ordinalsApi.getInscription(inscriptionId); + if (inscriptionDetails) { + setDelegateInscriptionDetails(inscriptionDetails); + } + }, []); + + useEffect(() => { + if (etch?.delegateInscriptionId) { + fetchInscriptionDetails(etch.delegateInscriptionId); + } + }, [fetchInscriptionDetails, etch?.delegateInscriptionId]); + + if (!etch) return null; + + return ( + <> + +
+ + {t('YOU_WILL_ISSUE')} + +
+
+ + {t('NAME')} + + + {etch.runeName} + +
+
+ + {t('SYMBOL')} + + + {etch.symbol ?? RUNE_DISPLAY_DEFAULTS.symbol} + +
+
+ + {t('DIVISIBILITY')} + + + {etch.divisibility || RUNE_DISPLAY_DEFAULTS.divisibility} + +
+
+ + {t('MINTABLE')} + + + {etch.isMintable ? t('YES') : t('NO')} + +
+
+ + {t('MINT_AMOUNT')} + + + ( + + {value} + + )} + /> + +
+
+ + {t('MINTING_LIMIT')} + + ( + + {value} + + )} + /> +
+ {etch.terms?.heightStart || + (etch.terms?.heightEnd && ( +
+ + {t('RUNE_BLOCK_HEIGHT_TERM')} + + + {`${etch.terms.heightStart}/${etch.terms.heightEnd}`} + +
+ ))} + {etch.terms?.offsetStart || + (etch.terms?.offsetEnd && ( +
+ + {t('RUNE_BLOCK_OFFSET_TERM')} + + + {`${etch.terms.offsetStart ? etch.terms.offsetStart : '-'}/${etch.terms.offsetEnd}`} + +
+ ))} +
+ {etch.premine && ( + +
+ + {t('YOU_WILL_RECEIVE')} + + + + + {' '} + {etch.destinationAddress && etch.destinationAddress !== ordinalsAddress + ? getShortTruncatedAddress(etch.destinationAddress) + : t('YOUR_ORDINAL_ADDRESS')} + + +
+
+ + {etch.runeName} + +
+
+ + + + {etch.symbol || '¤'} + + + + + {t('AMOUNT')} + + {etch && ( + + {t('RUNE_SIZE')}: {RUNE_DISPLAY_DEFAULTS.size} Sats + + )} + + + ( + + + {value} + + + {etch?.symbol ?? '¤'} + + + )} + /> +
+
+ )} + {etch.inscriptionDetails && ( + + )} + {etch.delegateInscriptionId && delegateInscriptionDetails && ( + + )} + + ); +} + +export default EtchSection; diff --git a/src/app/components/confirmBtcTransaction/index.tsx b/src/app/components/confirmBtcTransaction/index.tsx index b14384ab3..e9c4bcf76 100644 --- a/src/app/components/confirmBtcTransaction/index.tsx +++ b/src/app/components/confirmBtcTransaction/index.tsx @@ -1,12 +1,17 @@ import { delay } from '@common/utils/ledger'; -import BottomModal from '@components/bottomModal'; import ActionButton from '@components/button'; import { Tab } from '@components/tabBar'; -import useWalletSelector from '@hooks/useWalletSelector'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import TransportFactory from '@ledgerhq/hw-transport-webusb'; -import { RuneSummary, Transport, btcTransaction } from '@secretkeylabs/xverse-core'; +import { + RuneSummary, + RuneSummaryActions, + Transport, + btcTransaction, +} from '@secretkeylabs/xverse-core'; import Callout from '@ui-library/callout'; import { StickyHorizontalSplitButtonContainer, StyledP } from '@ui-library/common.styled'; +import Sheet from '@ui-library/sheet'; import Spinner from '@ui-library/spinner'; import { isLedgerAccount } from '@utils/helper'; import { useState } from 'react'; @@ -47,7 +52,7 @@ type Props = { inputs: btcTransaction.EnhancedInput[]; outputs: btcTransaction.EnhancedOutput[]; feeOutput?: btcTransaction.TransactionFeeOutput; - runeSummary?: RuneSummary; + runeSummary?: RuneSummaryActions | RuneSummary; showCenotaphCallout: boolean; isLoading: boolean; isSubmitting: boolean; @@ -113,7 +118,7 @@ function ConfirmBtcTransaction({ const { t: signatureRequestTranslate } = useTranslation('translation', { keyPrefix: 'SIGNATURE_REQUEST', }); - const { selectedAccount } = useWalletSelector(); + const selectedAccount = useSelectedAccount(); const hideBackButton = !onBackClick; const hasInsufficientRunes = @@ -228,7 +233,7 @@ function ConfirmBtcTransaction({ )} - setIsModalVisible(false)}> + setIsModalVisible(false)}> )} - + ); } diff --git a/src/app/components/confirmBtcTransaction/inscribeSection/index.tsx b/src/app/components/confirmBtcTransaction/inscribeSection/index.tsx new file mode 100644 index 000000000..3a5c3586e --- /dev/null +++ b/src/app/components/confirmBtcTransaction/inscribeSection/index.tsx @@ -0,0 +1,78 @@ +import OrdinalsIcon from '@assets/img/nftDashboard/white_ordinals_icon.svg'; +import { ArrowDown } from '@phosphor-icons/react'; +import { StyledP } from '@ui-library/common.styled'; +import { getShortTruncatedAddress } from '@utils/helper'; +import { useTranslation } from 'react-i18next'; +import ContentLabel from '../ContentLabel'; +import { Pill, StyledPillLabel } from '../runes'; +import { + ButtonIcon, + CardContainer, + CardRow, + IconLabel, + InfoIconContainer, + YourAddress, +} from './styles'; + +type InscribeSectionProps = { + contentType: string; + content: string; + payloadType: 'BASE_64' | 'PLAIN_TEXT'; + ordinalsAddress: string; + repeat?: number; + inscriptionId?: string; +}; + +function InscribeSection({ + repeat, + content, + contentType, + ordinalsAddress, + payloadType, + inscriptionId, +}: InscribeSectionProps) { + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + return ( + + + + {t('INSCRIBE.TITLE')} + {repeat && {`x${repeat}`}} + + + + +
+ +
+
{t('INSCRIBE.ORDINAL')}
+
+ +
+ + + + + + {t('INSCRIBE.TO')} + + + + {getShortTruncatedAddress(ordinalsAddress)} + + + {t('INSCRIBE.YOUR_ADDRESS')} + + + +
+ ); +} + +export default InscribeSection; diff --git a/src/app/components/confirmBtcTransaction/inscribeSection/styles.tsx b/src/app/components/confirmBtcTransaction/inscribeSection/styles.tsx new file mode 100644 index 000000000..b4475f295 --- /dev/null +++ b/src/app/components/confirmBtcTransaction/inscribeSection/styles.tsx @@ -0,0 +1,59 @@ +import styled from 'styled-components'; + +export const CardContainer = styled.div<{ bottomPadding?: boolean }>((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_200, + display: 'flex', + flexDirection: 'column', + gap: props.theme.space.m, + background: props.theme.colors.elevation1, + borderRadius: 12, + padding: props.theme.spacing(8), + paddingBottom: props.bottomPadding ? props.theme.spacing(12) : props.theme.spacing(8), + justifyContent: 'center', + marginBottom: props.theme.spacing(6), + fontSize: 14, +})); + +type CardRowProps = { + topMargin?: boolean; + center?: boolean; +}; +export const CardRow = styled.div((props) => ({ + display: 'flex', + flexDirection: 'row', + alignItems: props.center ? 'center' : 'flex-start', + justifyContent: 'space-between', + marginTop: props.topMargin ? props.theme.spacing(8) : 0, +})); + +export const IconLabel = styled.div((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_200, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', +})); + +export const ButtonIcon = styled.img((props) => ({ + width: 32, + height: 32, + marginRight: props.theme.spacing(4), +})); + +export const InfoIconContainer = styled.div((props) => ({ + background: props.theme.colors.white_0, + color: props.theme.colors.elevation0, + width: 32, + height: 32, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + borderRadius: '50%', + marginRight: props.theme.spacing(5), +})); + +export const YourAddress = styled.div` + text-align: right; +`; diff --git a/src/app/components/confirmBtcTransaction/itemRow/amount.tsx b/src/app/components/confirmBtcTransaction/itemRow/amount.tsx index 1f57dc801..0a2f98d04 100644 --- a/src/app/components/confirmBtcTransaction/itemRow/amount.tsx +++ b/src/app/components/confirmBtcTransaction/itemRow/amount.tsx @@ -1,7 +1,8 @@ +import FiatAmountText from '@components/fiatAmountText'; import TokenImage from '@components/tokenImage'; import useCoinRates from '@hooks/queries/useCoinRates'; import useWalletSelector from '@hooks/useWalletSelector'; -import { currencySymbolMap, getBtcFiatEquivalent, satsToBtc } from '@secretkeylabs/xverse-core'; +import { getBtcFiatEquivalent, satsToBtc } from '@secretkeylabs/xverse-core'; import Avatar from '@ui-library/avatar'; import { StyledP } from '@ui-library/common.styled'; import BigNumber from 'bignumber.js'; @@ -9,10 +10,6 @@ import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; import styled from 'styled-components'; -type Props = { - amount: number; -}; - const RowCenter = styled.div<{ spaceBetween?: boolean }>((props) => ({ display: 'flex', flex: 1, @@ -29,36 +26,15 @@ const AvatarContainer = styled.div` margin-right: ${(props) => props.theme.space.xs}; `; +type Props = { + amount: number; +}; + export default function Amount({ amount }: Props) { const { fiatCurrency } = useWalletSelector(); const { btcFiatRate } = useCoinRates(); const { t } = useTranslation('translation'); - const getFiatAmountString = (amountParam: number, btcFiatRateParam: string) => { - const fiatAmount = getBtcFiatEquivalent( - new BigNumber(amountParam), - BigNumber(btcFiatRateParam), - ); - if (!fiatAmount) { - return ''; - } - - if (fiatAmount.isLessThan(0.01)) { - return `<${currencySymbolMap[fiatCurrency]}0.01 ${fiatCurrency}`; - } - - return ( - `~ ${value}`} - /> - ); - }; - return ( @@ -82,13 +58,11 @@ export default function Amount({ amount }: Props) { )} /> - - {getFiatAmountString(amount, btcFiatRate)} - + diff --git a/src/app/components/confirmBtcTransaction/mintSection.tsx b/src/app/components/confirmBtcTransaction/mintSection.tsx index 1edcedcc5..cd3c9b7aa 100644 --- a/src/app/components/confirmBtcTransaction/mintSection.tsx +++ b/src/app/components/confirmBtcTransaction/mintSection.tsx @@ -1,45 +1,32 @@ +import useSelectedAccount from '@hooks/useSelectedAccount'; import { ArrowRight } from '@phosphor-icons/react'; -import { RuneSummary } from '@secretkeylabs/xverse-core'; +import { MintActionDetails, RuneSummary } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; -import { ftDecimals } from '@utils/helper'; +import { ftDecimals, getShortTruncatedAddress } from '@utils/helper'; import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; -import styled from 'styled-components'; import Theme from '../../../theme'; - -const Container = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - background: props.theme.colors.elevation1, - borderRadius: 12, - paddingTop: props.theme.space.m, - justifyContent: 'center', - marginBottom: props.theme.space.s, -})); - -const RowCenter = styled.div({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', -}); - -const AddressLabel = styled(StyledP)((props) => ({ - marginLeft: props.theme.space.xxs, -})); - -const Header = styled(RowCenter)((props) => ({ - marginBottom: props.theme.space.m, - padding: `0 ${props.theme.space.m}`, -})); +import { + AddressLabel, + Container, + Header, + Pill, + RowCenter, + RuneAmount, + RuneData, + RuneImage, + RuneSymbol, + RuneValue, + StyledPillLabel, +} from './runes'; type Props = { - mints?: RuneSummary['mint'][]; + mints?: RuneSummary['mint'][] | MintActionDetails[]; }; function MintSection({ mints }: Props) { const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - + const { ordinalsAddress } = useSelectedAccount(); if (!mints) return null; return ( @@ -47,46 +34,57 @@ function MintSection({ mints }: Props) { {mints.map( (mint) => mint && ( - +
- + {t('YOU_WILL_MINT')} - + {mint.repeats && {`x${mint.repeats}`}} + - {t('YOUR_ORDINAL_ADDRESS')} + {mint.destinationAddress && mint.destinationAddress !== ordinalsAddress + ? getShortTruncatedAddress(mint.destinationAddress) + : t('YOUR_ORDINAL_ADDRESS')}
- - {t('NAME')} - {mint?.runeName}
- - {t('SYMBOL')} - - - {mint?.symbol} - -
-
- - {t('AMOUNT')} - + + + + {mint?.symbol} + + + + + {t('AMOUNT')} + + {mint.runeSize && ( // This is the only place where runeSize is used + + {t('RUNE_SIZE')}: {mint.runeSize} Sats + + )} + + ( - - {value} - + + + {value} + + + {mint?.symbol} + + )} />
diff --git a/src/app/components/confirmBtcTransaction/receiveSection.tsx b/src/app/components/confirmBtcTransaction/receiveSection.tsx index 8558d6d0d..bd364dfc4 100644 --- a/src/app/components/confirmBtcTransaction/receiveSection.tsx +++ b/src/app/components/confirmBtcTransaction/receiveSection.tsx @@ -1,4 +1,5 @@ import RuneAmount from '@components/confirmBtcTransaction/itemRow/runeAmount'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import { ArrowRight } from '@phosphor-icons/react'; import { btcTransaction, RuneSummary } from '@secretkeylabs/xverse-core'; @@ -54,7 +55,8 @@ function ReceiveSection({ runeReceipts, transactionIsFinal, }: Props) { - const { btcAddress, ordinalsAddress, hasActivatedRareSatsKey } = useWalletSelector(); + const { btcAddress, ordinalsAddress } = useSelectedAccount(); + const { hasActivatedRareSatsKey } = useWalletSelector(); const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); const { outputsToPayment, outputsToOrdinal } = getOutputsWithAssetsToUserAddress({ diff --git a/src/app/components/confirmBtcTransaction/runes.tsx b/src/app/components/confirmBtcTransaction/runes.tsx new file mode 100644 index 000000000..13fbcb731 --- /dev/null +++ b/src/app/components/confirmBtcTransaction/runes.tsx @@ -0,0 +1,74 @@ +import { StyledP } from '@ui-library/common.styled'; +import styled from 'styled-components'; + +export const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + background: props.theme.colors.elevation1, + borderRadius: 12, + paddingTop: props.theme.space.m, + justifyContent: 'center', + marginBottom: props.theme.space.s, +})); + +export const RowCenter = styled.div({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', +}); + +export const AddressLabel = styled(StyledP)((props) => ({ + marginLeft: props.theme.space.xxs, +})); + +export const Header = styled(RowCenter)((props) => ({ + marginBottom: props.theme.space.m, + padding: `0 ${props.theme.space.m}`, +})); + +export const StyledPillLabel = styled.p` + display: flex; + align-items: center; + gap: ${(props) => props.theme.space.s}; +`; + +export const Pill = styled.span` + ${(props) => props.theme.typography.body_bold_s} + color: ${(props) => props.theme.colors.elevation0}; + background-color: ${(props) => props.theme.colors.white_0}; + padding: 3px 6px; + border-radius: 40px; +`; + +export const RuneValue = styled.div({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', +}); + +export const RuneSymbol = styled(StyledP)((props) => ({ + marginLeft: props.theme.space.xxs, +})); + +export const RuneData = styled.div({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', +}); + +export const RuneImage = styled.div((props) => ({ + height: 32, + width: 32, + borderRadius: '50%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + backgroundColor: props.theme.colors.white_850, +})); + +export const RuneAmount = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + marginLeft: props.theme.space.xs, +})); diff --git a/src/app/components/confirmBtcTransaction/transactionSummary.tsx b/src/app/components/confirmBtcTransaction/transactionSummary.tsx index 52078570a..54c9157b7 100644 --- a/src/app/components/confirmBtcTransaction/transactionSummary.tsx +++ b/src/app/components/confirmBtcTransaction/transactionSummary.tsx @@ -7,20 +7,26 @@ import MintSection from '@components/confirmBtcTransaction/mintSection'; import TransferFeeView from '@components/transferFeeView'; import useCoinRates from '@hooks/queries/useCoinRates'; import useBtcFeeRate from '@hooks/useBtcFeeRate'; -import { btcTransaction, getBtcFiatEquivalent, RuneSummary } from '@secretkeylabs/xverse-core'; +import useSelectedAccount from '@hooks/useSelectedAccount'; +import { + btcTransaction, + getBtcFiatEquivalent, + RuneSummary, + RuneSummaryActions, +} from '@secretkeylabs/xverse-core'; import SelectFeeRate from '@ui-components/selectFeeRate'; import Callout from '@ui-library/callout'; -import { BLOG_LINK } from '@utils/constants'; import BigNumber from 'bignumber.js'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import DelegateSection from './delegateSection'; +import EtchSection from './etchSection'; import AmountWithInscriptionSatribute from './itemRow/amountWithInscriptionSatribute'; import ReceiveSection from './receiveSection'; import TransferSection from './transferSection'; import TxInOutput from './txInOutput/txInOutput'; -import { getNetAmount, isScriptOutput, isSpendOutput } from './utils'; +import { getNetAmount, isScriptOutput } from './utils'; const Container = styled.div((props) => ({ background: props.theme.colors.elevation1, @@ -39,7 +45,7 @@ type Props = { inputs: btcTransaction.EnhancedInput[]; outputs: btcTransaction.EnhancedOutput[]; feeOutput?: btcTransaction.TransactionFeeOutput; - runeSummary?: RuneSummary; + runeSummary?: RuneSummaryActions | RuneSummary; getFeeForFeeRate?: ( feeRate: number, useEffectiveFeeRate?: boolean, @@ -68,10 +74,9 @@ function TransactionSummary({ const { btcFiatRate } = useCoinRates(); const { network, fiatCurrency } = useWalletSelector(); const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - const { t: rareSatsT } = useTranslation('translation', { keyPrefix: 'RARE_SATS' }); const { t: tUnits } = useTranslation('translation', { keyPrefix: 'UNITS' }); - const { btcAddress, ordinalsAddress } = useWalletSelector(); + const { btcAddress, ordinalsAddress } = useSelectedAccount(); const { data: recommendedFees } = useBtcFeeRate(); const hasOutputScript = outputs.some((output) => isScriptOutput(output)); @@ -83,26 +88,12 @@ function TransactionSummary({ ordinalsAddress, }); - const isUnConfirmedInput = inputs.some((input) => !input.extendedUtxo.utxo.status.confirmed); - - // if transaction is not final, we don't know where the rare sats will go, so check inputs instead of outputs - const paymentHasInscribedRareSats = !transactionIsFinal - ? inputs.some( - (input) => - input.extendedUtxo.address === btcAddress && - (input.inscriptions.length || input.satributes.length), - ) - : outputs.some( - (output) => - isSpendOutput(output) && - (output.inscriptions.some((inscription) => inscription.fromAddress === btcAddress) || - output.satributes.some((satribute) => satribute.fromAddress === btcAddress)), - ); - const feesHaveInscribedRareSats = feeOutput?.inscriptions.length || feeOutput?.satributes.length; - const showInscribeRareSatWarning = paymentHasInscribedRareSats || feesHaveInscribedRareSats; + const isUnConfirmedInput = inputs.some( + (input) => !input.extendedUtxo.utxo.status.confirmed && input.walletWillSign, + ); const satsToFiat = (sats: string) => - getBtcFiatEquivalent(new BigNumber(sats), new BigNumber(btcFiatRate)).toNumber().toFixed(2); + getBtcFiatEquivalent(new BigNumber(sats), new BigNumber(btcFiatRate)).toString(); const showFeeSelector = !!(feeRate && getFeeForFeeRate && onFeeRateSet); @@ -120,15 +111,6 @@ function TransactionSummary({ }} /> )} - - {!!showInscribeRareSatWarning && ( - - )} {isUnConfirmedInput && ( )} @@ -159,6 +141,7 @@ function TransactionSummary({ /> {!hasRuneDelegation && } + {hasOutputScript && !runeSummary && } diff --git a/src/app/components/confirmBtcTransaction/transferSection.tsx b/src/app/components/confirmBtcTransaction/transferSection.tsx index 20f4b71ac..155c83d67 100644 --- a/src/app/components/confirmBtcTransaction/transferSection.tsx +++ b/src/app/components/confirmBtcTransaction/transferSection.tsx @@ -1,5 +1,5 @@ import RuneAmount from '@components/confirmBtcTransaction/itemRow/runeAmount'; -import useWalletSelector from '@hooks/useWalletSelector'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import { btcTransaction, RuneSummary } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; import { useTranslation } from 'react-i18next'; @@ -53,7 +53,7 @@ function TransferSection({ netAmount, onShowInscription, }: Props) { - const { btcAddress, ordinalsAddress } = useWalletSelector(); + const { btcAddress, ordinalsAddress } = useSelectedAccount(); const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); const { inputFromPayment, inputFromOrdinal } = getInputsWitAssetsFromUserAddress({ diff --git a/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx b/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx index dad13aef8..58ab5b3c4 100644 --- a/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx +++ b/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx @@ -1,6 +1,6 @@ import IconBitcoin from '@assets/img/dashboard/bitcoin_icon.svg'; import TransferDetailView from '@components/transferDetailView'; -import useWalletSelector from '@hooks/useWalletSelector'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import { btcTransaction, satsToBtc } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; import { getTruncatedAddress } from '@utils/helper'; @@ -35,7 +35,7 @@ type Props = { }; function TransactionInput({ input }: Props) { - const { btcAddress, ordinalsAddress } = useWalletSelector(); + const { btcAddress, ordinalsAddress } = useSelectedAccount(); const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); const isPaymentsAddress = input.extendedUtxo.address === btcAddress; diff --git a/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx b/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx index c379c5e4a..0a8baf0be 100644 --- a/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx +++ b/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx @@ -1,13 +1,13 @@ import ScriptIcon from '@assets/img/transactions/ScriptIcon.svg'; import OutputIcon from '@assets/img/transactions/output.svg'; import TransferDetailView from '@components/transferDetailView'; -import useWalletSelector from '@hooks/useWalletSelector'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import { btcTransaction, satsToBtc } from '@secretkeylabs/xverse-core'; import { getTruncatedAddress } from '@utils/helper'; import BigNumber from 'bignumber.js'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; -import { isScriptOutput, isSpendOutput } from '../utils'; +import { isAddressOutput, isPubKeyOutput, isScriptOutput } from '../utils'; const TransferDetailContainer = styled.div((props) => ({ paddingBottom: props.theme.spacing(8), @@ -19,6 +19,10 @@ const SubValueText = styled.h1((props) => ({ color: props.theme.colors.white_400, })); +const HighlightText = styled.h1((props) => ({ + color: props.theme.colors.white_0, +})); + const YourAddressText = styled.h1((props) => ({ ...props.theme.typography.body_m, fontSize: 12, @@ -37,24 +41,51 @@ type Props = { }; function TransactionOutput({ output, scriptOutputCount }: Props) { - const { btcAddress, ordinalsAddress } = useWalletSelector(); + const { btcAddress, ordinalsAddress, btcPublicKey, ordinalsPublicKey } = useSelectedAccount(); const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - const outputWithScript = isScriptOutput(output); + const isOutputWithScript = isScriptOutput(output); + const isOutputWithPubKey = isPubKeyOutput(output); - const detailViewIcon = outputWithScript ? ScriptIcon : OutputIcon; - const detailViewHideCopyButton = outputWithScript - ? true - : btcAddress === output.address || ordinalsAddress === output.address; - const detailViewValue = outputWithScript ? ( - {`${t('SCRIPT_OUTPUT')} #${scriptOutputCount}`} - ) : output.address === btcAddress || output.address === ordinalsAddress ? ( - - ({t('YOUR_ADDRESS')}) - {getTruncatedAddress(output.address)} - - ) : ( - {getTruncatedAddress(output.address)} - ); + const detailViewIcon = isOutputWithScript ? ScriptIcon : OutputIcon; + const detailViewHideCopyButton = + isOutputWithScript || isOutputWithPubKey + ? true + : btcAddress === output.address || ordinalsAddress === output.address; + + const detailView = () => { + if (isOutputWithScript) { + return {`${t('SCRIPT_OUTPUT')} #${scriptOutputCount}`}; + } + if (isOutputWithPubKey) { + const outputType = output.type === 'pk' ? t('PUBLIC_KEY') : t('MULTISIG'); + const toOwnKey = + output.pubKeys?.includes(btcPublicKey) || output.pubKeys?.includes(ordinalsPublicKey); + const toOwnString = toOwnKey ? ` (${t('YOUR_PUBLIC_KEY')})` : ''; + return ( + + {outputType} + {toOwnString} + + ); + } + + if (output.address === btcAddress || output.address === ordinalsAddress) { + return ( + + ({t('YOUR_ADDRESS')}) + + {getTruncatedAddress(output.address)} + + + ); + } + + return ( + + {getTruncatedAddress(output.address)} + + ); + }; return ( @@ -64,13 +95,13 @@ function TransactionOutput({ output, scriptOutputCount }: Props) { hideCopyButton={detailViewHideCopyButton} dataTestID="confirm-amount" amount={`${satsToBtc( - new BigNumber(isSpendOutput(output) ? output.amount.toString() : '0'), + new BigNumber(isAddressOutput(output) ? output.amount.toString() : '0'), ).toFixed()} BTC`} - address={outputWithScript ? '' : output.address} - outputScript={outputWithScript ? output.script : undefined} - outputScriptIndex={outputWithScript ? scriptOutputCount : undefined} + address={isOutputWithScript || isOutputWithPubKey ? '' : output.address} + outputScript={isOutputWithScript ? output.script : undefined} + outputScriptIndex={isOutputWithScript ? scriptOutputCount : undefined} > - {detailViewValue} + {detailView()} ); diff --git a/src/app/components/confirmBtcTransaction/utils.ts b/src/app/components/confirmBtcTransaction/utils.ts index fc2805338..1e8b89ca3 100644 --- a/src/app/components/confirmBtcTransaction/utils.ts +++ b/src/app/components/confirmBtcTransaction/utils.ts @@ -18,7 +18,12 @@ export const isScriptOutput = ( ): output is btcTransaction.TransactionScriptOutput => (output as btcTransaction.TransactionScriptOutput).script !== undefined; -export const isSpendOutput = ( +export const isPubKeyOutput = ( + output: btcTransaction.EnhancedOutput, +): output is btcTransaction.TransactionPubKeyOutput => + !!(output as btcTransaction.TransactionPubKeyOutput).pubKeys?.length; + +export const isAddressOutput = ( output: btcTransaction.EnhancedOutput, ): output is btcTransaction.TransactionOutput => (output as btcTransaction.TransactionOutput).address !== undefined; @@ -48,7 +53,7 @@ export const getNetAmount = ({ const totalUserReceive = outputs.reduce((accumulator: number, output) => { const isToUserAddress = - isSpendOutput(output) && [btcAddress, ordinalsAddress].includes(output.address); + isAddressOutput(output) && [btcAddress, ordinalsAddress].includes(output.address); if (isToUserAddress) { return accumulator + output.amount; } @@ -64,8 +69,14 @@ export const getOutputsWithAssetsFromUserAddress = ({ outputs, }: Omit) => { // we want to discard outputs that are script, are not from user address and do not have inscriptions or satributes - const outputsFromPayment: btcTransaction.TransactionOutput[] = []; - const outputsFromOrdinal: btcTransaction.TransactionOutput[] = []; + const outputsFromPayment: ( + | btcTransaction.TransactionOutput + | btcTransaction.TransactionPubKeyOutput + )[] = []; + const outputsFromOrdinal: ( + | btcTransaction.TransactionOutput + | btcTransaction.TransactionPubKeyOutput + )[] = []; outputs.forEach((output) => { if (isScriptOutput(output)) { return; @@ -126,7 +137,11 @@ export const getOutputsWithAssetsToUserAddress = ({ const outputsToOrdinal: btcTransaction.TransactionOutput[] = []; outputs.forEach((output) => { // we want to discard outputs that are not spendable or are not to user address - if (isScriptOutput(output) || ![btcAddress, ordinalsAddress].includes(output.address)) { + if ( + isScriptOutput(output) || + isPubKeyOutput(output) || + ![btcAddress, ordinalsAddress].includes(output.address) + ) { return; } diff --git a/src/app/components/confirmBtcTransactionComponent/index.tsx b/src/app/components/confirmBtcTransactionComponent/index.tsx index 7ff9bb9e0..5fee38893 100644 --- a/src/app/components/confirmBtcTransactionComponent/index.tsx +++ b/src/app/components/confirmBtcTransactionComponent/index.tsx @@ -4,11 +4,12 @@ import ActionButton from '@components/button'; import RecipientComponent from '@components/recipientComponent'; import TransactionSettingAlert from '@components/transactionSetting'; import TransferFeeView from '@components/transferFeeView'; +import useBtcClient from '@hooks/apiClients/useBtcClient'; import useCoinRates from '@hooks/queries/useCoinRates'; import useNftDataSelector from '@hooks/stores/useNftDataSelector'; -import useBtcClient from '@hooks/useBtcClient'; import useOrdinalsByAddress from '@hooks/useOrdinalsByAddress'; import useSeedVault from '@hooks/useSeedVault'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import { Bundle, @@ -35,11 +36,7 @@ import styled from 'styled-components'; import TransactionDetailComponent from '../transactionDetailComponent'; import SatsBundle from './bundle'; -interface MainContainerProps { - isGalleryOpen: boolean; -} - -const OuterContainer = styled.div` +const OuterContainer = styled.div` display: flex; flex-direction: column; `; @@ -73,15 +70,14 @@ const ErrorContainer = styled.div((props) => ({ marginRight: props.theme.spacing(8), })); -const ErrorText = styled.h1((props) => ({ +const ErrorText = styled.p((props) => ({ ...props.theme.typography.body_s, color: props.theme.colors.danger_medium, })); -interface ReviewTransactionTitleProps { +const ReviewTransactionText = styled.h1<{ centerAligned: boolean; -} -const ReviewTransactionText = styled.h1((props) => ({ +}>((props) => ({ ...props.theme.typography.headline_s, color: props.theme.colors.white_0, marginBottom: props.theme.spacing(16), @@ -93,7 +89,7 @@ const CalloutContainer = styled.div((props) => ({ marginhorizontal: props.theme.spacing(8), })); -interface Props { +type Props = { currentFee: BigNumber; feePerVByte: BigNumber; // TODO tim: is this the same as currentFeeRate? refactor to be clear loadingBroadcastedTx: boolean; @@ -114,7 +110,7 @@ interface Props { setCurrentFeeRate: (feeRate: BigNumber) => void; onConfirmClick: (signedTxHex: string) => void; onCancelClick: () => void; -} +}; function ConfirmBtcTransactionComponent({ currentFee, @@ -139,9 +135,10 @@ function ConfirmBtcTransactionComponent({ onCancelClick, }: Props) { const { t } = useTranslation('translation'); - const isGalleryOpen: boolean = document.documentElement.clientWidth > 360; const [loading, setLoading] = useState(false); - const { btcAddress, selectedAccount, network, feeMultipliers } = useWalletSelector(); + const selectedAccount = useSelectedAccount(); + const { btcAddress } = selectedAccount; + const { network, feeMultipliers } = useWalletSelector(); const { btcFiatRate } = useCoinRates(); const { selectedSatBundle } = useNftDataSelector(); const { getSeed } = useSeedVault(); @@ -353,7 +350,7 @@ function ConfirmBtcTransactionComponent({ return ( <> - + {showFeeWarning && ( @@ -377,6 +374,7 @@ function ConfirmBtcTransactionComponent({ {currencyType !== 'BTC' && bundle && } {ordinalTxUtxo ? ( ( ({ - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - background: props.theme.colors.elevation1, - borderRadius: 12, - overflowY: 'auto', - padding: props.theme.spacing(6), - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), - marginBottom: props.theme.spacing(6), -})); - -const TransferDetailContainer = styled.div((props) => ({ - paddingBottom: props.theme.spacing(8), -})); - -const TitleText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_200, -})); - -const OutputTitleText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_200, - marginBottom: props.theme.spacing(6), -})); - -const SubValueText = styled.h1((props) => ({ - ...props.theme.body_m, - fontSize: 12, - color: props.theme.colors.white_400, -})); - -const TxIdText = styled.h1((props) => ({ - ...props.theme.body_m, - fontSize: 12, - color: props.theme.colors.white_0, - marginLeft: props.theme.spacing(2), -})); - -const YourAddressText = styled.h1((props) => ({ - ...props.theme.body_m, - fontSize: 12, - color: props.theme.colors.white_0, - marginRight: props.theme.spacing(2), -})); - -const RowContainer = styled.div({ - display: 'flex', - flexDirection: 'row', - width: '100%', - justifyContent: 'flex-end', -}); - -const DropDownContainer = styled.div({ - display: 'flex', - flexDirection: 'row', - flex: 1, - alignItems: 'center', - justifyContent: 'flex-end', -}); - -const TxIdContainer = styled.div({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', -}); - -const ExpandedContainer = styled(animated.div)({ - display: 'flex', - flexDirection: 'column', - marginTop: 16, -}); - -const Button = styled.button((props) => ({ - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - background: 'transparent', - marginLeft: props.theme.spacing(4), -})); - -interface Props { - address: string[]; - parsedPsbt: ParsedPSBT | undefined; - isExpanded: boolean; - onArrowClick: () => void; -} - -function InputOutputComponent({ address, parsedPsbt, isExpanded, onArrowClick }: Props) { - const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - const { btcAddress, ordinalsAddress } = useSelector((state: StoreState) => state.walletState); - let scriptOutputCount = 1; - const slideInStyles = useSpring({ - config: { ...config.gentle, duration: 400 }, - from: { opacity: 0, height: 0 }, - to: { - opacity: isExpanded ? 1 : 0, - height: isExpanded ? 'auto' : 0, - }, - }); - - const arrowRotation = useSpring({ - transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)', - config: { ...config.stiff }, - }); - - const renderAddress = (addressToBeDisplayed: string) => - addressToBeDisplayed === btcAddress || addressToBeDisplayed === ordinalsAddress ? ( - - (Your Address) - {getTruncatedAddress(addressToBeDisplayed)} - - ) : ( - {getTruncatedAddress(addressToBeDisplayed)} - ); - const renderSubValue = (input: PSBTInput, signedAddress: string) => - input.userSigns ? ( - renderAddress(signedAddress) - ) : ( - - {getTruncatedAddress(input.txid)} - (txid) - - ); - - function showPsbtOutput(output: PSBTOutput) { - const detailViewIcon = output.outputScript ? ScriptIcon : OutputIcon; - const detailViewHideCopyButton = output.outputScript - ? true - : btcAddress === output.address || ordinalsAddress === output.address; - const showAddress = - output.address === btcAddress || output.address === ordinalsAddress ? ( - - (Your Address) - {getTruncatedAddress(output.address)} - - ) : ( - {getTruncatedAddress(output.address)} - ); - const detailViewValue = output.outputScript ? ( - {`${t('SCRIPT_OUTPUT')} #${scriptOutputCount}`} - ) : ( - showAddress - ); - return ( - - {detailViewValue} - - ); - } - - return ( - - - {isExpanded ? t('INPUT') : t('INPUT_AND_OUTPUT')} - - - - - - {isExpanded && ( - - {parsedPsbt?.inputs.map((input, index) => ( - - - {renderSubValue(input, address[index])} - - - ))} - - {t('OUTPUT')} - {parsedPsbt?.outputs.map((output) => ( - {showPsbtOutput(output)} - ))} - - )} - - ); -} - -export default InputOutputComponent; diff --git a/src/app/components/confirmStxTransactionComponent/index.styled.ts b/src/app/components/confirmStxTransactionComponent/index.styled.ts new file mode 100644 index 000000000..8de1152e3 --- /dev/null +++ b/src/app/components/confirmStxTransactionComponent/index.styled.ts @@ -0,0 +1,90 @@ +import Button from '@ui-library/button'; +import styled from 'styled-components'; + +export const Container = styled.div` + display: flex; + flex-direction: column; + flex: 1; + padding-top: 22px; + padding-left: ${(props) => props.theme.space.m}; + padding-right: ${(props) => props.theme.space.m}; + overflow-y: auto; + + &::-webkit-scrollbar { + display: none; + } +`; + +export const ButtonsContainer = styled.div((props) => ({ + display: 'flex', + padding: `${props.theme.space.l} ${props.theme.space.m}`, + columnGap: props.theme.space.s, +})); + +export const EditNonceButton = styled(Button)((props) => ({ + justifyContent: 'flex-start', + padding: props.theme.space.xxs, + '&.tertiary': { + color: props.theme.colors.tangerine, + '&:focus-visible': { + color: props.theme.colors.tangerine, + opacity: 0.8, + }, + '&:hover:enabled': { + color: props.theme.colors.tangerine, + opacity: 0.8, + }, + '&:active:enabled': { + color: props.theme.colors.tangerine, + opacity: 0.6, + }, + '&:disabled': { + color: props.theme.colors.tangerine, + opacity: 0.6, + }, + }, +})); + +export const SponsoredInfoText = styled.p((props) => ({ + ...props.theme.typography.body_m, + color: props.theme.colors.white_400, +})); + +export const SuccessActionsContainer = styled.div((props) => ({ + width: '100%', + display: 'flex', + flexDirection: 'column', + gap: props.theme.space.s, + paddingLeft: props.theme.space.m, + paddingRight: props.theme.space.m, + marginBottom: props.theme.space.xxl, + marginTop: props.theme.space.xxl, +})); + +export const ReviewTransactionText = styled.p((props) => ({ + ...props.theme.typography.headline_s, + color: props.theme.colors.white_0, + textAlign: 'left', +})); + +export const RequestedByText = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_400, + marginTop: props.theme.space.xs, + textAlign: 'left', +})); + +export const TitleContainer = styled.div((props) => ({ + marginBottom: props.theme.space.l, +})); + +export const WarningWrapper = styled.div((props) => ({ + marginBottom: props.theme.space.m, +})); + +export const FeeRateContainer = styled.div` + margin-bottom: ${(props) => props.theme.space.m}; + background-color: ${(props) => props.theme.colors.background.elevation1}; + border-radius: ${(props) => props.theme.space.s}; + padding: ${(props) => props.theme.space.m}; +`; diff --git a/src/app/components/confirmStxTransactionComponent/index.tsx b/src/app/components/confirmStxTransactionComponent/index.tsx index 2b5de848e..166543f2d 100644 --- a/src/app/components/confirmStxTransactionComponent/index.tsx +++ b/src/app/components/confirmStxTransactionComponent/index.tsx @@ -1,124 +1,53 @@ -import SettingIcon from '@assets/img/dashboard/faders_horizontal.svg'; import ledgerConnectDefaultIcon from '@assets/img/ledger/ledger_connect_default.svg'; import ledgerConnectStxIcon from '@assets/img/ledger/ledger_import_connect_stx.svg'; import { delay } from '@common/utils/ledger'; import BottomModal from '@components/bottomModal'; import ActionButton from '@components/button'; -import InfoContainer from '@components/infoContainer'; import LedgerConnectionView from '@components/ledger/connectLedgerView'; import TransactionSettingAlert from '@components/transactionSetting'; -import TransferFeeView from '@components/transferFeeView'; +import useCoinRates from '@hooks/queries/useCoinRates'; +import useStxWalletData from '@hooks/queries/useStxWalletData'; import useNetworkSelector from '@hooks/useNetwork'; import useSeedVault from '@hooks/useSeedVault'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import Transport from '@ledgerhq/hw-transport-webusb'; +import { FadersHorizontal } from '@phosphor-icons/react'; import type { StacksTransaction } from '@secretkeylabs/xverse-core'; import { getNonce, + getStxFiatEquivalent, microstacksToStx, - setFee, - setNonce, signLedgerStxTransaction, signMultiStxTransactions, signTransaction, stxToMicrostacks, } from '@secretkeylabs/xverse-core'; +import { estimateTransaction } from '@stacks/transactions'; +import SelectFeeRate from '@ui-components/selectFeeRate'; +import Button from '@ui-library/button'; +import Callout from '@ui-library/callout'; import { isHardwareAccount } from '@utils/helper'; +import { modifyRecommendedStxFees } from '@utils/transactions/transactions'; import BigNumber from 'bignumber.js'; import { ReactNode, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; - -const Container = styled.div` - display: flex; - flex-direction: column; - flex: 1; - margin-top: 22px; - margin-left: 16px; - margin-right: 16px; - overflow-y: auto; - - &::-webkit-scrollbar { - display: none; - } -`; - -export const ButtonContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - paddingTop: props.theme.spacing(12), - paddingBottom: props.theme.spacing(12), - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), - backgroundColor: props.theme.colors.background.elevation0, -})); - -const TransparentButtonContainer = styled.div((props) => ({ - marginLeft: props.theme.spacing(2), - marginRight: props.theme.spacing(2), - width: '100%', -})); - -const Button = styled.button((props) => ({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - borderRadius: props.theme.radius(1), - backgroundColor: 'transparent', - width: '100%', - marginTop: props.theme.spacing(10), -})); - -const ButtonText = styled.div((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_0, - textAlign: 'center', -})); - -const ButtonImage = styled.img((props) => ({ - marginRight: props.theme.spacing(3), - alignSelf: 'center', - transform: 'all', -})); - -const SponsoredInfoText = styled.h1((props) => ({ - ...props.theme.body_m, - color: props.theme.colors.white_400, -})); - -const SuccessActionsContainer = styled.div((props) => ({ - width: '100%', - display: 'flex', - flexDirection: 'column', - gap: props.theme.spacing(6), - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), - marginBottom: props.theme.spacing(20), - marginTop: props.theme.spacing(20), -})); - -const ReviewTransactionText = styled.h1((props) => ({ - ...props.theme.headline_s, - color: props.theme.colors.white_0, - textAlign: 'left', -})); - -const RequestedByText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_400, - marginTop: props.theme.spacing(4), - textAlign: 'left', -})); - -const TitleContainer = styled.div((props) => ({ - marginBottom: props.theme.spacing(16), -})); - -const WarningWrapper = styled.div((props) => ({ - marginBottom: props.theme.spacing(8), -})); - -interface Props { +import Theme from 'theme'; +import { + ButtonsContainer, + Container, + EditNonceButton, + FeeRateContainer, + RequestedByText, + ReviewTransactionText, + SponsoredInfoText, + SuccessActionsContainer, + TitleContainer, + WarningWrapper, +} from './index.styled'; + +// todo: make fee non option - that'll require change in all components using it +type Props = { initialStxTransactions: StacksTransaction[]; loading: boolean; onCancelClick: () => void; @@ -130,8 +59,9 @@ interface Props { title?: string; subTitle?: string; hasSignatures?: boolean; - onFeeChange?: (fee: BigNumber) => void; -} + fee?: string | undefined; + setFeeRate?: (feeRate: string) => void; +}; function ConfirmStxTransactionComponent({ initialStxTransactions, @@ -145,16 +75,23 @@ function ConfirmStxTransactionComponent({ onCancelClick, skipModal = false, hasSignatures = false, - onFeeChange, + fee, + setFeeRate, }: Props) { const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); const { t: signatureRequestTranslate } = useTranslation('translation', { keyPrefix: 'SIGNATURE_REQUEST', }); + const { t: settingsTranslate } = useTranslation('translation', { + keyPrefix: 'TRANSACTION_SETTING', + }); const selectedNetwork = useNetworkSelector(); + const { stxBtcRate, btcFiatRate } = useCoinRates(); + const { data: stxData } = useStxWalletData(); const { getSeed } = useSeedVault(); const [showFeeSettings, setShowFeeSettings] = useState(false); - const { selectedAccount, feeMultipliers } = useWalletSelector(); + const selectedAccount = useSelectedAccount(); + const { feeMultipliers, fiatCurrency } = useWalletSelector(); const [openTransactionSettingModal, setOpenTransactionSettingModal] = useState(false); const [buttonLoading, setButtonLoading] = useState(loading); const [isModalVisible, setIsModalVisible] = useState(false); @@ -165,30 +102,82 @@ function ConfirmStxTransactionComponent({ const [isTxApproved, setIsTxApproved] = useState(false); const [isTxRejected, setIsTxRejected] = useState(false); const [showFeeWarning, setShowFeeWarning] = useState(false); + const [feesLoading, setFeesLoading] = useState(false); + + const [feeRates, setFeeRates] = useState({ low: 0, medium: 0, high: 0 }); + + const stxBalance = stxData?.availableBalance.toString() ?? '0'; + // TODO: fix type error as any + const amount = (initialStxTransactions[0]?.payload as any)?.amount; useEffect(() => { setButtonLoading(loading); }, [loading]); + // Reactively estimate fees + useEffect(() => { + const fetchStxFees = async () => { + try { + setFeesLoading(true); + const [low, medium, high] = await estimateTransaction( + initialStxTransactions[0].payload, + undefined, + selectedNetwork, + ); + + const modifiedFees = modifyRecommendedStxFees( + { + low: low.fee, + medium: medium.fee, + high: high.fee, + }, + feeMultipliers, + ); + + setFeeRates({ + low: microstacksToStx(BigNumber(modifiedFees.low)).toNumber(), + medium: microstacksToStx(BigNumber(modifiedFees.medium)).toNumber(), + high: microstacksToStx(BigNumber(modifiedFees.high)).toNumber(), + }); + if (!fee) setFeeRate?.(Number(microstacksToStx(BigNumber(medium.fee))).toString()); + } catch (e) { + console.error(e); + } finally { + setFeesLoading(false); + } + }; + + fetchStxFees(); + }, [selectedNetwork, initialStxTransactions]); + useEffect(() => { - const fee = new BigNumber(initialStxTransactions[0].auth.spendingCondition.fee.toString()); + const stxTxFee = BigNumber(initialStxTransactions[0].auth.spendingCondition.fee.toString()); - if (feeMultipliers && fee.isGreaterThan(new BigNumber(feeMultipliers.thresholdHighStacksFee))) { + if ( + feeMultipliers && + stxTxFee.isGreaterThan(BigNumber(feeMultipliers.thresholdHighStacksFee)) + ) { setShowFeeWarning(true); } else if (showFeeWarning) { setShowFeeWarning(false); } }, [initialStxTransactions, feeMultipliers]); - const getFee = () => - isSponsored - ? new BigNumber(0) - : new BigNumber( - initialStxTransactions - .map((tx) => tx?.auth?.spendingCondition?.fee ?? BigInt(0)) - .reduce((prev, curr) => prev + curr, BigInt(0)) - .toString(10), - ); + const stxToFiat = (stx: string) => + getStxFiatEquivalent( + stxToMicrostacks(BigNumber(stx)), + BigNumber(stxBtcRate), + BigNumber(btcFiatRate), + ).toString(); + + const getFee = () => { + const defaultFee = isSponsored + ? BigNumber(0) + : fee + ? BigNumber(fee) + : BigNumber(feeRates.medium); + return defaultFee; + }; const getTxNonce = (): string => { const nonce = getNonce(initialStxTransactions[0]); @@ -215,6 +204,13 @@ function ConfirmStxTransactionComponent({ const seed = await getSeed(); let signedTxs: StacksTransaction[] = []; + + if (fee) { + for (let i = 0; i < initialStxTransactions.length; i++) { + initialStxTransactions[i].setFee(stxToMicrostacks(BigNumber(fee)).toString()); + } + } + if (initialStxTransactions.length === 1) { const signedContractCall = await signTransaction( initialStxTransactions[0], @@ -242,18 +238,8 @@ function ConfirmStxTransactionComponent({ feeRate?: string; nonce?: string; }) => { - const fee = stxToMicrostacks(new BigNumber(settingFee)); - - if (feeMultipliers && fee.isGreaterThan(new BigNumber(feeMultipliers.thresholdHighStacksFee))) { - setShowFeeWarning(true); - } else if (showFeeWarning) { - setShowFeeWarning(false); - } - - setFee(initialStxTransactions[0], BigInt(fee.toString())); - onFeeChange?.(fee); if (nonce && nonce !== '') { - setNonce(initialStxTransactions[0], BigInt(nonce)); + initialStxTransactions[0].setNonce(BigInt(nonce)); } setOpenTransactionSettingModal(false); }; @@ -269,7 +255,6 @@ function ConfirmStxTransactionComponent({ return; } setIsButtonDisabled(true); - const transport = await Transport.create(); if (!transport) { @@ -283,6 +268,12 @@ function ConfirmStxTransactionComponent({ await delay(1500); setCurrentStepIndex(1); try { + if (fee) { + for (let i = 0; i < initialStxTransactions.length; i++) { + initialStxTransactions[i].setFee(stxToMicrostacks(BigNumber(fee)).toString()); + } + } + const signedTxs = await signLedgerStxTransaction({ transport, transactionBuffer: Buffer.from(initialStxTransactions[0].serialize()), @@ -306,6 +297,34 @@ function ConfirmStxTransactionComponent({ setCurrentStepIndex(0); }; + const setTxFee = (stxFee: string) => { + const feeToSet = stxToMicrostacks(BigNumber(stxFee)); + + if ( + feeMultipliers && + feeToSet.isGreaterThan(BigNumber(feeMultipliers.thresholdHighStacksFee)) + ) { + setShowFeeWarning(true); + } else if (showFeeWarning) { + setShowFeeWarning(false); + } + setFeeRate?.(stxFee); + }; + + const checkIfEnoughBalance = (totalFee: number) => { + const hasInsufficientFunds = + amount && + BigNumber(stxBalance).isLessThan( + BigNumber(amount).plus(stxToMicrostacks(BigNumber(totalFee ?? 0))), + ); + + if (hasInsufficientFunds) { + return Promise.resolve(undefined); + } + + return Promise.resolve(totalFee); + }; + return ( <> @@ -318,34 +337,38 @@ function ConfirmStxTransactionComponent({ {showFeeWarning && ( - + )} {children} - - {/* TODO fix type error as any */} - {(initialStxTransactions[0]?.payload as any)?.amount && ( - + - )} + + {isSponsored ? ( {t('SPONSORED_TX_INFO')} ) : ( !hasSignatures && ( - + } + title={settingsTranslate('ADVANCED_SETTING_NONCE_OPTION')} + variant="tertiary" + onClick={onAdvancedSettingClick} + /> ) )} - - - - - + {text} diff --git a/src/app/components/startupLoadingScreen/index.tsx b/src/app/components/startupLoadingScreen/index.tsx new file mode 100644 index 000000000..28f1a3ab3 --- /dev/null +++ b/src/app/components/startupLoadingScreen/index.tsx @@ -0,0 +1,45 @@ +import logo from '@assets/img/xverse_logo.svg'; +import ErrorDisplay from '@components/errorDisplay'; +import rootStore from '@stores/index'; +import { useEffect, useState } from 'react'; +import styled from 'styled-components'; + +const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + height: '100%', + width: '100%', + backgroundColor: props.theme.colors.elevation0, +})); + +function StartupLoadingScreen(): React.ReactNode { + const [error, setError] = useState(''); + + useEffect(() => { + let currentError = error; + const intervalId = setInterval(() => { + if (!currentError && rootStore.rehydrateError.current) { + setError(rootStore.rehydrateError.current); + currentError = rootStore.rehydrateError.current; + } + }, 1000); + + return () => { + clearInterval(intervalId); + }; + }, []); + + if (error) { + return ; + } + + return ( + + logo + + ); +} + +export default StartupLoadingScreen; diff --git a/src/app/components/steps/index.tsx b/src/app/components/steps/index.tsx deleted file mode 100644 index f8a24959a..000000000 --- a/src/app/components/steps/index.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { nanoid } from 'nanoid'; -import styled from 'styled-components'; - -const StepsContainer = styled.div({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', -}); - -const StepsDot = styled.div((props) => ({ - width: 8, - height: 8, - borderRadius: 4, - backgroundColor: props.active ? props.theme.colors.action.classic : props.theme.colors.elevation3, - marginRight: props.theme.spacing(4), -})); - -interface StepsProps { - data: any[]; - activeIndex: number; - dotStrategy?: 'completion' | 'selection'; -} - -interface StepDotProps { - active: boolean; -} - -export default function Steps(props: StepsProps): JSX.Element { - const { data, activeIndex, dotStrategy } = props; - const getStrategy = (index: number) => { - if (dotStrategy === 'selection') { - return index === activeIndex; - } - return index <= activeIndex; - }; - return ( - - {data.map((view, index) => ( - - ))} - - ); -} diff --git a/src/app/components/tabs/index.tsx b/src/app/components/tabs/index.tsx new file mode 100644 index 000000000..dac97d295 --- /dev/null +++ b/src/app/components/tabs/index.tsx @@ -0,0 +1,54 @@ +import styled, { css } from 'styled-components'; + +const TabContainer = styled.div` + display: flex; + gap: ${(props) => props.theme.space.xxs}; +`; + +const TabItem = styled.div<{ $active?: boolean }>` + padding: 7px 12px 8px; + border-radius: 12px; + ${(props) => props.theme.typography.body_bold_s}; + color: ${(props) => props.theme.colors.white_200}; + text-transform: uppercase; + cursor: pointer; + user-select: none; + transition: color 0.1s ease; + + ${({ $active }) => + $active + ? css` + color: ${(props) => props.theme.colors.white_0}; + background-color: ${({ theme }) => theme.colors.elevation3}; + cursor: default; + ` + : css` + &:hover { + color: ${(props) => props.theme.colors.white_0}; + } + `} +`; + +interface TabProps { + tabs: { label: string; value: T }[]; + activeTab: T; + onTabClick: (value: T) => void; + className?: string; +} +function Tabs({ tabs, activeTab, onTabClick, className }: TabProps) { + return ( + + {tabs.map((tab) => ( + onTabClick(tab.value)} + > + {tab.label} + + ))} + + ); +} + +export default Tabs; diff --git a/src/app/components/tokenTile/index.tsx b/src/app/components/tokenTile/index.tsx index 0c954b2b5..c16990094 100644 --- a/src/app/components/tokenTile/index.tsx +++ b/src/app/components/tokenTile/index.tsx @@ -4,11 +4,10 @@ import TokenImage from '@components/tokenImage'; import useBtcWalletData from '@hooks/queries/useBtcWalletData'; import useCoinRates from '@hooks/queries/useCoinRates'; import useStxWalletData from '@hooks/queries/useStxWalletData'; -import type { FungibleToken } from '@secretkeylabs/xverse-core'; -import { microstacksToStx, satsToBtc } from '@secretkeylabs/xverse-core'; +import { FungibleToken, getFiatEquivalent } from '@secretkeylabs/xverse-core'; import { StoreState } from '@stores/index'; import { CurrencyTypes } from '@utils/constants'; -import { getFtBalance, getFtTicker } from '@utils/tokens'; +import { getBalanceAmount, getFtTicker } from '@utils/tokens'; import BigNumber from 'bignumber.js'; import { NumericFormat } from 'react-number-format'; import { useSelector } from 'react-redux'; @@ -105,7 +104,7 @@ interface Props { title: string; loading: boolean; currency: CurrencyTypes; - onPress: (coin: CurrencyTypes, ftKey: string | undefined) => void; + onPress: (coin: CurrencyTypes, fungibleToken: FungibleToken | undefined) => void; fungibleToken?: FungibleToken; enlargeTicker?: boolean; className?: string; @@ -132,38 +131,22 @@ function TokenTile({ return `${getFtTicker(fungibleToken as FungibleToken)}`; }; - const getBalanceAmount = () => { - switch (currency) { - case 'STX': - return microstacksToStx(new BigNumber(stxData?.balance ?? 0)).toString(); - case 'BTC': - return satsToBtc(new BigNumber(btcBalance ?? 0)).toString(); - case 'FT': - return fungibleToken ? getFtBalance(fungibleToken) : ''; - default: - return undefined; + const handleTokenPressed = () => onPress(currency, fungibleToken); + + const getFiatAmount = () => { + const fiatAmount = getFiatEquivalent( + Number(getBalanceAmount(currency, fungibleToken, stxData, btcBalance)), + currency, + BigNumber(stxBtcRate), + BigNumber(btcFiatRate), + fungibleToken, + ); + if (fiatAmount) { + return BigNumber(fiatAmount); } + return undefined; }; - const getFiatEquivalent = (): BigNumber | undefined => { - switch (currency) { - case 'STX': - return microstacksToStx(new BigNumber(stxData?.balance ?? 0)) - .multipliedBy(stxBtcRate) - .multipliedBy(btcFiatRate); - case 'BTC': - return satsToBtc(new BigNumber(btcBalance ?? 0)).multipliedBy(btcFiatRate); - case 'FT': - return fungibleToken?.tokenFiatRate - ? new BigNumber(getFtBalance(fungibleToken)).multipliedBy(fungibleToken.tokenFiatRate) - : undefined; - default: - return undefined; - } - }; - - const handleTokenPressed = () => onPress(currency, fungibleToken?.principal); - return ( @@ -186,12 +169,12 @@ function TokenTile({ ) : ( {value}} /> - + )} diff --git a/src/app/components/topRow/index.tsx b/src/app/components/topRow/index.tsx index 4d4df3274..4d2621063 100644 --- a/src/app/components/topRow/index.tsx +++ b/src/app/components/topRow/index.tsx @@ -30,9 +30,6 @@ const BackButton = styled.button((props) => ({ '&:hover': { backgroundColor: props.theme.colors.white_900, }, - '&:focus': { - backgroundColor: props.theme.colors.white_850, - }, })); const MenuButton = styled.button((props) => ({ @@ -44,13 +41,13 @@ const MenuButton = styled.button((props) => ({ right: props.theme.space.xxs, })); -interface Props { +type Props = { title?: string; onClick: (e: React.MouseEvent) => void; showBackButton?: boolean; className?: string; onMenuClick?: (e: React.MouseEvent) => void; -} +}; function TopRow({ title, onClick, showBackButton = true, className, onMenuClick }: Props) { return ( diff --git a/src/app/components/transactionDetailComponent/index.tsx b/src/app/components/transactionDetailComponent/index.tsx index f3fc866ce..5b03b91e2 100644 --- a/src/app/components/transactionDetailComponent/index.tsx +++ b/src/app/components/transactionDetailComponent/index.tsx @@ -1,34 +1,31 @@ -import { currencySymbolMap } from '@secretkeylabs/xverse-core'; -import { StoreState } from '@stores/index'; +import FiatAmountText from '@components/fiatAmountText'; +import useWalletSelector from '@hooks/useWalletSelector'; import BigNumber from 'bignumber.js'; -import { NumericFormat } from 'react-number-format'; -import { useSelector } from 'react-redux'; import styled from 'styled-components'; const Container = styled.div((props) => ({ display: 'flex', flexDirection: 'row', background: props.theme.colors.elevation1, - borderRadius: 12, - padding: '12px 16px', + borderRadius: props.theme.radius(2), + padding: props.theme.space.m, justifyContent: 'center', alignItems: 'center', - marginBottom: 12, + marginBottom: props.theme.space.s, })); -const TitleText = styled.h1((props) => ({ - ...props.theme.body_medium_m, +const TitleText = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, color: props.theme.colors.white_200, })); -const ValueText = styled.h1((props) => ({ - ...props.theme.body_medium_m, +const ValueText = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, color: props.theme.colors.white_0, })); -const SubValueText = styled.h1((props) => ({ - ...props.theme.body_m, - fontSize: 12, +const SubValueText = styled.p((props) => ({ + ...props.theme.typography.body_s, textAlign: 'right', color: props.theme.colors.white_400, })); @@ -45,34 +42,17 @@ const TitleContainer = styled.div({ flexDirection: 'column', }); -interface Props { +type Props = { title: string; subTitle?: string; value?: string | React.ReactNode; description?: string; subValue?: BigNumber; -} +}; function TransactionDetailComponent({ title, subTitle, value, subValue, description }: Props) { - const { fiatCurrency } = useSelector((state: StoreState) => state.walletState); + const { fiatCurrency } = useWalletSelector(); - const getFiatAmountString = (fiatAmount: BigNumber) => { - if (fiatAmount) { - if (fiatAmount.isLessThan(0.01)) { - return `<${currencySymbolMap[fiatCurrency]}0.01 ${fiatCurrency}`; - } - return ( - - ); - } - return ''; - }; return ( @@ -82,7 +62,7 @@ function TransactionDetailComponent({ title, subTitle, value, subValue, descript {value && {value}} {description && {description}} - {subValue && {getFiatAmountString(subValue)}} + {subValue && } ); diff --git a/src/app/components/transactionSetting/editBtcFee.tsx b/src/app/components/transactionSetting/editBtcFee.tsx index fc63a7616..f16745e6a 100644 --- a/src/app/components/transactionSetting/editBtcFee.tsx +++ b/src/app/components/transactionSetting/editBtcFee.tsx @@ -1,12 +1,13 @@ +import FiatAmountText from '@components/fiatAmountText'; +import useBtcClient from '@hooks/apiClients/useBtcClient'; import useCoinRates from '@hooks/queries/useCoinRates'; -import useBtcClient from '@hooks/useBtcClient'; import useBtcFees from '@hooks/useBtcFees'; import useDebounce from '@hooks/useDebounce'; import useOrdinalsByAddress from '@hooks/useOrdinalsByAddress'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import { Faders } from '@phosphor-icons/react'; import { - currencySymbolMap, ErrorCodes, getBtcFees, getBtcFeesForNonOrdinalBtcSend, @@ -27,8 +28,6 @@ import FeeItem from './feeItem'; const Container = styled.div((props) => ({ display: 'flex', flexDirection: 'column', - marginLeft: props.theme.space.m, - marginRight: props.theme.space.m, paddingBottom: props.theme.space.m, })); @@ -37,10 +36,9 @@ const DetailText = styled.h1((props) => ({ color: props.theme.colors.white_200, })); -interface InputContainerProps { +const InputContainer = styled.div<{ withError?: boolean; -} -const InputContainer = styled.div((props) => ({ +}>((props) => ({ display: 'flex', flexDirection: 'row', alignItems: 'center', @@ -74,7 +72,7 @@ const InputField = styled.input((props) => ({ }, })); -const FeeText = styled.h1((props) => ({ +const FeeText = styled.span((props) => ({ ...props.theme.typography.body_m, color: props.theme.colors.white_0, })); @@ -96,11 +94,9 @@ const FeePrioritiesContainer = styled.div` flex-direction: column; `; -interface FeeContainerProps { +const FeeItemContainer = styled.button<{ isSelected: boolean; -} - -const FeeItemContainer = styled.button` +}>` display: flex; padding: ${(props) => props.theme.space.s} ${(props) => props.theme.space.m}; align-items: center; @@ -138,7 +134,7 @@ const TotalFeeText = styled(StyledP)` margin-right: ${(props) => props.theme.space.xxs}; `; -interface Props { +type Props = { type?: string; fee: string; feeRate?: BigNumber | string; @@ -157,7 +153,8 @@ interface Props { setError: (error: string) => void; setCustomFeeSelected: (selected: boolean) => void; feeOptionSelected: (feeRate: string, totalFee: string) => void; -} +}; + function EditBtcFee({ type, fee, @@ -180,8 +177,9 @@ function EditBtcFee({ }: Props) { const { t } = useTranslation('translation'); - const { network, btcAddress, fiatCurrency, selectedAccount, ordinalsAddress } = - useWalletSelector(); + const selectedAccount = useSelectedAccount(); + const { btcAddress, ordinalsAddress } = selectedAccount; + const { network, fiatCurrency } = useWalletSelector(); const { btcFiatRate } = useCoinRates(); const [totalFee, setTotalFee] = useState(fee); const [feeRateInput, setFeeRateInput] = useState(feeRate?.toString() ?? ''); @@ -281,33 +279,6 @@ function EditBtcFee({ } }, [debouncedFeeRateInput]); - function getFiatEquivalent() { - return getBtcFiatEquivalent(new BigNumber(totalFee), BigNumber(btcFiatRate)); - } - - const getFiatAmountString = (fiatAmount: BigNumber) => { - if (fiatAmount) { - if (fiatAmount.isLessThan(0.01)) { - return `<${currencySymbolMap[fiatCurrency]}0.01 ${fiatCurrency}`; - } - return ( - ( - - {value} - - )} - /> - ); - } - return ''; - }; - const onInputEditFeesChange = ({ target: { value } }: React.ChangeEvent) => { if (error) { setError(''); @@ -336,8 +307,9 @@ function EditBtcFee({ time="~10 mins" feeRate={feeData?.highFeeRate} totalFee={feeData?.highTotalFee} - fiat={getFiatAmountString( - getBtcFiatEquivalent(new BigNumber(feeData.highTotalFee), BigNumber(btcFiatRate)), + fiatAmount={getBtcFiatEquivalent( + new BigNumber(feeData.highTotalFee), + BigNumber(btcFiatRate), )} onClick={() => { feeOptionSelected(feeData?.highFeeRate?.toString() || '', feeData?.highTotalFee); @@ -351,8 +323,9 @@ function EditBtcFee({ time="~30 mins" feeRate={feeData?.standardFeeRate} totalFee={feeData?.standardTotalFee} - fiat={getFiatAmountString( - getBtcFiatEquivalent(new BigNumber(feeData.standardTotalFee), BigNumber(btcFiatRate)), + fiatAmount={getBtcFiatEquivalent( + new BigNumber(feeData.standardTotalFee), + BigNumber(btcFiatRate), )} onClick={() => { feeOptionSelected( @@ -411,9 +384,10 @@ function EditBtcFee({ )} /> - - {getFiatAmountString(getFiatEquivalent())} - + {error && {error}} diff --git a/src/app/components/transactionSetting/editNonce.tsx b/src/app/components/transactionSetting/editNonce.tsx index 5f28bf038..2b4bbbaa2 100644 --- a/src/app/components/transactionSetting/editNonce.tsx +++ b/src/app/components/transactionSetting/editNonce.tsx @@ -1,58 +1,31 @@ -import InfoContainer from '@components/infoContainer'; +import Callout from '@ui-library/callout'; +import Input from '@ui-library/input'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; +import Theme from 'theme'; -const NonceContainer = styled.div((props) => ({ +const Container = styled.div({ display: 'flex', flexDirection: 'column', - marginLeft: props.theme.spacing(8), - marginRight: props.theme.spacing(8), -})); +}); -const DetailText = styled.h1((props) => ({ - ...props.theme.body_m, +const Description = styled.p((props) => ({ + ...props.theme.typography.body_m, color: props.theme.colors.white_200, - marginTop: props.theme.spacing(8), -})); - -const Text = styled.h1((props) => ({ - ...props.theme.body_medium_m, - marginTop: props.theme.spacing(8), -})); - -const InputContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - marginTop: props.theme.spacing(4), - marginBottom: props.theme.spacing(6), - border: `1px solid ${props.theme.colors.elevation6}`, - backgroundColor: props.theme.colors.elevation1, - borderRadius: 8, - paddingLeft: props.theme.spacing(5), - paddingRight: props.theme.spacing(5), - paddingTop: props.theme.spacing(5), - paddingBottom: props.theme.spacing(5), -})); - -const InputField = styled.input((props) => ({ - ...props.theme.body_m, - backgroundColor: props.theme.colors.elevation1, - color: props.theme.colors.white_400, - width: '100%', - border: 'transparent', + marginBottom: props.theme.space.l, })); -interface Props { +type Props = { nonce: string; setNonce: (nonce: string) => void; -} +}; + function EditNonce({ nonce, setNonce }: Props) { const { t } = useTranslation('translation', { keyPrefix: 'TRANSACTION_SETTING' }); const [nonceInput, setNonceInput] = useState(nonce); - const onInputEditNonceChange = (e: React.ChangeEvent) => { + const handleOnChange = (e: React.ChangeEvent) => { setNonceInput(e.target.value); }; @@ -61,14 +34,20 @@ function EditNonce({ nonce, setNonce }: Props) { }, [nonceInput, setNonce]); return ( - - {t('NONCE_INFO')} - {t('NONCE')} - - - - - + + {t('NONCE_INFO')} + +
+ +
); } diff --git a/src/app/components/transactionSetting/editStxFee.tsx b/src/app/components/transactionSetting/editStxFee.tsx index 203b22121..117c68e9e 100644 --- a/src/app/components/transactionSetting/editStxFee.tsx +++ b/src/app/components/transactionSetting/editStxFee.tsx @@ -1,14 +1,10 @@ +import FiatAmountText from '@components/fiatAmountText'; import useCoinRates from '@hooks/queries/useCoinRates'; import useWalletSelector from '@hooks/useWalletSelector'; -import { - currencySymbolMap, - getStxFiatEquivalent, - stxToMicrostacks, -} from '@secretkeylabs/xverse-core'; +import { getStxFiatEquivalent, stxToMicrostacks } from '@secretkeylabs/xverse-core'; import BigNumber from 'bignumber.js'; import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { NumericFormat } from 'react-number-format'; import styled from 'styled-components'; const Container = styled.div((props) => ({ @@ -19,25 +15,19 @@ const Container = styled.div((props) => ({ marginBottom: props.theme.spacing(2), })); -const FiatAmountText = styled.h1((props) => ({ - ...props.theme.body_xs, - color: props.theme.colors.white_400, -})); - const DetailText = styled.h1((props) => ({ - ...props.theme.body_m, + ...props.theme.typography.body_m, color: props.theme.colors.white_200, })); const Text = styled.h1((props) => ({ - ...props.theme.body_medium_m, + ...props.theme.typography.body_medium_m, marginTop: props.theme.spacing(8), })); -interface InputContainerProps { +const InputContainer = styled.div<{ withError?: boolean; -} -const InputContainer = styled.div((props) => ({ +}>((props) => ({ display: 'flex', flexDirection: 'row', alignItems: 'center', @@ -52,7 +42,7 @@ const InputContainer = styled.div((props) => ({ })); const InputField = styled.input((props) => ({ - ...props.theme.body_m, + ...props.theme.typography.body_m, backgroundColor: 'transparent', color: props.theme.colors.white_0, border: 'transparent', @@ -70,17 +60,11 @@ const InputField = styled.input((props) => ({ }, })); -const SubText = styled.h1((props) => ({ - ...props.theme.body_xs, - color: props.theme.colors.white_400, -})); - -interface ButtonProps { +const FeeButton = styled.button<{ isSelected: boolean; isLastInRow?: boolean; -} -const FeeButton = styled.button((props) => ({ - ...props.theme.body_medium_m, +}>((props) => ({ + ...props.theme.typography.body_medium_m, color: `${props.isSelected ? props.theme.colors.elevation2 : props.theme.colors.white_400}`, background: `${props.isSelected ? props.theme.colors.white : 'transparent'}`, border: `1px solid ${props.isSelected ? 'transparent' : props.theme.colors.elevation6}`, @@ -113,14 +97,14 @@ const TickerContainer = styled.div({ flex: 1, }); -const ErrorText = styled.h1((props) => ({ +const ErrorText = styled.p((props) => ({ ...props.theme.body_xs, color: props.theme.colors.feedback.error, marginBottom: props.theme.spacing(2), })); // TODO tim: this component needs refactoring. separate business logic from presentation -interface Props { +type Props = { type?: string; fee: string; feeRate?: BigNumber | string; @@ -130,7 +114,8 @@ interface Props { setFeeRate: (feeRate: string) => void; setFeeMode: (feeMode: string) => void; setError: (error: string) => void; -} +}; + function EditStxFee({ type, fee, @@ -191,33 +176,6 @@ function EditStxFee({ } }, [feeRateInput]); - function getFiatEquivalent() { - return getStxFiatEquivalent( - stxToMicrostacks(new BigNumber(totalFee)), - BigNumber(stxBtcRate), - BigNumber(btcFiatRate), - ); - } - - const getFiatAmountString = (fiatAmount: BigNumber) => { - if (fiatAmount) { - if (fiatAmount.isLessThan(0.01)) { - return `<${currencySymbolMap[fiatCurrency]}0.01 ${fiatCurrency}`; - } - return ( - {value}} - /> - ); - } - return ''; - }; - const onInputEditFeesChange = ({ target: { value } }: React.ChangeEvent) => { if (error) { setError(''); @@ -244,7 +202,14 @@ function EditStxFee({ onChange={onInputEditFeesChange} /> - {getFiatAmountString(getFiatEquivalent())} + {error && {error}} diff --git a/src/app/components/transactionSetting/feeItem.tsx b/src/app/components/transactionSetting/feeItem.tsx index 1a0d9184a..b24541cd3 100644 --- a/src/app/components/transactionSetting/feeItem.tsx +++ b/src/app/components/transactionSetting/feeItem.tsx @@ -1,16 +1,17 @@ +import FiatAmountText from '@components/fiatAmountText'; +import useWalletSelector from '@hooks/useWalletSelector'; import { Bicycle, CarProfile, RocketLaunch } from '@phosphor-icons/react'; import { ErrorCodes } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; import Spinner from '@ui-library/spinner'; +import BigNumber from 'bignumber.js'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import Theme from 'theme'; -interface FeeContainer { +const FeeItemContainer = styled.button<{ isSelected: boolean; -} - -const FeeItemContainer = styled.button` +}>` display: flex; padding: ${(props) => props.theme.space.s} ${(props) => props.theme.space.m}; align-items: center; @@ -65,30 +66,37 @@ const LoaderContainer = styled.div` flex: 1; `; +const StyledFiatAmountText = styled(FiatAmountText)` + ${(props) => props.theme.typography.body_medium_s} + color: ${(props) => props.theme.colors.white_200}; +`; + type FeePriority = 'high' | 'medium' | 'low'; -interface FeeItemProps { +type Props = { priority: FeePriority; time: string; feeRate: string; totalFee: string; - fiat: string | JSX.Element; + fiatAmount: BigNumber; selected: boolean; onClick?: () => void; error?: string; -} +}; function FeeItem({ priority, time, feeRate, totalFee, - fiat, + fiatAmount, selected, error, onClick, -}: FeeItemProps) { +}: Props) { const { t } = useTranslation('translation'); + const { fiatCurrency } = useWalletSelector(); + const getIcon = () => { switch (priority) { case 'high': @@ -148,9 +156,7 @@ function FeeItem({ {`${totalFee} Sats`} )} - - {fiat} - + {error && ( {getErrorMessage(error)} diff --git a/src/app/components/transactionSetting/index.tsx b/src/app/components/transactionSetting/index.tsx index 40273a0dc..5d8e1ecd7 100644 --- a/src/app/components/transactionSetting/index.tsx +++ b/src/app/components/transactionSetting/index.tsx @@ -1,10 +1,10 @@ import ArrowIcon from '@assets/img/settings/arrow.svg'; -import BottomModal from '@components/bottomModal'; -import ActionButton from '@components/button'; import useBtcWalletData from '@hooks/queries/useBtcWalletData'; import useStxWalletData from '@hooks/queries/useStxWalletData'; import useWalletSelector from '@hooks/useWalletSelector'; import { isCustomFeesAllowed, Recipient, stxToMicrostacks, UTXO } from '@secretkeylabs/xverse-core'; +import Button from '@ui-library/button'; +import Sheet from '@ui-library/sheet'; import BigNumber from 'bignumber.js'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -14,37 +14,15 @@ import EditBtcFee from './editBtcFee'; import EditNonce from './editNonce'; import EditStxFee from './editStxFee'; -const ButtonContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - marginTop: props.theme.spacing(10), - marginBottom: props.theme.spacing(20), - marginLeft: props.theme.spacing(8), - marginRight: props.theme.spacing(8), -})); - const ButtonsContainer = styled.div` display: flex; - flex-direction: row; - margin-left: ${(props) => props.theme.space.m}; - margin-right: ${(props) => props.theme.space.m}; - margin-bottom: ${(props) => props.theme.space.m}; -`; - -const LeftButton = styled.div` - display: flex; - margin-right: ${(props) => props.theme.space.xs}; - flex: 1; -`; - -const RightButton = styled.div` - display: flex; - margin-left: ${(props) => props.theme.space.xs}; - flex: 1; + column-gap: ${(props) => props.theme.space.s}; + margin-top: ${(props) => props.theme.space.l}; + margin-bottom: ${(props) => props.theme.space.xxl}; `; const TransactionSettingOptionText = styled.h1((props) => ({ - ...props.theme.body_medium_l, + ...props.theme.typography.body_medium_l, color: props.theme.colors.white_200, })); @@ -55,8 +33,6 @@ const TransactionSettingOptionButton = styled.button((props) => ({ width: '100%', marginTop: props.theme.spacing(16), marginBottom: props.theme.spacing(16), - paddingLeft: props.theme.spacing(12), - paddingRight: props.theme.spacing(12), justifyContent: 'space-between', })); @@ -66,14 +42,12 @@ const TransactionSettingNonceOptionButton = styled.button((props) => ({ flexDirection: 'row', width: '100%', marginBottom: props.theme.spacing(20), - paddingLeft: props.theme.spacing(12), - paddingRight: props.theme.spacing(12), justifyContent: 'space-between', })); type TxType = 'STX' | 'BTC' | 'Ordinals'; -interface Props { +type Props = { visible: boolean; fee: string; feePerVByte?: BigNumber; @@ -87,8 +61,9 @@ interface Props { isRestoreFlow?: boolean; nonOrdinalUtxos?: UTXO[]; showFeeSettings: boolean; + nonceSettings?: boolean; setShowFeeSettings: (value: boolean) => void; -} +}; function TransactionSettingAlert({ visible, @@ -104,6 +79,7 @@ function TransactionSettingAlert({ isRestoreFlow, nonOrdinalUtxos, showFeeSettings, + nonceSettings = false, setShowFeeSettings, }: Props) { const { t } = useTranslation('translation'); @@ -139,12 +115,17 @@ function TransactionSettingAlert({ return; } } - setShowNonceSettings(false); setShowFeeSettings(false); setError(''); onApplyClick({ fee: feeInput.toString(), nonce: nonceInput }); }; + const applyClickForNonceStx = () => { + setShowNonceSettings(false); + setError(''); + onApplyClick({ fee: feeInput.toString(), nonce: nonceInput }); + }; + const applyClickForBtc = async () => { const currentFee = new BigNumber(feeInput); if (currentFee.gt(btcBalance ?? 0)) { @@ -202,7 +183,7 @@ function TransactionSettingAlert({ }; const renderContent = () => { - if (showNonceSettings) { + if (showNonceSettings || nonceSettings) { return ; } @@ -254,14 +235,14 @@ function TransactionSettingAlert({ {t('TRANSACTION_SETTING.ADVANCED_SETTING_FEE_OPTION')} - Arrow + Arrow {type === 'STX' && ( {t('TRANSACTION_SETTING.ADVANCED_SETTING_NONCE_OPTION')} - Arrow + Arrow )} @@ -269,58 +250,47 @@ function TransactionSettingAlert({ }; return ( - {renderContent()} - {type === 'STX' && (showFeeSettings || showNonceSettings) && ( - - + diff --git a/src/app/screens/coinDashboard/coinHeader.styled.ts b/src/app/screens/coinDashboard/coinHeader.styled.ts new file mode 100644 index 000000000..f5a0721d6 --- /dev/null +++ b/src/app/screens/coinDashboard/coinHeader.styled.ts @@ -0,0 +1,122 @@ +import styled from 'styled-components'; + +export const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + paddingLeft: props.theme.spacing(8), + paddingRight: props.theme.spacing(8), +})); + +export const RowContainer = styled.div({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', +}); + +export const ProtocolText = styled.p((props) => ({ + ...props.theme.headline_category_s, + fontWeight: 700, + height: 15, + marginTop: props.theme.spacing(3), + textTransform: 'uppercase', + marginLeft: props.theme.spacing(2), + backgroundColor: props.theme.colors.white_400, + padding: '1px 6px 1px', + color: props.theme.colors.elevation0, + borderRadius: props.theme.radius(2), +})); + +export const BalanceInfoContainer = styled.div({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', +}); + +export const BalanceValuesContainer = styled.div({ + display: 'flex', + flexDirection: 'column', +}); + +export const CoinBalanceText = styled.p((props) => ({ + ...props.theme.typography.headline_l, + fontSize: '1.5rem', + color: props.theme.colors.white_0, + textAlign: 'center', + wordBreak: 'break-all', +})); + +export const FiatAmountText = styled.p((props) => ({ + ...props.theme.headline_category_s, + color: props.theme.colors.white_200, + fontSize: '0.875rem', + marginTop: props.theme.spacing(2), + textAlign: 'center', +})); + +export const BalanceTitleText = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_400, + textAlign: 'center', + marginTop: props.theme.spacing(4), +})); + +export const RowButtonContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + marginTop: props.theme.spacing(11), + columnGap: props.theme.space.l, +})); + +export const HeaderSeparator = styled.div((props) => ({ + border: `0.5px solid ${props.theme.colors.white_400}`, + width: '50%', + alignSelf: 'center', + marginTop: props.theme.spacing(8), + marginBottom: props.theme.spacing(8), +})); + +export const StxLockedText = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, +})); + +export const LockedStxContainer = styled.div((props) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + span: { + color: props.theme.colors.white_400, + marginRight: props.theme.spacing(3), + }, + img: { + marginRight: props.theme.spacing(3), + }, +})); + +export const AvailableStxContainer = styled.div((props) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + marginTop: props.theme.spacing(4), + span: { + color: props.theme.colors.white_400, + marginRight: props.theme.spacing(3), + }, +})); + +export const VerifyOrViewContainer = styled.div((props) => ({ + margin: props.theme.spacing(8), + marginTop: props.theme.spacing(16), + marginBottom: props.theme.spacing(20), +})); + +export const VerifyButtonContainer = styled.div((props) => ({ + marginBottom: props.theme.spacing(6), +})); + +export const StacksLockedInfoText = styled.span((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_400, + textAlign: 'left', +})); diff --git a/src/app/screens/coinDashboard/coinHeader.tsx b/src/app/screens/coinDashboard/coinHeader.tsx index 1fc008791..537dd0ece 100644 --- a/src/app/screens/coinDashboard/coinHeader.tsx +++ b/src/app/screens/coinDashboard/coinHeader.tsx @@ -1,6 +1,8 @@ import ArrowDown from '@assets/img/dashboard/arrow_down.svg'; import ArrowUp from '@assets/img/dashboard/arrow_up.svg'; import Buy from '@assets/img/dashboard/black_plus.svg'; +import List from '@assets/img/dashboard/list.svg'; +import ArrowSwap from '@assets/img/icons/ArrowSwap.svg'; import Lock from '@assets/img/transactions/Lock.svg'; import BottomModal from '@components/bottomModal'; import ActionButton from '@components/button'; @@ -9,163 +11,51 @@ import TokenImage from '@components/tokenImage'; import useBtcWalletData from '@hooks/queries/useBtcWalletData'; import useCoinRates from '@hooks/queries/useCoinRates'; import useStxWalletData from '@hooks/queries/useStxWalletData'; +import useHasFeature from '@hooks/useHasFeature'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import { - currencySymbolMap, + FeatureId, FungibleToken, + currencySymbolMap, + getFiatEquivalent, microstacksToStx, - satsToBtc, } from '@secretkeylabs/xverse-core'; import { CurrencyTypes } from '@utils/constants'; import { isInOptions, isLedgerAccount } from '@utils/helper'; -import { getFtBalance, getFtTicker } from '@utils/tokens'; +import { getBalanceAmount, getFtTicker } from '@utils/tokens'; import BigNumber from 'bignumber.js'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; import { useNavigate } from 'react-router-dom'; -import styled from 'styled-components'; - -interface CoinBalanceProps { - coin: CurrencyTypes; +import { + AvailableStxContainer, + BalanceInfoContainer, + BalanceTitleText, + BalanceValuesContainer, + CoinBalanceText, + Container, + FiatAmountText, + HeaderSeparator, + LockedStxContainer, + ProtocolText, + RowButtonContainer, + RowContainer, + StacksLockedInfoText, + StxLockedText, + VerifyButtonContainer, + VerifyOrViewContainer, +} from './coinHeader.styled'; + +type Props = { + currency: CurrencyTypes; fungibleToken?: FungibleToken; -} - -const Container = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), -})); - -const RowContainer = styled.div({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'center', - alignItems: 'center', -}); - -const ProtocolText = styled.p((props) => ({ - ...props.theme.headline_category_s, - fontWeight: 700, - height: 15, - marginTop: props.theme.spacing(3), - textTransform: 'uppercase', - marginLeft: props.theme.spacing(2), - backgroundColor: props.theme.colors.white_400, - padding: '1px 6px 1px', - color: props.theme.colors.elevation0, - borderRadius: props.theme.radius(2), -})); - -const BalanceInfoContainer = styled.div({ - display: 'flex', - flexDirection: 'column', - alignItems: 'center', -}); - -const BalanceValuesContainer = styled.div({ - display: 'flex', - flexDirection: 'column', -}); - -const CoinBalanceText = styled.p((props) => ({ - ...props.theme.headline_l, - fontSize: '1.5rem', - color: props.theme.colors.white_0, - textAlign: 'center', - wordBreak: 'break-all', -})); - -const FiatAmountText = styled.p((props) => ({ - ...props.theme.headline_category_s, - color: props.theme.colors.white_200, - fontSize: '0.875rem', - marginTop: props.theme.spacing(2), - textAlign: 'center', -})); - -const BalanceTitleText = styled.p((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_400, - textAlign: 'center', - marginTop: props.theme.spacing(4), -})); - -const RowButtonContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'center', - marginTop: props.theme.spacing(11), -})); - -const ButtonContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - position: 'relative', - marginRight: props.theme.spacing(12), -})); - -const RecieveButtonContainer = styled.div({ - display: 'flex', - flexDirection: 'column', -}); - -const HeaderSeparator = styled.div((props) => ({ - border: `0.5px solid ${props.theme.colors.white_400}`, - width: '50%', - alignSelf: 'center', - marginTop: props.theme.spacing(8), - marginBottom: props.theme.spacing(8), -})); - -const StxLockedText = styled.p((props) => ({ - ...props.theme.body_medium_m, -})); - -const LockedStxContainer = styled.div((props) => ({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - span: { - color: props.theme.colors.white_400, - marginRight: props.theme.spacing(3), - }, - img: { - marginRight: props.theme.spacing(3), - }, -})); +}; -const AvailableStxContainer = styled.div((props) => ({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - marginTop: props.theme.spacing(4), - span: { - color: props.theme.colors.white_400, - marginRight: props.theme.spacing(3), - }, -})); - -const VerifyOrViewContainer = styled.div((props) => ({ - margin: props.theme.spacing(8), - marginTop: props.theme.spacing(16), - marginBottom: props.theme.spacing(20), -})); - -const VerifyButtonContainer = styled.div((props) => ({ - marginBottom: props.theme.spacing(6), -})); - -const StacksLockedInfoText = styled.span((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_400, - textAlign: 'left', -})); - -export default function CoinHeader(props: CoinBalanceProps) { - const { coin, fungibleToken } = props; - const { fiatCurrency, selectedAccount } = useWalletSelector(); +export default function CoinHeader({ currency, fungibleToken }: Props) { + const selectedAccount = useSelectedAccount(); + const { fiatCurrency, network } = useWalletSelector(); const { data: btcBalance } = useBtcWalletData(); const { data: stxData } = useStxWalletData(); const { btcFiatRate, stxBtcRate } = useCoinRates(); @@ -173,6 +63,15 @@ export default function CoinHeader(props: CoinBalanceProps) { const { t } = useTranslation('translation', { keyPrefix: 'COIN_DASHBOARD_SCREEN' }); const [openReceiveModal, setOpenReceiveModal] = useState(false); const isReceivingAddressesVisible = !isLedgerAccount(selectedAccount); + const showSwaps = + useHasFeature(FeatureId.SWAPS) && + currency === 'STX' && + !isLedgerAccount(selectedAccount) && + network.type !== 'Testnet'; + + const showRunesListing = + (useHasFeature(FeatureId.RUNES_LISTING) || process.env.NODE_ENV === 'development') && + network.type === 'Mainnet'; const handleReceiveModalOpen = () => { setOpenReceiveModal(true); @@ -182,58 +81,18 @@ export default function CoinHeader(props: CoinBalanceProps) { setOpenReceiveModal(false); }; - function getBalanceAmount() { - switch (coin) { - case 'STX': - return microstacksToStx(new BigNumber(stxData?.balance ?? 0)).toString(); - case 'BTC': - return satsToBtc(new BigNumber(btcBalance ?? 0)).toString(); - default: - return fungibleToken ? getFtBalance(fungibleToken) : ''; - } - } - - function getFtFiatEquivalent() { - if (fungibleToken?.tokenFiatRate) { - const balance = new BigNumber(getFtBalance(fungibleToken)); - const rate = new BigNumber(fungibleToken.tokenFiatRate); - return balance.multipliedBy(rate).toFixed(2).toString(); - } - return ''; - } - const getTokenTicker = () => { - if (coin === 'STX' || coin === 'BTC') { - return coin; + if (currency === 'STX' || currency === 'BTC') { + return currency; } - if (coin === 'FT' && fungibleToken) { + if (currency === 'FT' && fungibleToken) { return getFtTicker(fungibleToken); } return ''; }; - function getFiatEquivalent() { - switch (coin) { - case 'STX': - return microstacksToStx(new BigNumber(stxData?.balance ?? '0')) - .multipliedBy(new BigNumber(stxBtcRate)) - .multipliedBy(new BigNumber(btcFiatRate)) - .toFixed(2) - .toString(); - case 'BTC': - return satsToBtc(new BigNumber(btcBalance ?? 0)) - .multipliedBy(new BigNumber(btcFiatRate)) - .toFixed(2) - .toString(); - case 'FT': - return getFtFiatEquivalent(); - default: - return ''; - } - } - const renderStackingBalances = () => { - if (!new BigNumber(stxData?.locked ?? 0).eq(0) && coin === 'STX') { + if (!new BigNumber(stxData?.locked ?? 0).eq(0) && currency === 'STX') { return ( <> @@ -264,78 +123,31 @@ export default function CoinHeader(props: CoinBalanceProps) { }; const goToSendScreen = async () => { - if (isLedgerAccount(selectedAccount) && !isInOptions()) { - switch (coin) { - case 'BTC': - await chrome.tabs.create({ - url: chrome.runtime.getURL('options.html#/send-btc'), - }); - return; - case 'STX': - await chrome.tabs.create({ - url: chrome.runtime.getURL('options.html#/send-stx'), - }); - return; - default: - break; - } + let route = ''; + if (currency === 'BTC' || currency === 'STX') { + route = `/send-${currency}`; + } else { switch (fungibleToken?.protocol) { case 'stacks': - await chrome.tabs.create({ - url: chrome.runtime.getURL( - `options.html#/send-sip10?coinTicker=${fungibleToken?.ticker}`, - ), - }); - return; + route = `/send-sip10?principal=${fungibleToken?.principal}`; + break; case 'brc-20': - // TODO replace with send-brc20-one-step route, when ledger support is ready - await chrome.tabs.create({ - url: chrome.runtime.getURL( - `options.html#/send-brc20?coinTicker=${fungibleToken?.ticker}`, - ), - }); - return; + route = `/send-brc20-one-step?principal=${fungibleToken?.principal}`; + break; case 'runes': - await chrome.tabs.create({ - url: chrome.runtime.getURL(`options.html#/send-rune?coinTicker=${fungibleToken?.name}`), - }); - return; + route = `/send-rune?principal=${fungibleToken?.principal}`; + break; default: break; } } - switch (coin) { - case 'BTC': - case 'STX': - navigate(`/send-${coin}`); - break; - default: - break; - } - switch (fungibleToken?.protocol) { - case 'stacks': - navigate('/send-sip10', { - state: { - fungibleToken, - }, - }); - break; - case 'brc-20': - navigate('/send-brc20-one-step', { - state: { - fungibleToken, - }, - }); - break; - case 'runes': - navigate('/send-rune', { - state: { - fungibleToken, - }, - }); - break; - default: - break; + + if (isLedgerAccount(selectedAccount) && !isInOptions()) { + await chrome.tabs.create({ + url: chrome.runtime.getURL(`options.html#${route}`), + }); + } else { + navigate(route); } }; @@ -343,53 +155,25 @@ export default function CoinHeader(props: CoinBalanceProps) { if (fungibleToken?.name) { return `${fungibleToken.name} ${t('BALANCE')}`; } - if (coin === 'STX') { + + if (!currency) { + return ''; + } + + if (currency === 'STX') { return `Stacks ${t('BALANCE')}`; } - if (coin === 'BTC') { + if (currency === 'BTC') { return `Bitcoin ${t('BALANCE')}`; } - if (coin) { - return `${coin} ${t('BALANCE')}`; - } - return ''; + return `${currency} ${t('BALANCE')}`; }; - const verifyOrViewAddresses = ( - - - { - await chrome.tabs.create({ - url: chrome.runtime.getURL( - `options.html#/verify-ledger?currency=${ - !fungibleToken ? coin : fungibleToken?.protocol === 'stacks' ? 'STX' : 'ORD' - }`, - ), - }); - }} - /> - - { - navigate( - `/receive/${ - !fungibleToken ? coin : fungibleToken?.protocol === 'stacks' ? 'STX' : 'ORD' - }`, - ); - }} - /> - - ); - return ( @@ -405,7 +189,7 @@ export default function CoinHeader(props: CoinBalanceProps) { ( @@ -413,10 +197,16 @@ export default function CoinHeader(props: CoinBalanceProps) { )} /> {value}} /> @@ -424,31 +214,9 @@ export default function CoinHeader(props: CoinBalanceProps) { {renderStackingBalances()} - {/* ENG-4020 - Disable BRC20 Sending on Ledger */} - {!(fungibleToken?.protocol === 'brc-20' && isLedgerAccount(selectedAccount)) && ( - - goToSendScreen()} /> - - )} - {!fungibleToken ? ( + goToSendScreen()} /> + {fungibleToken ? ( <> - - { - if (isReceivingAddressesVisible) { - navigate(`/receive/${coin}`); - } else { - handleReceiveModalOpen(); - } - }} - /> - - navigate(`/buy/${coin}`)} /> - - ) : ( - - + {showRunesListing && fungibleToken.protocol === 'runes' && ( + navigate(`/list-rune/${fungibleToken.principal}`)} + /> + )} + + ) : ( + <> + { + if (isReceivingAddressesVisible) { + navigate(`/receive/${currency}`); + } else { + handleReceiveModalOpen(); + } + }} + /> + {showSwaps && ( + navigate(`/swap?from=${currency}`)} + /> + )} + navigate(`/buy/${currency}`)} + /> + )} - - {verifyOrViewAddresses} + + + { + await chrome.tabs.create({ + url: chrome.runtime.getURL( + `options.html#/verify-ledger?currency=${ + !fungibleToken + ? currency + : fungibleToken?.protocol === 'stacks' + ? 'STX' + : 'ORD' + }`, + ), + }); + }} + /> + + { + navigate( + `/receive/${ + !fungibleToken ? currency : fungibleToken?.protocol === 'stacks' ? 'STX' : 'ORD' + }`, + ); + }} + /> + ); diff --git a/src/app/screens/coinDashboard/index.tsx b/src/app/screens/coinDashboard/index.tsx index da8d3ec25..76754d38e 100644 --- a/src/app/screens/coinDashboard/index.tsx +++ b/src/app/screens/coinDashboard/index.tsx @@ -4,12 +4,14 @@ import OptionsDialog from '@components/optionsDialog/optionsDialog'; import BottomBar from '@components/tabBar'; import TopRow from '@components/topRow'; import { useVisibleBrc20FungibleTokens } from '@hooks/queries/ordinals/useGetBrc20FungibleTokens'; -import { useVisibleRuneFungibleTokens } from '@hooks/queries/runes/useGetRuneFungibleTokens'; +import { useVisibleRuneFungibleTokens } from '@hooks/queries/runes/useRuneFungibleTokensQuery'; import { useVisibleSip10FungibleTokens } from '@hooks/queries/stx/useGetSip10FungibleTokens'; import useBtcWalletData from '@hooks/queries/useBtcWalletData'; import useSpamTokens from '@hooks/queries/useSpamTokens'; +import useResetUserFlow, { broadcastResetUserFlow } from '@hooks/useResetUserFlow'; import useTrackMixPanelPageViewed from '@hooks/useTrackMixPanelPageViewed'; import { Flag } from '@phosphor-icons/react'; +import { FungibleToken } from '@secretkeylabs/xverse-core'; import { setBrc20ManageTokensAction, setRunesManageTokensAction, @@ -22,7 +24,7 @@ import { getExplorerUrl } from '@utils/helper'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; -import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; +import { useParams, useSearchParams } from 'react-router-dom'; import styled from 'styled-components'; import Theme from 'theme'; import CoinHeader from './coinHeader'; @@ -142,7 +144,6 @@ const TokenText = styled(StyledP)` export default function CoinDashboard() { const { t } = useTranslation('translation', { keyPrefix: 'COIN_DASHBOARD_SCREEN' }); - const navigate = useNavigate(); const [showFtContractDetails, setShowFtContractDetails] = useState(false); const [showOptionsDialog, setShowOptionsDialog] = useState(false); const [optionsDialogIndents, setOptionsDialogIndents] = useState< @@ -155,28 +156,32 @@ export default function CoinDashboard() { const { visible: runesCoinsList } = useVisibleRuneFungibleTokens(); const { visible: sip10CoinsList } = useVisibleSip10FungibleTokens(); const { visible: brc20CoinsList } = useVisibleBrc20FungibleTokens(); - const ftKey = searchParams.get('ftKey'); - - useBtcWalletData(); - const handleGoBack = () => { - navigate(-1); - }; - - let selectedFt = sip10CoinsList.find((ft) => ft.principal === ftKey); - let selectedProtocol = 'stacks'; + const ftKey = searchParams.get('ftKey'); + const protocol = searchParams.get('protocol'); + let selectedFt: FungibleToken | undefined; - if (!selectedFt) { - selectedFt = brc20CoinsList.find((ft) => ft.principal === ftKey); - selectedProtocol = 'brc-20'; + if (ftKey && protocol) { + switch (protocol) { + case 'stacks': + selectedFt = sip10CoinsList.find((ft) => ft.principal === ftKey); + break; + case 'brc-20': + selectedFt = brc20CoinsList.find((ft) => ft.principal === ftKey); + break; + case 'runes': + selectedFt = runesCoinsList.find((ft) => ft.principal === ftKey); + break; + default: + selectedFt = undefined; + } } - if (!selectedFt) { - selectedFt = runesCoinsList.find((ft) => ft.principal === ftKey); - selectedProtocol = 'runes'; - } + useResetUserFlow('/coinDashboard'); + useBtcWalletData(); + + const handleGoBack = () => broadcastResetUserFlow(); - const protocol = selectedProtocol || selectedFt?.protocol; useTrackMixPanelPageViewed( protocol ? { @@ -229,13 +234,11 @@ export default function CoinDashboard() { handleGoBack(); return; } - // set the visibility to false const payload = { principal: selectedFt.principal, isEnabled: false, }; - if (protocol === 'runes') { dispatch(setRunesManageTokensAction(payload)); } else if (protocol === 'stacks') { @@ -258,7 +261,7 @@ export default function CoinDashboard() { )} - + {protocol === 'stacks' && ( diff --git a/src/app/screens/settings/connectedAppsAndPermissions/README.md b/src/app/screens/settings/connectedAppsAndPermissions/README.md new file mode 100644 index 000000000..6db6f27d0 --- /dev/null +++ b/src/app/screens/settings/connectedAppsAndPermissions/README.md @@ -0,0 +1,8 @@ +Queries are being persisted using `@tanstack/react-query-persist-client`. The key files involved are, + +- [`src/pages/Popup/index.tsx`](/src/pages/Popup/index.tsx) +- [`src/app/utils/query.ts`](/src/app/utils/query.ts) + +When restoring persited data, more advanced data types like maps and sets are incorrectly restored as empty objects. The permissions store uses maps and sets, causing the Connected Apps and Permissions screen to break when using React Query's restored data. + +To avoid errors, the component should not use stale data from React Query, which can be avoided with an `isFetching` check while the query function runs. The data returned by the query function is provided by permissions-related utitlites that return data in the expected shape. diff --git a/src/app/screens/settings/connectedAppsAndPermissions/index.styles.ts b/src/app/screens/settings/connectedAppsAndPermissions/index.styles.ts new file mode 100644 index 000000000..0cd594b60 --- /dev/null +++ b/src/app/screens/settings/connectedAppsAndPermissions/index.styles.ts @@ -0,0 +1,43 @@ +/* eslint-disable import/prefer-default-export */ + +import styled from 'styled-components'; + +export const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + flex: 1, + marginTop: props.theme.spacing(20), + paddingLeft: props.theme.spacing(8), + paddingRight: props.theme.spacing(8), +})); + +export const ClientHeader = styled('div')({ + display: 'flex', + justifyContent: 'space-between', +}); + +export const ClientName = styled('div')({ + fontWeight: 'bold', +}); + +export const Row = styled('div')({ + paddingLeft: '10px', +}); + +export const PermissionContainer = styled('div')({ + display: 'flex', + flexDirection: 'column', +}); + +export const PermissionTitle = styled('div')({ + fontWeight: 'bold', +}); + +export const PermissionDescription = styled('div')({ + paddingLeft: '10px', +}); + +export const Button = styled('button')({ + borderRadius: '4px', + padding: '0.2em 0.5em', +}); diff --git a/src/app/screens/settings/connectedAppsAndPermissions/index.tsx b/src/app/screens/settings/connectedAppsAndPermissions/index.tsx new file mode 100644 index 000000000..60bef473d --- /dev/null +++ b/src/app/screens/settings/connectedAppsAndPermissions/index.tsx @@ -0,0 +1,83 @@ +import { usePermissionsStore, usePermissionsUtils } from '@components/permissionsManager'; +import * as utils from '@components/permissionsManager/utils'; +import BottomBar from '@components/tabBar'; +import TopRow from '@components/topRow'; +import { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + Button, + ClientHeader, + ClientName, + Container, + PermissionContainer, + PermissionDescription, + PermissionTitle, + Row, +} from './index.styles'; + +function ConnectedAppsAndPermissionsScreen() { + const navigate = useNavigate(); + const { removeClient } = usePermissionsUtils(); + const { store } = usePermissionsStore(); + + const handleBackButtonClick = useCallback(() => { + navigate('/settings'); + }, [navigate]); + + if (!store) { + return null; + } + + return ( + <> + + + {[...store.clients].map((client) => ( +
+ + {client.name} + + + {utils + .getClientPermissions(store.permissions, client.id) + .sort((p1, p2) => p1.resourceId.localeCompare(p2.resourceId)) + .map((p) => ( +
+ {(() => { + const resource = utils.getResource(store.resources, p.resourceId); + + if (!resource) { + return null; + } + + return ( + + + {resource.name} + + {[...p.actions].map((a) => ( +
{a}
+ ))} +
+
+
+ ); + })()} +
+ ))} +
+ ))} +
+ + + ); +} + +export default ConnectedAppsAndPermissionsScreen; diff --git a/src/app/screens/settings/fiatCurrency/currencyRow.tsx b/src/app/screens/settings/fiatCurrency/currencyRow.tsx index a30542240..8bbfde1a3 100644 --- a/src/app/screens/settings/fiatCurrency/currencyRow.tsx +++ b/src/app/screens/settings/fiatCurrency/currencyRow.tsx @@ -3,33 +3,34 @@ import type { SupportedCurrency } from '@secretkeylabs/xverse-core'; import { Currency } from '@utils/currency'; import styled, { useTheme } from 'styled-components'; -interface TitleProps { - color: string; -} - -interface ButtonProps { - border: string; -} - -const Button = styled.button((props) => ({ +const Button = styled.button<{ + $border: string; + $color: string; +}>((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.$color, display: 'flex', - flexDirection: 'row', alignItems: 'center', - background: 'transparent', - justifyContent: 'flex-start', - paddingBottom: props.theme.spacing(10), - paddingTop: props.theme.spacing(10), - borderBottom: props.border, + justifyContent: 'space-between', + padding: `${props.theme.space.m} 0`, + backgroundColor: 'transparent', + borderBottom: props.$border, + transition: 'color 0.1s ease', + '&:hover': { + color: props.theme.colors.white_200, + }, })); -const Text = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.color, - flex: 1, - textAlign: 'left', - marginLeft: props.theme.spacing(6), +const CurrencyWrapper = styled.div((props) => ({ + display: 'flex', + alignItems: 'center', + columnGap: props.theme.space.s, })); +const StyledImg = styled.img({ + width: 21, +}); + interface Props { currency: Currency; isSelected: boolean; @@ -42,16 +43,18 @@ function CurrencyRow({ currency, isSelected, onCurrencySelected, showDivider }: const onClick = () => { onCurrencySelected(currency.name); }; + return ( ); diff --git a/src/app/screens/settings/fiatCurrency/index.tsx b/src/app/screens/settings/fiatCurrency/index.tsx index ba62981f8..b2dfd1e98 100644 --- a/src/app/screens/settings/fiatCurrency/index.tsx +++ b/src/app/screens/settings/fiatCurrency/index.tsx @@ -37,8 +37,9 @@ function FiatCurrencyScreen() { navigate('/settings'); }; - const onClick = (currency: SupportedCurrency) => { + const handleCurrencyClick = (currency: SupportedCurrency) => { dispatch(ChangeFiatCurrencyAction(currency)); + navigate(-1); }; function showDivider(index: number): boolean { @@ -53,7 +54,7 @@ function FiatCurrencyScreen() { diff --git a/src/app/screens/settings/index.tsx b/src/app/screens/settings/index.tsx index 8576c24eb..e632f9ecf 100644 --- a/src/app/screens/settings/index.tsx +++ b/src/app/screens/settings/index.tsx @@ -1,10 +1,12 @@ import ArrowSquareOut from '@assets/img/arrow_square_out.svg'; import XverseLogo from '@assets/img/full_logo_horizontal.svg'; import ArrowIcon from '@assets/img/settings/arrow.svg'; +import RequestsRoutes from '@common/utils/route-urls'; import PasswordInput from '@components/passwordInput'; import BottomBar from '@components/tabBar'; import useChromeLocalStorage from '@hooks/useChromeLocalStorage'; import useSeedVault from '@hooks/useSeedVault'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletReducer from '@hooks/useWalletReducer'; import useWalletSelector from '@hooks/useWalletSelector'; import { @@ -15,6 +17,7 @@ import { import { chromeLocalStorageKeys } from '@utils/chromeLocalStorage'; import { PRIVACY_POLICY_LINK, SUPPORT_LINK, TERMS_LINK } from '@utils/constants'; import { getLockCountdownLabel, isInOptions, isLedgerAccount } from '@utils/helper'; +import RoutePaths from 'app/routes/paths'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; @@ -68,8 +71,8 @@ function Setting() { hasActivatedOrdinalsKey, hasActivatedRareSatsKey, hasActivatedRBFKey, - selectedAccount, } = useWalletSelector(); + const selectedAccount = useSelectedAccount(); const [isPriorityWallet, setIsPriorityWallet] = useChromeLocalStorage( chromeLocalStorageKeys.isPriorityWallet, true, @@ -107,6 +110,10 @@ function Setting() { navigate('/backup-wallet'); }; + const openConnectedAppsAndPermissionsScreen = () => { + navigate(RoutePaths.ConnectedAppsAndPermissions); + }; + const switchIsPriorityWallet = () => { setIsPriorityWallet(!isPriorityWallet); }; @@ -228,6 +235,14 @@ function Setting() { icon={ArrowIcon} showDivider /> + {process.env.NODE_ENV !== 'production' && ( + + )} >; function SignBatchPsbtRequest() { - const { btcAddress, ordinalsAddress, selectedAccount, network } = useWalletSelector(); + const selectedAccount = useSelectedAccount(); + const { network } = useWalletSelector(); const navigate = useNavigate(); const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); const { payload, confirmSignPsbt, cancelSignPsbt, requestToken } = useSignBatchPsbtTx(); @@ -152,7 +155,7 @@ function SignBatchPsbtRequest() { const [inscriptionToShow, setInscriptionToShow] = useState< btcTransaction.IOInscription | undefined >(undefined); - const hasRunesSupport = useHasFeature('RUNES_SUPPORT'); + const hasRunesSupport = useHasFeature(FeatureId.RUNES_SUPPORT); useTrackMixPanelPageViewed(); const [parsedPsbts, setParsedPsbts] = useState< @@ -198,7 +201,10 @@ function SignBatchPsbtRequest() { }, [payload.psbts.length, handlePsbtParsing]); const checkAddressMismatch = (input) => { - if (input.address !== btcAddress && input.address !== ordinalsAddress) { + if ( + input.address !== selectedAccount.btcAddress && + input.address !== selectedAccount.ordinalsAddress + ) { navigate('/tx-status', { state: { txid: '', @@ -258,7 +264,7 @@ function SignBatchPsbtRequest() { trackMixPanel(AnalyticsEvents.TransactionConfirmed, { protocol: 'bitcoin', action: 'sign-psbt', - wallet_type: selectedAccount?.accountType || 'software', + wallet_type: selectedAccount.accountType || 'software', batch: payload.psbts.length, }); @@ -310,8 +316,8 @@ function SignBatchPsbtRequest() { getNetAmount({ inputs: psbt.summary.inputs, outputs: psbt.summary.outputs, - btcAddress, - ordinalsAddress, + btcAddress: selectedAccount.btcAddress, + ordinalsAddress: selectedAccount.ordinalsAddress, }), ), ) diff --git a/src/app/screens/signMessageRequest/index.styled.ts b/src/app/screens/signMessageRequest/index.styled.ts new file mode 100644 index 000000000..2b2b349ad --- /dev/null +++ b/src/app/screens/signMessageRequest/index.styled.ts @@ -0,0 +1,80 @@ +import styled from 'styled-components'; + +export const MainContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + padding: `0 ${props.theme.space.m}`, +})); + +export const RequestType = styled.h1((props) => ({ + ...props.theme.typography.headline_s, + marginTop: props.theme.space.s, + color: props.theme.colors.white_0, + textAlign: 'left', + marginBottom: props.theme.space.l, +})); + +export const MessageHash = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, + textAlign: 'left', + lineHeight: 1.6, + wordWrap: 'break-word', + color: props.theme.colors.white_0, + marginBottom: props.theme.space.xs, +})); + +export const SigningAddressContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + background: props.theme.colors.elevation1, + borderRadius: 12, + padding: `${props.theme.space.s} ${props.theme.space.m}`, + marginBottom: props.theme.space.s, + flex: 1, +})); + +export const SigningAddressTitle = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, + wordWrap: 'break-word', + color: props.theme.colors.white_200, + marginBottom: props.theme.space.xs, +})); + +export const SigningAddress = styled.div({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', +}); + +export const SigningAddressType = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, + textAlign: 'left', + wordWrap: 'break-word', + color: props.theme.colors.white_0, + marginBottom: props.theme.space.xs, +})); + +export const SigningAddressValue = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, + textAlign: 'left', + wordWrap: 'break-word', + color: props.theme.colors.white_0, + marginBottom: props.theme.space.xs, +})); + +export const ActionDisclaimer = styled.p((props) => ({ + ...props.theme.typography.body_m, + color: props.theme.colors.white_400, + marginTop: props.theme.space.xs, + marginBottom: props.theme.space.m, +})); + +export const SuccessActionsContainer = styled.div((props) => ({ + width: '100%', + display: 'flex', + flexDirection: 'column', + gap: props.theme.space.s, + padding: `0 ${props.theme.space.m}`, + marginBottom: props.theme.space.xxl, + marginTop: props.theme.space.xxl, +})); diff --git a/src/app/screens/signMessageRequest/index.tsx b/src/app/screens/signMessageRequest/index.tsx index 603eb54c5..a6c800aac 100644 --- a/src/app/screens/signMessageRequest/index.tsx +++ b/src/app/screens/signMessageRequest/index.tsx @@ -9,6 +9,7 @@ import ConfirmScreen from '@components/confirmScreen'; import InfoContainer from '@components/infoContainer'; import LedgerConnectionView from '@components/ledger/connectLedgerView'; import RequestError from '@components/requests/requestError'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import Transport from '@ledgerhq/hw-transport-webusb'; import { Return, RpcErrorCode } from '@sats-connect/core'; @@ -21,93 +22,24 @@ import { getTruncatedAddress, isHardwareAccount } from '@utils/helper'; import { handleBip322LedgerMessageSigning } from '@utils/ledger'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; +import { + ActionDisclaimer, + MainContainer, + MessageHash, + RequestType, + SigningAddress, + SigningAddressContainer, + SigningAddressTitle, + SigningAddressType, + SigningAddressValue, + SuccessActionsContainer, +} from './index.styled'; import { useSignMessageRequest, useSignMessageValidation } from './useSignMessageRequest'; -const MainContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), -})); - -const RequestType = styled.h1((props) => ({ - ...props.theme.typography.headline_s, - marginTop: props.theme.spacing(11), - color: props.theme.colors.white_0, - textAlign: 'left', - marginBottom: props.theme.spacing(12), -})); - -const MessageHash = styled.p((props) => ({ - ...props.theme.typography.body_medium_m, - textAlign: 'left', - lineHeight: 1.6, - wordWrap: 'break-word', - color: props.theme.colors.white_0, - marginBottom: props.theme.spacing(4), -})); - -const SigningAddressContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - background: props.theme.colors.elevation1, - borderRadius: 12, - padding: '12px 16px', - marginBottom: props.theme.spacing(6), - flex: 1, -})); - -const SigningAddressTitle = styled.p((props) => ({ - ...props.theme.typography.body_medium_m, - wordWrap: 'break-word', - color: props.theme.colors.white_200, - marginBottom: props.theme.spacing(4), -})); - -const SigningAddress = styled.div({ - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', -}); - -const SigningAddressType = styled.p((props) => ({ - ...props.theme.typography.body_medium_m, - textAlign: 'left', - wordWrap: 'break-word', - color: props.theme.colors.white_0, - marginBottom: props.theme.spacing(4), -})); - -const SigningAddressValue = styled.p((props) => ({ - ...props.theme.typography.body_medium_m, - textAlign: 'left', - wordWrap: 'break-word', - color: props.theme.colors.white_0, - marginBottom: props.theme.spacing(4), -})); - -const ActionDisclaimer = styled.p((props) => ({ - ...props.theme.typography.body_m, - color: props.theme.colors.white_400, - marginTop: props.theme.spacing(4), - marginBottom: props.theme.spacing(8), -})); - -const SuccessActionsContainer = styled.div((props) => ({ - width: '100%', - display: 'flex', - flexDirection: 'column', - gap: props.theme.spacing(6), - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), - marginBottom: props.theme.spacing(20), - marginTop: props.theme.spacing(20), -})); - function SignMessageRequest() { const { t } = useTranslation('translation'); - const { accountsList, selectedAccount, network } = useWalletSelector(); + const selectedAccount = useSelectedAccount(); + const { accountsList, network } = useWalletSelector(); const { payload, tabId, requestToken, confirmSignMessage, requestId } = useSignMessageRequest(); const { validationError } = useSignMessageValidation(payload); @@ -282,7 +214,11 @@ function SignMessageRequest() { setCurrentStepIndex(0); }; - return !validationError ? ( + if (validationError) { + return ; + } + + return ( <>
- ) : ( - ); } diff --git a/src/app/screens/signMessageRequest/useSignMessageRequest.ts b/src/app/screens/signMessageRequest/useSignMessageRequest.ts index a9e810342..b6d45ef34 100644 --- a/src/app/screens/signMessageRequest/useSignMessageRequest.ts +++ b/src/app/screens/signMessageRequest/useSignMessageRequest.ts @@ -1,11 +1,12 @@ import useSeedVault from '@hooks/useSeedVault'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletReducer from '@hooks/useWalletReducer'; import useWalletSelector from '@hooks/useWalletSelector'; import { BitcoinNetworkType, SignMessageOptions, SignMessagePayload } from '@sats-connect/core'; import { SettingsNetwork, signBip322Message } from '@secretkeylabs/xverse-core'; import { isHardwareAccount } from '@utils/helper'; import { decodeToken } from 'jsontokens'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; @@ -30,14 +31,9 @@ const useSignMessageRequestParams = (network: SettingsNetwork) => { const rpcPayload: SignMessagePayload = { message, address, - network: - network.type === 'Mainnet' - ? { - type: BitcoinNetworkType.Mainnet, - } - : { - type: BitcoinNetworkType.Testnet, - }, + network: { + type: BitcoinNetworkType[network.type], + }, }; return { payload: rpcPayload, @@ -56,7 +52,8 @@ type ValidationError = { export const useSignMessageValidation = (requestPayload: SignMessagePayload | undefined) => { const [validationError, setValidationError] = useState(null); const { t } = useTranslation('translation', { keyPrefix: 'REQUEST_ERRORS' }); - const { accountsList, selectedAccount, network } = useWalletSelector(); + const selectedAccount = useSelectedAccount(); + const { accountsList, network } = useWalletSelector(); const { switchAccount } = useWalletReducer(); const checkAddressAvailability = () => { @@ -128,18 +125,3 @@ export const useSignMessageRequest = () => { confirmSignMessage, }; }; - -export function useSignBip322Message(message: string, address: string) { - const { accountsList, network } = useWalletSelector(); - const { getSeed } = useSeedVault(); - return useCallback(async () => { - const seedPhrase = await getSeed(); - return signBip322Message({ - accounts: accountsList, - message, - signatureAddress: address, - seedPhrase, - network: network.type, - }); - }, []); -} diff --git a/src/app/screens/signMessageRequestInApp/index.tsx b/src/app/screens/signMessageRequestInApp/index.tsx new file mode 100644 index 000000000..b8fa2ffbc --- /dev/null +++ b/src/app/screens/signMessageRequestInApp/index.tsx @@ -0,0 +1,294 @@ +import ledgerConnectDefaultIcon from '@assets/img/ledger/ledger_connect_default.svg'; +import ledgerConnectBtcIcon from '@assets/img/ledger/ledger_import_connect_btc.svg'; +import { delay } from '@common/utils/ledger'; +import ConfirmScreen from '@components/confirmScreen'; +import InfoContainer from '@components/infoContainer'; +import LedgerConnectionView from '@components/ledger/connectLedgerView'; +import TopRow from '@components/topRow'; +import useRunesApi from '@hooks/apiClients/useRunesApi'; +import useSeedVault from '@hooks/useSeedVault'; +import useSelectedAccount from '@hooks/useSelectedAccount'; +import useWalletSelector from '@hooks/useWalletSelector'; +import Transport from '@ledgerhq/hw-transport-webusb'; +import CollapsableContainer from '@screens/signatureRequest/collapsableContainer'; +import SignatureRequestMessage from '@screens/signatureRequest/signatureRequestMessage'; +import { bip0322Hash, signBip322Message } from '@secretkeylabs/xverse-core'; +import Button from '@ui-library/button'; +import Sheet from '@ui-library/sheet'; +import { getTruncatedAddress, isHardwareAccount } from '@utils/helper'; +import { handleBip322LedgerMessageSigning } from '@utils/ledger'; +import { useEffect, useState } from 'react'; +import toast from 'react-hot-toast'; +import { useTranslation } from 'react-i18next'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { + ActionDisclaimer, + MainContainer, + MessageHash, + RequestType, + SigningAddress, + SigningAddressContainer, + SigningAddressTitle, + SigningAddressType, + SigningAddressValue, + SuccessActionsContainer, +} from '../signMessageRequest/index.styled'; + +function SignMessageRequestInApp() { + const { t } = useTranslation('translation'); + const { accountsList, network } = useWalletSelector(); + const selectedAccount = useSelectedAccount(); + const location = useLocation(); + const { payload } = location.state?.requestPayload || {}; + const navigate = useNavigate(); + const { getSeed } = useSeedVault(); + const runesApi = useRunesApi(); + + const [addressType, setAddressType] = useState(''); + const [isSigning, setIsSigning] = useState(false); + + // Ledger state + const [isModalVisible, setIsModalVisible] = useState(false); + const [isConnectSuccess, setIsConnectSuccess] = useState(false); + const [isConnectFailed, setIsConnectFailed] = useState(false); + const [isButtonDisabled, setIsButtonDisabled] = useState(false); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isTxRejected, setIsTxRejected] = useState(false); + const [isTxInvalid, setIsTxInvalid] = useState(false); + + useEffect(() => { + const checkAddressAvailability = () => { + const account = accountsList.filter((acc) => { + if (acc.btcAddress === payload.address) { + setAddressType(t('SIGNATURE_REQUEST.SIGNING_ADDRESS_SEGWIT')); + return true; + } + if (acc.ordinalsAddress === payload?.address) { + setAddressType(t('SIGNATURE_REQUEST.SIGNING_ADDRESS_TAPROOT')); + return true; + } + return false; + }); + return isHardwareAccount(selectedAccount) ? account[0] || selectedAccount : account[0]; + }; + checkAddressAvailability(); + }, [accountsList, payload, selectedAccount, t]); + + const getConfirmationError = (type: 'title' | 'subtitle') => { + if (type === 'title') { + if (isTxRejected) { + return t('SIGNATURE_REQUEST.LEDGER.CONFIRM.DENIED.ERROR_TITLE'); + } + + if (isTxInvalid) { + return t('SIGNATURE_REQUEST.LEDGER.CONFIRM.INVALID.ERROR_TITLE'); + } + + return t('SIGNATURE_REQUEST.LEDGER.CONFIRM.ERROR_TITLE'); + } + + if (isTxRejected) { + return t('SIGNATURE_REQUEST.LEDGER.CONFIRM.DENIED.ERROR_SUBTITLE'); + } + + if (isTxInvalid) { + return t('SIGNATURE_REQUEST.LEDGER.CONFIRM.INVALID.ERROR_SUBTITLE'); + } + + return t('SIGNATURE_REQUEST.LEDGER.CONFIRM.ERROR_SUBTITLE'); + }; + + const handleCancelClick = () => { + navigate(`/coinDashboard/FT?ftKey=${payload.selectedRuneId}&protocol=runes`); + }; + + const handleRetry = async () => { + setIsTxRejected(false); + setIsTxInvalid(false); + setIsConnectFailed(false); + setIsConnectSuccess(false); + setCurrentStepIndex(0); + }; + + const handleGoBack = () => navigate(-1); + + const handleConnectAndConfirm = async () => { + if (!selectedAccount) { + return; + } + setIsButtonDisabled(true); + + const transport = await Transport.create(); + + if (!transport) { + setIsConnectSuccess(false); + setIsConnectFailed(true); + setIsButtonDisabled(false); + return; + } + + setIsConnectSuccess(true); + await delay(1500); + setCurrentStepIndex(1); + + try { + const bip322signature = await handleBip322LedgerMessageSigning({ + transport, + addressIndex: selectedAccount.deviceAccountIndex, + address: payload.address, + networkType: network.type, + message: payload.message, + }); + + await runesApi.submitCancelRunesSellOrder({ + orderIds: payload.orderIds, + makerPublicKey: selectedAccount?.ordinalsPublicKey!, + makerAddress: selectedAccount?.ordinalsAddress!, + token: payload.token, + signature: bip322signature, + }); + + handleGoBack(); + toast(`${t('SIGNATURE_REQUEST.UNLISTED_SUCCESS')}`); + } catch (e: any) { + if (e.name === 'LockedDeviceError') { + setCurrentStepIndex(0); + setIsConnectSuccess(false); + setIsConnectFailed(true); + } else if (e.statusCode === 28160) { + setIsConnectSuccess(false); + setIsConnectFailed(true); + } else if (e.cause === 27012) { + setIsTxInvalid(true); + } else { + setIsTxRejected(true); + } + } finally { + await transport.close(); + setIsButtonDisabled(false); + } + }; + + const confirmSignMessage = async () => { + const seedPhrase = await getSeed(); + return signBip322Message({ + accounts: accountsList, + message: payload.message, + signatureAddress: payload.address, + seedPhrase, + network: network.type, + }); + }; + + const confirmCallback = async () => { + if (!payload) return; + try { + setIsSigning(true); + if (isHardwareAccount(selectedAccount)) { + setIsModalVisible(true); + return; + } + const bip322signature = await confirmSignMessage(); + + await runesApi.submitCancelRunesSellOrder({ + orderIds: payload.orderIds, + makerPublicKey: selectedAccount?.ordinalsPublicKey!, + makerAddress: selectedAccount?.ordinalsAddress!, + token: payload.token, + signature: bip322signature, + }); + + handleGoBack(); + toast(`${t('SIGNATURE_REQUEST.UNLISTED_SUCCESS')}`); + } catch (err) { + toast(`${t('SIGNATURE_REQUEST.UNLISTED_ERROR')}`); + } finally { + setIsSigning(false); + } + }; + + return ( + <> + + + + {t('SIGNATURE_REQUEST.TITLE')} + + + {bip0322Hash(payload.message)} + + + + {t('SIGNATURE_REQUEST.SIGNING_ADDRESS_TITLE')} + + + {addressType && {addressType}} + + {getTruncatedAddress(payload.address, 6)} + + + + {t('SIGNATURE_REQUEST.ACTION_DISCLAIMER')} + + + + setIsModalVisible(false)}> + {currentStepIndex === 0 && ( + + )} + {currentStepIndex === 1 && ( + + )} + + + ); @@ -234,6 +237,28 @@ function TransactionStatus() { ); + if (runeListed) { + return ( + + {renderTransactionSuccessStatus} + +