diff --git a/.eslintignore b/.eslintignore index ef65f761..6cad02c6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,7 +5,6 @@ src/lib/big.js/ src/lib/rlottie/rlottie-wasm.js src/lib/aes-js/index.js src/lib/noble-ed25519/index.js -src/lib/webextension-polyfill/browser.js jest.config.js playwright.config.ts postcss.config.js diff --git a/.github/workflows/electron-release.yml b/.github/workflows/package-and-publish.yml similarity index 59% rename from .github/workflows/electron-release.yml rename to .github/workflows/package-and-publish.yml index aef4121b..17461076 100644 --- a/.github/workflows/electron-release.yml +++ b/.github/workflows/package-and-publish.yml @@ -1,16 +1,26 @@ -name: Electron release +# Terms: +# "build" - Compile web project using webpack. +# "package" - Produce a distributive package for a specific platform as a workflow artifact. +# "publish" - Send a package to corresponding store and GitHub release page. +# "release" - build + package + publish +# +# Jobs in this workflow will skip the "publish" step when `PUBLISH_REPO` is not set. + +name: Package and publish on: workflow_dispatch: push: - branches: master + branches: + - master env: APP_NAME: MyTonWallet jobs: - release: - runs-on: macOS-latest + electron-release: + name: Build, package and publish Electron + runs-on: macos-latest steps: - name: Checkout uses: actions/checkout@v3 @@ -33,7 +43,7 @@ jobs: if: steps.npm-cache.outputs.cache-hit != 'true' run: npm ci - - name: Import MacOS Signing Certificate + - name: Import MacOS signing certificate env: APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} @@ -48,7 +58,7 @@ jobs: security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k actions $KEY_CHAIN security find-identity -v -p codesigning $KEY_CHAIN - - name: Build and release + - name: Build, package and publish env: TONHTTPAPI_MAINNET_URL: ${{ vars.TONHTTPAPI_MAINNET_URL }} TONAPIIO_MAINNET_URL: ${{ vars.TONAPIIO_MAINNET_URL }} @@ -65,43 +75,45 @@ jobs: GH_TOKEN: ${{ secrets.GH_TOKEN }} run: | if [ -z "$PUBLISH_REPO" ]; then - npm run electron:staging + npm run electron:package:staging else - npm run deploy:electron + npm run electron:release:production fi - - uses: actions/upload-artifact@v3 + - name: Upload macOS x64 artifact + uses: actions/upload-artifact@v3 with: name: ${{ env.APP_NAME }}-x64.dmg path: dist-electron/${{ env.APP_NAME }}-x64.dmg - - uses: actions/upload-artifact@v3 + - name: Upload macOS arm64 artifact + uses: actions/upload-artifact@v3 with: name: ${{ env.APP_NAME }}-arm64.dmg path: dist-electron/${{ env.APP_NAME }}-arm64.dmg - - uses: actions/upload-artifact@v3 + - name: Upload Linux artifact + uses: actions/upload-artifact@v3 with: name: ${{ env.APP_NAME }}-x86_64.AppImage path: dist-electron/${{ env.APP_NAME }}-x86_64.AppImage - - uses: actions/upload-artifact@v3 + - name: Upload Windows artifact + uses: actions/upload-artifact@v3 with: name: ${{ env.APP_NAME }}-x64.exe path: dist-electron/${{ env.APP_NAME }}-x64.exe - windowsSigning: - needs: release + electron-sign-for-windows: + name: Sign and re-publish Windows package + needs: electron-release runs-on: windows-latest if: vars.PUBLISH_REPO != '' env: GH_TOKEN: ${{ secrets.GH_TOKEN }} PUBLISH_REPO: ${{ vars.PUBLISH_REPO }} steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Setup Certificate + - name: Setup certificate shell: bash run: echo "${{ secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12 @@ -137,7 +149,7 @@ jobs: with: name: ${{ env.FILE_NAME }} - - name: Signing package + - name: Sign package env: KEYPAIR_ALIAS: ${{ secrets.KEYPAIR_ALIAS }} FILE_PATH: ${{ steps.download-artifact.outputs.download-path }} @@ -183,3 +195,106 @@ jobs: shell: bash run: | curl -X PATCH -H "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/$PUBLISH_REPO/releases/$RELEASE_ID" -d '{"draft": false}' + + extensions-package: + name: Build and package extensions + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Use Node.js 18.x + uses: actions/setup-node@v3 + with: + node-version: 18.x + + - name: Cache node modules + id: npm-cache + uses: actions/cache@v3 + with: + path: node_modules + key: ${{ runner.os }}-build-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build- + + - name: Install dependencies + if: steps.npm-cache.outputs.cache-hit != 'true' + run: npm ci + + - name: Build and package + env: + TONHTTPAPI_MAINNET_URL: ${{ vars.TONHTTPAPI_MAINNET_URL }} + TONAPIIO_MAINNET_URL: ${{ vars.TONAPIIO_MAINNET_URL }} + TONHTTPAPI_TESTNET_URL: ${{ vars.TONHTTPAPI_TESTNET_URL }} + TONAPIIO_TESTNET_URL: ${{ vars.TONAPIIO_TESTNET_URL }} + PROXY_HOSTS: ${{ vars.PROXY_HOSTS }} + STAKING_POOLS: ${{ vars.STAKING_POOLS }} + + PUBLISH_REPO: ${{ vars.PUBLISH_REPO }} + run: | + if [ -z "$PUBLISH_REPO" ]; then + npm run extension-chrome:package:staging + npm run extension-firefox:package:staging + else + npm run extension-chrome:package:production + npm run extension-firefox:package:production + fi + + - name: Upload Chrome extension artifact + uses: actions/upload-artifact@v3 + with: + name: ${{ env.APP_NAME }}-chrome.zip + path: ${{ env.APP_NAME }}-chrome.zip + + - name: Upload Firefox extension artifact + uses: actions/upload-artifact@v3 + with: + name: ${{ env.APP_NAME }}-firefox.zip + path: ${{ env.APP_NAME }}-firefox.zip + + extensions-publish: + name: Publish extensions + needs: extensions-package + runs-on: ubuntu-latest + if: vars.PUBLISH_REPO != '' + steps: + - name: Use Node.js 18.x + uses: actions/setup-node@v3 + with: + node-version: 18.x + + - name: Set environment variables + id: variables + shell: bash + run: | + echo "CHROME_FILE_NAME=${{ env.APP_NAME }}-chrome.zip" >> "$GITHUB_ENV" + echo "FIREFOX_FILE_NAME=${{ env.APP_NAME }}-firefox.zip" >> "$GITHUB_ENV" + + - name: Download Chrome extension artifact + uses: actions/download-artifact@v3 + with: + name: ${{ env.CHROME_FILE_NAME }} + + - name: Publish to Chrome store + env: + EXTENSION_ID: ${{ vars.CHROME_EXTENSION_ID }} + CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} + CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} + REFRESH_TOKEN: ${{ secrets.GOOGLE_REFRESH_TOKEN }} + run: npx --yes chrome-webstore-upload-cli@2 upload --auto-publish --source ${{ env.CHROME_FILE_NAME }} + + - name: Download Firefox extension artifact + uses: actions/download-artifact@v3 + with: + name: ${{ env.FIREFOX_FILE_NAME }} + + - name: Publish to Firefox addons + env: + WEB_EXT_API_KEY: ${{ secrets.FIREFOX_API_KEY }} + WEB_EXT_API_SECRET: ${{ secrets.FIREFOX_API_SECRET }} + if: ${{ env.WEB_EXT_API_KEY != '' }} + run: | + UNZIP_DIR=/tmp/${{ env.APP_NAME }}-firefox + mkdir $UNZIP_DIR + unzip ${{ env.FIREFOX_FILE_NAME }} -d $UNZIP_DIR + npx --yes web-ext-submit@7 --source-dir=$UNZIP_DIR/dist diff --git a/.github/workflows/statoscope-comment.jora b/.github/workflows/statoscope-comment.jora new file mode 100644 index 00000000..1a8a0cad --- /dev/null +++ b/.github/workflows/statoscope-comment.jora @@ -0,0 +1,43 @@ +// Original: https://github.com/statoscope/statoscope.tech/blob/main/.github/workflows/statoscope-comment.jora + +// variables +$after: resolveInputFile(); +$inputCompilation: $after.compilations.pick(); +$inputInitialCompilation: $after.compilations.chunks.filter(); +$before: resolveReferenceFile(); +$referenceCompilation: $before.compilations.pick(); +$referenceInitialCompilation: $before.compilations.chunks.filter(); + +// helpers +$getSizeByChunks: => files.(getAssetSize($$, true)).reduce(=> size + $$, 0); + +// output +{ + initialSize: { + $after: $inputInitialCompilation.$getSizeByChunks($inputCompilation.hash); + $before: $referenceInitialCompilation.$getSizeByChunks($referenceCompilation.hash); + $after, + $before, + diff: { + value: $after - $before, + percent: $after.percentFrom($before, 2), + formatted: { type: 'size', a: $before, b: $after } | formatDiff() + ` (${b.percentFrom(a, 2)}%)`, + } + }, + bundleSize: { + $after: $inputCompilation.chunks.$getSizeByChunks($inputCompilation.hash); + $before: $referenceCompilation.chunks.$getSizeByChunks($referenceCompilation.hash); + $after, + $before, + diff: { + value: $after - $before, + percent: $after.percentFrom($before, 2), + formatted: { type: 'size', a: $before, b: $after } | formatDiff() + ` (${b.percentFrom(a, 2)}%)`, + } + }, + validation: { + $messages: resolveInputFile().compilations.[hash].(hash.validation_getItems()); + $messages, + total: $messages.size() + } +} diff --git a/.github/workflows/statoscope-comment.js b/.github/workflows/statoscope-comment.js new file mode 100644 index 00000000..4965c849 --- /dev/null +++ b/.github/workflows/statoscope-comment.js @@ -0,0 +1,10 @@ +module.exports = ({ initialSize, bundleSize, validation, prNumber}) => `**📦 Statoscope quick diff with master branch:** + +**⚖️ Initial size:** ${initialSize.diff.percent > 1.5 ? '🔴' : (initialSize.diff.percent < 0 ? '🟢' : '⚪️')} ${initialSize.diff.percent > 0 ? '+' : ''}${initialSize.diff.formatted} + +**⚖️ Total bundle size:** ${bundleSize.diff.percent > 1.5 ? '🔴' : (bundleSize.diff.percent < 0 ? '🟢' : '⚪️')} ${bundleSize.diff.percent > 0 ? '+' : ''}${bundleSize.diff.formatted} + +**🕵️ Validation errors:** ${validation.total > 0 ? validation.total : '✅'} + +Full Statoscope report could be found [here️](https://deploy-preview-${prNumber}--mytonwallet-e5kxpi8iga.netlify.app/report.html) +`; diff --git a/.github/workflows/statoscope.yml b/.github/workflows/statoscope.yml new file mode 100644 index 00000000..b90d16e6 --- /dev/null +++ b/.github/workflows/statoscope.yml @@ -0,0 +1,82 @@ +name: Statoscope Bundle Analytics + +on: + pull_request: + branches: + - '*' + +jobs: + install: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x] + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + persist-credentials: false + - name: Reconfigure git to use HTTPS authentication + uses: GuillaumeFalourd/SSH-to-HTTPS@v1 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + - name: Install + run: npm ci + - name: Cache results + uses: actions/cache@v3 + id: cache-results + with: + path: | + node_modules + key: ${{ github.sha }} + statoscope: + needs: + - install + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x] + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + persist-credentials: false + - name: Reconfigure git to use HTTPS authentication + uses: GuillaumeFalourd/SSH-to-HTTPS@v1 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - name: Restore cache + uses: actions/cache@v3 + id: restore-cache + with: + path: | + node_modules + key: ${{ github.sha }} + - name: Build + run: npm run build:production; cp public/build-stats.json input.json + - name: Download reference stats + uses: dawidd6/action-download-artifact@v2 + with: + workflow: upload-main-stats.yml + workflow_conclusion: success + name: main-stats + path: ./ + continue-on-error: true + - name: Validate + run: npm run statoscope:validate-diff + - name: Query stats + if: "github.event_name == 'pull_request'" + run: cat .github/workflows/statoscope-comment.jora | npx --no-install @statoscope/cli query --input input.json --input reference.json > result.json + - name: Hide bot comments + uses: int128/hide-comment-action@v1 + - name: Comment PR + if: "github.event_name == 'pull_request'" + uses: actions/github-script@v6.0.0 + with: + script: | + const createStatoscopeComment = require('./dev/createStatoscopeComment'); + await createStatoscopeComment({ github, context, core }) diff --git a/.github/workflows/upload-main-stats.yml b/.github/workflows/upload-main-stats.yml new file mode 100644 index 00000000..967b3f66 --- /dev/null +++ b/.github/workflows/upload-main-stats.yml @@ -0,0 +1,31 @@ +name: Upload main stats + +on: + push: + branches: [ master ] + +jobs: + build_and_upload: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x] + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + persist-credentials: false + - name: Reconfigure git to use HTTPS authentication + uses: GuillaumeFalourd/SSH-to-HTTPS@v1 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + - name: Install + run: npm ci + - name: Build + run: npm run build:production; cp ./public/build-stats.json ./reference.json + - uses: actions/upload-artifact@v2 + with: + name: main-stats + path: ./reference.json diff --git a/.gitignore b/.gitignore index abb30d47..0982c478 100644 --- a/.gitignore +++ b/.gitignore @@ -20,5 +20,3 @@ trash/ coverage/ src/i18n/en.json notarization-error.log -.github/workflows/* -!.github/workflows/electron-release.yml diff --git a/package-lock.json b/package-lock.json index 3229696f..1c87e438 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mytonwallet", - "version": "1.15.1", + "version": "1.15.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mytonwallet", - "version": "1.15.1", + "version": "1.15.2", "license": "GPL-3.0-or-later", "dependencies": { "@ledgerhq/hw-transport-webhid": "^6.27.12", @@ -14,6 +14,7 @@ "buffer": "^6.0.3", "idb-keyval": "^6.2.0", "pako": "^2.1.0", + "qr-code-styling": "github:troman29/qr-code-styling#c00d0", "qrcode-generator": "^1.4.4", "ton": "^13.4.1", "ton-core": "^0.49.0", @@ -21,7 +22,8 @@ "tonweb": "github:troman29/tonweb#3d5e2f3", "tonweb-mnemonic": "^1.0.1", "tweetnacl": "^1.0.3", - "v8-compile-cache": "^2.3.0" + "v8-compile-cache": "^2.3.0", + "webextension-polyfill": "^0.10.0" }, "devDependencies": { "@babel/core": "^7.21.3", @@ -11595,6 +11597,14 @@ "dev": true, "license": "MIT" }, + "node_modules/growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/gzip-size": { "version": "6.0.0", "dev": true, @@ -14651,9 +14661,9 @@ } }, "node_modules/keyv": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", - "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", + "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", "dev": true, "dependencies": { "json-buffer": "3.0.1" @@ -16152,6 +16162,61 @@ "dev": true, "license": "MIT" }, + "node_modules/node-notifier": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-10.0.1.tgz", + "integrity": "sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "growly": "^1.3.0", + "is-wsl": "^2.2.0", + "semver": "^7.3.5", + "shellwords": "^0.1.1", + "uuid": "^8.3.2", + "which": "^2.0.2" + } + }, + "node_modules/node-notifier/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-notifier/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-notifier/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/node-releases": { "version": "2.0.12", "dev": true, @@ -17606,6 +17671,14 @@ "node": ">=6.0.0" } }, + "node_modules/qr-code-styling": { + "version": "1.5.1", + "resolved": "git+ssh://git@github.com/troman29/qr-code-styling.git#c00d009f37768205582a87d4b0673ef38c225a53", + "license": "MIT", + "dependencies": { + "qrcode-generator": "^1.4.3" + } + }, "node_modules/qrcode-generator": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.4.4.tgz", @@ -18875,6 +18948,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/side-channel": { "version": "1.0.4", "dev": true, @@ -19002,14 +19083,6 @@ "websocket-driver": "^0.7.4" } }, - "node_modules/sockjs/node_modules/uuid": { - "version": "8.3.2", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/socks": { "version": "2.7.1", "dev": true, @@ -20751,6 +20824,15 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache": { "version": "2.3.0", "license": "MIT" @@ -20905,6 +20987,11 @@ "tslib": "^2.4.0" } }, + "node_modules/webextension-polyfill": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/webextension-polyfill/-/webextension-polyfill-0.10.0.tgz", + "integrity": "sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g==" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index c8914d71..8c3ed862 100644 --- a/package.json +++ b/package.json @@ -1,34 +1,32 @@ { "name": "mytonwallet", - "version": "1.15.1", + "version": "1.15.2", "description": "The most feature-rich web wallet and browser extension for TON – with support of multi-accounts, tokens (jettons), NFT, TON DNS, TON Sites, TON Proxy, and TON Magic.", "main": "index.js", "scripts": { "dev": "cross-env APP_ENV=development webpack serve --mode development", - "dev:electron": "npm run electron:webpack && IS_ELECTRON=true concurrently -n main,renderer,electron \"npm run electron:webpack -- --watch\" \"npm run dev\" \"electronmon dist/electron\"", - "dev:mocked": "cross-env APP_ENV=test APP_MOCKED_CLIENT=1 webpack serve --mode development --port 1235", - "build:mocked": "cross-env APP_ENV=test APP_MOCKED_CLIENT=1 webpack --mode development && bash ./deploy/copy_to_dist.sh", - "build:dev": "cross-env APP_ENV=development webpack --mode development && bash ./deploy/copy_to_dist.sh", - "build:staging": "cross-env APP_ENV=staging webpack && bash ./deploy/copy_to_dist.sh", - "build:production": "webpack && bash ./deploy/copy_to_dist.sh", - "build:extension:dev": "cross-env ENV_EXTENSION=1 APP_ENV=development webpack --mode development && bash ./deploy/copy_to_dist.sh", - "build:extension:production": "npm i && cross-env ENV_EXTENSION=1 webpack && bash ./deploy/copy_to_dist.sh && rm -f MyTonWallet.zip && zip -r -X MyTonWallet.zip dist/*", - "build:extension:dev:firefox": "cross-env IS_FIREFOX_EXTENSION=1 npm run build:extension:dev", - "build:extension:production:firefox": "npm i && cross-env IS_FIREFOX_EXTENSION=1 ENV_EXTENSION=1 webpack && bash ./deploy/copy_to_dist.sh && rm -f MyTonWallet-firefox.zip && cd dist && rm -f reference.json && zip -r -X ../MyTonWallet-firefox.zip ./*", - "deploy:electron": "npm run electron:production -- -p always", - "start:extension:dev:arch": "npm run build:extension:dev && google-chrome-stable --load-extension=\"`pwd`/dist\"", - "start:extension:dev:mac": "npm run build:extension:dev && open -a \"Google Chrome\" --load-extension=\"`pwd`/dist\"", + "build": "webpack && bash ./deploy/copy_to_dist.sh", + "build:staging": "cross-env APP_ENV=staging npm run build", + "build:production": "npm run build", + "extension:dev": "cross-env ENV_EXTENSION=1 APP_ENV=development webpack --mode development && bash ./deploy/copy_to_dist.sh", + "extension-chrome:package": "cross-env ENV_EXTENSION=1 webpack && bash ./deploy/copy_to_dist.sh && rm -f MyTonWallet-chrome.zip && rm -f dist/reference.json && zip -r -X MyTonWallet-chrome.zip dist/*", + "extension-chrome:package:staging": "APP_ENV=staging npm run extension-chrome:package", + "extension-chrome:package:production": "npm run extension-chrome:package", + "extension-firefox:package": "cross-env IS_FIREFOX_EXTENSION=1 ENV_EXTENSION=1 webpack && bash ./deploy/copy_to_dist.sh && rm -f MyTonWallet-firefox.zip && rm -f dist/reference.json && zip -r -X MyTonWallet-firefox.zip dist/*", + "extension-firefox:package:staging": "cross-env APP_ENV=staging npm run extension-firefox:package", + "extension-firefox:package:production": "npm run extension-firefox:package", + "electron:dev": "npm run electron:webpack && IS_ELECTRON=true concurrently -n main,renderer,electron \"npm run electron:webpack -- --watch\" \"npm run dev\" \"electronmon dist/electron\"", + "electron:webpack": "cross-env APP_ENV=$ENV webpack --config ./webpack-electron.config.ts", + "electron:build": "IS_ELECTRON=true npm run build:$ENV && electron-builder install-app-deps && electron-rebuild && ENV=$ENV npm run electron:webpack", + "electron:package": "npm run electron:build && npx rimraf dist-electron && electron-builder build --win --mac --linux --config src/electron/config.yml", + "electron:package:staging": "ENV=staging npm run electron:package -- -p never", + "electron:release:production": "ENV=production npm run electron:package -- -p always", "build:icons": "fantasticon", "check": "tsc && stylelint \"**/*.{css,scss}\" && eslint . --ext .ts,.tsx", "check:fix": "npm run check -- --fix", "test": "cross-env APP_ENV=test jest --verbose --forceExit", "test:playwright": "playwright test", "test:record": "playwright codegen localhost:1235", - "electron:webpack": "cross-env APP_ENV=$ENV webpack --config ./webpack-electron.config.ts", - "electron:build": "IS_ELECTRON=true npm run build:$ENV && electron-builder install-app-deps && electron-rebuild && ENV=$ENV npm run electron:webpack", - "electron:staging": "ENV=staging npm run electron:package -- -p never", - "electron:production": "ENV=production npm run electron:package --", - "electron:package": "npm run electron:build && npx rimraf dist-electron && electron-builder build --win --mac --linux --config src/electron/config.yml", "prepare": "husky install", "statoscope:validate-diff": "statoscope validate --input input.json --reference reference.json" }, @@ -163,6 +161,7 @@ "buffer": "^6.0.3", "idb-keyval": "^6.2.0", "pako": "^2.1.0", + "qr-code-styling": "github:troman29/qr-code-styling#c00d0", "qrcode-generator": "^1.4.4", "ton": "^13.4.1", "ton-core": "^0.49.0", @@ -170,6 +169,7 @@ "tonweb": "github:troman29/tonweb#3d5e2f3", "tonweb-mnemonic": "^1.0.1", "tweetnacl": "^1.0.3", - "v8-compile-cache": "^2.3.0" + "v8-compile-cache": "^2.3.0", + "webextension-polyfill": "^0.10.0" } } diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index f95ca781..52c8a4c4 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -42,6 +42,9 @@ type AnyAsyncFunction = (...args: any[]) => Promise; type AnyToVoidFunction = (...args: any[]) => void; type NoneToVoidFunction = () => void; +type ValueOf = T[keyof T]; +type Entries = [keyof T, ValueOf][]; + type EmojiCategory = { id: string; name: string; diff --git a/src/api/blockchains/ton/address.ts b/src/api/blockchains/ton/address.ts index 969c3da1..6d684ce1 100644 --- a/src/api/blockchains/ton/address.ts +++ b/src/api/blockchains/ton/address.ts @@ -1,15 +1,18 @@ import type { ApiNetwork } from '../../types'; import dns from '../../../util/dns'; -import { getTonWeb } from './util/tonweb'; +import { getTonWeb, toBase64Address } from './util/tonweb'; const { DnsCollection } = require('tonweb/src/contract/dns/DnsCollection'); const VIP_DNS_COLLECTION = 'EQBWG4EBbPDv4Xj7xlPwzxd7hSyHMzwwLB5O6rY-0BBeaixS'; -export async function resolveAddress(network: ApiNetwork, address: string) { +export async function resolveAddress(network: ApiNetwork, address: string): Promise<{ + address: string; + domain?: string; +} | undefined> { if (!dns.isDnsDomain(address)) { - return address; + return { address }; } const domain = address; @@ -24,7 +27,12 @@ export async function resolveAddress(network: ApiNetwork, address: string) { }).resolve(base, 'wallet'))?.toString(true, true, true); } - return (await tonweb.dns.getWalletAddress(domain))?.toString(true, true, true); + const addressObj = await tonweb.dns.getWalletAddress(domain); + if (!addressObj) { + return undefined; + } + + return { address: toBase64Address(addressObj), domain }; } catch (err: any) { if (err.message !== 'http provider parse response error') { throw err; diff --git a/src/api/blockchains/ton/transactions.ts b/src/api/blockchains/ton/transactions.ts index eda487b3..77c24adb 100644 --- a/src/api/blockchains/ton/transactions.ts +++ b/src/api/blockchains/ton/transactions.ts @@ -25,6 +25,7 @@ import { fetchNewestTxId, fetchTransactions, getWalletPublicKey, + parseBase64, resolveTokenWalletAddress, toBase64Address, } from './util/tonweb'; @@ -44,7 +45,7 @@ import { } from './wallet'; type SubmitTransferResult = { - resolvedAddress: string; + normalizedAddress: string; amount: string; seqno: number; encryptedComment?: string; @@ -84,51 +85,72 @@ export async function checkTransactionDraft( tokenSlug: string, toAddress: string, amount: string, - data?: string | Uint8Array | Cell, + data?: AnyPayload, stateInit?: Cell, shouldEncrypt?: boolean, + isBase64Data?: boolean, ) { const { network } = parseAccountId(accountId); const result: { - error?: ApiTransactionDraftError; fee?: string; addressName?: string; isScam?: boolean; + resolvedAddress?: string; + normalizedAddress?: string; } = {}; - const resolvedAddress = await resolveAddress(network, toAddress); - if (!resolvedAddress) { - result.error = ApiTransactionDraftError.DomainNotResolved; - return result; + const resolved = await resolveAddress(network, toAddress); + if (resolved) { + result.addressName = resolved.domain; + toAddress = resolved.address; + } else { + return { ...result, error: ApiTransactionDraftError.DomainNotResolved }; } - toAddress = resolvedAddress; if (!Address.isValid(toAddress)) { - result.error = ApiTransactionDraftError.InvalidToAddress; - return result; + return { ...result, error: ApiTransactionDraftError.InvalidToAddress }; + } + + const { + isUserFriendly, isTestOnly, isBounceable, + } = new Address(toAddress); + + const regex = /[+=/]/; // Temp check for `isUrlSafe`. Remove after TonWeb fixes the issue + const isUrlSafe = !regex.test(toAddress); + + if (!isUserFriendly || !isUrlSafe || (network === 'mainnet' && isTestOnly)) { + return { ...result, error: ApiTransactionDraftError.InvalidAddressFormat }; } + if (tokenSlug === TON_TOKEN_SLUG && isBounceable && !(await isWalletInitialized(network, toAddress))) { + toAddress = toBase64Address(toAddress, false); + } + + result.resolvedAddress = toAddress; + result.normalizedAddress = toBase64Address(toAddress); + const addressInfo = await getAddressInfo(toAddress); - result.addressName = addressInfo?.name; - result.isScam = addressInfo?.isScam; + if (addressInfo?.name) result.addressName = addressInfo.name; + if (addressInfo?.isScam) result.isScam = addressInfo.isScam; if (BigInt(amount) < BigInt(0)) { - result.error = ApiTransactionDraftError.InvalidAmount; - return result; + return { ...result, error: ApiTransactionDraftError.InvalidAmount }; } const wallet = await pickAccountWallet(accountId); if (!wallet) { - result.error = ApiTransactionDraftError.Unexpected; - return result; + return { ...result, error: ApiTransactionDraftError.Unexpected }; + } + + if (typeof data === 'string' && isBase64Data) { + data = parseBase64(data); } if (data && typeof data === 'string' && shouldEncrypt) { const toPublicKey = await getWalletPublicKey(network, toAddress); if (!toPublicKey) { - result.error = ApiTransactionDraftError.WalletNotInitialized; - return result; + return { ...result, error: ApiTransactionDraftError.WalletNotInitialized }; } } @@ -136,8 +158,7 @@ export async function checkTransactionDraft( const account = await fetchStoredAccount(accountId); if (data && account?.ledger) { if (typeof data !== 'string' || shouldEncrypt || !isValidLedgerComment(data)) { - result.error = ApiTransactionDraftError.UnsupportedHardwarePayload; - return result; + return { ...result, error: ApiTransactionDraftError.UnsupportedHardwarePayload }; } } } else { @@ -153,23 +174,27 @@ export async function checkTransactionDraft( const tokenBalance = await getTokenWalletBalance(tokenWallet!); if (BigInt(tokenBalance) < BigInt(tokenAmount!)) { - result.error = ApiTransactionDraftError.InsufficientBalance; - return result; + return { ...result, error: ApiTransactionDraftError.InsufficientBalance }; } } - const isInitialized = await isWalletInitialized(network, wallet); - result.fee = await calculateFee(isInitialized, async () => (await signTransaction( + const isOurWalletInitialized = await isWalletInitialized(network, wallet); + result.fee = await calculateFee(isOurWalletInitialized, async () => (await signTransaction( network, wallet, toAddress, amount, data, stateInit, )).query); const balance = await getWalletBalance(network, wallet); if (BigInt(balance) < BigInt(amount) + BigInt(result.fee)) { - result.error = ApiTransactionDraftError.InsufficientBalance; - return result; + return { ...result, error: ApiTransactionDraftError.InsufficientBalance }; } - return result; + return result as { + fee: string; + resolvedAddress: string; + normalizedAddress: string; + addressName?: string; + isScam?: boolean; + }; } export async function submitTransfer( @@ -181,6 +206,7 @@ export async function submitTransfer( data?: AnyPayload, stateInit?: Cell, shouldEncrypt?: boolean, + isBase64Data?: boolean, ): Promise { const { network } = parseAccountId(accountId); @@ -193,7 +219,12 @@ export async function submitTransfer( const { publicKey, secretKey } = keyPair!; let encryptedComment: string | undefined; - let resolvedAddress = await resolveAddress(network, toAddress); + // Force default bounceable address for `waitTxComplete` to work properly + const normalizedAddress = toBase64Address(toAddress); + + if (typeof data === 'string' && isBase64Data) { + data = parseBase64(data); + } if (data && typeof data === 'string' && shouldEncrypt) { const toPublicKey = (await getWalletPublicKey(network, toAddress))!; @@ -204,25 +235,14 @@ export async function submitTransfer( if (tokenSlug !== TON_TOKEN_SLUG) { ({ amount, - toAddress: resolvedAddress, + toAddress, payload: data, - } = await buildTokenTransfer(network, tokenSlug, fromAddress, resolvedAddress, amount, data)); + } = await buildTokenTransfer(network, tokenSlug, fromAddress, toAddress, amount, data)); } - // Force default bounceable address for `waitTxComplete` to work properly - resolvedAddress = toBase64Address(resolvedAddress); - await waitLastTransfer(network, fromAddress); - const [{ isInitialized, balance }, toWalletInfo] = await Promise.all([ - getWalletInfo(network, wallet!), - getWalletInfo(network, resolvedAddress), - ]); - - // Force non-bounceable for non-initialized recipients - toAddress = toWalletInfo.isInitialized - ? resolvedAddress - : toBase64Address(resolvedAddress, false); + const { isInitialized, balance } = await getWalletInfo(network, wallet!); const { seqno, query } = await signTransaction(network, wallet!, toAddress, amount, data, stateInit, secretKey); @@ -236,7 +256,7 @@ export async function submitTransfer( updateLastTransfer(network, fromAddress, seqno); return { - resolvedAddress, amount, seqno, encryptedComment, + normalizedAddress, amount, seqno, encryptedComment, }; } catch (err: any) { logDebugError('submitTransfer', err); @@ -393,7 +413,6 @@ export async function checkMultiTransactionDraft(accountId: string, messages: To const { network } = parseAccountId(accountId); const result: { - error?: ApiTransactionDraftError; fee?: string; totalAmount?: string; } = {}; @@ -402,12 +421,10 @@ export async function checkMultiTransactionDraft(accountId: string, messages: To for (const { toAddress, amount } of messages) { if (BigInt(amount) < BigInt(0)) { - result.error = ApiTransactionDraftError.InvalidAmount; - return result; + return { ...result, error: ApiTransactionDraftError.InvalidAmount }; } if (!Address.isValid(toAddress)) { - result.error = ApiTransactionDraftError.InvalidToAddress; - return result; + return { ...result, error: ApiTransactionDraftError.InvalidToAddress }; } totalAmount += BigInt(amount); } @@ -415,8 +432,7 @@ export async function checkMultiTransactionDraft(accountId: string, messages: To const wallet = await pickAccountWallet(accountId); if (!wallet) { - result.error = ApiTransactionDraftError.Unexpected; - return result; + return { ...result, error: ApiTransactionDraftError.Unexpected }; } const { isInitialized, balance } = await getWalletInfo(network, wallet); @@ -427,11 +443,10 @@ export async function checkMultiTransactionDraft(accountId: string, messages: To result.totalAmount = totalAmount.toString(); if (BigInt(balance) < totalAmount + BigInt(result.fee)) { - result.error = ApiTransactionDraftError.InsufficientBalance; - return result; + return { ...result, error: ApiTransactionDraftError.InsufficientBalance }; } - return result; + return result as { fee: string; totalAmount: string }; } export async function submitMultiTransfer( @@ -506,6 +521,12 @@ async function signMultiTransaction( expireAt = Math.round(Date.now() / 1000) + DEFAULT_EXPIRE_AT_TIMEOUT_SEC; } + for (const message of messages) { + if (message.payload && typeof message.payload === 'string' && message.isBase64Payload) { + message.payload = parseBase64(message.payload); + } + } + // TODO Uncomment after fixing types in tonweb // @ts-ignore const query = wallet.methods.transfers({ diff --git a/src/api/blockchains/ton/types.ts b/src/api/blockchains/ton/types.ts index 4f211970..ef9a370d 100644 --- a/src/api/blockchains/ton/types.ts +++ b/src/api/blockchains/ton/types.ts @@ -37,6 +37,7 @@ export interface TonTransferParams { amount: string; payload?: AnyPayload; stateInit?: Cell; + isBase64Payload?: boolean; } export interface JettonMetadata { diff --git a/src/api/blockchains/ton/util/tonweb.ts b/src/api/blockchains/ton/util/tonweb.ts index 45a01a25..fe978417 100644 --- a/src/api/blockchains/ton/util/tonweb.ts +++ b/src/api/blockchains/ton/util/tonweb.ts @@ -14,8 +14,9 @@ import { TONHTTPAPI_TESTNET_API_KEY, TONHTTPAPI_TESTNET_URL, } from '../../../../config'; +import { logDebugError } from '../../../../util/logs'; import withCacheAsync from '../../../../util/withCacheAsync'; -import { hexToBytes } from '../../../common/utils'; +import { base64ToBytes, hexToBytes } from '../../../common/utils'; import { JettonOpCode } from '../constants'; import { parseTxId, stringifyTxId } from './index'; @@ -235,3 +236,12 @@ export function buildTokenTransferBody(params: TokenTransferBodyParams) { export function bnToAddress(value: BN) { return new Address(`0:${value.toString('hex', 64)}`).toString(true, true, true); } + +export function parseBase64(base64: string) { + try { + return Cell.oneFromBoc(base64ToBytes(base64)); + } catch (err) { + logDebugError('parseBase64', err); + return Uint8Array.from(Buffer.from(base64, 'base64')); + } +} diff --git a/src/api/common/accounts.ts b/src/api/common/accounts.ts index d690dda1..e0c2e17e 100644 --- a/src/api/common/accounts.ts +++ b/src/api/common/accounts.ts @@ -4,7 +4,6 @@ import type { ApiAccountInfo, ApiNetwork } from '../types'; import { buildAccountId, parseAccountId } from '../../util/account'; import { buildCollectionByKey } from '../../util/iteratees'; import { storage } from '../storages'; -import { toInternalAccountId } from './helpers'; const MIN_ACCOUNT_NUMBER = 0; @@ -76,23 +75,47 @@ export function fetchStoredAddress(accountId: string): Promise { } export async function getAccountValue(accountId: string, key: StorageKey) { - const internalId = toInternalAccountId(accountId); - return (await storage.getItem(key))?.[internalId]; + return (await storage.getItem(key))?.[accountId]; } export async function removeAccountValue(accountId: string, key: StorageKey) { - const internalId = toInternalAccountId(accountId); const data = await storage.getItem(key); if (!data) return; - const { [internalId]: removedValue, ...restData } = data; + const { [accountId]: removedValue, ...restData } = data; await storage.setItem(key, restData); } export async function setAccountValue(accountId: string, key: StorageKey, value: any) { - const internalId = toInternalAccountId(accountId); const data = await storage.getItem(key); - await storage.setItem(key, { ...data, [internalId]: value }); + await storage.setItem(key, { ...data, [accountId]: value }); +} + +export async function removeNetworkAccountsValue(network: string, key: StorageKey) { + const data = await storage.getItem(key); + if (!data) return; + + for (const accountId of Object.keys(data)) { + if (parseAccountId(accountId).network === network) { + delete data[accountId]; + } + } + + await storage.setItem(key, data); +} + +export async function getCurrentNetwork() { + const accountId = await getCurrentAccountId(); + if (!accountId) return undefined; + return parseAccountId(accountId).network; +} + +export async function getCurrentAccountIdOrFail() { + const accountId = await getCurrentAccountId(); + if (!accountId) { + throw new Error('The user is not authorized in the wallet'); + } + return accountId; } export function getCurrentAccountId(): Promise { diff --git a/src/api/common/helpers.ts b/src/api/common/helpers.ts index 7ba28f35..752b3ef7 100644 --- a/src/api/common/helpers.ts +++ b/src/api/common/helpers.ts @@ -5,7 +5,7 @@ import type { } from '../types'; import { MAIN_ACCOUNT_ID } from '../../config'; -import { parseAccountId } from '../../util/account'; +import { buildAccountId, parseAccountId } from '../../util/account'; import { IS_EXTENSION } from '../environment'; import { storage } from '../storages'; import idbStorage from '../storages/idb'; @@ -15,7 +15,7 @@ import { whenTxComplete } from './txCallbacks'; let localCounter = 0; const getNextLocalId = () => `${Date.now()}|${localCounter++}`; -const actualStateVersion = 6; +const actualStateVersion = 7; let migrationEnsurePromise: Promise; export function resolveBlockchainKey(accountId: string) { @@ -214,4 +214,27 @@ export async function migrateStorage() { version = 6; await storage.setItem('stateVersion', version); } + + if (version === 6) { + for (const key of ['addresses', 'mnemonicsEncrypted', 'publicKeys', 'accounts', 'dapps'] as StorageKey[]) { + let data = await storage.getItem(key) as AnyLiteral; + if (!data) continue; + + data = Object.entries(data).reduce((byAccountId, [internalAccountId, accountData]) => { + const parsed = parseAccountId(internalAccountId); + const mainnetAccountId = buildAccountId({ ...parsed, network: 'mainnet' }); + const testnetAccountId = buildAccountId({ ...parsed, network: 'testnet' }); + return { + ...byAccountId, + [mainnetAccountId]: accountData, + [testnetAccountId]: accountData, + }; + }, {} as AnyLiteral); + + await storage.setItem(key, data); + } + + version = 7; + await storage.setItem('stateVersion', version); + } } diff --git a/src/api/common/utils.ts b/src/api/common/utils.ts index 6bebdb50..91f4bcc9 100644 --- a/src/api/common/utils.ts +++ b/src/api/common/utils.ts @@ -14,8 +14,8 @@ export function bytesToBase64(bytes: Uint8Array) { return TonWeb.utils.bytesToBase64(bytes); } -export function base64ToBytes(hex: string) { - return TonWeb.utils.base64ToBytes(hex); +export function base64ToBytes(base64: string) { + return TonWeb.utils.base64ToBytes(base64); } export function hexToBase64(hex: string) { diff --git a/src/api/dappMethods/types.ts b/src/api/dappMethods/types.ts deleted file mode 100644 index a99e9d48..00000000 --- a/src/api/dappMethods/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type * as dappMethods from './index'; -import type * as legacyDappMethods from './legacy'; - -export type DappMethods = typeof dappMethods; -export type DappMethodArgs = Parameters; -export type DappMethodResponse = ReturnType; - -export type LegacyDappMethods = typeof legacyDappMethods; -export type LegacyDappMethodArgs = Parameters; -export type LegacyDappMethodResponse = ReturnType; diff --git a/src/api/methods/extension.ts b/src/api/extensionMethods/extension.ts similarity index 94% rename from src/api/methods/extension.ts rename to src/api/extensionMethods/extension.ts index 9ffeeaa8..8b38cca0 100644 --- a/src/api/methods/extension.ts +++ b/src/api/extensionMethods/extension.ts @@ -1,12 +1,12 @@ -import extension from '../../lib/webextension-polyfill'; +import extension from 'webextension-polyfill'; import type { OnApiUpdate } from '../types'; import { PROXY_HOSTS } from '../../config'; import { sample } from '../../util/random'; -import { updateDapps } from '../dappMethods'; import { IS_FIREFOX_EXTENSION } from '../environment'; import { storage } from '../storages'; +import { updateSites } from './sites'; type ProxyType = 'http' | 'https' | 'socks' | 'socks5'; @@ -45,7 +45,7 @@ export async function initExtension(_onUpdate: OnApiUpdate) { onUpdate = _onUpdate; const isTonProxyEnabled = await storage.getItem('isTonProxyEnabled'); - doProxy(isTonProxyEnabled); + void doProxy(isTonProxyEnabled); const isDeeplinkHookEnabled = await storage.getItem('isDeeplinkHookEnabled'); doDeeplinkHook(isDeeplinkHookEnabled); @@ -60,7 +60,7 @@ export function setupDefaultExtensionFeatures() { } export async function clearExtensionFeatures() { - doProxy(false); + void doProxy(false); doMagic(false); doDeeplinkHook(false); @@ -116,7 +116,7 @@ function firefoxOnRequest(): FirefoxProxyInfo | FirefoxProxyInfo[] { export function doMagic(isEnabled: boolean) { void storage.setItem('isTonMagicEnabled', isEnabled); - updateDapps({ + updateSites({ type: 'updateTonMagic', isEnabled, }); @@ -125,7 +125,7 @@ export function doMagic(isEnabled: boolean) { export function doDeeplinkHook(isEnabled: boolean) { void storage.setItem('isDeeplinkHookEnabled', isEnabled); - updateDapps({ + updateSites({ type: 'updateDeeplinkHook', isEnabled, }); diff --git a/src/api/extensionMethods/index.ts b/src/api/extensionMethods/index.ts new file mode 100644 index 00000000..f86dc29d --- /dev/null +++ b/src/api/extensionMethods/index.ts @@ -0,0 +1 @@ +export * from './extension'; diff --git a/src/api/extensionMethods/init.ts b/src/api/extensionMethods/init.ts new file mode 100644 index 00000000..8f8aa42c --- /dev/null +++ b/src/api/extensionMethods/init.ts @@ -0,0 +1,26 @@ +import type { OnApiUpdate } from '../types'; + +import * as legacyDappMethods from './legacy'; +import * as siteMethods from './sites'; +import { openPopupWindow } from './window'; +import * as extensionMethods from '.'; + +import { addHooks } from '../hooks'; + +addHooks({ + onWindowNeeded: openPopupWindow, + onFirstLogin: extensionMethods.setupDefaultExtensionFeatures, + onFullLogout: extensionMethods.clearExtensionFeatures, + onDappDisconnected: () => { + siteMethods.updateSites({ + type: 'disconnectSite', + origin, + }); + }, +}); + +export default function init(onUpdate: OnApiUpdate) { + void extensionMethods.initExtension(onUpdate); + legacyDappMethods.initLegacyDappMethods(onUpdate); + siteMethods.initSiteMethods(onUpdate); +} diff --git a/src/api/dappMethods/legacy.ts b/src/api/extensionMethods/legacy.ts similarity index 95% rename from src/api/dappMethods/legacy.ts rename to src/api/extensionMethods/legacy.ts index 9f6ee8a3..d06798e7 100644 --- a/src/api/dappMethods/legacy.ts +++ b/src/api/extensionMethods/legacy.ts @@ -1,4 +1,4 @@ -import type { OnApiDappUpdate } from '../types/dappUpdates'; +import type { OnApiSiteUpdate } from '../types/dappUpdates'; import type { ApiSignedTransfer, OnApiUpdate } from '../types'; import { TON_TOKEN_SLUG } from '../../config'; @@ -6,12 +6,12 @@ import { parseAccountId } from '../../util/account'; import { logDebugError } from '../../util/logs'; import blockchains from '../blockchains'; import { - fetchStoredAccount, fetchStoredAddress, fetchStoredPublicKey, getCurrentAccountId, waitLogin, + fetchStoredAccount, fetchStoredAddress, fetchStoredPublicKey, getCurrentAccountId, getCurrentAccountIdOrFail, + waitLogin, } from '../common/accounts'; import { createDappPromise } from '../common/dappPromises'; import { createLocalTransaction } from '../common/helpers'; import { base64ToBytes, hexToBytes } from '../common/utils'; -import { getCurrentAccountIdOrFail } from './index'; import { openPopupWindow } from './window'; const ton = blockchains.ton; @@ -21,7 +21,7 @@ export function initLegacyDappMethods(_onPopupUpdate: OnApiUpdate) { onPopupUpdate = _onPopupUpdate; } -export async function onDappSendUpdates(onDappUpdate: OnApiDappUpdate) { +export async function onDappSendUpdates(onDappUpdate: OnApiSiteUpdate) { const accounts = await requestAccounts(); onDappUpdate({ @@ -104,10 +104,10 @@ export async function sendTransaction(params: { accountId, TON_TOKEN_SLUG, toAddress, amount, processedData, processedStateInit, ); - if (!checkResult || checkResult?.error) { + if ('error' in checkResult) { onPopupUpdate({ type: 'showError', - error: checkResult?.error, + error: checkResult.error, }); return false; diff --git a/src/api/dappMethods/index.ts b/src/api/extensionMethods/sites.ts similarity index 61% rename from src/api/dappMethods/index.ts rename to src/api/extensionMethods/sites.ts index 13a50981..2983ba0a 100644 --- a/src/api/dappMethods/index.ts +++ b/src/api/extensionMethods/sites.ts @@ -1,7 +1,7 @@ -import type { ApiDappUpdate, OnApiDappUpdate } from '../types/dappUpdates'; +import type { ApiSiteUpdate, OnApiSiteUpdate } from '../types/dappUpdates'; import type { OnApiUpdate } from '../types'; -import { getCurrentAccountId, waitLogin } from '../common/accounts'; +import { getCurrentAccountIdOrFail, waitLogin } from '../common/accounts'; import { resolveDappPromise } from '../common/dappPromises'; import storage from '../storages/extension'; import { clearCache, openPopupWindow } from './window'; @@ -11,49 +11,49 @@ let onPopupUpdate: OnApiUpdate; // Sometimes (e.g. when Dev Tools is open) dapp needs more time to subscribe to provider const INIT_UPDATE_DELAY = 50; -const dappUpdaters: OnApiDappUpdate[] = []; +const siteUpdaters: OnApiSiteUpdate[] = []; // This method is called from `initApi` which in turn is called when popup is open -export function initDappMethods(_onPopupUpdate: OnApiUpdate) { +export function initSiteMethods(_onPopupUpdate: OnApiUpdate) { onPopupUpdate = _onPopupUpdate; resolveDappPromise('whenPopupReady'); } -export async function connectDapp( - onDappUpdate: OnApiDappUpdate, - onDappSendUpdates: (x: OnApiDappUpdate) => Promise, // TODO Remove this when deleting the legacy provider +export async function connectSite( + onSiteUpdate: OnApiSiteUpdate, + onSiteSendUpdates: (x: OnApiSiteUpdate) => Promise, // TODO Remove this when deleting the legacy provider ) { - dappUpdaters.push(onDappUpdate); + siteUpdaters.push(onSiteUpdate); const isTonMagicEnabled = await storage.getItem('isTonMagicEnabled'); const isDeeplinkHookEnabled = await storage.getItem('isDeeplinkHookEnabled'); function sendUpdates() { - onDappUpdate({ + onSiteUpdate({ type: 'updateTonMagic', isEnabled: Boolean(isTonMagicEnabled), }); - onDappUpdate({ + onSiteUpdate({ type: 'updateDeeplinkHook', isEnabled: Boolean(isDeeplinkHookEnabled), }); - onDappSendUpdates(onDappUpdate); + onSiteSendUpdates(onSiteUpdate); } sendUpdates(); setTimeout(sendUpdates, INIT_UPDATE_DELAY); } -export function deactivateDapp(onDappUpdate: OnApiDappUpdate) { - const index = dappUpdaters.findIndex((updater) => updater === onDappUpdate); +export function deactivateSite(onDappUpdate: OnApiSiteUpdate) { + const index = siteUpdaters.findIndex((updater) => updater === onDappUpdate); if (index !== -1) { - dappUpdaters.splice(index, 1); + siteUpdaters.splice(index, 1); } } -export function updateDapps(update: ApiDappUpdate) { - dappUpdaters.forEach((onDappUpdate) => { +export function updateSites(update: ApiSiteUpdate) { + siteUpdaters.forEach((onDappUpdate) => { onDappUpdate(update); }); } @@ -81,11 +81,3 @@ export async function prepareTransaction(params: { export async function flushMemoryCache() { await clearCache(); } - -export async function getCurrentAccountIdOrFail() { - const accountId = await getCurrentAccountId(); - if (!accountId) { - throw new Error('The user is not authorized in the wallet'); - } - return accountId; -} diff --git a/src/api/extensionMethods/types.ts b/src/api/extensionMethods/types.ts new file mode 100644 index 00000000..65f489c2 --- /dev/null +++ b/src/api/extensionMethods/types.ts @@ -0,0 +1,15 @@ +import type * as extensionMethods from './extension'; +import type * as legacyDappMethods from './legacy'; +import type * as siteMethods from './sites'; + +export type ExtensionMethods = typeof extensionMethods; +export type ExtensionMethodArgs = Parameters; +export type ExtensionMethodResponse = ReturnType; + +export type SiteMethods = typeof siteMethods; +export type SiteMethodArgs = Parameters; +export type SiteMethodResponse = ReturnType; + +export type LegacyDappMethods = typeof legacyDappMethods; +export type LegacyDappMethodArgs = Parameters; +export type LegacyDappMethodResponse = ReturnType; diff --git a/src/api/dappMethods/window.ts b/src/api/extensionMethods/window.ts similarity index 85% rename from src/api/dappMethods/window.ts rename to src/api/extensionMethods/window.ts index e64b1f77..f665747d 100644 --- a/src/api/dappMethods/window.ts +++ b/src/api/extensionMethods/window.ts @@ -1,4 +1,4 @@ -import extension from '../../lib/webextension-polyfill'; +import extension from 'webextension-polyfill'; import { createDappPromise, rejectAllDappPromises } from '../common/dappPromises'; import storage from '../storages/extension'; @@ -17,6 +17,7 @@ const WINDOW_DEFAULTS = { }; const MARGIN_RIGHT = 20; const WINDOW_STATE_MONITOR_INTERVAL = 3000; +const MINIMAL_WINDOW = 100; (function init() { if (!chrome) { @@ -50,12 +51,23 @@ const WINDOW_STATE_MONITOR_INTERVAL = 3000; return; } + const { height = 0, width = 0 } = currentWindow; + const correctHeight = Math.max(height, MINIMAL_WINDOW); + const correctWidth = Math.max(width, MINIMAL_WINDOW); + void storage.setItem('windowState', { top: currentWindow.top, left: currentWindow.left, - height: currentWindow.height, - width: currentWindow.width, + height: correctHeight, + width: correctWidth, }); + + if (height < MINIMAL_WINDOW || width < MINIMAL_WINDOW) { + await extension.windows.update(currentWindowId!, { + height: MINIMAL_WINDOW, + width: MINIMAL_WINDOW, + }); + } }, WINDOW_STATE_MONITOR_INTERVAL); }()); diff --git a/src/api/hooks.ts b/src/api/hooks.ts new file mode 100644 index 00000000..d5161a8f --- /dev/null +++ b/src/api/hooks.ts @@ -0,0 +1,30 @@ +import { logDebugError } from '../util/logs'; + +interface Hooks { + onFirstLogin: AnyFunction; + onFullLogout: AnyFunction; + onWindowNeeded: AnyFunction; + onDappDisconnected: (accountId: string, origin: string) => any; + onDappsChanged: AnyFunction; +} + +const hooks: Partial<{ + [K in keyof Hooks]: Hooks[K][]; +}> = {}; + +export function addHooks(partial: Partial) { + for (const [name, hook] of Object.entries(partial) as Entries) { + hooks[name] = (hooks[name] ?? []).concat([hook]); + } +} + +export async function callHook(name: T, ...args: Parameters) { + for (const hook of hooks[name] ?? []) { + try { + // @ts-ignore + await hook(...args); + } catch (err) { + logDebugError(`callHooks:${name}`, err); + } + } +} diff --git a/src/api/methods/accounts.ts b/src/api/methods/accounts.ts index 3d35efe6..56047f43 100644 --- a/src/api/methods/accounts.ts +++ b/src/api/methods/accounts.ts @@ -6,13 +6,14 @@ import { waitStorageMigration } from '../common/helpers'; import { IS_EXTENSION } from '../environment'; import { storage } from '../storages'; import { deactivateAccountDapp, deactivateAllDapps, onActiveDappAccountUpdated } from './dapps'; -import { clearExtensionFeatures, setupDefaultExtensionFeatures } from './extension'; import { sendUpdateTokens, setupBackendStakingStatePolling, setupBalanceBasedPolling, } from './polling'; +import { callHook } from '../hooks'; + let activeAccountId: string | undefined; export async function activateAccount(accountId: string, newestTxIds?: ApiTxIdBySlug) { @@ -30,9 +31,7 @@ export async function activateAccount(accountId: string, newestTxIds?: ApiTxIdBy deactivateAllDapps(); } - if (isFirstLogin) { - setupDefaultExtensionFeatures(); - } + callHook('onFirstLogin'); onActiveDappAccountUpdated(accountId); } @@ -51,7 +50,7 @@ export function deactivateAllAccounts() { if (IS_EXTENSION) { deactivateAllDapps(); - void clearExtensionFeatures(); + callHook('onFullLogout'); } } diff --git a/src/api/methods/auth.ts b/src/api/methods/auth.ts index f8709b56..8c1656f8 100644 --- a/src/api/methods/auth.ts +++ b/src/api/methods/auth.ts @@ -2,12 +2,14 @@ import type { LedgerWalletInfo } from '../../util/ledger/types'; import type { ApiAccountInfo, ApiNetwork, ApiTxIdBySlug } from '../types'; import blockchains from '../blockchains'; -import { getNewAccountId, removeAccountValue, setAccountValue } from '../common/accounts'; +import { + getNewAccountId, removeAccountValue, removeNetworkAccountsValue, setAccountValue, +} from '../common/accounts'; import { bytesToHex } from '../common/utils'; import { IS_DAPP_SUPPORTED } from '../environment'; import { storage } from '../storages'; import { activateAccount, deactivateAllAccounts, deactivateCurrentAccount } from './accounts'; -import { removeAccountDapps, removeAllDapps } from './dapps'; +import { removeAccountDapps, removeAllDapps, removeNetworkDapps } from './dapps'; export function generateMnemonic() { return blockchains.ton.generateMnemonic(); @@ -122,6 +124,18 @@ async function storeAccount( ]); } +export async function removeNetworkAccounts(network: ApiNetwork) { + deactivateAllAccounts(); + + await Promise.all([ + removeNetworkAccountsValue(network, 'addresses'), + removeNetworkAccountsValue(network, 'publicKeys'), + removeNetworkAccountsValue(network, 'mnemonicsEncrypted'), + removeNetworkAccountsValue(network, 'accounts'), + IS_DAPP_SUPPORTED && removeNetworkDapps(network), + ]); +} + export async function resetAccounts() { deactivateAllAccounts(); diff --git a/src/api/methods/dapps.ts b/src/api/methods/dapps.ts index 2aa75fe3..1546f755 100644 --- a/src/api/methods/dapps.ts +++ b/src/api/methods/dapps.ts @@ -3,30 +3,20 @@ import type { } from '../types'; import { buildAccountId, parseAccountId } from '../../util/account'; -import { getAccountValue, removeAccountValue, setAccountValue } from '../common/accounts'; -import { isUpdaterAlive, toInternalAccountId } from '../common/helpers'; -import { updateDapps } from '../dappMethods'; +import { + getAccountValue, removeAccountValue, removeNetworkAccountsValue, setAccountValue, +} from '../common/accounts'; +import { isUpdaterAlive } from '../common/helpers'; import { storage } from '../storages'; -type OnDappDisconnected = (accountId: string, origin: string) => Promise | void; +import { callHook } from '../hooks'; const activeDappByAccountId: Record = {}; let onUpdate: OnApiUpdate; -let onDappsChanged: AnyToVoidFunction = () => {}; -let onDappDisconnected: OnDappDisconnected = () => {}; - -export function initDapps( - _onUpdate: OnApiUpdate, - _onDappsChanged?: AnyToVoidFunction, - _onDappDisconnected?: OnDappDisconnected, -) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars + +export function initDapps(_onUpdate: OnApiUpdate) { onUpdate = _onUpdate; - if (_onDappsChanged && _onDappDisconnected) { - onDappsChanged = _onDappsChanged; - onDappDisconnected = _onDappDisconnected; - } } export function onActiveDappAccountUpdated(accountId: string) { @@ -133,19 +123,6 @@ export async function addDapp(accountId: string, dapp: ApiDapp) { await setAccountValue(accountId, 'dapps', dapps); } -export async function addDappToAccounts(dapp: ApiDapp, accountIds: string[]) { - const dappsByAccount = await storage.getItem('dapps') || {}; - - accountIds.forEach((accountId) => { - const internalId = toInternalAccountId(accountId); - const dapps = dappsByAccount[internalId] || {}; - dapps[dapp.origin] = dapp; - - dappsByAccount[internalId] = dapps; - }); - await storage.setItem('dapps', dappsByAccount); -} - export async function deleteDapp(accountId: string, origin: string, dontNotifyDapp?: boolean) { const dapps = await getDappsByOrigin(accountId); if (!(origin in dapps)) { @@ -168,14 +145,10 @@ export async function deleteDapp(accountId: string, origin: string, dontNotifyDa } if (!dontNotifyDapp) { - updateDapps({ - type: 'disconnectDapp', - origin, - }); - await onDappDisconnected(accountId, origin); + callHook('onDappDisconnected', accountId, origin); } - onDappsChanged(); + callHook('onDappsChanged'); return true; } @@ -186,16 +159,16 @@ export async function deleteAllDapps(accountId: string) { const origins = Object.keys(await getDappsByOrigin(accountId)); await setAccountValue(accountId, 'dapps', {}); - await Promise.all(origins.map(async (origin) => { + origins.forEach((origin) => { onUpdate({ type: 'dappDisconnect', accountId, origin, }); - await onDappDisconnected(accountId, origin); - })); + callHook('onDappDisconnected', accountId, origin); + }); - onDappsChanged(); + callHook('onDappsChanged'); } export async function getDapps(accountId: string): Promise { @@ -240,12 +213,18 @@ export function getDappsState(): Promise { export async function removeAccountDapps(accountId: string) { await removeAccountValue(accountId, 'dapps'); - onDappsChanged(); + + callHook('onDappsChanged'); } export async function removeAllDapps() { await storage.removeItem('dapps'); - onDappsChanged(); + + callHook('onDappsChanged'); +} + +export function removeNetworkDapps(network: ApiNetwork) { + return removeNetworkAccountsValue(network, 'dapps'); } export function getSseLastEventId(): Promise { diff --git a/src/api/methods/index.ts b/src/api/methods/index.ts index 2202f03f..64d5cadf 100644 --- a/src/api/methods/index.ts +++ b/src/api/methods/index.ts @@ -2,7 +2,6 @@ export * from './auth'; export * from './wallet'; export * from './transactions'; export * from './nfts'; -export * from './extension'; export * from './polling'; export * from './accounts'; export * from './staking'; @@ -20,4 +19,3 @@ export { export { startSseConnection, } from '../tonConnect/sse'; -export * from './swap'; diff --git a/src/api/methods/init.ts b/src/api/methods/init.ts index a97a561e..00fd16c1 100644 --- a/src/api/methods/init.ts +++ b/src/api/methods/init.ts @@ -1,17 +1,20 @@ -import type { ApiInitArgs, ApiUpdate, OnApiUpdate } from '../types'; +import type { ApiInitArgs, OnApiUpdate } from '../types'; import { IS_SSE_SUPPORTED } from '../../config'; import { connectUpdater, startStorageMigration } from '../common/helpers'; -import * as dappMethods from '../dappMethods'; -import * as legacyDappMethods from '../dappMethods/legacy'; -import { IS_DAPP_SUPPORTED, IS_EXTENSION } from '../environment'; +import { IS_DAPP_SUPPORTED } from '../environment'; import * as tonConnect from '../tonConnect'; import { resetupSseConnection, sendSseDisconnect } from '../tonConnect/sse'; import * as methods from '.'; -export default async function init(_onUpdate: OnApiUpdate, args: ApiInitArgs) { - const onUpdate: OnApiUpdate = (update: ApiUpdate) => _onUpdate(update); +import { addHooks } from '../hooks'; +addHooks({ + onDappDisconnected: sendSseDisconnect, + onDappsChanged: resetupSseConnection, +}); + +export default async function init(onUpdate: OnApiUpdate, args: ApiInitArgs) { connectUpdater(onUpdate); methods.initPolling(onUpdate, methods.isAccountActive, args); @@ -20,16 +23,9 @@ export default async function init(_onUpdate: OnApiUpdate, args: ApiInitArgs) { methods.initStaking(onUpdate); if (IS_DAPP_SUPPORTED) { - const onDappChanged = IS_SSE_SUPPORTED ? resetupSseConnection : undefined; - const onDappDisconnected = IS_SSE_SUPPORTED ? sendSseDisconnect : undefined; - methods.initDapps(onUpdate, onDappChanged, onDappDisconnected); + methods.initDapps(onUpdate); tonConnect.initTonConnect(onUpdate); } - if (IS_EXTENSION) { - void methods.initExtension(onUpdate); - legacyDappMethods.initLegacyDappMethods(onUpdate); - dappMethods.initDappMethods(onUpdate); - } await startStorageMigration(); diff --git a/src/api/methods/polling.ts b/src/api/methods/polling.ts index fea6a70e..354a38fa 100644 --- a/src/api/methods/polling.ts +++ b/src/api/methods/polling.ts @@ -1,3 +1,5 @@ +import { randomBytes } from 'tweetnacl'; + import type { ApiBaseToken, ApiInitArgs, @@ -9,7 +11,7 @@ import type { OnApiUpdate, } from '../types'; -import { APP_VERSION, TON_TOKEN_SLUG } from '../../config'; +import { APP_ENV, APP_VERSION, TON_TOKEN_SLUG } from '../../config'; import { compareTransactions } from '../../util/compareTransactions'; import { logDebugError } from '../../util/logs'; import { pause } from '../../util/schedulers'; @@ -20,6 +22,7 @@ import { tryUpdateKnownAddresses } from '../common/addresses'; import { callBackendGet } from '../common/backend'; import { isUpdaterAlive, resolveBlockchainKey } from '../common/helpers'; import { txCallbacks } from '../common/txCallbacks'; +import { storage } from '../storages'; import { getBackendStakingState } from './staking'; type IsAccountActiveFn = (accountId: string) => boolean; @@ -28,7 +31,7 @@ const POLLING_INTERVAL = 1100; // 1.1 sec const BACKEND_POLLING_INTERVAL = 30000; // 30 sec const LONG_BACKEND_POLLING_INTERVAL = 60000; // 1 min -const TRANSACTIONS_WAITING_PAUSE = 2000; // 2 sec +const PAUSE_AFTER_BALANCE_CHANGE = 1000; // 1 sec const FIRST_TRANSACTIONS_LIMIT = 20; const NFT_FULL_POLLING_INTERVAL = 30000; // 30 sec @@ -37,6 +40,7 @@ const NFT_FULL_UPDATE_FREQUNCY = Math.round(NFT_FULL_POLLING_INTERVAL / POLLING_ let onUpdate: OnApiUpdate; let isAccountActive: IsAccountActiveFn; let origin: string; +let clientId: string | undefined; let preloadEnsurePromise: Promise; let pricesBySlug: Record; @@ -90,7 +94,6 @@ export async function setupBalanceBasedPolling(accountId: string, newestTxIds: A delete lastBalanceCache[accountId]; - let isFirstRun = true; let nftFromSec = Math.round(Date.now() / 1000); let nftUpdates: ApiNftUpdate[]; let i = 0; @@ -148,6 +151,8 @@ export async function setupBalanceBasedPolling(accountId: string, newestTxIds: A balance, }; + await pause(PAUSE_AFTER_BALANCE_CHANGE); + // Fetch and process token balances const tokenBalances = await blockchain.getAccountTokenBalances(accountId).catch(logAndRescue); if (!isUpdaterAlive(localOnUpdate) || !isAccountActive(accountId)) return; @@ -179,9 +184,6 @@ export async function setupBalanceBasedPolling(accountId: string, newestTxIds: A // Fetch transactions for tokens with a changed balance if (changedTokenSlugs.length) { - if (!isFirstRun) { - await pause(TRANSACTIONS_WAITING_PAUSE); - } const newTxIds = await processNewTokenTransactions(accountId, newestTxIds, changedTokenSlugs); newestTxIds = { ...newestTxIds, ...newTxIds }; } @@ -191,7 +193,6 @@ export async function setupBalanceBasedPolling(accountId: string, newestTxIds: A if (!isUpdaterAlive(localOnUpdate) || !isAccountActive(accountId)) return; nftUpdates.forEach(onUpdate); - isFirstRun = false; i++; } catch (err) { logDebugError('setupBalancePolling', err); @@ -276,6 +277,8 @@ export async function tryUpdateTokens(localOnUpdate: OnApiUpdate) { callBackendGet('/prices', undefined, { 'X-App-Origin': origin, 'X-App-Version': APP_VERSION, + 'X-App-ClientID': clientId ?? await getClientId(), + 'X-App-Env': APP_ENV, }) as Promise>, callBackendGet('/known-tokens') as Promise, ]); @@ -297,6 +300,15 @@ export async function tryUpdateTokens(localOnUpdate: OnApiUpdate) { } } +async function getClientId() { + clientId = await storage.getItem('clientId'); + if (!clientId) { + clientId = Buffer.from(randomBytes(10)).toString('hex'); + await storage.setItem('clientId', clientId); + } + return clientId; +} + export function sendUpdateTokens() { const tokens = getKnownTokens(); Object.values(tokens).forEach((token) => { diff --git a/src/api/methods/staking.ts b/src/api/methods/staking.ts index 8dd46a2e..8a88fcca 100644 --- a/src/api/methods/staking.ts +++ b/src/api/methods/staking.ts @@ -36,7 +36,7 @@ export async function submitStake(accountId: string, password: string, amount: s const localTransaction = createLocalTransaction(onUpdate, accountId, { amount: result.amount, fromAddress, - toAddress: result.resolvedAddress, + toAddress: result.normalizedAddress, comment: STAKE_COMMENT, fee: fee || '0', type: 'stake', @@ -51,7 +51,7 @@ export async function submitStake(accountId: string, password: string, amount: s export async function submitUnstake(accountId: string, password: string, fee?: string) { const blockchain = blockchains[resolveBlockchainKey(accountId)!]; - const toAddress = await fetchStoredAddress(accountId); + const fromAddress = await fetchStoredAddress(accountId); const result = await blockchain.submitUnstake(accountId, password); if ('error' in result) { @@ -60,8 +60,8 @@ export async function submitUnstake(accountId: string, password: string, fee?: s const localTransaction = createLocalTransaction(onUpdate, accountId, { amount: result.amount, - fromAddress: result.resolvedAddress, - toAddress, + fromAddress, + toAddress: result.normalizedAddress, comment: UNSTAKE_COMMENT, fee: fee || '0', type: 'unstakeRequest', diff --git a/src/api/methods/swap.ts b/src/api/methods/swap.ts deleted file mode 100644 index 148f7d05..00000000 --- a/src/api/methods/swap.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { - ApiSwapBuildRequest, - ApiSwapBuildResponse, - ApiSwapCurrency, - ApiSwapEstimateRequest, - ApiSwapEstimateResponse, - ApiSwapShortCurrency, - ApiSwapTonCurrency, -} from '../types'; - -import { callBackendGet, callBackendPost } from '../common/backend'; - -export function swapEstimate(params: ApiSwapEstimateRequest): Promise { - return callBackendPost('/swap/ton/estimate', params); -} - -export function swapBuild(params: ApiSwapBuildRequest): Promise { - return callBackendPost('/swap/ton/build', params); -} - -export function swapGetCurrencies(): Promise { - return callBackendGet('/swap/currencies'); -} - -export function swapGetTonCurrencies(): Promise { - return callBackendGet('/swap/ton/tokens'); -} - -export function swapGetTonPairs(): Promise { - return callBackendGet('/swap/ton/pairs'); -} diff --git a/src/api/methods/transactions.ts b/src/api/methods/transactions.ts index 47ab9a56..282a723a 100644 --- a/src/api/methods/transactions.ts +++ b/src/api/methods/transactions.ts @@ -60,7 +60,7 @@ export async function submitTransfer(options: ApiSubmitTransferOptions) { const localTransaction = createLocalTransaction(onUpdate, accountId, { amount, fromAddress, - toAddress, + toAddress: result.normalizedAddress, comment: shouldEncrypt ? undefined : comment, encryptedComment, fee: fee || '0', diff --git a/src/api/methods/wallet.ts b/src/api/methods/wallet.ts index 24c5ac32..a9989c67 100644 --- a/src/api/methods/wallet.ts +++ b/src/api/methods/wallet.ts @@ -60,7 +60,7 @@ export function confirmDappRequest(promiseId: string, data: any) { export function confirmDappRequestConnect(promiseId: string, data: { password?: string; - additionalAccountIds?: string[]; + accountId?: string; signature?: string; }) { dappPromises.resolveDappPromise(promiseId, data); diff --git a/src/api/providers/direct/connector.ts b/src/api/providers/direct/connector.ts index 6dc4266c..23396b23 100644 --- a/src/api/providers/direct/connector.ts +++ b/src/api/providers/direct/connector.ts @@ -1,4 +1,4 @@ -import type { MethodArgs, MethodResponse, Methods } from '../../methods/types'; +import type { AllMethodArgs, AllMethodResponse, AllMethods } from '../../types/methods'; import type { ApiInitArgs, OnApiUpdate } from '../../types'; import * as methods from '../../methods'; @@ -10,7 +10,7 @@ export function initApi(onUpdate: OnApiUpdate, initArgs: ApiInitArgs | (() => Ap init(onUpdate, args); } -export function callApi(fnName: T, ...args: MethodArgs): MethodResponse { +export function callApi(fnName: T, ...args: AllMethodArgs): AllMethodResponse { // @ts-ignore - return methods[fnName](...args) as MethodResponse; + return methods[fnName](...args) as AllMethodResponse; } diff --git a/src/api/providers/extension/connectorForPageScript.ts b/src/api/providers/extension/connectorForPageScript.ts index f0d00018..dc7087f4 100644 --- a/src/api/providers/extension/connectorForPageScript.ts +++ b/src/api/providers/extension/connectorForPageScript.ts @@ -1,8 +1,10 @@ -import type { OnApiDappUpdate } from '../../types/dappUpdates'; +import type { OnApiSiteUpdate } from '../../types/dappUpdates'; import type { - DappMethodResponse, DappMethods, LegacyDappMethodResponse, + LegacyDappMethodResponse, LegacyDappMethods, -} from '../../dappMethods/types'; + SiteMethodResponse, + SiteMethods, +} from '../../extensionMethods/types'; import { PAGE_CONNECTOR_CHANNEL } from './config'; import { logDebugError } from '../../../util/logs'; @@ -11,16 +13,16 @@ import { createConnector } from '../../../util/PostMessageConnector'; let connector: Connector; -type Methods = DappMethods & LegacyDappMethods; +type Methods = SiteMethods & LegacyDappMethods; type MethodResponse = ( - T extends keyof DappMethods - ? DappMethodResponse + T extends keyof SiteMethods + ? SiteMethodResponse : T extends keyof LegacyDappMethods ? LegacyDappMethodResponse : never ); -export function initApi(onUpdate: OnApiDappUpdate) { +export function initApi(onUpdate: OnApiSiteUpdate) { connector = createConnector(window, onUpdate, PAGE_CONNECTOR_CHANNEL, window.location.href); return connector; } diff --git a/src/api/providers/extension/pageContentProxy.ts b/src/api/providers/extension/pageContentProxy.ts index 3a675e2c..4f36c0e3 100644 --- a/src/api/providers/extension/pageContentProxy.ts +++ b/src/api/providers/extension/pageContentProxy.ts @@ -1,11 +1,8 @@ -import type { Runtime } from 'webextension-polyfill'; -import extension from '../../../lib/webextension-polyfill'; - import { CONTENT_SCRIPT_PORT, PAGE_CONNECTOR_CHANNEL } from './config'; const PAGE_ORIGIN = window.location.href; -let port: Runtime.Port; +let port: chrome.runtime.Port; window.addEventListener('message', handlePageMessage); @@ -18,7 +15,7 @@ function handlePageMessage(e: MessageEvent) { } function connectPort() { - port = extension.runtime.connect({ name: CONTENT_SCRIPT_PORT }); + port = chrome.runtime.connect({ name: CONTENT_SCRIPT_PORT }); port.onMessage.addListener(sendToPage); } diff --git a/src/api/providers/extension/providerForContentScript.ts b/src/api/providers/extension/providerForContentScript.ts index 44bdbd2c..b9aed2e6 100644 --- a/src/api/providers/extension/providerForContentScript.ts +++ b/src/api/providers/extension/providerForContentScript.ts @@ -1,16 +1,16 @@ import type { TonConnectMethodArgs, TonConnectMethods } from '../../tonConnect/types/misc'; -import type { OnApiDappUpdate } from '../../types/dappUpdates'; +import type { OnApiSiteUpdate } from '../../types/dappUpdates'; import type { - DappMethodArgs, - DappMethods, LegacyDappMethodArgs, LegacyDappMethods, -} from '../../dappMethods/types'; + SiteMethodArgs, + SiteMethods, +} from '../../extensionMethods/types'; import { CONTENT_SCRIPT_PORT, PAGE_CONNECTOR_CHANNEL } from './config'; import { createExtensionInterface } from '../../../util/createPostMessageInterface'; -import * as dappApi from '../../dappMethods'; -import * as legacyDappApi from '../../dappMethods/legacy'; +import * as legacyDappApi from '../../extensionMethods/legacy'; +import * as siteApi from '../../extensionMethods/sites'; import * as tonConnectApi from '../../tonConnect'; const ALLOWED_METHODS = new Set([ @@ -32,7 +32,7 @@ createExtensionInterface(CONTENT_SCRIPT_PORT, ( name: string, origin?: string, ...args: any[] ) => { if (name === 'init') { - return dappApi.connectDapp(args[0] as OnApiDappUpdate, legacyDappApi.onDappSendUpdates); + return siteApi.connectSite(args[0] as OnApiSiteUpdate, legacyDappApi.onDappSendUpdates); } if (!ALLOWED_METHODS.has(name)) { @@ -54,9 +54,9 @@ createExtensionInterface(CONTENT_SCRIPT_PORT, ( return method(...[request].concat(args) as TonConnectMethodArgs); } - const method = dappApi[name as keyof DappMethods]; + const method = siteApi[name as keyof SiteMethods]; // @ts-ignore - return method(...args as DappMethodArgs); -}, PAGE_CONNECTOR_CHANNEL, (onUpdate: OnApiDappUpdate) => { - dappApi.deactivateDapp(onUpdate); + return method(...args as SiteMethodArgs); +}, PAGE_CONNECTOR_CHANNEL, (onUpdate: OnApiSiteUpdate) => { + siteApi.deactivateSite(onUpdate); }, true); diff --git a/src/api/providers/extension/providerForPopup.ts b/src/api/providers/extension/providerForPopup.ts index 575fa05c..ba0fcbd8 100644 --- a/src/api/providers/extension/providerForPopup.ts +++ b/src/api/providers/extension/providerForPopup.ts @@ -1,16 +1,25 @@ +import type { ExtensionMethodArgs, ExtensionMethods } from '../../extensionMethods/types'; import type { MethodArgs, Methods } from '../../methods/types'; import type { ApiInitArgs, OnApiUpdate } from '../../types'; import { POPUP_PORT } from './config'; import { createExtensionInterface } from '../../../util/createPostMessageInterface'; import { disconnectUpdater } from '../../common/helpers'; +import * as extensionMethods from '../../extensionMethods'; +import initExtensionMethods from '../../extensionMethods/init'; import * as methods from '../../methods'; -import init from '../../methods/init'; +import initMethods from '../../methods/init'; -createExtensionInterface(POPUP_PORT, (name: string, origin?: string, ...args: any[]) => { +void createExtensionInterface(POPUP_PORT, (name: string, origin?: string, ...args: any[]) => { if (name === 'init') { - return init(args[0] as OnApiUpdate, args[1] as ApiInitArgs); + void initMethods(args[0] as OnApiUpdate, args[1] as ApiInitArgs); + return initExtensionMethods(args[0] as OnApiUpdate); } else { + if (name in extensionMethods) { + // @ts-ignore + return extensionMethods[name](...args as ExtensionMethodArgs); + } + const method = methods[name as keyof Methods]; // @ts-ignore return method(...args as MethodArgs); diff --git a/src/api/providers/worker/connector.ts b/src/api/providers/worker/connector.ts index b35b13c7..8cd976c4 100644 --- a/src/api/providers/worker/connector.ts +++ b/src/api/providers/worker/connector.ts @@ -1,4 +1,4 @@ -import type { MethodArgs, MethodResponse, Methods } from '../../methods/types'; +import type { AllMethodArgs, AllMethodResponse, AllMethods } from '../../types/methods'; import type { ApiInitArgs, OnApiUpdate } from '../../types'; import { logDebugError } from '../../../util/logs'; @@ -16,7 +16,7 @@ export function initApi(onUpdate: OnApiUpdate, initArgs: ApiInitArgs | (() => Ap return connector.init(args); } -export async function callApi(fnName: T, ...args: MethodArgs) { +export async function callApi(fnName: T, ...args: AllMethodArgs) { if (!connector) { logDebugError('API is not initialized'); return undefined; @@ -26,7 +26,7 @@ export async function callApi(fnName: T, ...args: Metho return await (connector.request({ name: fnName, args, - }) as MethodResponse); + }) as AllMethodResponse); } catch (err) { logDebugError('callApi', err); return undefined; diff --git a/src/api/storages/extension.ts b/src/api/storages/extension.ts index 51ce819b..b79285d8 100644 --- a/src/api/storages/extension.ts +++ b/src/api/storages/extension.ts @@ -1,11 +1,9 @@ -import extension from '../../lib/webextension-polyfill'; - import type { Storage } from './types'; import { IS_EXTENSION } from '../environment'; // eslint-disable-next-line no-restricted-globals -const storage = IS_EXTENSION ? extension.storage.local : undefined; +const storage = IS_EXTENSION ? self.chrome.storage.local : undefined; export default ((storage && { getItem: async (key) => (await storage.get(key))?.[key], diff --git a/src/api/storages/types.ts b/src/api/storages/types.ts index 8866d00f..cb3c1955 100644 --- a/src/api/storages/types.ts +++ b/src/api/storages/types.ts @@ -26,6 +26,7 @@ export type StorageKey = 'addresses' | 'accounts' | 'stateVersion' | 'currentAccountId' +| 'clientId' // For extension | 'dapps' | 'dappMethods:lastAccountId' diff --git a/src/api/tonConnect/index.ts b/src/api/tonConnect/index.ts index 11d59f4e..982c45ea 100644 --- a/src/api/tonConnect/index.ts +++ b/src/api/tonConnect/index.ts @@ -40,21 +40,18 @@ import { fetchKeyPair } from '../blockchains/ton/auth'; import { LEDGER_SUPPORTED_PAYLOADS } from '../blockchains/ton/constants'; import { toBase64Address, toRawAddress } from '../blockchains/ton/util/tonweb'; import { - fetchStoredAccount, fetchStoredAddress, fetchStoredPublicKey, getCurrentAccountId, + fetchStoredAccount, fetchStoredAddress, fetchStoredPublicKey, getCurrentAccountId, getCurrentAccountIdOrFail, } from '../common/accounts'; import { createDappPromise } from '../common/dappPromises'; import { createLocalTransaction, isUpdaterAlive } from '../common/helpers'; import { base64ToBytes, bytesToBase64, handleFetchErrors, sha256, } from '../common/utils'; -import { getCurrentAccountIdOrFail } from '../dappMethods'; -import { openPopupWindow } from '../dappMethods/window'; import { IS_EXTENSION } from '../environment'; import * as apiErrors from '../errors'; import { activateDapp, addDapp, - addDappToAccounts, deactivateAccountDapp, deactivateDapp, deleteDapp, @@ -66,6 +63,8 @@ import * as errors from './errors'; import { BadRequestError } from './errors'; import { isValidString, isValidUrl } from './utils'; +import { callHook } from '../hooks'; + const { Address } = TonWeb.utils; const ton = blockchains.ton; @@ -92,6 +91,7 @@ export async function connect( const dapp = { ...await fetchDappMetadata(origin, message.manifestUrl), connectedAt: Date.now(), + ...('sseOptions' in request && { sse: request.sseOptions }), }; const addressItem = message.items.find(({ name }) => name === 'ton_addr'); @@ -106,18 +106,20 @@ export async function connect( throw new errors.BadRequestError("Missing 'ton_addr'"); } - await openExtensionPopup(); - const accountId = await getCurrentAccountOrFail(); + const isOpened = await openExtensionPopup(); + let accountId = await getCurrentAccountOrFail(); const isConnected = await isDappConnected(accountId, origin); let promiseResult: { - additionalAccountIds?: string[]; + accountId?: string; password?: string; signature?: string; } | undefined; if (!isConnected || proof) { - await openExtensionPopup(true); + if (!isOpened) { + await openExtensionPopup(true); + } const { promiseId, promise } = createDappPromise(); @@ -135,12 +137,8 @@ export async function connect( promiseResult = await promise; - const { additionalAccountIds } = promiseResult!; - if (additionalAccountIds) { - await addDappToAccounts(dapp, [accountId].concat(additionalAccountIds)); - } else { - await addDapp(accountId, dapp); - } + accountId = promiseResult!.accountId!; + await addDapp(accountId, dapp); } const result = await reconnect(request, id); @@ -358,7 +356,9 @@ async function checkTransactionMessages(accountId: string, messages: Transaction }); const checkResult = await ton.checkMultiTransactionDraft(accountId, preparedMessages); - handleDraftError(checkResult); + if ('error' in checkResult) { + handleDraftError(checkResult.error); + } return { preparedMessages, @@ -561,23 +561,23 @@ async function validateRequest(request: ApiDappRequest, skipConnection = false) return { origin, accountId }; } -function handleDraftError({ error }: { error?: ApiTransactionDraftError }) { - if (error) { - onPopupUpdate({ - type: 'showError', - error, - }); - throw new errors.BadRequestError(error); - } +function handleDraftError(error: ApiTransactionDraftError) { + onPopupUpdate({ + type: 'showError', + error, + }); + throw new errors.BadRequestError(error); } async function openExtensionPopup(force?: boolean) { if (!IS_EXTENSION || (!force && onPopupUpdate && isUpdaterAlive(onPopupUpdate))) { - return; + return false; } - await openPopupWindow(); + await callHook('onWindowNeeded'); await initPromise; + + return true; } async function getCurrentAccountOrFail() { diff --git a/src/api/tonConnect/sse.ts b/src/api/tonConnect/sse.ts index df4eb60a..f88f7d6f 100644 --- a/src/api/tonConnect/sse.ts +++ b/src/api/tonConnect/sse.ts @@ -7,19 +7,17 @@ import type { } from '@tonconnect/protocol'; import nacl, { randomBytes } from 'tweetnacl'; -import type { ApiSseOptions } from '../types'; +import type { ApiDappRequest, ApiSseOptions } from '../types'; -import { buildAccountId, parseAccountId } from '../../util/account'; +import { parseAccountId } from '../../util/account'; import { extractKey } from '../../util/iteratees'; import { logDebug } from '../../util/logs'; -import { waitLogin } from '../common/accounts'; +import { getCurrentNetwork, waitLogin } from '../common/accounts'; import { bytesToHex, handleFetchErrors } from '../common/utils'; -import { getActiveAccountId } from '../methods/accounts'; import { getDappsState, getSseLastEventId, setSseLastEventId, - updateDapp, } from '../methods/dapps'; import * as tonConnect from './index'; @@ -42,60 +40,61 @@ export async function startSseConnection(url: string, deviceInfo: DeviceInfo) { const version = Number(params.get('v') as string); const appClientId = params.get('id') as string; - const request = JSON.parse(params.get('r') as string) as ConnectRequest; + const connectRequest = JSON.parse(params.get('r') as string) as ConnectRequest; const ret = params.get('ret') as 'back' | 'none' | string | null; - const origin = new URL(request.manifestUrl).origin; + const origin = new URL(connectRequest.manifestUrl).origin; logDebug('SSE Start connection:', { - version, appClientId, request, ret, origin, + version, appClientId, connectRequest, ret, origin, }); + const { secretKey: secretKeyArray, publicKey: publicKeyArray } = nacl.box.keyPair(); + const secretKey = bytesToHex(secretKeyArray); + const clientId = bytesToHex(publicKeyArray); + const lastOutputId = 0; - const accountId = getActiveAccountId()!; - const result = await tonConnect.connect({ origin }, request, lastOutputId) as ConnectEvent; + const request: ApiDappRequest = { + origin, + sseOptions: { + clientId, + appClientId, + secretKey, + lastOutputId, + }, + }; + + const result = await tonConnect.connect(request, connectRequest, lastOutputId) as ConnectEvent; if (result.event === 'connect') { result.payload.device = deviceInfo; } - const { secretKey: secretKeyArray, publicKey: publicKeyArray } = nacl.box.keyPair(); - const secretKey = bytesToHex(secretKeyArray); - const clientId = bytesToHex(publicKeyArray); - await sendMessage(result, secretKey, clientId, appClientId); if (result.event === 'connect_error') { return; } - await updateDapp(accountId, origin, (dapp) => ({ - ...dapp, - sse: { - clientId, - appClientId, - secretKey, - lastOutputId, - }, - })); - void resetupSseConnection(); } export async function resetupSseConnection() { closeEventSource(); - const [lastEventId, dappsState] = await Promise.all([ + const [lastEventId, dappsState, network] = await Promise.all([ getSseLastEventId(), getDappsState(), + getCurrentNetwork(), ]); - if (!dappsState) { + if (!dappsState || !network) { return; } - sseDapps = Object.entries(dappsState).reduce((result, [internalAccountId, dapps]) => { - const accountId = buildAccountId(parseAccountId(internalAccountId)); // TODO Issue #471 - for (const dapp of Object.values(dapps)) { - result.push({ ...dapp.sse!, accountId, origin: dapp.origin }); + sseDapps = Object.entries(dappsState).reduce((result, [accountId, dapps]) => { + if (parseAccountId(accountId).network === network) { + for (const dapp of Object.values(dapps)) { + result.push({ ...dapp.sse!, accountId, origin: dapp.origin }); + } } return result; }, [] as SseDapp[]); diff --git a/src/api/types/backend.ts b/src/api/types/backend.ts deleted file mode 100644 index 333103d9..00000000 --- a/src/api/types/backend.ts +++ /dev/null @@ -1,51 +0,0 @@ -export type ApiSwapEstimateRequest = { - from: string; - fromAmount: string; - to: string; - slippage: number; -}; - -export type ApiSwapEstimateResponse = ApiSwapEstimateRequest & { - toAmount: string; - toMinAmount: string; - networkFee: number; - swapFee: number; - impact: number; -}; - -export type ApiSwapBuildRequest = ApiSwapEstimateRequest & { - toAmount: string; - toMinAmount: string; - slippage: number; - fromAddress: string; - dexLabel: string; -}; - -export type ApiSwapBuildResponse = ApiSwapBuildRequest & { - transfer: { - toAddress: string; - amount: string; - payload: string; - }; -}; - -export type ApiSwapCurrency = { - name: string; - symbol: string; - image: string; - blockchain: string; - slug: string; - contract?: string; - decimals?: number; -}; - -export type ApiSwapTonCurrency = ApiSwapCurrency & { - blockchain: 'ton'; - decimals: number; -}; - -export type ApiSwapShortCurrency = { - name: string; - symbol: string; - slug: string; -}; diff --git a/src/api/types/dappUpdates.ts b/src/api/types/dappUpdates.ts index 5384cdb6..50aa6436 100644 --- a/src/api/types/dappUpdates.ts +++ b/src/api/types/dappUpdates.ts @@ -8,26 +8,26 @@ export type ApiDappUpdateAccounts = { accounts: string[]; }; -export type ApiDappUpdateTonMagic = { +export type ApiSiteUpdateTonMagic = { type: 'updateTonMagic'; isEnabled: boolean; }; -export type ApiDappUpdateDeeplinkHook = { +export type ApiSiteUpdateDeeplinkHook = { type: 'updateDeeplinkHook'; isEnabled: boolean; }; -export type ApiDappDisconnect = { - type: 'disconnectDapp'; +export type ApiSiteDisconnect = { + type: 'disconnectSite'; origin: string; }; -export type ApiDappUpdate = ApiLegacyDappUpdate -| ApiDappUpdateTonMagic -| ApiDappUpdateDeeplinkHook -| ApiDappDisconnect; +export type ApiSiteUpdate = ApiLegacyDappUpdate +| ApiSiteUpdateTonMagic +| ApiSiteUpdateDeeplinkHook +| ApiSiteDisconnect; export type ApiLegacyDappUpdate = ApiDappUpdateBalance | ApiDappUpdateAccounts; -export type OnApiDappUpdate = (update: ApiDappUpdate) => void; +export type OnApiSiteUpdate = (update: ApiSiteUpdate) => void; diff --git a/src/api/types/errors.ts b/src/api/types/errors.ts index 58a89d25..2affec70 100644 --- a/src/api/types/errors.ts +++ b/src/api/types/errors.ts @@ -6,6 +6,7 @@ export enum ApiTransactionDraftError { DomainNotResolved = 'DomainNotResolved', WalletNotInitialized = 'WalletNotInitialized', UnsupportedHardwarePayload = 'UnsupportedHardwarePayload', + InvalidAddressFormat = 'InvalidAddressFormat', } export enum ApiTransactionError { diff --git a/src/api/types/index.ts b/src/api/types/index.ts index 2fca2e58..805b540f 100644 --- a/src/api/types/index.ts +++ b/src/api/types/index.ts @@ -2,5 +2,4 @@ export * from './updates'; export * from './misc'; export * from './payload'; export * from './errors'; -export * from './backend'; export * from './storage'; diff --git a/src/api/types/methods.ts b/src/api/types/methods.ts new file mode 100644 index 00000000..c6a90835 --- /dev/null +++ b/src/api/types/methods.ts @@ -0,0 +1,6 @@ +import type { ExtensionMethods } from '../extensionMethods/types'; +import type { Methods } from '../methods/types'; + +export type AllMethods = Methods & ExtensionMethods; +export type AllMethodArgs = Parameters; +export type AllMethodResponse = ReturnType; diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index 5f25fcc1..6151d173 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -1,6 +1,7 @@ import type TonWeb from 'tonweb'; import type { ApiParsedPayload } from './payload'; +import type { ApiSseOptions } from './storage'; export type ApiWalletVersion = keyof typeof TonWeb.Wallets['all']; @@ -119,6 +120,7 @@ export interface ApiDappPermissions { export type ApiDappRequest = { origin?: string; accountId?: string; + sseOptions?: ApiSseOptions; } | { origin: string; accountId: string; diff --git a/src/assets/font-icons/accept.svg b/src/assets/font-icons/accept.svg index f657783b..1d551a69 100644 --- a/src/assets/font-icons/accept.svg +++ b/src/assets/font-icons/accept.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/dot.svg b/src/assets/font-icons/dot.svg index 58eda085..974982d4 100644 --- a/src/assets/font-icons/dot.svg +++ b/src/assets/font-icons/dot.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/params.svg b/src/assets/font-icons/params.svg new file mode 100644 index 00000000..3bf98138 --- /dev/null +++ b/src/assets/font-icons/params.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/replace.svg b/src/assets/font-icons/replace.svg new file mode 100644 index 00000000..0cbb4857 --- /dev/null +++ b/src/assets/font-icons/replace.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/search.svg b/src/assets/font-icons/search.svg index 65437dfc..66e33b2c 100644 --- a/src/assets/font-icons/search.svg +++ b/src/assets/font-icons/search.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/swap.svg b/src/assets/font-icons/swap.svg new file mode 100644 index 00000000..0981ff6d --- /dev/null +++ b/src/assets/font-icons/swap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/lottie/duck_run.tgs b/src/assets/lottie/duck_run.tgs new file mode 100644 index 00000000..0fc5098b Binary files /dev/null and b/src/assets/lottie/duck_run.tgs differ diff --git a/src/assets/lottiePreview/duck_run.png b/src/assets/lottiePreview/duck_run.png new file mode 100644 index 00000000..f3c64d5d Binary files /dev/null and b/src/assets/lottiePreview/duck_run.png differ diff --git a/src/components/App.tsx b/src/components/App.tsx index 89f0c775..2e0aaa97 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,5 +1,4 @@ import React, { memo, useEffect } from '../lib/teact/teact'; -import extension from '../lib/webextension-polyfill'; import { AppState } from '../global/types'; @@ -171,6 +170,6 @@ export default memo(withGlobal((global): StateProps => { })(App)); async function handleCloseBrowserTab() { - const tab = await extension.tabs.getCurrent(); - await extension.tabs.remove(tab.id!); + const tab = await chrome.tabs.getCurrent(); + await chrome.tabs.remove(tab.id!); } diff --git a/src/components/auth/Auth.module.scss b/src/components/auth/Auth.module.scss index a0956d6a..0f97f6ab 100644 --- a/src/components/auth/Auth.module.scss +++ b/src/components/auth/Auth.module.scss @@ -106,6 +106,42 @@ color: var(--color-black); } +.infoBlock { + padding: 1rem; + + font-size: 0.9375rem; + color: var(--color-gray-1); + + background: var(--color-background-first); + border-radius: var(--border-radius-default); +} + +.text { + margin-bottom: 0; + + line-height: 1.1875rem; + + & + & { + margin-top: 1.1875rem; + } +} + +.informationCheckbox { + align-self: flex-start; + + margin-top: 1.5rem; + margin-bottom: auto !important; + margin-inline-start: 0.25rem; + padding-bottom: 2rem; + + font-weight: 600; +} + +.informationCheckboxContent::before, +.informationCheckboxContent::after { + top: -0.0625rem !important; +} + .info { width: 100%; @@ -223,6 +259,42 @@ } } +.stickerAndTitle { + display: flex; + column-gap: 1rem; + align-items: center; + + margin-top: 3.375rem; + margin-bottom: 1.5rem; + + :global(html.is-electron) & { + margin-top: 0; + } + + > .sticker { + margin-top: 0; + } + + > .title { + margin: 0; + + text-align: left; + } +} + +.backupNotice { + margin: 2rem 1.5rem 0; + + font-size: 0.9375rem; +} + +.backupNoticeButtons { + display: flex; + column-gap: 1rem; + + margin: 1.5rem 1rem 1rem; +} + .modalSticker { margin: -0.375rem auto 1.25rem; } @@ -320,6 +392,10 @@ } } + &_wide { + width: 100%; + } + &_mini { min-width: unset !important; } diff --git a/src/components/auth/Auth.tsx b/src/components/auth/Auth.tsx index 9f9bd7d0..45d7c3a2 100644 --- a/src/components/auth/Auth.tsx +++ b/src/components/auth/Auth.tsx @@ -12,9 +12,9 @@ import { useDeviceScreen } from '../../hooks/useDeviceScreen'; import SettingsAbout from '../settings/SettingsAbout'; import Transition from '../ui/Transition'; -import AuthCreateBackup from './AuthCreateBackup'; import AuthCreatePassword from './AuthCreatePassword'; import AuthCreatingWallet from './AuthCreatingWallet'; +import AuthDisclaimer from './AuthDisclaimer'; import AuthImportMnemonic from './AuthImportMnemonic'; import AuthStart from './AuthStart'; @@ -59,10 +59,27 @@ const Auth = ({ return ; case AuthState.createPassword: return ; - case AuthState.createBackup: - return ; + case AuthState.disclaimerAndBackup: + return ( + + ); case AuthState.importWallet: return ; + case AuthState.disclaimer: + return ( + + ); case AuthState.importWalletCreatePassword: return ; case AuthState.about: diff --git a/src/components/auth/AuthCreateBackup.tsx b/src/components/auth/AuthDisclaimer.tsx similarity index 52% rename from src/components/auth/AuthCreateBackup.tsx rename to src/components/auth/AuthDisclaimer.tsx index 3cff9c6e..afe0c181 100644 --- a/src/components/auth/AuthCreateBackup.tsx +++ b/src/components/auth/AuthDisclaimer.tsx @@ -1,5 +1,6 @@ import React, { memo, useCallback, useState } from '../../lib/teact/teact'; +import { ANIMATED_STICKER_MIDDLE_SIZE_PX } from '../../config'; import { getActions } from '../../global'; import renderText from '../../global/helpers/renderText'; import buildClassName from '../../util/buildClassName'; @@ -7,9 +8,12 @@ import { ANIMATED_STICKERS_PATHS } from '../ui/helpers/animatedAssets'; import useFlag from '../../hooks/useFlag'; import useLang from '../../hooks/useLang'; +import useLastCallback from '../../hooks/useLastCallback'; +import useShowTransition from '../../hooks/useShowTransition'; import AnimatedIconWithPreview from '../ui/AnimatedIconWithPreview'; import Button from '../ui/Button'; +import Checkbox from '../ui/Checkbox'; import Modal from '../ui/Modal'; import Transition from '../ui/Transition'; import MnemonicCheck from './MnemonicCheck'; @@ -21,6 +25,7 @@ import styles from './Auth.module.scss'; interface OwnProps { isActive?: boolean; + isImport?: boolean; mnemonic?: string[]; checkIndexes?: number[]; } @@ -33,24 +38,40 @@ enum BackupState { const SLIDE_ANIMATION_DURATION_MS = 250; -const AuthCreateBackup = ({ isActive, mnemonic, checkIndexes }: OwnProps) => { - const { afterCheckMnemonic, skipCheckMnemonic, restartCheckMnemonicIndexes } = getActions(); +const AuthDisclaimer = ({ + isActive, isImport, mnemonic, checkIndexes, +}: OwnProps) => { + const { + afterCheckMnemonic, + skipCheckMnemonic, + restartCheckMnemonicIndexes, + confirmDisclaimer, + } = getActions(); const lang = useLang(); const [isModalOpen, openModal, closeModal] = useFlag(); + const [isInformationConfirmed, setIsInformationConfirmed] = useState(false); + const { + shouldRender: shouldRenderStartButton, + transitionClassNames: startButtonTransitionClassNames, + } = useShowTransition(isInformationConfirmed && isImport); const [renderingKey, setRenderingKey] = useState(BackupState.Accept); const [nextKey, setNextKey] = useState(BackupState.View); - const handleModalClose = useCallback(() => { + const handleCloseBackupWarningModal = useLastCallback(() => { + setIsInformationConfirmed(false); + }); + + const handleModalClose = useLastCallback(() => { setRenderingKey(BackupState.Accept); setNextKey(BackupState.View); - }, []); + }); - const handleMnemonicView = useCallback(() => { + const handleMnemonicView = useLastCallback(() => { setRenderingKey(BackupState.View); setNextKey(BackupState.Confirm); - }, []); + }); const handleRestartCheckMnemonic = useCallback(() => { handleMnemonicView(); @@ -60,10 +81,10 @@ const AuthCreateBackup = ({ isActive, mnemonic, checkIndexes }: OwnProps) => { }, SLIDE_ANIMATION_DURATION_MS); }, [handleMnemonicView, restartCheckMnemonicIndexes]); - const handleShowMnemonicCheck = useCallback(() => { + const handleShowMnemonicCheck = useLastCallback(() => { setRenderingKey(BackupState.Confirm); setNextKey(undefined); - }, []); + }); const handleMnemonicCheckSubmit = useCallback(() => { closeModal(); @@ -103,34 +124,63 @@ const AuthCreateBackup = ({ isActive, mnemonic, checkIndexes }: OwnProps) => { return (
- -
{lang('Create Backup')}
-
-

{renderText(lang('$auth_backup_description1'))}

-

{renderText(lang('$auth_backup_description2'))}

-

{renderText(lang('$auth_backup_description3'))}

+
+ +
{lang('Use Responsibly')}
+
+
+

{renderText(lang('$auth_responsibly_description1'))}

+

{renderText(lang('$auth_responsibly_description2'))}

+

{renderText(lang('$auth_responsibly_description3'))}

+

{renderText(lang('$auth_responsibly_description4'))}

-
- +
+ )} +
+ + +

{renderText(lang('$auth_backup_warning_notice'))}

+
+
-
+ { ); }; -export default memo(AuthCreateBackup); +export default memo(AuthDisclaimer); diff --git a/src/components/auth/AuthStart.tsx b/src/components/auth/AuthStart.tsx index fc813379..282c16f6 100644 --- a/src/components/auth/AuthStart.tsx +++ b/src/components/auth/AuthStart.tsx @@ -50,7 +50,7 @@ const AuthStart = () => { onClick={openAbout} > {lang('More about MyTonWallet')} - +
@@ -189,7 +189,7 @@ function AccountSelector({ )}
diff --git a/src/components/main/sections/Content/Activity.tsx b/src/components/main/sections/Content/Activity.tsx index bca22ac0..0f885d91 100644 --- a/src/components/main/sections/Content/Activity.tsx +++ b/src/components/main/sections/Content/Activity.tsx @@ -1,12 +1,12 @@ import React, { - memo, useEffect, useMemo, + memo, useEffect, useMemo, useRef, useState, } from '../../../../lib/teact/teact'; import type { ApiToken, ApiTransaction } from '../../../../api/types'; import { ANIMATED_STICKER_BIG_SIZE_PX, TON_TOKEN_SLUG } from '../../../../config'; import { getActions, withGlobal } from '../../../../global'; -import { getIsTxIdLocal, getIsTynyTransaction } from '../../../../global/helpers'; +import { getIsTinyTransaction, getIsTxIdLocal } from '../../../../global/helpers'; import { selectCurrentAccountState, selectIsNewWallet } from '../../../../global/selectors'; import buildClassName from '../../../../util/buildClassName'; import { compareTransactions } from '../../../../util/compareTransactions'; @@ -41,6 +41,7 @@ type StateProps = { tokensBySlug?: Record; apyValue: number; savedAddresses?: Record; + isHistoryEndReached?: boolean; }; interface TransactionDateGroup { @@ -49,6 +50,7 @@ interface TransactionDateGroup { } const FURTHER_SLICE = 50; +const LOAD_MORE_REQUEST_TIMEOUT = 3_000; function Activity({ isActive, @@ -62,11 +64,16 @@ function Activity({ areTinyTransfersHidden, apyValue, savedAddresses, + isHistoryEndReached, }: OwnProps & StateProps) { - const { fetchTokenTransactions, fetchAllTransactions, showTransactionInfo } = getActions(); + const { + fetchTokenTransactions, fetchAllTransactions, showTransactionInfo, resetIsHistoryEndReached, + } = getActions(); const lang = useLang(); const { isLandscape } = useDeviceScreen(); + const [isFetching, setIsFetching] = useState(!isNewWallet); + const loadMoreTimeout = useRef(); const txIds = useMemo(() => { let idList: string[] | undefined; @@ -96,7 +103,7 @@ function Activity({ const transactions = useMemo(() => { if (!txIds) { - return undefined; + return []; } const allTransactions = txIds @@ -105,7 +112,7 @@ function Activity({ return Boolean( transaction?.slug && (!slug || transaction.slug === slug) - && (!areTinyTransfersHidden || !getIsTynyTransaction(transaction, tokensBySlug![transaction.slug])), + && (!areTinyTransfersHidden || !getIsTinyTransaction(transaction, tokensBySlug![transaction.slug])), ); }) as ApiTransaction[]; @@ -147,13 +154,30 @@ function Activity({ } }); - const { handleIntersection } = useInfiniteLoader({ isLoading, loadMore }); + const isLoadingDisabled = isHistoryEndReached || isLoading; + const { handleIntersection } = useInfiniteLoader({ isDisabled: isLoadingDisabled, isLoading, loadMore }); + + const handleFetchingState = useLastCallback(() => { + clearTimeout(loadMoreTimeout.current); + loadMoreTimeout.current = setTimeout(() => { + setIsFetching(false); + }, LOAD_MORE_REQUEST_TIMEOUT); + }); + + useEffect(() => { + if (isActive) { + setIsFetching(!isNewWallet); + resetIsHistoryEndReached(); + handleFetchingState(); + } + }, [handleFetchingState, isActive, isNewWallet, loadMore, slug]); useEffect(() => { - if (!transactions?.length && txIds?.length) { + if (!transactions.length) { loadMore(); + handleFetchingState(); } - }, [loadMore, txIds, transactions]); + }, [handleFetchingState, loadMore, transactions, txIds]); const handleTransactionClick = useLastCallback((txId: string) => { showTransactionInfo({ txId }); @@ -187,8 +211,7 @@ function Activity({ )); } - - if (transactions === undefined || (!transactions?.length && txIds?.length)) { + if (!transactions.length && isFetching) { return (
@@ -232,7 +255,9 @@ export default memo( const accountState = selectCurrentAccountState(global); const isNewWallet = selectIsNewWallet(global); const slug = accountState?.currentTokenSlug; - const { txIdsBySlug, byTxId, isLoading } = accountState?.transactions || {}; + const { + txIdsBySlug, byTxId, isLoading, isHistoryEndReached, + } = accountState?.transactions || {}; return { currentAccountId: currentAccountId!, slug, @@ -244,6 +269,7 @@ export default memo( areTinyTransfersHidden: global.settings.areTinyTransfersHidden, apyValue: accountState?.poolState?.lastApy || 0, savedAddresses: accountState?.savedAddresses, + isHistoryEndReached, }; })(Activity), ); diff --git a/src/components/main/sections/Content/Content.tsx b/src/components/main/sections/Content/Content.tsx index 8e5d6b73..02c42f16 100644 --- a/src/components/main/sections/Content/Content.tsx +++ b/src/components/main/sections/Content/Content.tsx @@ -1,7 +1,7 @@ import React, { memo, useCallback, useMemo } from '../../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../../global'; -import { selectCurrentAccountTokens } from '../../../../global/selectors'; +import { selectCurrentAccountTokens, selectIsHardwareAccount } from '../../../../global/selectors'; import buildClassName from '../../../../util/buildClassName'; import { useDeviceScreen } from '../../../../hooks/useDeviceScreen'; @@ -23,12 +23,13 @@ interface OwnProps { interface StateProps { tokenCount: number; + isNftSupported: boolean; } const MIN_ASSETS_FOR_DESKTOP_TAB_VIEW = 5; function Content({ - activeTabIndex, tokenCount, setActiveTabIndex, onStakedTokenClick, + activeTabIndex, tokenCount, setActiveTabIndex, onStakedTokenClick, isNftSupported, }: OwnProps & StateProps) { const { selectToken } = getActions(); const { isLandscape } = useDeviceScreen(); @@ -42,9 +43,9 @@ function Content({ ? [{ id: 'assets', title: lang('Assets') as string, className: styles.tab }] : []), { id: 'activity', title: lang('Activity') as string, className: styles.tab }, - { id: 'nft', title: lang('NFT') as string, className: styles.tab }, + ...(isNftSupported ? [{ id: 'nft', title: lang('NFT') as string, className: styles.tab }] : []), ], - [lang, shouldShowSeparateAssetsPanel], + [lang, shouldShowSeparateAssetsPanel, isNftSupported], ); activeTabIndex = Math.min(activeTabIndex, TABS.length - 1); @@ -116,9 +117,11 @@ export default memo( detachWhenChanged(global.currentAccountId); const tokens = selectCurrentAccountTokens(global); + const isLedger = selectIsHardwareAccount(global); return { tokenCount: tokens?.length ?? 0, + isNftSupported: !isLedger, }; })(Content), ); diff --git a/src/components/main/sections/Content/Token.tsx b/src/components/main/sections/Content/Token.tsx index 6a94f0dc..b1784c30 100644 --- a/src/components/main/sections/Content/Token.tsx +++ b/src/components/main/sections/Content/Token.tsx @@ -75,7 +75,12 @@ function Token({ return undefined; } - return 0 ? 'icon-arrow-up' : 'icon-arrow-down')} />; + return ( + 0 ? 'icon-arrow-up' : 'icon-arrow-down')} + aria-hidden + /> + ); } function renderInvestorView() { @@ -83,7 +88,10 @@ function Token({
{renderChangeIcon()}% - +
@@ -122,7 +130,10 @@ function Token({ )} @@ -487,15 +490,17 @@ function TransferInitial({ - + {isCommentSupported && ( + + )}
{withMenu && ( = ({ + message, + iconClassName, + tooltipClassName, +}) => { + const [isOpen, open, close] = useFlag(); + const { transitionClassNames, shouldRender } = useShowTransition(isOpen); + + // eslint-disable-next-line no-null/no-null + const iconRef = useRef(null); + // eslint-disable-next-line no-null/no-null + const tooltipRef = useRef(null); + + const tooltipStyle = useRef(); + const arrowStyle = useRef(); + + useEffect(() => { + if (!iconRef.current || !tooltipRef.current) return; + + const { top, left, width } = iconRef.current.getBoundingClientRect(); + const { + width: tooltipWidth, + height: tooltipHeight, + } = tooltipRef.current.getBoundingClientRect(); + + const tooltipCenter = (window.innerWidth - tooltipWidth) / 2; + const tooltipVerticalStyle = `top: ${top - tooltipHeight - ARROW_WIDTH}px;`; + const tooltipHorizontalStyle = `left: ${tooltipCenter}px;`; + const arrowHorizontalStyle = `left: ${left - tooltipCenter + width / 2 - ARROW_WIDTH / 2}px;`; + const arrowVerticalStyle = `top: ${tooltipHeight - ARROW_WIDTH / 2 - 1}px;`; + + tooltipStyle.current = `${tooltipVerticalStyle} ${tooltipHorizontalStyle}`; + arrowStyle.current = `${arrowVerticalStyle} ${arrowHorizontalStyle}`; + }, [shouldRender]); + + return ( +
+ {shouldRender && ( + +
+
+ {message} +
+
+
+ + )} + +
+ ); +}; + +export default memo(IconWithTooltip); diff --git a/src/components/ui/Input.module.scss b/src/components/ui/Input.module.scss index 2761a1c3..71bb2816 100644 --- a/src/components/ui/Input.module.scss +++ b/src/components/ui/Input.module.scss @@ -286,3 +286,15 @@ textarea.input { white-space: normal; } } + +.swapCorner { + &::after { + box-shadow: inset 0 0 0 0.125rem var(--color-blue); + } + + &_error { + &::after { + box-shadow: inset 0 0 0 0.125rem var(--color-red); + } + } +} \ No newline at end of file diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx index 1adcc46e..623a9955 100644 --- a/src/components/ui/Input.tsx +++ b/src/components/ui/Input.tsx @@ -139,7 +139,7 @@ function Input({ aria-label={lang('Change password visibility')} tabIndex={-1} > - + )} {error && !label && ( diff --git a/src/components/ui/RichNumberInput.tsx b/src/components/ui/RichNumberInput.tsx index aa9845ac..968e3b29 100644 --- a/src/components/ui/RichNumberInput.tsx +++ b/src/components/ui/RichNumberInput.tsx @@ -1,7 +1,7 @@ +import { Big } from '../../lib/big.js'; import type { TeactNode } from '../../lib/teact/teact'; import React, { - memo, - useCallback, useEffect, useRef, + memo, useEffect, useRef, } from '../../lib/teact/teact'; import { FRACTION_DIGITS } from '../../config'; @@ -11,12 +11,13 @@ import { saveCaretPosition } from '../../util/saveCaretPosition'; import useFlag from '../../hooks/useFlag'; import useLang from '../../hooks/useLang'; +import useLastCallback from '../../hooks/useLastCallback'; import styles from './Input.module.scss'; type OwnProps = { id?: string; - labelText?: string; + labelText?: React.ReactNode; value?: number; hasError?: boolean; suffix?: string; @@ -24,11 +25,14 @@ type OwnProps = { inputClassName?: string; labelClassName?: string; valueClassName?: string; + cornerClassName?: string; children?: TeactNode; onChange?: (value?: number) => void; onBlur?: NoneToVoidFunction; + onFocus?: NoneToVoidFunction; onPressEnter?: (e: React.KeyboardEvent) => void; decimals?: number; + disabled?: boolean; }; function RichNumberInput({ @@ -42,17 +46,20 @@ function RichNumberInput({ inputClassName, labelClassName, valueClassName, + cornerClassName, onChange, onBlur, + onFocus, onPressEnter, decimals = FRACTION_DIGITS, + disabled, }: OwnProps) { // eslint-disable-next-line no-null/no-null const inputRef = useRef(null); const lang = useLang(); const [hasFocus, markHasFocus, unmarkHasFocus] = useFlag(false); - const updateHtml = useCallback((parts?: RegExpMatchArray) => { + const updateHtml = useLastCallback((parts?: RegExpMatchArray) => { const input = inputRef.current!; const newHtml = parts ? buildContentHtml(parts, suffix, decimals) : ''; @@ -64,7 +71,7 @@ function RichNumberInput({ // Trick to remove pseudo-element with placeholder in this tick input.classList.toggle(styles.isEmpty, !newHtml.length); - }, [decimals, suffix]); + }); useEffect(() => { const newValue = castValue(value, decimals); @@ -75,7 +82,7 @@ function RichNumberInput({ if (value !== newValue) { onChange?.(newValue); } - }, [decimals, onChange, updateHtml, value]); + }, [decimals, onChange, updateHtml, value, suffix]); function handleChange(e: React.FormEvent) { const inputValue = e.currentTarget.innerText.trim(); @@ -94,10 +101,19 @@ function RichNumberInput({ } } - const handleBlur = useCallback(() => { + const handleFocus = useLastCallback(() => { + if (disabled) return; + + markHasFocus(); + onFocus?.(); + }); + + const handleBlur = useLastCallback(() => { + if (disabled) return; + unmarkHasFocus(); onBlur?.(); - }, [onBlur, unmarkHasFocus]); + }); const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && onPressEnter) { @@ -122,10 +138,15 @@ function RichNumberInput({ hasError && styles.error, labelClassName, ); + const cornerFullClass = buildClassName( + cornerClassName, + hasFocus && styles.swapCorner, + hasError && styles.swapCorner_error, + ); return (
- {labelText && ( + {Boolean(labelText) && (
); } function getParts(value: string, decimals: number) { - return value.match(new RegExp(`^(\\d+)([.,])?(\\d{1,${decimals}})?$`)) || undefined; + const regex = new RegExp(`^(\\d+)([.,])?(\\d{1,${decimals}})?$`); + // Correct problem with numbers like 1e-8 + if (value.includes('e-')) { + Big.NE = -decimals - 1; + return new Big(value).toString().match(regex) || undefined; + } + return value.match(regex) || undefined; } function castValue(value?: number, decimals?: number) { diff --git a/src/components/ui/Tab.tsx b/src/components/ui/Tab.tsx index 1ae688bb..ec2dcce0 100644 --- a/src/components/ui/Tab.tsx +++ b/src/components/ui/Tab.tsx @@ -84,7 +84,7 @@ function Tab({ > {title} - +
); diff --git a/src/components/ui/Tooltip.tsx b/src/components/ui/Tooltip.tsx deleted file mode 100644 index f0780b80..00000000 --- a/src/components/ui/Tooltip.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import type { FC } from '../../lib/teact/teact'; -import React, { memo } from '../../lib/teact/teact'; - -import buildClassName from '../../util/buildClassName'; -import stopEvent from '../../util/stopEvent'; - -import useShowTransition from '../../hooks/useShowTransition'; - -import styles from './Tooltip.module.scss'; - -type OwnProps = { - isOpen?: boolean; - message: string; - className?: string; -}; - -const Tooltip: FC = ({ - isOpen, - message, - className, -}) => { - const { transitionClassNames, shouldRender } = useShowTransition(isOpen); - - if (!shouldRender) { - return undefined; - } - - return ( -
-
- {message} -
-
- ); -}; - -export default memo(Tooltip); diff --git a/src/components/ui/helpers/animatedAssets.ts b/src/components/ui/helpers/animatedAssets.ts index 144e2581..b1125900 100644 --- a/src/components/ui/helpers/animatedAssets.ts +++ b/src/components/ui/helpers/animatedAssets.ts @@ -3,6 +3,7 @@ import forge from '../../../assets/lottie/duck_forges.tgs'; import happy from '../../../assets/lottie/duck_happy.tgs'; import hello from '../../../assets/lottie/duck_hello.tgs'; import noData from '../../../assets/lottie/duck_no-data.tgs'; +import run from '../../../assets/lottie/duck_run.tgs'; import snitch from '../../../assets/lottie/duck_snitch.tgs'; import thumbUp from '../../../assets/lottie/duck_thumb.tgs'; import holdTon from '../../../assets/lottie/duck_ton.tgs'; @@ -12,6 +13,7 @@ import forgePreview from '../../../assets/lottiePreview/duck_forges.png'; import happyPreview from '../../../assets/lottiePreview/duck_happy.png'; import helloPreview from '../../../assets/lottiePreview/duck_hello.png'; import noDataPreview from '../../../assets/lottiePreview/duck_no-data.png'; +import runPreview from '../../../assets/lottiePreview/duck_run.png'; import snitchPreview from '../../../assets/lottiePreview/duck_snitch.png'; import thumbUpPreview from '../../../assets/lottiePreview/duck_thumb.png'; import holdTonPreview from '../../../assets/lottiePreview/duck_ton.png'; @@ -27,6 +29,7 @@ export const ANIMATED_STICKERS_PATHS = { noData, forge, wait, + run, helloPreview, snitchPreview, billPreview, @@ -36,4 +39,5 @@ export const ANIMATED_STICKERS_PATHS = { noDataPreview, forgePreview, waitPreview, + runPreview, }; diff --git a/src/config.ts b/src/config.ts index f7121088..9df39267 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,16 +1,15 @@ import type { LangItem } from './global/types'; +export const APP_ENV = process.env.APP_ENV; + export const APP_NAME = process.env.APP_NAME || 'MyTonWallet'; export const APP_VERSION = process.env.APP_VERSION!; -export const DEBUG = ( - process.env.APP_ENV !== 'production' && process.env.APP_ENV !== 'perf' && process.env.APP_ENV !== 'test' -); +export const DEBUG = APP_ENV !== 'production' && APP_ENV !== 'perf' && APP_ENV !== 'test'; export const DEBUG_MORE = false; -export const IS_MOCKED_CLIENT = process.env.APP_MOCKED_CLIENT === '1'; -export const IS_TEST = process.env.APP_ENV === 'test'; -export const IS_PERF = process.env.APP_ENV === 'perf'; +export const IS_TEST = APP_ENV === 'test'; +export const IS_PERF = APP_ENV === 'perf'; export const IS_ELECTRON = process.env.IS_ELECTRON; export const IS_SSE_SUPPORTED = IS_ELECTRON; @@ -26,6 +25,7 @@ export const MOBILE_SCREEN_MAX_WIDTH = 700; // px export const ANIMATION_END_DELAY = 50; +export const ANIMATED_STICKER_TINY_SIZE_PX = 70; export const ANIMATED_STICKER_SMALL_SIZE_PX = 110; export const ANIMATED_STICKER_MIDDLE_SIZE_PX = 120; export const ANIMATED_STICKER_DEFAULT_PX = 150; @@ -38,6 +38,8 @@ export const DEFAULT_LANDSCAPE_ACTION_TAB_ID = 1; export const DEFAULT_DECIMAL_PLACES = 9; +export const DEFAULT_SLIPPAGE_VALUE = 0.5; + export const TOKEN_INFO = { toncoin: { name: 'Toncoin', @@ -85,12 +87,13 @@ export const GETGEMS_BASE_MAINNET_URL = 'https://getgems.io/'; export const GETGEMS_BASE_TESTNET_URL = 'https://testnet.getgems.io/'; export const TON_TOKEN_SLUG = 'toncoin'; +export const JWBTC_TOKEN_SLUG = 'ton-eqdcbkghmc'; export const PROXY_HOSTS = process.env.PROXY_HOSTS; export const TINY_TRANSFER_MAX_COST = 0.01; -export const LANG_CACHE_NAME = 'mtw-lang-15'; +export const LANG_CACHE_NAME = 'mtw-lang-23'; export const LANG_LIST: LangItem[] = [{ langCode: 'en', diff --git a/src/electron/config.yml b/src/electron/config.yml index d9b17a3b..169796d7 100644 --- a/src/electron/config.yml +++ b/src/electron/config.yml @@ -7,6 +7,7 @@ extraMetadata: files: - "dist" - "package.json" + - "!dist/get" - "!node_modules" directories: buildResources: "./public" @@ -20,7 +21,7 @@ publish: provider: "github" owner: "mytonwalletorg" repo: "mytonwallet" - releaseType: "release" + releaseType: "draft" win: target: "nsis" icon: "public/icon-electron-windows.ico" diff --git a/src/extension/contentScript.ts b/src/extension/contentScript.ts index c1ed5165..87a9da50 100644 --- a/src/extension/contentScript.ts +++ b/src/extension/contentScript.ts @@ -1,11 +1,9 @@ -import extension from '../lib/webextension-polyfill'; - import '../api/providers/extension/pageContentProxy'; (function injectScript() { const scriptTag = document.createElement('script'); scriptTag.async = true; - scriptTag.src = extension.runtime.getURL('/extensionPageScript.js'); + scriptTag.src = chrome.runtime.getURL('/extensionPageScript.js'); const container = document.head || document.documentElement; container.appendChild(scriptTag); diff --git a/src/extension/manifest.json b/src/extension/manifest.json index 3c7aeeb5..be0167da 100644 --- a/src/extension/manifest.json +++ b/src/extension/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "MyTonWallet · My TON Wallet", "description": "The most feature-rich TON extension – with support of multi-accounts, tokens, NFT, TON DNS, TON Sites, TON Proxy, and TON Magic.", - "version": "%%VERSION%%", + "version": "%%SET BY WEBPACK%%", "icons": { "192": "icon-192x192.png", "384": "icon-384x384.png", @@ -47,7 +47,5 @@ ] } ], - "content_security_policy": { - "extension_pages": "default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; font-src https://fonts.gstatic.com/; style-src 'self' https://fonts.googleapis.com/; img-src 'self' data: https:; connect-src https: http://localhost:8081" - } + "content_security_policy": "%%SET BY WEBPACK%%" } diff --git a/src/extension/pageScript/index.ts b/src/extension/pageScript/index.ts index 4bc3e2d0..170ea023 100644 --- a/src/extension/pageScript/index.ts +++ b/src/extension/pageScript/index.ts @@ -1,4 +1,4 @@ -import type { ApiDappUpdate } from '../../api/types/dappUpdates'; +import type { ApiSiteUpdate } from '../../api/types/dappUpdates'; import { callApi, initApi } from '../../api/providers/extension/connectorForPageScript'; import { doDeeplinkHook } from './deeplinkHook'; @@ -12,7 +12,7 @@ const apiConnector = initApi(onUpdate); const tonProvider = initTonProvider(apiConnector); const tonConnect = initTonConnect(apiConnector); -function onUpdate(update: ApiDappUpdate) { +function onUpdate(update: ApiSiteUpdate) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { type, ...args } = update; @@ -31,7 +31,7 @@ function onUpdate(update: ApiDappUpdate) { return; } - if (type === 'disconnectDapp') { + if (type === 'disconnectSite') { const { origin } = update; if (origin === siteOrigin) { tonConnect.onDisconnect(); diff --git a/src/global/actions/api/auth.ts b/src/global/actions/api/auth.ts index 2abc900f..1185b268 100644 --- a/src/global/actions/api/auth.ts +++ b/src/global/actions/api/auth.ts @@ -1,25 +1,37 @@ import { AppState, AuthState, HardwareConnectState } from '../../types'; import { MNEMONIC_CHECK_COUNT, MNEMONIC_COUNT } from '../../../config'; -import { buildAccountId, parseAccountId } from '../../../util/account'; +import { parseAccountId } from '../../../util/account'; import { cloneDeep } from '../../../util/iteratees'; -import { - connectLedger, getFirstLedgerWallets, importLedgerWallet, waitLedgerTonApp, -} from '../../../util/ledger'; import { pause } from '../../../util/schedulers'; import { callApi } from '../../../api'; import { addActionHandler, getGlobal, setGlobal } from '../..'; import { INITIAL_STATE } from '../../initialState'; import { - createAccount, updateAuth, updateCurrentAccountsState, updateHardware, + clearCurrentTransfer, + createAccount, + updateAuth, + updateCurrentAccountState, + updateHardware, + updateSettings, } from '../../reducers'; -import { selectCurrentNetwork, selectFirstNonHardwareAccount, selectNewestTxIds } from '../../selectors'; +import { + selectCurrentNetwork, + selectFirstNonHardwareAccount, + selectNetworkAccountsMemoized, + selectNewestTxIds, +} from '../../selectors'; const CREATING_DURATION = 3300; addActionHandler('restartAuth', (global) => { if (global.currentAccountId) { global = { ...global, appState: AppState.Main }; + + // Restore the network when refreshing the page during the switching networks + global = updateSettings(global, { + isTestnet: parseAccountId(global.currentAccountId!).network === 'testnet', + }); } global = { ...global, auth: cloneDeep(INITIAL_STATE.auth) }; @@ -107,7 +119,7 @@ addActionHandler('createAccount', async (global, actions, { password, isImportin actions.afterSignIn(); } else { global = updateAuth(global, { - state: AuthState.createBackup, + state: AuthState.disclaimerAndBackup, accountId, address, }); @@ -123,7 +135,12 @@ addActionHandler('createHardwareAccounts', async (global, actions) => { const { hardwareSelectedIndices = [] } = getGlobal().hardware; const network = selectCurrentNetwork(getGlobal()); - const wallets = await Promise.all(hardwareSelectedIndices.map((wallet) => importLedgerWallet(network, wallet))); + const ledgerApi = await import('../../../util/ledger'); + const wallets = await Promise.all( + hardwareSelectedIndices.map( + (wallet) => ledgerApi.importLedgerWallet(network, wallet), + ), + ); const updatedGlobal = wallets.reduce((currentGlobal, wallet) => { if (!wallet) { @@ -158,7 +175,7 @@ addActionHandler('createHardwareAccounts', async (global, actions) => { addActionHandler('afterCheckMnemonic', (global, actions) => { global = { ...global, currentAccountId: global.auth.accountId! }; - global = updateCurrentAccountsState(global, {}); + global = updateCurrentAccountState(global, {}); global = createAccount(global, global.auth.accountId!, global.auth.address!); setGlobal(global); @@ -175,7 +192,7 @@ addActionHandler('restartCheckMnemonicIndexes', (global) => { addActionHandler('skipCheckMnemonic', (global, actions) => { global = { ...global, currentAccountId: global.auth.accountId! }; - global = updateCurrentAccountsState(global, { + global = updateCurrentAccountState(global, { isBackupRequired: true, }); global = createAccount(global, global.auth.accountId!, global.auth.address!); @@ -217,8 +234,12 @@ addActionHandler('afterImportMnemonic', async (global, actions, { mnemonic }) => global = updateAuth(getGlobal(), { mnemonic, error: undefined, + state: AuthState.disclaimer, }); + setGlobal(global); +}); +addActionHandler('confirmDisclaimer', (global, actions) => { const firstNonHardwareAccount = selectFirstNonHardwareAccount(global); if (firstNonHardwareAccount) { @@ -246,22 +267,33 @@ export function selectMnemonicForCheck() { } addActionHandler('startChangingNetwork', (global, actions, { network }) => { - const accountId = buildAccountId({ - ...parseAccountId(global.currentAccountId!), - network, - }); + const accountIds = Object.keys(selectNetworkAccountsMemoized(network, global.accounts!.byId)!); - actions.switchAccount({ accountId, newNetwork: network }); + if (accountIds.length) { + const accountId = accountIds[0]; + actions.switchAccount({ accountId, newNetwork: network }); + } else { + setGlobal({ + ...global, + areSettingsOpen: false, + appState: AppState.Auth, + }); + actions.changeNetwork({ network }); + } }); addActionHandler('switchAccount', async (global, actions, { accountId, newNetwork }) => { const newestTxIds = selectNewestTxIds(global, accountId); await callApi('activateAccount', accountId, newestTxIds); - setGlobal({ + global = { ...getGlobal(), currentAccountId: accountId, - }); + }; + + global = clearCurrentTransfer(global); + + setGlobal(global); if (newNetwork) { actions.changeNetwork({ network: newNetwork }); @@ -279,7 +311,9 @@ addActionHandler('connectHardwareWallet', async (global) => { }), ); - const isLedgerConnected = await connectLedger(); + const ledgerApi = await import('../../../util/ledger'); + + const isLedgerConnected = await ledgerApi.connectLedger(); if (!isLedgerConnected) { setGlobal( updateHardware(getGlobal(), { @@ -296,7 +330,7 @@ addActionHandler('connectHardwareWallet', async (global) => { }), ); - const isTonAppConnected = await waitLedgerTonApp(); + const isTonAppConnected = await ledgerApi.waitLedgerTonApp(); if (!isTonAppConnected) { setGlobal( @@ -316,7 +350,7 @@ addActionHandler('connectHardwareWallet', async (global) => { try { const network = selectCurrentNetwork(getGlobal()); - const hardwareWallets = await getFirstLedgerWallets(network); + const hardwareWallets = await ledgerApi.getFirstLedgerWallets(network); setGlobal( updateHardware(getGlobal(), { diff --git a/src/global/actions/api/dapps.ts b/src/global/actions/api/dapps.ts index c7f535b4..e91c329f 100644 --- a/src/global/actions/api/dapps.ts +++ b/src/global/actions/api/dapps.ts @@ -1,6 +1,5 @@ import { DappConnectState, TransferState } from '../../types'; -import { signLedgerProof, signLedgerTransactions } from '../../../util/ledger'; import { callApi } from '../../../api'; import { ApiUserRejectsError } from '../../../api/errors'; import { addActionHandler, getGlobal, setGlobal } from '../../index'; @@ -14,7 +13,7 @@ import { updateDappConnectRequest, } from '../../reducers'; -addActionHandler('submitDappConnectRequestConfirm', async (global, actions, { password, additionalAccountIds }) => { +addActionHandler('submitDappConnectRequestConfirm', async (global, actions, { password, accountId }) => { const { promiseId, permissions, } = global.dappConnectRequest!; @@ -26,9 +25,10 @@ addActionHandler('submitDappConnectRequestConfirm', async (global, actions, { pa return; } + actions.switchAccount({ accountId }); await callApi('confirmDappRequestConnect', promiseId!, { + accountId, password, - additionalAccountIds, }); global = getGlobal(); @@ -48,47 +48,53 @@ addActionHandler('submitDappConnectRequestConfirm', async (global, actions, { pa setGlobal(global); }); -addActionHandler('submitDappConnectRequestConfirmHardware', async (global, actions, { additionalAccountIds }) => { - const { - accountId, promiseId, proof, - } = global.dappConnectRequest!; +addActionHandler( + 'submitDappConnectRequestConfirmHardware', + async (global, actions, { accountId: connectAccountId }) => { + const { + accountId, promiseId, proof, + } = global.dappConnectRequest!; - global = getGlobal(); - global = updateDappConnectRequest(global, { - error: undefined, - state: DappConnectState.ConfirmHardware, - }); - setGlobal(global); - - try { - const signature = await signLedgerProof(accountId!, proof!); - await callApi('confirmDappRequestConnect', promiseId!, { - signature, - additionalAccountIds, + global = getGlobal(); + global = updateDappConnectRequest(global, { + error: undefined, + state: DappConnectState.ConfirmHardware, }); - } catch (err) { - setGlobal(updateDappConnectRequest(getGlobal(), { - error: 'Canceled by the user', - })); - return; - } + setGlobal(global); - global = getGlobal(); - global = clearDappConnectRequest(global); - setGlobal(global); + const ledgerApi = await import('../../../util/ledger'); + + try { + const signature = await ledgerApi.signLedgerProof(accountId!, proof!); + actions.switchAccount({ accountId: connectAccountId }); + await callApi('confirmDappRequestConnect', promiseId!, { + accountId: connectAccountId, + signature, + }); + } catch (err) { + setGlobal(updateDappConnectRequest(getGlobal(), { + error: 'Canceled by the user', + })); + return; + } - const { currentAccountId } = global; + global = getGlobal(); + global = clearDappConnectRequest(global); + setGlobal(global); - const result = await callApi('getDapps', currentAccountId!); + const { currentAccountId } = global; - if (!result) { - return; - } + const result = await callApi('getDapps', currentAccountId!); - global = getGlobal(); - global = updateConnectedDapps(global, { dapps: result }); - setGlobal(global); -}); + if (!result) { + return; + } + + global = getGlobal(); + global = updateConnectedDapps(global, { dapps: result }); + setGlobal(global); + }, +); addActionHandler('cancelDappConnectRequestConfirm', (global) => { const { promiseId } = global.dappConnectRequest || {}; @@ -165,9 +171,10 @@ addActionHandler('submitDappTransferHardware', async (global) => { setGlobal(global); const accountId = global.currentAccountId!; + const ledgerApi = await import('../../../util/ledger'); try { - const signedMessages = await signLedgerTransactions(accountId, transactions!); + const signedMessages = await ledgerApi.signLedgerTransactions(accountId, transactions!); void callApi('confirmDappRequest', promiseId, signedMessages); } catch (err) { if (err instanceof ApiUserRejectsError) { diff --git a/src/global/actions/api/staking.ts b/src/global/actions/api/staking.ts index 798383a1..6f937eaa 100644 --- a/src/global/actions/api/staking.ts +++ b/src/global/actions/api/staking.ts @@ -74,7 +74,7 @@ addActionHandler('submitStakingInitial', async (global, actions, payload) => { global = updateStaking(global, { isLoading: false }); if (result) { - if (result.error) { + if ('error' in result) { global = updateStaking(global, { error: result.error }); } else { global = updateStaking(global, { @@ -95,7 +95,7 @@ addActionHandler('submitStakingInitial', async (global, actions, payload) => { global = updateStaking(global, { isLoading: false }); if (result) { - if (result.error) { + if ('error' in result) { global = updateStaking(global, { error: result.error }); } else { global = updateStaking(global, { diff --git a/src/global/actions/api/wallet.ts b/src/global/actions/api/wallet.ts index f716bb03..446e0479 100644 --- a/src/global/actions/api/wallet.ts +++ b/src/global/actions/api/wallet.ts @@ -6,21 +6,22 @@ import type { UserToken } from '../../types'; import { buildCollectionByKey, findLast, mapValues, unique, } from '../../../util/iteratees'; -import { signLedgerTransactions, submitLedgerTransfer } from '../../../util/ledger'; import { pause } from '../../../util/schedulers'; import { callApi } from '../../../api'; import { ApiUserRejectsError } from '../../../api/errors'; import { bigStrToHuman, getIsTxIdLocal, humanToBigStr } from '../../helpers'; -import { addActionHandler, getGlobal, setGlobal } from '../../index'; +import { + addActionHandler, getActions, getGlobal, setGlobal, +} from '../../index'; import { clearCurrentTransfer, updateAccountState, - updateCurrentAccountsState, updateCurrentAccountState, updateCurrentSignature, updateCurrentTransfer, updateSendingLoading, updateSettings, + updateTransactionsIsHistoryEndReached, updateTransactionsIsLoading, } from '../../reducers'; import { @@ -54,6 +55,38 @@ addActionHandler('setTransferScreen', (global, actions, payload) => { setGlobal(updateCurrentTransfer(global, { state })); }); +addActionHandler('setTransferAmount', (global, actions, { amount }) => { + setGlobal( + updateCurrentTransfer(global, { + amount, + }), + ); +}); + +addActionHandler('setTransferToAddress', (global, actions, { toAddress }) => { + setGlobal( + updateCurrentTransfer(global, { + toAddress, + }), + ); +}); + +addActionHandler('setTransferComment', (global, actions, { comment }) => { + setGlobal( + updateCurrentTransfer(global, { + comment, + }), + ); +}); + +addActionHandler('setTransferShouldEncrypt', (global, actions, { shouldEncrypt }) => { + setGlobal( + updateCurrentTransfer(global, { + shouldEncrypt, + }), + ); +}); + addActionHandler('submitTransferInitial', async (global, actions, payload) => { const { tokenSlug, toAddress, amount, comment, shouldEncrypt, @@ -75,7 +108,7 @@ addActionHandler('submitTransferInitial', async (global, actions, payload) => { global = getGlobal(); global = updateSendingLoading(global, false); - if (!result || result.error) { + if (!result || 'error' in result) { if (result?.addressName) { global = updateCurrentTransfer(global, { toAddressName: result.addressName }); } @@ -98,6 +131,8 @@ addActionHandler('submitTransferInitial', async (global, actions, payload) => { state: TransferState.Confirm, error: undefined, toAddress, + resolvedAddress: result.resolvedAddress, + normalizedAddress: result.normalizedAddress, amount, comment, shouldEncrypt, @@ -145,7 +180,7 @@ addActionHandler('submitTransferConfirm', (global, actions) => { addActionHandler('submitTransferPassword', async (global, actions, payload) => { const { password } = payload; const { - toAddress, + resolvedAddress, comment, amount, promiseId, @@ -175,7 +210,7 @@ addActionHandler('submitTransferPassword', async (global, actions, payload) => { accountId: global.currentAccountId!, password, slug: tokenSlug!, - toAddress: toAddress!, + toAddress: resolvedAddress!, amount: humanToBigStr(amount!, decimals), comment, fee, @@ -196,6 +231,7 @@ addActionHandler('submitTransferPassword', async (global, actions, payload) => { addActionHandler('submitTransferHardware', async (global) => { const { toAddress, + resolvedAddress, comment, amount, promiseId, @@ -215,6 +251,8 @@ addActionHandler('submitTransferHardware', async (global) => { state: TransferState.ConfirmHardware, })); + const ledgerApi = await import('../../../util/ledger'); + if (promiseId) { const message: ApiDappTransaction = { toAddress: toAddress!, @@ -225,7 +263,7 @@ addActionHandler('submitTransferHardware', async (global) => { }; try { - const signedMessage = await signLedgerTransactions(accountId, [message]); + const signedMessage = await ledgerApi.signLedgerTransactions(accountId, [message]); void callApi('confirmDappRequest', promiseId, signedMessage); } catch (err) { if (err instanceof ApiUserRejectsError) { @@ -244,13 +282,13 @@ addActionHandler('submitTransferHardware', async (global) => { accountId: global.currentAccountId!, password: '', slug: tokenSlug!, - toAddress: toAddress!, + toAddress: resolvedAddress!, amount: humanToBigStr(amount!, decimals), comment, fee, }; - const result = await submitLedgerTransfer(options); + const result = await ledgerApi.submitLedgerTransfer(options); const error = result === undefined ? 'Transfer error' : undefined; @@ -265,13 +303,16 @@ addActionHandler('clearTransferError', (global) => { }); addActionHandler('cancelTransfer', (global) => { - const { promiseId } = global.currentTransfer; + const { promiseId, tokenSlug } = global.currentTransfer; if (promiseId) { void callApi('cancelDappRequest', promiseId, 'Canceled by the user'); } - setGlobal(clearCurrentTransfer(global)); + global = clearCurrentTransfer(global); + global = updateCurrentTransfer(global, { tokenSlug }); + + setGlobal(global); }); addActionHandler('fetchTokenTransactions', async (global, actions, payload) => { @@ -290,7 +331,8 @@ addActionHandler('fetchTokenTransactions', async (global, actions, payload) => { global = getGlobal(); global = updateTransactionsIsLoading(global, false); - if (!result) { + if (!result || !result.length) { + global = updateTransactionsIsHistoryEndReached(global, true); setGlobal(global); return; } @@ -327,6 +369,7 @@ addActionHandler('fetchAllTransactions', async (global, actions, payload) => { global = updateTransactionsIsLoading(global, false); if (!result || !result.length) { + global = updateTransactionsIsHistoryEndReached(global, true); setGlobal(global); return; } @@ -353,10 +396,15 @@ addActionHandler('fetchAllTransactions', async (global, actions, payload) => { setGlobal(global); }); +addActionHandler('resetIsHistoryEndReached', (global) => { + global = updateTransactionsIsHistoryEndReached(global, false); + setGlobal(global); +}); + addActionHandler('setIsBackupRequired', (global, actions, { isMnemonicChecked }) => { const { isBackupRequired } = selectCurrentAccountState(global); - setGlobal(updateCurrentAccountsState(global, { + setGlobal(updateCurrentAccountState(global, { isBackupRequired: isMnemonicChecked ? undefined : isBackupRequired, })); }); @@ -500,7 +548,6 @@ addActionHandler('importToken', async (global, actions, { address }) => { change24h: 0, change7d: 0, change30d: 0, - isDisabled: false, keywords, }; @@ -525,3 +572,20 @@ addActionHandler('resetImportToken', (global) => { }), ); }); + +addActionHandler('verifyHardwareAddress', async (global) => { + const accountId = global.currentAccountId!; + + const ledgerApi = await import('../../../util/ledger'); + + if (!(await ledgerApi.reconnectLedger())) { + getActions().showError({ error: '$ledger_not_ready' }); + return; + } + + try { + await ledgerApi.verifyAddress(accountId); + } catch (err) { + getActions().showError({ error: err as string }); + } +}); diff --git a/src/global/actions/apiUpdates/transactions.ts b/src/global/actions/apiUpdates/transactions.ts index 716aada4..7e53ece3 100644 --- a/src/global/actions/apiUpdates/transactions.ts +++ b/src/global/actions/apiUpdates/transactions.ts @@ -1,7 +1,7 @@ import { TransferState } from '../../types'; import { playIncomingTransactionSound } from '../../../util/appSounds'; -import { bigStrToHuman, getIsTynyTransaction } from '../../helpers'; +import { bigStrToHuman, getIsTinyTransaction } from '../../helpers'; import { addActionHandler, setGlobal } from '../../index'; import { removeTransaction, @@ -28,7 +28,7 @@ addActionHandler('apiUpdate', (global, actions, update) => { if ( -bigStrToHuman(amount, decimals) === global.currentTransfer.amount - && toAddress === global.currentTransfer.toAddress + && toAddress === global.currentTransfer.normalizedAddress ) { global = updateCurrentTransfer(global, { txId, @@ -62,7 +62,7 @@ addActionHandler('apiUpdate', (global, actions, update) => { && (Date.now() - transaction.timestamp < TX_AGE_TO_PLAY_SOUND) && ( !global.settings.areTinyTransfersHidden - || getIsTynyTransaction(transaction, global.tokenInfo?.bySlug[transaction.slug!]) + || getIsTinyTransaction(transaction, global.tokenInfo?.bySlug[transaction.slug!]) ) ) { shouldPlaySound = true; diff --git a/src/global/actions/ui/initial.ts b/src/global/actions/ui/initial.ts index b20414ae..67124c96 100644 --- a/src/global/actions/ui/initial.ts +++ b/src/global/actions/ui/initial.ts @@ -1,8 +1,8 @@ import { ApiTransactionDraftError, ApiTransactionError } from '../../../api/types'; -import type { NotificationType } from '../../types'; +import type { Account, AccountState, NotificationType } from '../../types'; import { IS_ELECTRON } from '../../../config'; -import { genRelatedAccountIds } from '../../../util/account'; +import { parseAccountId } from '../../../util/account'; import { initializeSoundsForSafari } from '../../../util/appSounds'; import { omit } from '../../../util/iteratees'; import { clearPreviousLangpacks, setLanguage } from '../../../util/langProvider'; @@ -18,7 +18,12 @@ import { addActionHandler, getActions, getGlobal, setGlobal, } from '../../index'; import { updateCurrentAccountState } from '../../reducers'; -import { selectNetworkAccounts, selectNewestTxIds } from '../../selectors'; +import { + selectCurrentNetwork, + selectNetworkAccounts, + selectNetworkAccountsMemoized, + selectNewestTxIds, +} from '../../selectors'; addActionHandler('init', (_, actions) => { const { documentElement } = document; @@ -119,6 +124,12 @@ addActionHandler('showError', (global, actions, { error } = {}) => { }); break; + case ApiTransactionDraftError.InvalidAddressFormat: + actions.showDialog({ + message: 'Invalid address format. Only URL Safe Base64 format is allowed.', + }); + break; + case ApiTransactionError.PartialTransactionFailure: actions.showDialog({ message: 'Not all transactions were sent successfully' }); break; @@ -212,13 +223,53 @@ addActionHandler('toggleDeeplinkHook', (global, actions, { isEnabled }) => { addActionHandler('signOut', async (global, actions, payload) => { const { isFromAllAccounts } = payload || {}; + + const network = selectCurrentNetwork(global); const accountIds = Object.keys(selectNetworkAccounts(global)!); - if (isFromAllAccounts || accountIds.length === 1) { - await callApi('resetAccounts'); + const otherNetwork = network === 'mainnet' ? 'testnet' : 'mainnet'; + const otherNetworkAccountIds = Object.keys(selectNetworkAccountsMemoized(otherNetwork, global.accounts?.byId)!); - getActions().afterSignOut({ isFromAllAccounts: true }); - getActions().init(); + if (isFromAllAccounts || accountIds.length === 1) { + if (otherNetworkAccountIds.length) { + await callApi('removeNetworkAccounts', network); + + global = getGlobal(); + + const nextAccountId = otherNetworkAccountIds[0]; + const accountsById = Object.entries(global.accounts!.byId).reduce((byId, [accountId, account]) => { + if (parseAccountId(accountId).network !== network) { + byId[accountId] = account; + } + return byId; + }, {} as Record); + const byAccountId = Object.entries(global.byAccountId).reduce((byId, [accountId, state]) => { + if (parseAccountId(accountId).network !== network) { + byId[accountId] = state; + } + return byId; + }, {} as Record); + + global = { + ...global, + currentAccountId: nextAccountId, + accounts: { + ...global.accounts!, + byId: accountsById, + }, + byAccountId, + }; + + setGlobal(global); + + getActions().switchAccount({ accountId: nextAccountId, newNetwork: otherNetwork }); + getActions().afterSignOut(); + } else { + await callApi('resetAccounts'); + + getActions().afterSignOut({ isFromAllAccounts: true }); + getActions().init(); + } } else { const prevAccountId = global.currentAccountId!; const nextAccountId = accountIds.find((id) => id !== prevAccountId)!; @@ -228,9 +279,8 @@ addActionHandler('signOut', async (global, actions, payload) => { global = getGlobal(); - const prevAccountIds = genRelatedAccountIds(prevAccountId!); - const accountsById = omit(global.accounts!.byId, prevAccountIds); - const byAccountId = omit(global.byAccountId, prevAccountIds); + const accountsById = omit(global.accounts!.byId, [prevAccountId]); + const byAccountId = omit(global.byAccountId, [prevAccountId]); global = { ...global, diff --git a/src/global/actions/ui/misc.ts b/src/global/actions/ui/misc.ts index 703972de..c104b8c1 100644 --- a/src/global/actions/ui/misc.ts +++ b/src/global/actions/ui/misc.ts @@ -1,10 +1,7 @@ -import extension from '../../../lib/webextension-polyfill'; - import { AppState, HardwareConnectState } from '../../types'; import type { UserToken } from '../../types'; import { unique } from '../../../util/iteratees'; -import { connectLedger } from '../../../util/ledger'; import { onLedgerTabClose, openLedgerTab } from '../../../util/ledger/tab'; import { pause } from '../../../util/schedulers'; import { callApi } from '../../../api'; @@ -170,7 +167,9 @@ addActionHandler('openHardwareWalletModal', async (global, actions) => { actions.connectHardwareWallet(); }; - if (await connectLedger()) { + const ledgerApi = await import('../../../util/ledger'); + + if (await ledgerApi.connectLedger()) { startConnection(); return; } @@ -183,12 +182,12 @@ addActionHandler('openHardwareWalletModal', async (global, actions) => { await pause(OPEN_LEDGER_TAB_DELAY); const id = await openLedgerTab(); - const popup = await extension.windows.getCurrent(); + const popup = await chrome.windows.getCurrent(); onLedgerTabClose(id, async () => { - await extension.windows.update(popup.id!, { focused: true }); + await chrome.windows.update(popup.id!, { focused: true }); - if (!await connectLedger()) { + if (!await ledgerApi.connectLedger()) { actions.closeHardwareWalletModal(); return; } diff --git a/src/global/helpers/index.ts b/src/global/helpers/index.ts index a0a99d68..1906df4c 100644 --- a/src/global/helpers/index.ts +++ b/src/global/helpers/index.ts @@ -2,7 +2,7 @@ import type { ApiToken, ApiTransaction } from '../../api/types'; import { DEFAULT_DECIMAL_PLACES, TINY_TRANSFER_MAX_COST } from '../../config'; -export function getIsTynyTransaction(transaction: ApiTransaction, token?: ApiToken) { +export function getIsTinyTransaction(transaction: ApiTransaction, token?: ApiToken) { if (!token) return false; const decimals = token.decimals; const cost = Math.abs(bigStrToHuman(transaction.amount, decimals)) * token.quote.price; diff --git a/src/global/reducers/misc.ts b/src/global/reducers/misc.ts index 1a341cf2..ad4c5258 100644 --- a/src/global/reducers/misc.ts +++ b/src/global/reducers/misc.ts @@ -2,12 +2,11 @@ import type { ApiToken } from '../../api/types'; import type { Account, AccountState, GlobalState } from '../types'; import { TON_TOKEN_SLUG } from '../../config'; -import { genRelatedAccountIds } from '../../util/account'; import isPartialDeepEqual from '../../util/isPartialDeepEqual'; -import { fromKeyValueArrays } from '../../util/iteratees'; import { selectAccount, selectAccountState, + selectCurrentNetwork, selectNetworkAccounts, } from '../selectors'; @@ -36,8 +35,10 @@ export function updateAccounts( export function createAccount(global: GlobalState, accountId: string, address: string, partial?: Partial) { if (!partial?.title) { + const network = selectCurrentNetwork(global); const accounts = selectNetworkAccounts(global) || {}; - partial = { ...partial, title: `Wallet ${Object.keys(accounts).length + 1}` }; + const titlePrefix = network === 'mainnet' ? 'Wallet' : 'Testnet Wallet'; + partial = { ...partial, title: `${titlePrefix} ${Object.keys(accounts).length + 1}` }; } return updateAccount(global, accountId, { ...partial, address }); @@ -48,17 +49,16 @@ export function updateAccount( accountId: string, partial: Partial, ) { - let account = selectAccount(global, accountId); - account = { ...account, ...partial } as Account; - - const newAccountsById = fromKeyValueArrays(genRelatedAccountIds(accountId), account); return { ...global, accounts: { ...global.accounts, byId: { ...global.accounts?.byId, - ...newAccountsById, + [accountId]: { + ...selectAccount(global, accountId), + ...partial, + } as Account, }, }, }; @@ -128,13 +128,6 @@ export function updateCurrentAccountState(global: GlobalState, partial: Partial< return updateAccountState(global, global.currentAccountId!, partial); } -export function updateCurrentAccountsState(global: GlobalState, partial: Partial): GlobalState { - for (const accountId of genRelatedAccountIds(global.currentAccountId!)) { - global = updateAccountState(global, accountId, partial); - } - return global; -} - export function updateAccountState( global: GlobalState, accountId: string, partial: Partial, withDeepCompare = false, ): GlobalState { diff --git a/src/global/reducers/staking.ts b/src/global/reducers/staking.ts index 2c64541d..9c67b18a 100644 --- a/src/global/reducers/staking.ts +++ b/src/global/reducers/staking.ts @@ -4,7 +4,7 @@ import type { GlobalState } from '../types'; import isPartialDeepEqual from '../../util/isPartialDeepEqual'; import { selectCurrentAccountState } from '../selectors'; -import { updateCurrentAccountsState } from './misc'; +import { updateCurrentAccountState } from './misc'; export function updateStaking(global: GlobalState, update: Partial): GlobalState { return { @@ -35,7 +35,7 @@ export function updatePoolState(global: GlobalState, partial: ApiPoolState, with return global; } - return updateCurrentAccountsState(global, { + return updateCurrentAccountState(global, { poolState: { ...currentPoolState, ...partial, diff --git a/src/global/reducers/wallet.ts b/src/global/reducers/wallet.ts index 229174e7..08006810 100644 --- a/src/global/reducers/wallet.ts +++ b/src/global/reducers/wallet.ts @@ -51,6 +51,17 @@ export function updateTransactionsIsLoading(global: GlobalState, isLoading: bool }); } +export function updateTransactionsIsHistoryEndReached(global: GlobalState, isReached: boolean) { + const { transactions } = selectCurrentAccountState(global) || {}; + + return updateCurrentAccountState(global, { + transactions: { + ...transactions || { byTxId: {} }, + isHistoryEndReached: isReached, + }, + }); +} + export function updateTransactionsIsLoadingByAccount(global: GlobalState, accountId: string, isLoading: boolean) { const { transactions } = selectAccountState(global, accountId) || {}; diff --git a/src/global/selectors/index.ts b/src/global/selectors/index.ts index 04a6f7f7..7567326f 100644 --- a/src/global/selectors/index.ts +++ b/src/global/selectors/index.ts @@ -98,7 +98,6 @@ export const selectPopularTokensMemoized = memoized(( history24h, history7d, history30d, - isDisabled: false, keywords, } as UserToken; }); @@ -141,6 +140,10 @@ export function selectCurrentNetwork(global: GlobalState) { return global.settings.isTestnet ? 'testnet' : 'mainnet'; } +export function selectCurrentAccount(global: GlobalState) { + return selectAccount(global, global.currentAccountId!); +} + export function selectAccount(global: GlobalState, accountId: string) { return selectAccounts(global)?.[accountId]; } diff --git a/src/global/types.ts b/src/global/types.ts index 1bf9a0a2..40d386c6 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -53,8 +53,9 @@ export enum AuthState { none, creatingWallet, createPassword, - createBackup, + disclaimerAndBackup, importWallet, + disclaimer, importWalletCreatePassword, ready, about, @@ -98,6 +99,12 @@ export enum StakingState { UnstakeComplete, } +export enum ActiveTab { + Receive, + Transfer, + Stake, +} + export type UserToken = { amount: number; name: string; @@ -112,7 +119,7 @@ export type UserToken = { history24h?: ApiHistoryList; history7d?: ApiHistoryList; history30d?: ApiHistoryList; - isDisabled: boolean; + isDisabled?: boolean; keywords?: string[]; }; @@ -137,6 +144,7 @@ export interface AccountState { byTxId: Record; txIdsBySlug?: Record; newestTransactionsBySlug?: Record; + isHistoryEndReached?: boolean; }; nfts?: { byAddress: Record; @@ -190,6 +198,8 @@ export type GlobalState = { tokenSlug?: string; toAddress?: string; toAddressName?: string; + resolvedAddress?: string; + normalizedAddress?: string; error?: string; amount?: number; fee?: string; @@ -279,7 +289,7 @@ export type GlobalState = { currentAccountId?: string; isAddAccountModalOpen?: boolean; isBackupWalletModalOpen?: boolean; - landscapeActionsActiveTabIndex?: 0 | 1 | 2; + landscapeActionsActiveTabIndex?: ActiveTab; isHardwareModalOpen?: boolean; areSettingsOpen?: boolean; @@ -301,6 +311,7 @@ export interface ActionPayloads { startImportingWallet: undefined; afterImportMnemonic: { mnemonic: string[] }; startImportingHardwareWallet: { driver: ApiLedgerDriver }; + confirmDisclaimer: undefined; cleanAuthError: undefined; openAbout: undefined; closeAbout: undefined; @@ -317,6 +328,10 @@ export interface ActionPayloads { closeHardwareWalletModal: undefined; resetHardwareWalletConnect: undefined; setTransferScreen: { state: TransferState }; + setTransferAmount: { amount?: number }; + setTransferToAddress: { toAddress?: string }; + setTransferComment: { comment?: string }; + setTransferShouldEncrypt: { shouldEncrypt?: boolean }; startTransfer: { tokenSlug?: string; amount?: number; toAddress?: string; comment?: string } | undefined; changeTransferToken: { tokenSlug: string }; fetchFee: { @@ -353,9 +368,11 @@ export interface ActionPayloads { renameAccount: { accountId: string; title: string }; clearAccountError: undefined; validatePassword: { password: string }; + verifyHardwareAddress: undefined; fetchTokenTransactions: { limit: number; slug: string; offsetId?: string }; fetchAllTransactions: { limit: number }; + resetIsHistoryEndReached: undefined; fetchNfts: undefined; showTransactionInfo: { txId?: string } | undefined; closeTransactionInfo: undefined; @@ -371,7 +388,7 @@ export interface ActionPayloads { openAddAccountModal: undefined; closeAddAccountModal: undefined; - setLandscapeActionsActiveTabIndex: { index: 0 | 1 | 2 }; + setLandscapeActionsActiveTabIndex: { index: ActiveTab }; // Staking startStaking: { isUnstaking?: boolean } | undefined; @@ -412,8 +429,8 @@ export interface ActionPayloads { resetImportToken: undefined; // TON Connect - submitDappConnectRequestConfirm: { additionalAccountIds: string[]; password?: string }; - submitDappConnectRequestConfirmHardware: { additionalAccountIds: string[] }; + submitDappConnectRequestConfirm: { accountId: string; password?: string }; + submitDappConnectRequestConfirmHardware: { accountId: string }; clearDappConnectRequestError: undefined; cancelDappConnectRequestConfirm: undefined; setDappConnectRequestState: { state: DappConnectState }; diff --git a/src/i18n/en.yaml b/src/i18n/en.yaml index 2cc36a5b..da58c983 100644 --- a/src/i18n/en.yaml +++ b/src/i18n/en.yaml @@ -7,14 +7,7 @@ Import From %1$d Secret Words: Import From %1$d Secret Words More about MyTonWallet: More about MyTonWallet Creating Wallet...: Creating Wallet... On the count of three...: On the count of three... -$auth_backup_description1: | - This is a **secure wallet** - and is only **controlled by you**. -$auth_backup_description2: "And with great power comes **great responsibility**." -$auth_backup_description3: | - You need to manually **back up secret keys** in case you forget your password or lose access to this device. Back Up: Back Up -Skip For Now: Skip For Now Passwords must be equal.: Passwords must be equal. To protect your wallet as much as possible, use a password with: To protect your wallet as much as possible, use a password with $auth_password_rule_8chars: at least 8 characters @@ -181,7 +174,7 @@ Insufficient balance: Insufficient balance InsufficientBalance: Insufficient balance Optional: Optional $send_token_symbol: Send %1$s -$your_balance_is: "Your balance: %balance%" +$balance_is: "Balance: %balance%" Is it all ok?: Is it all ok? Receiving Address: Receiving Address Fee: Fee @@ -217,7 +210,6 @@ Appearance: Appearance Light: Light Dark: Dark System: System -Create Backup: Create Backup Stake TON: Stake TON Earn from your tokens while holding them: Earn from your tokens while holding them $est_apy_val: Est. APY %1$d% @@ -348,3 +340,22 @@ $dapp_ledger_warning1: You are about to send a multi-way transaction using your $dapp_ledger_warning2: Please take your time and do not interrupt the process. Agree: Agree The hardware wallet does not support this data format: The hardware wallet does not support this data format +Use Responsibly: Use Responsibly +$auth_responsibly_description1: | + MyTonWallet is a **self-custodial** wallet, which means that **only you** have full control and, most importantly, **full responsibility** for your funds. +$auth_responsibly_description2: | + Your private keys are stored on your device and are subject to **hacker attacks**. If your computer is infected with **malware**, your funds are likely to be stolen. +$auth_responsibly_description3: | + The MyTonWallet team is **not responsible** for the safety of your funds, just as your computer manufacturer or internet provider is not responsible. +$auth_responsibly_description4: | + **Never** store all your funds in one place. **Diversify** and use various software and hardware. Always **do your own research** and learn more about crypto security. +Start Wallet: Start Wallet +$auth_backup_warning_notice: | + Now you need to manually **back up secret keys** in a case you forget your password or lose access to this device. +Later: Later +Back Up Now: Back Up Now +I have read and accept this information: I have read and accept this information +$ledger_verify_address: Always verify pasted address using your Ledger device. +$ledger_not_ready: Ledger is not connected or TON app is not open. +Verify now: Verify now +Invalid address format. Only URL Safe Base64 format is allowed.: Invalid address format. Only URL Safe Base64 format is allowed. diff --git a/src/i18n/es.yaml b/src/i18n/es.yaml index 66a19cbb..9312dad4 100644 --- a/src/i18n/es.yaml +++ b/src/i18n/es.yaml @@ -7,14 +7,7 @@ Import From %1$d Secret Words: Importar usando la frase semilla About MyTonWallet: Acerca de MyTonWallet Creating Wallet...: Creando monedero... On the count of three...: A la cuenta de tres... -$auth_backup_description1: | - Este es un **monedero seguro** - y **totalmente bajo su control**. -$auth_backup_description2: "Y un gran poder conlleva una **gran responsabilidad**." -$auth_backup_description3: | - Debe hacer manualmente una **copia de seguridad de la frase semilla** que le permitirá recuperar su monedero en caso de que olvide su contraseña o pierda el acceso a este dispositivo. Back Up: Hacer copia de seguridad -Skip For Now: Omitir por ahora Passwords must be equal.: Las contraseñas deben coincidir. To protect your wallet as much as possible, use a password with: Para la máxima protección de su monedero, use una contraseña con $auth_password_rule_8chars: al menos 8 caracteres @@ -180,7 +173,7 @@ Insufficient balance: Saldo insuficiente InsufficientBalance: Saldo insuficiente Optional: Opcional $send_token_symbol: Enviar %1$s -$your_balance_is: "Su saldo: %balance%" +$balance_is: "Saldo: %balance%" Is it all ok?: ¿Está todo bien? Receiving Address: Dirección del receptor Fee: Comisión @@ -207,9 +200,7 @@ $tiny_transfers_help: Desactive esta opción para mostrar transacciones de menos Today: Hoy Yesterday: Ayer Now: Ahora -$receive_ton_description: | - Puede compartir esta dirección, mostrar el código QR - o crear una factura para recibir TON +$receive_ton_description: Puede compartir esta dirección, mostrar el código QR o crear una factura para recibir TON Your address: Tu dirección Wrong password, please try again: Contraseña incorrecta, inténtalo de nuevo Appearance: Apariencia @@ -217,7 +208,6 @@ Assets and Activity: Activos y Actividad Light: Claro Dark: Oscuro System: Sistema -Create Backup: Crear copia de seguridad Stake TON: Apostar TON Earn from your tokens while holding them: Gana con tus tokens mientras los mantienes $est_apy_val: Est. APY %1$d% @@ -349,3 +339,22 @@ $dapp_ledger_warning1: Estás a punto de enviar una transacción multi-direccion $dapp_ledger_warning2: Por favor, tómate tu tiempo y no interrumpas el proceso. Agree: Aceptar The hardware wallet does not support this data format: La billetera de hardware no admite este formato de datos +Use Responsibly: Usar responsablemente +$auth_responsibly_description1: | + MyTonWallet es una billetera con **autocustodia**, lo que significa que **solo usted** tiene el control total y, lo que es más importante, la **total responsabilidad** de sus fondos. +$auth_responsibly_description2: | + Sus claves privadas se almacenan en su dispositivo y están sujetas a **ataques de piratas informáticos**. Si su computadora está infectada con **malware**, es probable que le roben sus fondos. +$auth_responsibly_description3: | + El equipo de MyTonWallet **no es responsable** de la seguridad de sus fondos, al igual que el fabricante de su computadora o su proveedor de Internet no es responsable. +$auth_responsibly_description4: | + **Nunca** almacene todos sus fondos en un solo lugar. **Diversificar** y usar varios software y hardware. Siempre **haga su propia investigación** y obtenga más información sobre la criptoseguridad. +Start Wallet: Iniciar billetera +$auth_backup_warning_notice: | + Ahora debe realizar manualmente una **copia de seguridad de las claves secretas** en caso de que olvide su contraseña o pierda el acceso a este dispositivo. +Later: Más tarde +Back Up Now: Copia ahora +I have read and accept this information: He leído y acepto esta información +$ledger_verify_address: Siempre verifique la dirección pegada usando su dispositivo Ledger. +$ledger_not_ready: El libro mayor no está conectado o la aplicación TON no está abierta. +Verify now: Comprobar ahora +Invalid address format. Only URL Safe Base64 format is allowed.: Formato de dirección no válido. Solo se permite el formato urlsafe base64. diff --git a/src/i18n/ru.yaml b/src/i18n/ru.yaml index 667901a1..0797e279 100644 --- a/src/i18n/ru.yaml +++ b/src/i18n/ru.yaml @@ -7,20 +7,7 @@ Import From %1$d Secret Words: Восстановить из %1$d секретн More about MyTonWallet: Подробнее о MyTonWallet Creating Wallet...: Создаём кошелёк... On the count of three...: Сосчитайте до трёх... -$auth_backup_description1: | - Вы создали новый **безопасный кошелёк**, - доступ к которому **есть только у вас**. -$auth_backup_description2: | - Однако важно помнить: - **особые возможности** - требуют **особой ответственности**! -$auth_backup_description3: | - Сейчас необходимо сделать - **резервную копию** секретных слов. - Это поможет, если вы потеряете доступ - к этому устройству или забудете пароль. Back Up: Показать слова -Skip For Now: Сделаю позже Passwords must be equal.: Пароли не совпадают. To protect your wallet as much as possible, use a password with: Для обеспечения максимальной безопасности кошелька пароль должен содержать $auth_password_rule_8chars: не менее 8 символов @@ -187,7 +174,7 @@ Insufficient balance: Недостаточный баланс InsufficientBalance: Недостаточный баланс Optional: Необязательно $send_token_symbol: Отправить %1$s -$your_balance_is: "Ваш баланс: %balance%" +$balance_is: "Баланс: %balance%" Is it all ok?: Всё верно? Receiving Address: Адрес получателя Fee: Комиссия @@ -212,17 +199,13 @@ $tiny_transfers_help: Выключите этот параметр, чтобы Today: Сегодня Yesterday: Вчера Now: Сейчас -$receive_ton_description: | - Вы можете поделиться этим адресом, - отсканировать QR-код или создать инвойс - для получения TON +$receive_ton_description: Вы можете поделиться этим адресом, отсканировать QR-код или создать инвойс для получения TON Your address: Ваш адрес Wrong password, please try again: Неправильный пароль, попробуйте ещё раз Appearance: Внешний вид Light: Светлая Dark: Тёмная System: Системная -Create Backup: Резервная копия Stake TON: Продолжить Earn from your tokens while holding them: Получайте пассивный доход от хранения TON на надёжном официальном смарт-контракте $est_apy_val: Доходность ~%1$d% @@ -351,3 +334,22 @@ $dapp_ledger_warning1: Вы собираетесь отправить много $dapp_ledger_warning2: Пожалуйста, не торопитесь и не прерывайте процесс. Agree: Согласен The hardware wallet does not support this data format: Аппаратный кошелек не поддерживает данный формат данных +Use Responsibly: Используйте ответственно +$auth_responsibly_description1: | + MyTonWallet — это кошелек **самообслуживания**, что означает, что **только вы** имеете полный контроль и, самое главное, **полную ответственность** за свои средства. +$auth_responsibly_description2: | + Ваши закрытые ключи хранятся на вашем устройстве и могут быть подвержены **хакерским атакам**. Если ваш компьютер заражен **вредоносной программой**, ваши средства могут быть украдены. +$auth_responsibly_description3: | + Команда MyTonWallet **не несет ответственности** за сохранность ваших средств, как и производитель вашего компьютера или интернет-провайдер. +$auth_responsibly_description4: | + **Никогда** не храните все свои средства в одном месте. **Разнообразьте** и используйте различное программное и аппаратное обеспечение. Всегда **проводите собственные исследования** и узнавайте больше о криптобезопасности. +Start Wallet: Начать использование +$auth_backup_warning_notice: | + Теперь вам нужно вручную **сделать резервную копию** секретных слов на случай, если вы забудете пароль или потеряете доступ к этому устройству. +Later: Позже +Back Up Now: Показать слова сейчас +I have read and accept this information: Я прочитал и принимаю эту информацию +$ledger_verify_address: Всегда проверяйте вставленный адрес используя Ledger. +$ledger_not_ready: Ledger не подключен или приложение TON не открыто. +Verify now: Проверить сейчас +Invalid address format. Only URL Safe Base64 format is allowed.: Некорректный формат адреса. Разрешен только URL Safe Base64 формат. diff --git a/src/i18n/zh-Hans.yaml b/src/i18n/zh-Hans.yaml index dbbb28d3..75c67657 100644 --- a/src/i18n/zh-Hans.yaml +++ b/src/i18n/zh-Hans.yaml @@ -6,13 +6,7 @@ Import From %1$d Secret Words: 输入助记词 More about MyTonWallet: 更多关于 MyTonWallet Creating Wallet...: 正在创建钱包... On the count of three...: 倒数三个数··· -$auth_backup_description1: | - 这是一个**仅由您控制的**且**安全**的钱包。 -$auth_backup_description2: “能力越大,责任越大。” -$auth_backup_description3: | - 您需要手动**备份助记词**,以免忘记助记词以永远失去您的钱包。 Back Up: 备份 -Skip For Now: 跳过此步骤 Passwords must be equal.: 密码必须相同。 To protect your wallet as much as possible, use a password with: 为了尽可能的保障您钱包的安全,密码请遵循下列规则 $auth_password_rule_8chars: 至少8位字符 @@ -170,7 +164,7 @@ Insufficient balance: 余额不足 InsufficientBalance: 余额不足 Optional: 选项 $send_token_symbol: 发送 %1$s -$your_balance_is: "您的余额: %balance%" +$balance_is: "余额:%balance%" Is it all ok?: 确认全部正确? Receiving Address: 接收地址 Fee: 手续费 @@ -207,7 +201,6 @@ Appearance: 外观 Light: 白天模式 Dark: 黑夜模式 System: 系统 -Create Backup: 创建备份 Stake TON: 质押 TON Earn from your tokens while holding them: 从你持有的代币身上赚一笔。 $est_apy_val: 期望年回报率 %1$d% @@ -331,4 +324,23 @@ Message is encrypted.: 消息已加密。 $dapp_ledger_warning1: 您即将使用您的**Ledger**钱包发送多方交易。您需要手动**逐个**签署每笔底层交易。 $dapp_ledger_warning2: 请慢慢来,不要中断过程。 Agree: 同意 -The hardware wallet does not support this data format: 硬件钱包不支持该数据格式 +The hardware wallet does not support this data format: 硬件钱包不支持该数据格 +Use Responsibly: 负责任地使用 +$auth_responsibly_description1: | + MyTonWallet 是一个**自我托管**钱包,这意味着**只有您**拥有完全控制权,最重要的是,对您的资金**承担全部责任**。 +$auth_responsibly_description2: | + 您的私钥存储在您的设备上,并且容易受到**黑客攻击**。 如果您的计算机感染了**恶意软件**,您的资金可能会被盗。 +$auth_responsibly_description3: | + MyTonWallet 团队对您的资金安全**不负责**,就像您的计算机制造商或互联网提供商不负责一样。 +$auth_responsibly_description4: | + **永远不要**将您的所有资金存储在一个地方。 **多样化**并使用各种软件和硬件。 始终**进行自己的研究**并了解有关加密安全的更多信息。 +Start Wallet: 启动钱包 +$auth_backup_warning_notice: | + 现在,您需要手动**备份密钥**,以防忘记密码或无法访问该设备。 +Later: 之后 +Back Up Now: 立即备份 +I have read and accept this information: 我已阅读并接受此信息 +$ledger_verify_address: 请务必使用您的 Ledger 设备验证粘贴的地址。 +$ledger_not_ready: Ledger 未连接或 TON 应用程序未打开。 +Verify now: 现在检查 +Invalid address format. Only URL Safe Base64 format is allowed.: 地址格式无效。 仅允许使用 URL Safe Base64 格式。 diff --git a/src/i18n/zh-Hant.yaml b/src/i18n/zh-Hant.yaml index a851445c..ff1728fb 100644 --- a/src/i18n/zh-Hant.yaml +++ b/src/i18n/zh-Hant.yaml @@ -6,13 +6,7 @@ Import From %1$d Secret Words: 輸入註記詞 More about MyTonWallet: 更多關於 MyTonWallet Creating Wallet...: 錢包創建中... On the count of three...: 數到三... -$auth_backup_description1: | - 這是一個**僅由您控制**且**安全**的錢包。 -$auth_backup_description2: 「能力越強,責任越大。」 -$auth_backup_description3: | - 你需要手動**備份註記詞**,免得你忘記助記詞而無法登入此裝置。 Back Up: 備份 -Skip For Now: 跳過此步驟 Passwords must be equal.: 密碼必須相同。 To protect your wallet as much as possible, use a password with: 為了盡可能保護您的錢包,密碼請遵循下列規則 $auth_password_rule_8chars: 至少 8 個字元 @@ -171,7 +165,7 @@ Insufficient balance: 餘額不足 InsufficientBalance: 餘額不足 Optional: 選項 $send_token_symbol: 發送 %1$s -$your_balance_is: "你的餘額: %balance%" +$balance_is: "餘額:%balance%" Is it all ok?: 全部確認都 OK? Receiving Address: 接收地址 Fee: 手續費 @@ -207,7 +201,6 @@ Appearance: 外觀 Light: 亮色模式 Dark: 暗色模式 System: 系統 -Create Backup: 創建備份 Stake TON: 質押 TON Earn from your tokens while holding them: 從你持有中的代幣獲利 $est_apy_val: Est. APY %1$d% @@ -332,3 +325,22 @@ $dapp_ledger_warning1: 您即將使用您的**Ledger**錢包發送多方交易 $dapp_ledger_warning2: 請慢慢來,不要中斷過程。 Agree: 同意 The hardware wallet does not support this data format: 硬件錢包不支持該數據格式 +Use Responsibly: 負責任地使用 +$auth_responsibly_description1: | + MyTonWallet 是一個**自我託管**錢包,這意味著**只有您**擁有完全控制權,最重要的是,對您的資金**承擔全部責任**。 +$auth_responsibly_description2: | + 您的私鑰存儲在您的設備上,並且容易受到**黑客攻擊**。 如果您的計算機感染了**惡意軟件**,您的資金可能會被盜。 +$auth_responsibly_description3: | + MyTonWallet 團隊對您的資金安全**不負責**,就像您的計算機製造商或互聯網提供商不負責一樣。 +$auth_responsibly_description4: | + **永遠不要**將您的所有資金存儲在一個地方。 **多樣化**並使用各種軟件和硬件。 始終**進行自己的研究**並了解有關加密安全的更多信息。 +Start Wallet: 啟動錢包 +$auth_backup_warning_notice: | + 現在,您需要手動**備份密鑰**,以防忘記密碼或無法訪問該設備。 +Later: 之後 +Back Up Now: 立即備份 +I have read and accept this information: 我已閱讀並接受此信息 +$ledger_verify_address: 請務必使用您的 Ledger 設備驗證粘貼的地址。 +$ledger_not_ready: Ledger 未連接或 TON 應用程序未打開。 +Verify now: 現在檢查 +Invalid address format. Only URL Safe Base64 format is allowed.: 地址格式無效。 僅允許使用 URL Safe Base64 格式。 diff --git a/src/index.html b/src/index.html index 261d53c3..61bb1bd7 100644 --- a/src/index.html +++ b/src/index.html @@ -17,6 +17,7 @@ + diff --git a/src/lib/qr-code-styling/LICENSE b/src/lib/qr-code-styling/LICENSE deleted file mode 100644 index c2689e7c..00000000 --- a/src/lib/qr-code-styling/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019 Denys Kozak - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/src/lib/qr-code-styling/README.md b/src/lib/qr-code-styling/README.md deleted file mode 100644 index 6127ee67..00000000 --- a/src/lib/qr-code-styling/README.md +++ /dev/null @@ -1 +0,0 @@ -The original package can be found at https://github.com/signalive/qr-code-styling diff --git a/src/lib/qr-code-styling/constants/cornerDotTypes.ts b/src/lib/qr-code-styling/constants/cornerDotTypes.ts deleted file mode 100644 index fccfafcb..00000000 --- a/src/lib/qr-code-styling/constants/cornerDotTypes.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { CornerDotTypes } from '../types'; - -export default { - dot: 'dot', - square: 'square', -} as CornerDotTypes; diff --git a/src/lib/qr-code-styling/constants/cornerSquareTypes.ts b/src/lib/qr-code-styling/constants/cornerSquareTypes.ts deleted file mode 100644 index 7469ad0e..00000000 --- a/src/lib/qr-code-styling/constants/cornerSquareTypes.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { CornerSquareTypes } from '../types'; - -export default { - dot: 'dot', - square: 'square', - extraRounded: 'extra-rounded', -} as CornerSquareTypes; diff --git a/src/lib/qr-code-styling/constants/dotTypes.ts b/src/lib/qr-code-styling/constants/dotTypes.ts deleted file mode 100644 index f1f4c6f4..00000000 --- a/src/lib/qr-code-styling/constants/dotTypes.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { DotTypes } from '../types'; - -export default { - dots: 'dots', - rounded: 'rounded', - classy: 'classy', - classyRounded: 'classy-rounded', - square: 'square', - extraRounded: 'extra-rounded', -} as DotTypes; diff --git a/src/lib/qr-code-styling/constants/drawTypes.ts b/src/lib/qr-code-styling/constants/drawTypes.ts deleted file mode 100644 index ea22009f..00000000 --- a/src/lib/qr-code-styling/constants/drawTypes.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { DrawTypes } from '../types'; - -export default { - canvas: 'canvas', - svg: 'svg', -} as DrawTypes; diff --git a/src/lib/qr-code-styling/constants/errorCorrectionLevels.ts b/src/lib/qr-code-styling/constants/errorCorrectionLevels.ts deleted file mode 100644 index 442ab82c..00000000 --- a/src/lib/qr-code-styling/constants/errorCorrectionLevels.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { ErrorCorrectionLevel } from '../types'; - -interface ErrorCorrectionLevels { - [key: string]: ErrorCorrectionLevel; -} - -export default { - L: 'L', - M: 'M', - Q: 'Q', - H: 'H', -} as ErrorCorrectionLevels; diff --git a/src/lib/qr-code-styling/constants/errorCorrectionPercents.ts b/src/lib/qr-code-styling/constants/errorCorrectionPercents.ts deleted file mode 100644 index ec4da61c..00000000 --- a/src/lib/qr-code-styling/constants/errorCorrectionPercents.ts +++ /dev/null @@ -1,10 +0,0 @@ -interface ErrorCorrectionPercents { - [key: string]: number; -} - -export default { - L: 0.07, - M: 0.15, - Q: 0.25, - H: 0.3, -} as ErrorCorrectionPercents; diff --git a/src/lib/qr-code-styling/constants/gradientTypes.ts b/src/lib/qr-code-styling/constants/gradientTypes.ts deleted file mode 100644 index 64181646..00000000 --- a/src/lib/qr-code-styling/constants/gradientTypes.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { GradientTypes } from '../types'; - -export default { - radial: 'radial', - linear: 'linear', -} as GradientTypes; diff --git a/src/lib/qr-code-styling/constants/modes.ts b/src/lib/qr-code-styling/constants/modes.ts deleted file mode 100644 index acfba153..00000000 --- a/src/lib/qr-code-styling/constants/modes.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Mode } from '../types'; - -interface Modes { - [key: string]: Mode; -} - -export default { - numeric: 'Numeric', - alphanumeric: 'Alphanumeric', - byte: 'Byte', - kanji: 'Kanji', -} as Modes; diff --git a/src/lib/qr-code-styling/constants/qrTypes.ts b/src/lib/qr-code-styling/constants/qrTypes.ts deleted file mode 100644 index d659bcb6..00000000 --- a/src/lib/qr-code-styling/constants/qrTypes.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { TypeNumber } from '../types'; - -interface TypesMap { - [key: number]: TypeNumber; -} - -const qrTypes: TypesMap = {}; - -for (let type = 0; type <= 40; type++) { - qrTypes[type] = type as TypeNumber; -} - -// 0 types is autodetect - -// types = { -// 0: 0, -// 1: 1, -// ... -// 40: 40 -// } - -export default qrTypes; diff --git a/src/lib/qr-code-styling/core/QRCanvas.ts b/src/lib/qr-code-styling/core/QRCanvas.ts deleted file mode 100644 index f9295ae7..00000000 --- a/src/lib/qr-code-styling/core/QRCanvas.ts +++ /dev/null @@ -1,475 +0,0 @@ -/* eslint-disable */ -import type { FilterFunction, Gradient, QRCode } from '../types'; - -import errorCorrectionPercents from '../constants/errorCorrectionPercents'; -import gradientTypes from '../constants/gradientTypes'; -import QRCornerDot from '../figures/cornerDot/canvas/QRCornerDot'; -import QRCornerSquare from '../figures/cornerSquare/canvas/QRCornerSquare'; -import QRDot from '../figures/dot/canvas/QRDot'; -import calculateImageSize from '../tools/calculateImageSize'; - -import type { RequiredOptions } from './QROptions'; - -const squareMask = [ - [1, 1, 1, 1, 1, 1, 1], - [1, 0, 0, 0, 0, 0, 1], - [1, 0, 0, 0, 0, 0, 1], - [1, 0, 0, 0, 0, 0, 1], - [1, 0, 0, 0, 0, 0, 1], - [1, 0, 0, 0, 0, 0, 1], - [1, 1, 1, 1, 1, 1, 1], -]; - -const dotMask = [ - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 1, 1, 1, 0, 0], - [0, 0, 1, 1, 1, 0, 0], - [0, 0, 1, 1, 1, 0, 0], - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0], -]; - -export default class QRCanvas { - _canvas: HTMLCanvasElement; - - _options: RequiredOptions; - - _qr?: QRCode; - - _image?: HTMLImageElement; - - // TODO don't pass all options to this class - constructor(options: RequiredOptions) { - this._canvas = document.createElement('canvas'); - this._canvas.width = options.width; - this._canvas.height = options.height; - this._options = options; - } - - get context(): CanvasRenderingContext2D | null { - return this._canvas.getContext('2d'); - } - - get width(): number { - return this._canvas.width; - } - - get height(): number { - return this._canvas.height; - } - - getCanvas(): HTMLCanvasElement { - return this._canvas; - } - - clear(): void { - const canvasContext = this.context; - - if (canvasContext) { - canvasContext.clearRect(0, 0, this._canvas.width, this._canvas.height); - } - } - - async drawQR(qr: QRCode): Promise { - const count = qr.getModuleCount(); - const minSize = Math.min(this._options.width, this._options.height) - this._options.margin * 2; - const dotSize = Math.floor(minSize / count); - let drawImageSize = { - hideXDots: 0, - hideYDots: 0, - width: 0, - height: 0, - }; - - this._qr = qr; - - if (this._options.image) { - await this.loadImage(); - if (!this._image) return; - const { imageOptions, qrOptions } = this._options; - const coverLevel = imageOptions.imageSize * errorCorrectionPercents[qrOptions.errorCorrectionLevel]; - const maxHiddenDots = Math.floor(coverLevel * count * count); - - drawImageSize = calculateImageSize({ - originalWidth: this._image.width, - originalHeight: this._image.height, - maxHiddenDots, - maxHiddenAxisDots: count - 14, - dotSize, - }); - } - - this.clear(); - this.drawBackground(); - this.drawDots((i: number, j: number): boolean => { - if (this._options.imageOptions.hideBackgroundDots) { - if ( - i >= (count - drawImageSize.hideXDots) / 2 - && i < (count + drawImageSize.hideXDots) / 2 - && j >= (count - drawImageSize.hideYDots) / 2 - && j < (count + drawImageSize.hideYDots) / 2 - ) { - return false; - } - } - - if (squareMask[i]?.[j] || squareMask[i - count + 7]?.[j] || squareMask[i]?.[j - count + 7]) { - return false; - } - - if (dotMask[i]?.[j] || dotMask[i - count + 7]?.[j] || dotMask[i]?.[j - count + 7]) { - return false; - } - - return true; - }); - this.drawCorners(); - - if (this._options.image) { - this.drawImage({ - width: drawImageSize.width, height: drawImageSize.height, count, dotSize, - }); - } - } - - drawBackground(): void { - const canvasContext = this.context; - const options = this._options; - - if (canvasContext) { - if (options.backgroundOptions.gradient) { - const gradientOptions = options.backgroundOptions.gradient; - const gradient = this._createGradient({ - context: canvasContext, - options: gradientOptions, - additionalRotation: 0, - x: 0, - y: 0, - size: this._canvas.width > this._canvas.height ? this._canvas.width : this._canvas.height, - }); - - gradientOptions.colorStops.forEach(({ offset, color }: { offset: number; color: string }) => { - gradient.addColorStop(offset, color); - }); - - canvasContext.fillStyle = gradient; - } else if (options.backgroundOptions.color) { - canvasContext.fillStyle = options.backgroundOptions.color; - } - canvasContext.fillRect(0, 0, this._canvas.width, this._canvas.height); - } - } - - drawDots(filter?: FilterFunction): void { - if (!this._qr) { - throw new Error('QR code is not defined'); - } - - const canvasContext = this.context; - - if (!canvasContext) { - throw new Error('QR code is not defined'); - } - - const options = this._options; - const count = this._qr.getModuleCount(); - - if (count > options.width || count > options.height) { - throw new Error('The canvas is too small'); - } - - const minSize = Math.min(options.width, options.height) - options.margin * 2; - const dotSize = Math.floor(minSize / count); - const xBeginning = Math.floor((options.width - count * dotSize) / 2); - const yBeginning = Math.floor((options.height - count * dotSize) / 2); - const dot = new QRDot({ context: canvasContext, type: options.dotsOptions.type }); - - canvasContext.beginPath(); - - for (let i = 0; i < count; i++) { - for (let j = 0; j < count; j++) { - if (filter && !filter(i, j)) { - continue; - } - if (!this._qr.isDark(i, j)) { - continue; - } - dot.draw( - xBeginning + i * dotSize, - yBeginning + j * dotSize, - dotSize, - (xOffset: number, yOffset: number): boolean => { - if (i + xOffset < 0 || j + yOffset < 0 || i + xOffset >= count || j + yOffset >= count) return false; - if (filter && !filter(i + xOffset, j + yOffset)) return false; - return !!this._qr && this._qr.isDark(i + xOffset, j + yOffset); - }, - ); - } - } - - if (options.dotsOptions.gradient) { - const gradientOptions = options.dotsOptions.gradient; - const gradient = this._createGradient({ - context: canvasContext, - options: gradientOptions, - additionalRotation: 0, - x: xBeginning, - y: yBeginning, - size: count * dotSize, - }); - - gradientOptions.colorStops.forEach(({ offset, color }: { offset: number; color: string }) => { - gradient.addColorStop(offset, color); - }); - - canvasContext.fillStyle = canvasContext.strokeStyle = gradient; - } else if (options.dotsOptions.color) { - canvasContext.fillStyle = canvasContext.strokeStyle = options.dotsOptions.color; - } - - canvasContext.fill('evenodd'); - } - - drawCorners(filter?: FilterFunction): void { - if (!this._qr) { - throw new Error('QR code is not defined'); - } - - const canvasContext = this.context; - - if (!canvasContext) { - throw new Error('QR code is not defined'); - } - - const options = this._options; - - const count = this._qr.getModuleCount(); - const minSize = Math.min(options.width, options.height) - options.margin * 2; - const dotSize = Math.floor(minSize / count); - const cornersSquareSize = dotSize * 7; - const cornersDotSize = dotSize * 3; - const xBeginning = Math.floor((options.width - count * dotSize) / 2); - const yBeginning = Math.floor((options.height - count * dotSize) / 2); - - [ - [0, 0, 0], - [1, 0, Math.PI / 2], - [0, 1, -Math.PI / 2], - ].forEach(([column, row, rotation]) => { - if (filter && !filter(column, row)) { - return; - } - - const x = xBeginning + column * dotSize * (count - 7); - const y = yBeginning + row * dotSize * (count - 7); - - if (options.cornersSquareOptions?.type) { - const cornersSquare = new QRCornerSquare({ context: canvasContext, type: options.cornersSquareOptions?.type }); - - canvasContext.beginPath(); - cornersSquare.draw(x, y, cornersSquareSize, rotation); - } else { - const dot = new QRDot({ context: canvasContext, type: options.dotsOptions.type }); - - canvasContext.beginPath(); - - for (let i = 0; i < squareMask.length; i++) { - for (let j = 0; j < squareMask[i].length; j++) { - if (!squareMask[i]?.[j]) { - continue; - } - - dot.draw( - x + i * dotSize, - y + j * dotSize, - dotSize, - (xOffset: number, yOffset: number): boolean => !!squareMask[i + xOffset]?.[j + yOffset], - ); - } - } - } - - if (options.cornersSquareOptions?.gradient) { - const gradientOptions = options.cornersSquareOptions.gradient; - const gradient = this._createGradient({ - context: canvasContext, - options: gradientOptions, - additionalRotation: rotation, - x, - y, - size: cornersSquareSize, - }); - - gradientOptions.colorStops.forEach(({ offset, color }: { offset: number; color: string }) => { - gradient.addColorStop(offset, color); - }); - - canvasContext.fillStyle = canvasContext.strokeStyle = gradient; - } else if (options.cornersSquareOptions?.color) { - canvasContext.fillStyle = canvasContext.strokeStyle = options.cornersSquareOptions.color; - } - - canvasContext.fill('evenodd'); - - if (options.cornersDotOptions?.type) { - const cornersDot = new QRCornerDot({ context: canvasContext, type: options.cornersDotOptions?.type }); - - canvasContext.beginPath(); - cornersDot.draw(x + dotSize * 2, y + dotSize * 2, cornersDotSize, rotation); - } else { - const dot = new QRDot({ context: canvasContext, type: options.dotsOptions.type }); - - canvasContext.beginPath(); - - for (let i = 0; i < dotMask.length; i++) { - for (let j = 0; j < dotMask[i].length; j++) { - if (!dotMask[i]?.[j]) { - continue; - } - - dot.draw( - x + i * dotSize, - y + j * dotSize, - dotSize, - (xOffset: number, yOffset: number): boolean => !!dotMask[i + xOffset]?.[j + yOffset], - ); - } - } - } - - if (options.cornersDotOptions?.gradient) { - const gradientOptions = options.cornersDotOptions.gradient; - const gradient = this._createGradient({ - context: canvasContext, - options: gradientOptions, - additionalRotation: rotation, - x: x + dotSize * 2, - y: y + dotSize * 2, - size: cornersDotSize, - }); - - gradientOptions.colorStops.forEach(({ offset, color }: { offset: number; color: string }) => { - gradient.addColorStop(offset, color); - }); - - canvasContext.fillStyle = canvasContext.strokeStyle = gradient; - } else if (options.cornersDotOptions?.color) { - canvasContext.fillStyle = canvasContext.strokeStyle = options.cornersDotOptions.color; - } - - canvasContext.fill('evenodd'); - }); - } - - loadImage(): Promise { - return new Promise((resolve, reject) => { - const options = this._options; - const image = new Image(); - - if (!options.image) { - return reject('Image is not defined'); - } - - if (typeof options.imageOptions.crossOrigin === 'string') { - image.crossOrigin = options.imageOptions.crossOrigin; - } - - this._image = image; - image.onload = (): void => { - resolve(); - }; - image.src = options.image; - }); - } - - drawImage({ - width, - height, - count, - dotSize, - }: { - width: number; - height: number; - count: number; - dotSize: number; - }): void { - const canvasContext = this.context; - - if (!canvasContext) { - throw 'canvasContext is not defined'; - } - - if (!this._image) { - throw 'image is not defined'; - } - - const options = this._options; - const xBeginning = Math.floor((options.width - count * dotSize) / 2); - const yBeginning = Math.floor((options.height - count * dotSize) / 2); - const dx = xBeginning + options.imageOptions.margin + (count * dotSize - width) / 2; - const dy = yBeginning + options.imageOptions.margin + (count * dotSize - height) / 2; - const dw = width - options.imageOptions.margin * 2; - const dh = height - options.imageOptions.margin * 2; - - canvasContext.drawImage(this._image, dx, dy, dw < 0 ? 0 : dw, dh < 0 ? 0 : dh); - } - - _createGradient({ - context, - options, - additionalRotation, - x, - y, - size, - }: { - context: CanvasRenderingContext2D; - options: Gradient; - additionalRotation: number; - x: number; - y: number; - size: number; - }): CanvasGradient { - let gradient; - - if (options.type === gradientTypes.radial) { - gradient = context.createRadialGradient(x + size / 2, y + size / 2, 0, x + size / 2, y + size / 2, size / 2); - } else { - const rotation = ((options.rotation || 0) + additionalRotation) % (2 * Math.PI); - const positiveRotation = (rotation + 2 * Math.PI) % (2 * Math.PI); - let x0 = x + size / 2; - let y0 = y + size / 2; - let x1 = x + size / 2; - let y1 = y + size / 2; - - if ( - (positiveRotation >= 0 && positiveRotation <= 0.25 * Math.PI) - || (positiveRotation > 1.75 * Math.PI && positiveRotation <= 2 * Math.PI) - ) { - x0 -= size / 2; - y0 -= (size / 2) * Math.tan(rotation); - x1 += size / 2; - y1 += (size / 2) * Math.tan(rotation); - } else if (positiveRotation > 0.25 * Math.PI && positiveRotation <= 0.75 * Math.PI) { - y0 -= size / 2; - x0 -= size / 2 / Math.tan(rotation); - y1 += size / 2; - x1 += size / 2 / Math.tan(rotation); - } else if (positiveRotation > 0.75 * Math.PI && positiveRotation <= 1.25 * Math.PI) { - x0 += size / 2; - y0 += (size / 2) * Math.tan(rotation); - x1 -= size / 2; - y1 -= (size / 2) * Math.tan(rotation); - } else if (positiveRotation > 1.25 * Math.PI && positiveRotation <= 1.75 * Math.PI) { - y0 += size / 2; - x0 += size / 2 / Math.tan(rotation); - y1 -= size / 2; - x1 -= size / 2 / Math.tan(rotation); - } - - gradient = context.createLinearGradient(Math.round(x0), Math.round(y0), Math.round(x1), Math.round(y1)); - } - - return gradient; - } -} diff --git a/src/lib/qr-code-styling/core/QRCodeStyling.ts b/src/lib/qr-code-styling/core/QRCodeStyling.ts deleted file mode 100644 index f44beb16..00000000 --- a/src/lib/qr-code-styling/core/QRCodeStyling.ts +++ /dev/null @@ -1,180 +0,0 @@ -/* eslint-disable no-underscore-dangle,no-promise-executor-return */ -import qrcode from 'qrcode-generator'; - -import type { - DownloadOptions, Extension, Options, QRCode, -} from '../types'; - -import drawTypes from '../constants/drawTypes'; -import downloadURI from '../tools/downloadURI'; -import getMode from '../tools/getMode'; -import mergeDeep from '../tools/merge'; -import sanitizeOptions from '../tools/sanitizeOptions'; - -import QRCanvas from './QRCanvas'; -import type { RequiredOptions } from './QROptions'; -import defaultOptions from './QROptions'; -import QRSVG from './QRSVG'; - -export default class QRCodeStyling { - _options: RequiredOptions; - - _container?: HTMLElement; - - _canvas?: QRCanvas; - - _svg?: QRSVG; - - _qr?: QRCode; - - _canvasDrawingPromise?: Promise; - - _svgDrawingPromise?: Promise; - - constructor(options?: Partial) { - this._options = options ? sanitizeOptions(mergeDeep(defaultOptions, options) as RequiredOptions) : defaultOptions; - this.update(); - } - - static _clearContainer(container?: HTMLElement): void { - if (container) { - container.innerHTML = ''; - } - } - - async _getQRStylingElement(extension: Extension = 'png'): Promise { - if (!this._qr) throw new Error('QR code is empty'); - - if (extension.toLowerCase() === 'svg') { - let promise; let - svg: QRSVG; - - if (this._svg && this._svgDrawingPromise) { - svg = this._svg; - promise = this._svgDrawingPromise; - } else { - svg = new QRSVG(this._options); - promise = svg.drawQR(this._qr); - } - - await promise; - - return svg; - } else { - let promise; let - canvas: QRCanvas; - - if (this._canvas && this._canvasDrawingPromise) { - canvas = this._canvas; - promise = this._canvasDrawingPromise; - } else { - canvas = new QRCanvas(this._options); - promise = canvas.drawQR(this._qr); - } - - await promise; - - return canvas; - } - } - - update(options?: Partial): void { - QRCodeStyling._clearContainer(this._container); - this._options = options ? sanitizeOptions(mergeDeep(this._options, options) as RequiredOptions) : this._options; - - if (!this._options.data) { - return; - } - - this._qr = qrcode(this._options.qrOptions.typeNumber, this._options.qrOptions.errorCorrectionLevel); - this._qr.addData(this._options.data, this._options.qrOptions.mode || getMode(this._options.data)); - this._qr.make(); - - if (this._options.type === drawTypes.canvas) { - this._canvas = new QRCanvas(this._options); - this._canvasDrawingPromise = this._canvas.drawQR(this._qr); - this._svgDrawingPromise = undefined; - this._svg = undefined; - } else { - this._svg = new QRSVG(this._options); - this._svgDrawingPromise = this._svg.drawQR(this._qr); - this._canvasDrawingPromise = undefined; - this._canvas = undefined; - } - - this.append(this._container); - } - - append(container?: HTMLElement): void { - if (!container) { - return; - } - - if (typeof container.appendChild !== 'function') { - throw new Error('Container should be a single DOM node'); - } - - if (this._options.type === drawTypes.canvas) { - if (this._canvas) { - container.appendChild(this._canvas.getCanvas()); - } - } else if (this._svg) { - container.appendChild(this._svg.getElement()); - } - - this._container = container; - } - - async getRawData(extension: Extension = 'png'): Promise { - if (!this._qr) throw new Error('QR code is empty'); - const element = await this._getQRStylingElement(extension); - - if (extension.toLowerCase() === 'svg') { - const serializer = new XMLSerializer(); - const source = serializer.serializeToString(((element as unknown) as QRSVG).getElement()); - - return new Blob([`\r\n${source}`], { type: 'image/svg+xml' }); - } else { - // eslint-disable-next-line max-len - return new Promise((resolve) => ((element as unknown) as QRCanvas).getCanvas().toBlob(resolve, `image/${extension}`, 1)); - } - } - - async download(downloadOptions?: Partial | string): Promise { - if (!this._qr) throw new Error('QR code is empty'); - let extension = 'png' as Extension; - let name = 'qr'; - - // TODO remove deprecated code in the v2 - if (typeof downloadOptions === 'string') { - extension = downloadOptions as Extension; - // eslint-disable-next-line no-console - console.warn( - // eslint-disable-next-line max-len - "Extension is deprecated as argument for 'download' method, please pass object { name: '...', extension: '...' } as argument", - ); - // eslint-disable-next-line no-null/no-null - } else if (typeof downloadOptions === 'object' && downloadOptions !== null) { - if (downloadOptions.name) { - name = downloadOptions.name; - } - if (downloadOptions.extension) { - extension = downloadOptions.extension; - } - } - - const element = await this._getQRStylingElement(extension); - - if (extension.toLowerCase() === 'svg') { - const serializer = new XMLSerializer(); - let source = serializer.serializeToString(((element as unknown) as QRSVG).getElement()); - - source = `\r\n${source}`; - const url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(source)}`; - downloadURI(url, `${name}.svg`); - } else { - const url = ((element as unknown) as QRCanvas).getCanvas().toDataURL(`image/${extension}`); - downloadURI(url, `${name}.${extension}`); - } - } -} diff --git a/src/lib/qr-code-styling/core/QROptions.ts b/src/lib/qr-code-styling/core/QROptions.ts deleted file mode 100644 index 73ec8743..00000000 --- a/src/lib/qr-code-styling/core/QROptions.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { - DotType, DrawType, ErrorCorrectionLevel, Gradient, Mode, Options, TypeNumber, -} from '../types'; - -import drawTypes from '../constants/drawTypes'; -import errorCorrectionLevels from '../constants/errorCorrectionLevels'; -import qrTypes from '../constants/qrTypes'; - -export interface RequiredOptions extends Options { - type: DrawType; - width: number; - height: number; - margin: number; - data: string; - qrOptions: { - typeNumber: TypeNumber; - mode?: Mode; - errorCorrectionLevel: ErrorCorrectionLevel; - }; - imageOptions: { - hideBackgroundDots: boolean; - imageSize: number; - crossOrigin?: string; - margin: number; - }; - dotsOptions: { - type: DotType; - color: string; - gradient?: Gradient; - }; - backgroundOptions: { - color: string; - gradient?: Gradient; - }; -} - -const defaultOptions: RequiredOptions = { - type: drawTypes.canvas, - width: 300, - height: 300, - data: '', - margin: 0, - qrOptions: { - typeNumber: qrTypes[0], - mode: undefined, - errorCorrectionLevel: errorCorrectionLevels.Q, - }, - imageOptions: { - hideBackgroundDots: true, - imageSize: 0.4, - crossOrigin: undefined, - margin: 0, - }, - dotsOptions: { - type: 'square', - color: '#000', - }, - backgroundOptions: { - color: '#fff', - }, -}; - -export default defaultOptions; diff --git a/src/lib/qr-code-styling/core/QRSVG.ts b/src/lib/qr-code-styling/core/QRSVG.ts deleted file mode 100644 index 5bf92bdc..00000000 --- a/src/lib/qr-code-styling/core/QRSVG.ts +++ /dev/null @@ -1,504 +0,0 @@ -/* eslint-disable no-underscore-dangle,no-multi-assign,consistent-return */ -import type { FilterFunction, Gradient, QRCode } from '../types'; - -import errorCorrectionPercents from '../constants/errorCorrectionPercents'; -import gradientTypes from '../constants/gradientTypes'; -import QRCornerDot from '../figures/cornerDot/svg/QRCornerDot'; -import QRCornerSquare from '../figures/cornerSquare/svg/QRCornerSquare'; -import QRDot from '../figures/dot/svg/QRDot'; -import calculateImageSize from '../tools/calculateImageSize'; - -import type { RequiredOptions } from './QROptions'; - -const squareMask = [ - [1, 1, 1, 1, 1, 1, 1], - [1, 0, 0, 0, 0, 0, 1], - [1, 0, 0, 0, 0, 0, 1], - [1, 0, 0, 0, 0, 0, 1], - [1, 0, 0, 0, 0, 0, 1], - [1, 0, 0, 0, 0, 0, 1], - [1, 1, 1, 1, 1, 1, 1], -]; - -const dotMask = [ - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 1, 1, 1, 0, 0], - [0, 0, 1, 1, 1, 0, 0], - [0, 0, 1, 1, 1, 0, 0], - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0], -]; - -export default class QRSVG { - _element: SVGElement; - - _defs: SVGElement; - - _dotsClipPath?: SVGElement; - - _cornersSquareClipPath?: SVGElement; - - _cornersDotClipPath?: SVGElement; - - _options: RequiredOptions; - - _qr?: QRCode; - - _image?: HTMLImageElement; - - // TODO don't pass all options to this class - constructor(options: RequiredOptions) { - this._element = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - this._element.setAttribute('width', String(options.width)); - this._element.setAttribute('height', String(options.height)); - this._defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs'); - this._element.appendChild(this._defs); - - this._options = options; - } - - get width(): number { - return this._options.width; - } - - get height(): number { - return this._options.height; - } - - getElement(): SVGElement { - return this._element; - } - - clear(): void { - const oldElement = this._element; - this._element = oldElement.cloneNode(false) as SVGElement; - oldElement?.parentNode?.replaceChild(this._element, oldElement); - this._defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs'); - this._element.appendChild(this._defs); - } - - async drawQR(qr: QRCode): Promise { - const count = qr.getModuleCount(); - const minSize = Math.min(this._options.width, this._options.height) - this._options.margin * 2; - const dotSize = Math.floor(minSize / count); - let drawImageSize = { - hideXDots: 0, - hideYDots: 0, - width: 0, - height: 0, - }; - - this._qr = qr; - - if (this._options.image) { - // We need it to get image size - await this.loadImage(); - if (!this._image) return; - const { imageOptions, qrOptions } = this._options; - const coverLevel = imageOptions.imageSize * errorCorrectionPercents[qrOptions.errorCorrectionLevel]; - const maxHiddenDots = Math.floor(coverLevel * count * count); - - drawImageSize = calculateImageSize({ - originalWidth: this._image.width, - originalHeight: this._image.height, - maxHiddenDots, - maxHiddenAxisDots: count - 14, - dotSize, - }); - } - - this.clear(); - this.drawBackground(); - this.drawDots((i: number, j: number): boolean => { - if (this._options.imageOptions.hideBackgroundDots) { - if ( - i >= (count - drawImageSize.hideXDots) / 2 - && i < (count + drawImageSize.hideXDots) / 2 - && j >= (count - drawImageSize.hideYDots) / 2 - && j < (count + drawImageSize.hideYDots) / 2 - ) { - return false; - } - } - - if (squareMask[i]?.[j] || squareMask[i - count + 7]?.[j] || squareMask[i]?.[j - count + 7]) { - return false; - } - - if (dotMask[i]?.[j] || dotMask[i - count + 7]?.[j] || dotMask[i]?.[j - count + 7]) { - return false; - } - - return true; - }); - this.drawCorners(); - - if (this._options.image) { - this.drawImage({ - width: drawImageSize.width, height: drawImageSize.height, count, dotSize, - }); - } - } - - drawBackground(): void { - const element = this._element; - const options = this._options; - - if (element) { - const gradientOptions = options.backgroundOptions?.gradient; - const color = options.backgroundOptions?.color; - - if (gradientOptions || color) { - this._createColor({ - options: gradientOptions, - color, - additionalRotation: 0, - x: 0, - y: 0, - height: options.height, - width: options.width, - name: 'background-color', - }); - } - } - } - - drawDots(filter?: FilterFunction): void { - if (!this._qr) { - throw new Error('QR code is not defined'); - } - - const options = this._options; - const count = this._qr.getModuleCount(); - - if (count > options.width || count > options.height) { - throw new Error('The canvas is too small'); - } - - const minSize = Math.min(options.width, options.height) - options.margin * 2; - const dotSize = Math.floor(minSize / count); - const xBeginning = Math.floor((options.width - count * dotSize) / 2); - const yBeginning = Math.floor((options.height - count * dotSize) / 2); - const dot = new QRDot({ svg: this._element, type: options.dotsOptions.type }); - - this._dotsClipPath = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath'); - this._dotsClipPath.setAttribute('id', 'clip-path-dot-color'); - this._defs.appendChild(this._dotsClipPath); - - this._createColor({ - options: options.dotsOptions?.gradient, - color: options.dotsOptions.color, - additionalRotation: 0, - x: xBeginning, - y: yBeginning, - height: count * dotSize, - width: count * dotSize, - name: 'dot-color', - }); - - for (let i = 0; i < count; i++) { - for (let j = 0; j < count; j++) { - if (filter && !filter(i, j)) { - continue; - } - if (!this._qr?.isDark(i, j)) { - continue; - } - - dot.draw( - xBeginning + i * dotSize, - yBeginning + j * dotSize, - dotSize, - (xOffset: number, yOffset: number): boolean => { - if (i + xOffset < 0 || j + yOffset < 0 || i + xOffset >= count || j + yOffset >= count) return false; - if (filter && !filter(i + xOffset, j + yOffset)) return false; - return !!this._qr && this._qr.isDark(i + xOffset, j + yOffset); - }, - ); - - if (dot._element && this._dotsClipPath) { - this._dotsClipPath.appendChild(dot._element); - } - } - } - } - - drawCorners(): void { - if (!this._qr) { - throw new Error('QR code is not defined'); - } - - const element = this._element; - const options = this._options; - - if (!element) { - throw new Error('Element code is not defined'); - } - - const count = this._qr.getModuleCount(); - const minSize = Math.min(options.width, options.height) - options.margin * 2; - const dotSize = Math.floor(minSize / count); - const cornersSquareSize = dotSize * 7; - const cornersDotSize = dotSize * 3; - const xBeginning = Math.floor((options.width - count * dotSize) / 2); - const yBeginning = Math.floor((options.height - count * dotSize) / 2); - - [ - [0, 0, 0], - [1, 0, Math.PI / 2], - [0, 1, -Math.PI / 2], - ].forEach(([column, row, rotation]) => { - const x = xBeginning + column * dotSize * (count - 7); - const y = yBeginning + row * dotSize * (count - 7); - let cornersSquareClipPath = this._dotsClipPath; - let cornersDotClipPath = this._dotsClipPath; - - if (options.cornersSquareOptions?.gradient || options.cornersSquareOptions?.color) { - cornersSquareClipPath = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath'); - cornersSquareClipPath.setAttribute('id', `clip-path-corners-square-color-${column}-${row}`); - this._defs.appendChild(cornersSquareClipPath); - this._cornersSquareClipPath = this._cornersDotClipPath = cornersDotClipPath = cornersSquareClipPath; - - this._createColor({ - options: options.cornersSquareOptions?.gradient, - color: options.cornersSquareOptions?.color, - additionalRotation: rotation, - x, - y, - height: cornersSquareSize, - width: cornersSquareSize, - name: `corners-square-color-${column}-${row}`, - }); - } - - if (options.cornersSquareOptions?.type) { - const cornersSquare = new QRCornerSquare({ svg: this._element, type: options.cornersSquareOptions.type }); - - cornersSquare.draw(x, y, cornersSquareSize, rotation); - - if (cornersSquare._element && cornersSquareClipPath) { - cornersSquareClipPath.appendChild(cornersSquare._element); - } - } else { - const dot = new QRDot({ svg: this._element, type: options.dotsOptions.type }); - - for (let i = 0; i < squareMask.length; i++) { - for (let j = 0; j < squareMask[i].length; j++) { - if (!squareMask[i]?.[j]) { - continue; - } - - dot.draw( - x + i * dotSize, - y + j * dotSize, - dotSize, - (xOffset: number, yOffset: number): boolean => !!squareMask[i + xOffset]?.[j + yOffset], - ); - - if (dot._element && cornersSquareClipPath) { - cornersSquareClipPath.appendChild(dot._element); - } - } - } - } - - if (options.cornersDotOptions?.gradient || options.cornersDotOptions?.color) { - cornersDotClipPath = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath'); - cornersDotClipPath.setAttribute('id', `clip-path-corners-dot-color-${column}-${row}`); - this._defs.appendChild(cornersDotClipPath); - this._cornersDotClipPath = cornersDotClipPath; - - this._createColor({ - options: options.cornersDotOptions?.gradient, - color: options.cornersDotOptions?.color, - additionalRotation: rotation, - x: x + dotSize * 2, - y: y + dotSize * 2, - height: cornersDotSize, - width: cornersDotSize, - name: `corners-dot-color-${column}-${row}`, - }); - } - - if (options.cornersDotOptions?.type) { - const cornersDot = new QRCornerDot({ svg: this._element, type: options.cornersDotOptions.type }); - - cornersDot.draw(x + dotSize * 2, y + dotSize * 2, cornersDotSize, rotation); - - if (cornersDot._element && cornersDotClipPath) { - cornersDotClipPath.appendChild(cornersDot._element); - } - } else { - const dot = new QRDot({ svg: this._element, type: options.dotsOptions.type }); - - for (let i = 0; i < dotMask.length; i++) { - for (let j = 0; j < dotMask[i].length; j++) { - if (!dotMask[i]?.[j]) { - continue; - } - - dot.draw( - x + i * dotSize, - y + j * dotSize, - dotSize, - (xOffset: number, yOffset: number): boolean => !!dotMask[i + xOffset]?.[j + yOffset], - ); - - if (dot._element && cornersDotClipPath) { - cornersDotClipPath.appendChild(dot._element); - } - } - } - } - }); - } - - loadImage(): Promise { - return new Promise((resolve, reject) => { - const options = this._options; - const image = new Image(); - - if (!options.image) { - // eslint-disable-next-line prefer-promise-reject-errors,no-promise-executor-return - return reject('Image is not defined'); - } - - if (typeof options.imageOptions.crossOrigin === 'string') { - image.crossOrigin = options.imageOptions.crossOrigin; - } - - this._image = image; - image.onload = (): void => { - resolve(); - }; - image.src = options.image; - }); - } - - drawImage({ - width, - height, - count, - dotSize, - }: { - width: number; - height: number; - count: number; - dotSize: number; - }): void { - const options = this._options; - const xBeginning = Math.floor((options.width - count * dotSize) / 2); - const yBeginning = Math.floor((options.height - count * dotSize) / 2); - const dx = xBeginning + options.imageOptions.margin + (count * dotSize - width) / 2; - const dy = yBeginning + options.imageOptions.margin + (count * dotSize - height) / 2; - const dw = width - options.imageOptions.margin * 2; - const dh = height - options.imageOptions.margin * 2; - - const image = document.createElementNS('http://www.w3.org/2000/svg', 'image'); - image.setAttribute('href', options.image || ''); - image.setAttribute('x', String(dx)); - image.setAttribute('y', String(dy)); - image.setAttribute('width', `${dw}px`); - image.setAttribute('height', `${dh}px`); - - this._element.appendChild(image); - } - - _createColor({ - options, - color, - additionalRotation, - x, - y, - height, - width, - name, - }: { - options?: Gradient; - color?: string; - additionalRotation: number; - x: number; - y: number; - height: number; - width: number; - name: string; - }): void { - const size = width > height ? width : height; - const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); - rect.setAttribute('x', String(x)); - rect.setAttribute('y', String(y)); - rect.setAttribute('height', String(height)); - rect.setAttribute('width', String(width)); - rect.setAttribute('clip-path', `url('#clip-path-${name}')`); - - if (options) { - let gradient: SVGElement; - if (options.type === gradientTypes.radial) { - gradient = document.createElementNS('http://www.w3.org/2000/svg', 'radialGradient'); - gradient.setAttribute('id', name); - gradient.setAttribute('gradientUnits', 'userSpaceOnUse'); - gradient.setAttribute('fx', String(x + width / 2)); - gradient.setAttribute('fy', String(y + height / 2)); - gradient.setAttribute('cx', String(x + width / 2)); - gradient.setAttribute('cy', String(y + height / 2)); - gradient.setAttribute('r', String(size / 2)); - } else { - const rotation = ((options.rotation || 0) + additionalRotation) % (2 * Math.PI); - const positiveRotation = (rotation + 2 * Math.PI) % (2 * Math.PI); - let x0 = x + width / 2; - let y0 = y + height / 2; - let x1 = x + width / 2; - let y1 = y + height / 2; - - if ( - (positiveRotation >= 0 && positiveRotation <= 0.25 * Math.PI) - || (positiveRotation > 1.75 * Math.PI && positiveRotation <= 2 * Math.PI) - ) { - x0 -= width / 2; - y0 -= (height / 2) * Math.tan(rotation); - x1 += width / 2; - y1 += (height / 2) * Math.tan(rotation); - } else if (positiveRotation > 0.25 * Math.PI && positiveRotation <= 0.75 * Math.PI) { - y0 -= height / 2; - x0 -= width / 2 / Math.tan(rotation); - y1 += height / 2; - x1 += width / 2 / Math.tan(rotation); - } else if (positiveRotation > 0.75 * Math.PI && positiveRotation <= 1.25 * Math.PI) { - x0 += width / 2; - y0 += (height / 2) * Math.tan(rotation); - x1 -= width / 2; - y1 -= (height / 2) * Math.tan(rotation); - } else if (positiveRotation > 1.25 * Math.PI && positiveRotation <= 1.75 * Math.PI) { - y0 += height / 2; - x0 += width / 2 / Math.tan(rotation); - y1 -= height / 2; - x1 -= width / 2 / Math.tan(rotation); - } - - gradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient'); - gradient.setAttribute('id', name); - gradient.setAttribute('gradientUnits', 'userSpaceOnUse'); - gradient.setAttribute('x1', String(Math.round(x0))); - gradient.setAttribute('y1', String(Math.round(y0))); - gradient.setAttribute('x2', String(Math.round(x1))); - gradient.setAttribute('y2', String(Math.round(y1))); - } - - // eslint-disable-next-line @typescript-eslint/no-shadow - options.colorStops.forEach(({ offset, color }: { offset: number; color: string }) => { - const stop = document.createElementNS('http://www.w3.org/2000/svg', 'stop'); - stop.setAttribute('offset', `${100 * offset}%`); - stop.setAttribute('stop-color', color); - gradient.appendChild(stop); - }); - - rect.setAttribute('fill', `url('#${name}')`); - this._defs.appendChild(gradient); - } else if (color) { - rect.setAttribute('fill', color); - } - - this._element.appendChild(rect); - } -} diff --git a/src/lib/qr-code-styling/figures/cornerDot/canvas/QRCornerDot.ts b/src/lib/qr-code-styling/figures/cornerDot/canvas/QRCornerDot.ts deleted file mode 100644 index 6c47fb04..00000000 --- a/src/lib/qr-code-styling/figures/cornerDot/canvas/QRCornerDot.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* eslint-disable no-underscore-dangle,class-methods-use-this,@typescript-eslint/no-unused-expressions */ -import type { - BasicFigureDrawArgsCanvas, CornerDotType, DrawArgsCanvas, RotateFigureArgsCanvas, -} from '../../../types'; - -import cornerDotTypes from '../../../constants/cornerDotTypes'; - -export default class QRCornerDot { - _context: CanvasRenderingContext2D; - - _type: CornerDotType; - - constructor({ context, type }: { context: CanvasRenderingContext2D; type: CornerDotType }) { - this._context = context; - this._type = type; - } - - draw(x: number, y: number, size: number, rotation: number): void { - const context = this._context; - const type = this._type; - let drawFunction; - - switch (type) { - case cornerDotTypes.square: - drawFunction = this._drawSquare; - break; - case cornerDotTypes.dot: - default: - drawFunction = this._drawDot; - } - - drawFunction.call(this, { - x, y, size, context, rotation, - }); - } - - _rotateFigure({ - x, y, size, context, rotation = 0, draw, - }: RotateFigureArgsCanvas): void { - const cx = x + size / 2; - const cy = y + size / 2; - - context.translate(cx, cy); - rotation && context.rotate(rotation); - draw(); - context.closePath(); - rotation && context.rotate(-rotation); - context.translate(-cx, -cy); - } - - _basicDot(args: BasicFigureDrawArgsCanvas): void { - const { size, context } = args; - - this._rotateFigure({ - ...args, - draw: () => { - context.arc(0, 0, size / 2, 0, Math.PI * 2); - }, - }); - } - - _basicSquare(args: BasicFigureDrawArgsCanvas): void { - const { size, context } = args; - - this._rotateFigure({ - ...args, - draw: () => { - context.rect(-size / 2, -size / 2, size, size); - }, - }); - } - - _drawDot({ - x, y, size, context, rotation, - }: DrawArgsCanvas): void { - this._basicDot({ - x, y, size, context, rotation, - }); - } - - _drawSquare({ - x, y, size, context, rotation, - }: DrawArgsCanvas): void { - this._basicSquare({ - x, y, size, context, rotation, - }); - } -} diff --git a/src/lib/qr-code-styling/figures/cornerDot/svg/QRCornerDot.ts b/src/lib/qr-code-styling/figures/cornerDot/svg/QRCornerDot.ts deleted file mode 100644 index 66c7b896..00000000 --- a/src/lib/qr-code-styling/figures/cornerDot/svg/QRCornerDot.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* eslint-disable no-underscore-dangle */ -import type { - BasicFigureDrawArgs, CornerDotType, DrawArgs, RotateFigureArgs, -} from '../../../types'; - -import cornerDotTypes from '../../../constants/cornerDotTypes'; - -export default class QRCornerDot { - _element?: SVGElement; - - _svg: SVGElement; - - _type: CornerDotType; - - constructor({ svg, type }: { svg: SVGElement; type: CornerDotType }) { - this._svg = svg; - this._type = type; - } - - draw(x: number, y: number, size: number, rotation: number): void { - const type = this._type; - let drawFunction; - - switch (type) { - case cornerDotTypes.square: - drawFunction = this._drawSquare; - break; - case cornerDotTypes.dot: - default: - drawFunction = this._drawDot; - } - - drawFunction.call(this, { - x, y, size, rotation, - }); - } - - _rotateFigure({ - x, y, size, rotation = 0, draw, - }: RotateFigureArgs): void { - const cx = x + size / 2; - const cy = y + size / 2; - - draw(); - this._element?.setAttribute('transform', `rotate(${(180 * rotation) / Math.PI},${cx},${cy})`); - } - - _basicDot(args: BasicFigureDrawArgs): void { - const { size, x, y } = args; - - this._rotateFigure({ - ...args, - draw: () => { - this._element = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); - this._element.setAttribute('cx', String(x + size / 2)); - this._element.setAttribute('cy', String(y + size / 2)); - this._element.setAttribute('r', String(size / 2)); - }, - }); - } - - _basicSquare(args: BasicFigureDrawArgs): void { - const { size, x, y } = args; - - this._rotateFigure({ - ...args, - draw: () => { - this._element = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); - this._element.setAttribute('x', String(x)); - this._element.setAttribute('y', String(y)); - this._element.setAttribute('width', String(size)); - this._element.setAttribute('height', String(size)); - }, - }); - } - - _drawDot({ - x, y, size, rotation, - }: DrawArgs): void { - this._basicDot({ - x, y, size, rotation, - }); - } - - _drawSquare({ - x, y, size, rotation, - }: DrawArgs): void { - this._basicSquare({ - x, y, size, rotation, - }); - } -} diff --git a/src/lib/qr-code-styling/figures/cornerSquare/canvas/QRCornerSquare.ts b/src/lib/qr-code-styling/figures/cornerSquare/canvas/QRCornerSquare.ts deleted file mode 100644 index cd917899..00000000 --- a/src/lib/qr-code-styling/figures/cornerSquare/canvas/QRCornerSquare.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* eslint-disable no-underscore-dangle,class-methods-use-this,@typescript-eslint/no-unused-expressions */ -import type { - BasicFigureDrawArgsCanvas, CornerSquareType, DrawArgsCanvas, RotateFigureArgsCanvas, -} from '../../../types'; - -import cornerSquareTypes from '../../../constants/cornerSquareTypes'; - -export default class QRCornerSquare { - _context: CanvasRenderingContext2D; - - _type: CornerSquareType; - - constructor({ context, type }: { context: CanvasRenderingContext2D; type: CornerSquareType }) { - this._context = context; - this._type = type; - } - - draw(x: number, y: number, size: number, rotation: number): void { - const context = this._context; - const type = this._type; - let drawFunction; - - switch (type) { - case cornerSquareTypes.square: - drawFunction = this._drawSquare; - break; - case cornerSquareTypes.extraRounded: - drawFunction = this._drawExtraRounded; - break; - case cornerSquareTypes.dot: - default: - drawFunction = this._drawDot; - } - - drawFunction.call(this, { - x, y, size, context, rotation, - }); - } - - _rotateFigure({ - x, y, size, context, rotation = 0, draw, - }: RotateFigureArgsCanvas): void { - const cx = x + size / 2; - const cy = y + size / 2; - - context.translate(cx, cy); - rotation && context.rotate(rotation); - draw(); - context.closePath(); - rotation && context.rotate(-rotation); - context.translate(-cx, -cy); - } - - _basicDot(args: BasicFigureDrawArgsCanvas): void { - const { size, context } = args; - const dotSize = size / 7; - - this._rotateFigure({ - ...args, - draw: () => { - context.arc(0, 0, size / 2, 0, Math.PI * 2); - context.arc(0, 0, size / 2 - dotSize, 0, Math.PI * 2); - }, - }); - } - - _basicSquare(args: BasicFigureDrawArgsCanvas): void { - const { size, context } = args; - const dotSize = size / 7; - - this._rotateFigure({ - ...args, - draw: () => { - context.rect(-size / 2, -size / 2, size, size); - context.rect(-size / 2 + dotSize, -size / 2 + dotSize, size - 2 * dotSize, size - 2 * dotSize); - }, - }); - } - - _basicExtraRounded(args: BasicFigureDrawArgsCanvas): void { - const { size, context } = args; - const dotSize = size / 7; - - this._rotateFigure({ - ...args, - draw: () => { - context.arc(-dotSize, -dotSize, 2.5 * dotSize, Math.PI, -Math.PI / 2); - context.lineTo(dotSize, -3.5 * dotSize); - context.arc(dotSize, -dotSize, 2.5 * dotSize, -Math.PI / 2, 0); - context.lineTo(3.5 * dotSize, -dotSize); - context.arc(dotSize, dotSize, 2.5 * dotSize, 0, Math.PI / 2); - context.lineTo(-dotSize, 3.5 * dotSize); - context.arc(-dotSize, dotSize, 2.5 * dotSize, Math.PI / 2, Math.PI); - context.lineTo(-3.5 * dotSize, -dotSize); - - context.arc(-dotSize, -dotSize, 1.5 * dotSize, Math.PI, -Math.PI / 2); - context.lineTo(dotSize, -2.5 * dotSize); - context.arc(dotSize, -dotSize, 1.5 * dotSize, -Math.PI / 2, 0); - context.lineTo(2.5 * dotSize, -dotSize); - context.arc(dotSize, dotSize, 1.5 * dotSize, 0, Math.PI / 2); - context.lineTo(-dotSize, 2.5 * dotSize); - context.arc(-dotSize, dotSize, 1.5 * dotSize, Math.PI / 2, Math.PI); - context.lineTo(-2.5 * dotSize, -dotSize); - }, - }); - } - - _drawDot({ - x, y, size, context, rotation, - }: DrawArgsCanvas): void { - this._basicDot({ - x, y, size, context, rotation, - }); - } - - _drawSquare({ - x, y, size, context, rotation, - }: DrawArgsCanvas): void { - this._basicSquare({ - x, y, size, context, rotation, - }); - } - - _drawExtraRounded({ - x, y, size, context, rotation, - }: DrawArgsCanvas): void { - this._basicExtraRounded({ - x, y, size, context, rotation, - }); - } -} diff --git a/src/lib/qr-code-styling/figures/cornerSquare/svg/QRCornerSquare.ts b/src/lib/qr-code-styling/figures/cornerSquare/svg/QRCornerSquare.ts deleted file mode 100644 index 6bf1f2ea..00000000 --- a/src/lib/qr-code-styling/figures/cornerSquare/svg/QRCornerSquare.ts +++ /dev/null @@ -1,156 +0,0 @@ -/* eslint-disable no-underscore-dangle */ -import type { - BasicFigureDrawArgs, CornerSquareType, DrawArgs, RotateFigureArgs, -} from '../../../types'; - -import cornerSquareTypes from '../../../constants/cornerSquareTypes'; - -export default class QRCornerSquare { - _element?: SVGElement; - - _svg: SVGElement; - - _type: CornerSquareType; - - constructor({ svg, type }: { svg: SVGElement; type: CornerSquareType }) { - this._svg = svg; - this._type = type; - } - - draw(x: number, y: number, size: number, rotation: number): void { - const type = this._type; - let drawFunction; - - switch (type) { - case cornerSquareTypes.square: - drawFunction = this._drawSquare; - break; - case cornerSquareTypes.extraRounded: - drawFunction = this._drawExtraRounded; - break; - case cornerSquareTypes.dot: - default: - drawFunction = this._drawDot; - } - - drawFunction.call(this, { - x, y, size, rotation, - }); - } - - _rotateFigure({ - x, y, size, rotation = 0, draw, - }: RotateFigureArgs): void { - const cx = x + size / 2; - const cy = y + size / 2; - - draw(); - this._element?.setAttribute('transform', `rotate(${(180 * rotation) / Math.PI},${cx},${cy})`); - } - - _basicDot(args: BasicFigureDrawArgs): void { - const { size, x, y } = args; - const dotSize = size / 7; - - this._rotateFigure({ - ...args, - draw: () => { - this._element = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - this._element.setAttribute('clip-rule', 'evenodd'); - this._element.setAttribute( - 'd', - `M ${x + size / 2} ${y}` // M cx, y // Move to top of ring - + `a ${size / 2} ${size / 2} 0 1 0 0.1 0` // a outerRadius, outerRadius, 0, 1, 0, 1, 0 // Draw outer arc, but don't close it - + 'z' // Z // Close the outer shape - + `m 0 ${dotSize}` // m -1 outerRadius-innerRadius // Move to top point of inner radius - + `a ${size / 2 - dotSize} ${size / 2 - dotSize} 0 1 1 -0.1 0` // a innerRadius, innerRadius, 0, 1, 1, -1, 0 // Draw inner arc, but don't close it - + 'Z', // Z // Close the inner ring. Actually will still work without, but inner ring will have one unit missing in stroke - ); - }, - }); - } - - _basicSquare(args: BasicFigureDrawArgs): void { - const { size, x, y } = args; - const dotSize = size / 7; - - this._rotateFigure({ - ...args, - draw: () => { - this._element = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - this._element.setAttribute('clip-rule', 'evenodd'); - this._element.setAttribute( - 'd', - `M ${x} ${y}` - + `v ${size}` - + `h ${size}` - + `v ${-size}` - + 'z' - + `M ${x + dotSize} ${y + dotSize}` - + `h ${size - 2 * dotSize}` - + `v ${size - 2 * dotSize}` - + `h ${-size + 2 * dotSize}` - + 'z', - ); - }, - }); - } - - _basicExtraRounded(args: BasicFigureDrawArgs): void { - const { size, x, y } = args; - const dotSize = size / 7; - - this._rotateFigure({ - ...args, - draw: () => { - this._element = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - this._element.setAttribute('clip-rule', 'evenodd'); - this._element.setAttribute( - 'd', - `M ${x} ${y + 2.5 * dotSize}` - + `v ${2 * dotSize}` - + `a ${2.5 * dotSize} ${2.5 * dotSize}, 0, 0, 0, ${dotSize * 2.5} ${dotSize * 2.5}` - + `h ${2 * dotSize}` - + `a ${2.5 * dotSize} ${2.5 * dotSize}, 0, 0, 0, ${dotSize * 2.5} ${-dotSize * 2.5}` - + `v ${-2 * dotSize}` - + `a ${2.5 * dotSize} ${2.5 * dotSize}, 0, 0, 0, ${-dotSize * 2.5} ${-dotSize * 2.5}` - + `h ${-2 * dotSize}` - + `a ${2.5 * dotSize} ${2.5 * dotSize}, 0, 0, 0, ${-dotSize * 2.5} ${dotSize * 2.5}` - + `M ${x + 2.5 * dotSize} ${y + dotSize}` - + `h ${2 * dotSize}` - + `a ${1.5 * dotSize} ${1.5 * dotSize}, 0, 0, 1, ${dotSize * 1.5} ${dotSize * 1.5}` - + `v ${2 * dotSize}` - + `a ${1.5 * dotSize} ${1.5 * dotSize}, 0, 0, 1, ${-dotSize * 1.5} ${dotSize * 1.5}` - + `h ${-2 * dotSize}` - + `a ${1.5 * dotSize} ${1.5 * dotSize}, 0, 0, 1, ${-dotSize * 1.5} ${-dotSize * 1.5}` - + `v ${-2 * dotSize}` - + `a ${1.5 * dotSize} ${1.5 * dotSize}, 0, 0, 1, ${dotSize * 1.5} ${-dotSize * 1.5}`, - ); - }, - }); - } - - _drawDot({ - x, y, size, rotation, - }: DrawArgs): void { - this._basicDot({ - x, y, size, rotation, - }); - } - - _drawSquare({ - x, y, size, rotation, - }: DrawArgs): void { - this._basicSquare({ - x, y, size, rotation, - }); - } - - _drawExtraRounded({ - x, y, size, rotation, - }: DrawArgs): void { - this._basicExtraRounded({ - x, y, size, rotation, - }); - } -} diff --git a/src/lib/qr-code-styling/figures/dot/canvas/QRDot.ts b/src/lib/qr-code-styling/figures/dot/canvas/QRDot.ts deleted file mode 100644 index 81035a6d..00000000 --- a/src/lib/qr-code-styling/figures/dot/canvas/QRDot.ts +++ /dev/null @@ -1,365 +0,0 @@ -/* eslint-disable no-underscore-dangle,class-methods-use-this,@typescript-eslint/no-unused-expressions */ -import type { - BasicFigureDrawArgsCanvas, - DotType, - DrawArgsCanvas, - GetNeighbor, - RotateFigureArgsCanvas, -} from '../../../types'; - -import dotTypes from '../../../constants/dotTypes'; - -export default class QRDot { - _context: CanvasRenderingContext2D; - - _type: DotType; - - constructor({ context, type }: { context: CanvasRenderingContext2D; type: DotType }) { - this._context = context; - this._type = type; - } - - draw(x: number, y: number, size: number, getNeighbor: GetNeighbor): void { - const context = this._context; - const type = this._type; - let drawFunction; - - switch (type) { - case dotTypes.dots: - drawFunction = this._drawDot; - break; - case dotTypes.classy: - drawFunction = this._drawClassy; - break; - case dotTypes.classyRounded: - drawFunction = this._drawClassyRounded; - break; - case dotTypes.rounded: - drawFunction = this._drawRounded; - break; - case dotTypes.extraRounded: - drawFunction = this._drawExtraRounded; - break; - case dotTypes.square: - default: - drawFunction = this._drawSquare; - } - - drawFunction.call(this, { - x, y, size, context, getNeighbor, - }); - } - - _rotateFigure({ - x, y, size, context, rotation = 0, draw, - }: RotateFigureArgsCanvas): void { - const cx = x + size / 2; - const cy = y + size / 2; - - context.translate(cx, cy); - rotation && context.rotate(rotation); - draw(); - context.closePath(); - rotation && context.rotate(-rotation); - context.translate(-cx, -cy); - } - - _basicDot(args: BasicFigureDrawArgsCanvas): void { - const { size, context } = args; - - this._rotateFigure({ - ...args, - draw: () => { - context.arc(0, 0, size / 2, 0, Math.PI * 2); - }, - }); - } - - _basicSquare(args: BasicFigureDrawArgsCanvas): void { - const { size, context } = args; - - this._rotateFigure({ - ...args, - draw: () => { - context.rect(-size / 2, -size / 2, size, size); - }, - }); - } - - // if rotation === 0 - right side is rounded - _basicSideRounded(args: BasicFigureDrawArgsCanvas): void { - const { size, context } = args; - - this._rotateFigure({ - ...args, - draw: () => { - context.arc(0, 0, size / 2, -Math.PI / 2, Math.PI / 2); - context.lineTo(-size / 2, size / 2); - context.lineTo(-size / 2, -size / 2); - context.lineTo(0, -size / 2); - }, - }); - } - - // if rotation === 0 - top right corner is rounded - _basicCornerRounded(args: BasicFigureDrawArgsCanvas): void { - const { size, context } = args; - - this._rotateFigure({ - ...args, - draw: () => { - context.arc(0, 0, size / 2, -Math.PI / 2, 0); - context.lineTo(size / 2, size / 2); - context.lineTo(-size / 2, size / 2); - context.lineTo(-size / 2, -size / 2); - context.lineTo(0, -size / 2); - }, - }); - } - - // if rotation === 0 - top right corner is rounded - _basicCornerExtraRounded(args: BasicFigureDrawArgsCanvas): void { - const { size, context } = args; - - this._rotateFigure({ - ...args, - draw: () => { - context.arc(-size / 2, size / 2, size, -Math.PI / 2, 0); - context.lineTo(-size / 2, size / 2); - context.lineTo(-size / 2, -size / 2); - }, - }); - } - - _basicCornersRounded(args: BasicFigureDrawArgsCanvas): void { - const { size, context } = args; - - this._rotateFigure({ - ...args, - draw: () => { - context.arc(0, 0, size / 2, -Math.PI / 2, 0); - context.lineTo(size / 2, size / 2); - context.lineTo(0, size / 2); - context.arc(0, 0, size / 2, Math.PI / 2, Math.PI); - context.lineTo(-size / 2, -size / 2); - context.lineTo(0, -size / 2); - }, - }); - } - - _basicCornersExtraRounded(args: BasicFigureDrawArgsCanvas): void { - const { size, context } = args; - - this._rotateFigure({ - ...args, - draw: () => { - context.arc(-size / 2, size / 2, size, -Math.PI / 2, 0); - context.arc(size / 2, -size / 2, size, Math.PI / 2, Math.PI); - }, - }); - } - - _drawDot({ - x, y, size, context, - }: DrawArgsCanvas): void { - this._basicDot({ - x, y, size, context, rotation: 0, - }); - } - - _drawSquare({ - x, y, size, context, - }: DrawArgsCanvas): void { - this._basicSquare({ - x, y, size, context, rotation: 0, - }); - } - - _drawRounded({ - x, y, size, context, getNeighbor, - }: DrawArgsCanvas): void { - const leftNeighbor = getNeighbor ? Number(getNeighbor(-1, 0)) : 0; - const rightNeighbor = getNeighbor ? Number(getNeighbor(1, 0)) : 0; - const topNeighbor = getNeighbor ? Number(getNeighbor(0, -1)) : 0; - const bottomNeighbor = getNeighbor ? Number(getNeighbor(0, 1)) : 0; - - const neighborsCount = leftNeighbor + rightNeighbor + topNeighbor + bottomNeighbor; - - if (neighborsCount === 0) { - this._basicDot({ - x, y, size, context, rotation: 0, - }); - return; - } - - if (neighborsCount > 2 || (leftNeighbor && rightNeighbor) || (topNeighbor && bottomNeighbor)) { - this._basicSquare({ - x, y, size, context, rotation: 0, - }); - return; - } - - if (neighborsCount === 2) { - let rotation = 0; - - if (leftNeighbor && topNeighbor) { - rotation = Math.PI / 2; - } else if (topNeighbor && rightNeighbor) { - rotation = Math.PI; - } else if (rightNeighbor && bottomNeighbor) { - rotation = -Math.PI / 2; - } - - this._basicCornerRounded({ - x, y, size, context, rotation, - }); - return; - } - - if (neighborsCount === 1) { - let rotation = 0; - - if (topNeighbor) { - rotation = Math.PI / 2; - } else if (rightNeighbor) { - rotation = Math.PI; - } else if (bottomNeighbor) { - rotation = -Math.PI / 2; - } - - this._basicSideRounded({ - x, y, size, context, rotation, - }); - } - } - - _drawExtraRounded({ - x, y, size, context, getNeighbor, - }: DrawArgsCanvas): void { - const leftNeighbor = getNeighbor ? Number(getNeighbor(-1, 0)) : 0; - const rightNeighbor = getNeighbor ? Number(getNeighbor(1, 0)) : 0; - const topNeighbor = getNeighbor ? Number(getNeighbor(0, -1)) : 0; - const bottomNeighbor = getNeighbor ? Number(getNeighbor(0, 1)) : 0; - - const neighborsCount = leftNeighbor + rightNeighbor + topNeighbor + bottomNeighbor; - - if (neighborsCount === 0) { - this._basicDot({ - x, y, size, context, rotation: 0, - }); - return; - } - - if (neighborsCount > 2 || (leftNeighbor && rightNeighbor) || (topNeighbor && bottomNeighbor)) { - this._basicSquare({ - x, y, size, context, rotation: 0, - }); - return; - } - - if (neighborsCount === 2) { - let rotation = 0; - - if (leftNeighbor && topNeighbor) { - rotation = Math.PI / 2; - } else if (topNeighbor && rightNeighbor) { - rotation = Math.PI; - } else if (rightNeighbor && bottomNeighbor) { - rotation = -Math.PI / 2; - } - - this._basicCornerExtraRounded({ - x, y, size, context, rotation, - }); - return; - } - - if (neighborsCount === 1) { - let rotation = 0; - - if (topNeighbor) { - rotation = Math.PI / 2; - } else if (rightNeighbor) { - rotation = Math.PI; - } else if (bottomNeighbor) { - rotation = -Math.PI / 2; - } - - this._basicSideRounded({ - x, y, size, context, rotation, - }); - } - } - - _drawClassy({ - x, y, size, context, getNeighbor, - }: DrawArgsCanvas): void { - const leftNeighbor = getNeighbor ? Number(getNeighbor(-1, 0)) : 0; - const rightNeighbor = getNeighbor ? Number(getNeighbor(1, 0)) : 0; - const topNeighbor = getNeighbor ? Number(getNeighbor(0, -1)) : 0; - const bottomNeighbor = getNeighbor ? Number(getNeighbor(0, 1)) : 0; - - const neighborsCount = leftNeighbor + rightNeighbor + topNeighbor + bottomNeighbor; - - if (neighborsCount === 0) { - this._basicCornersRounded({ - x, y, size, context, rotation: Math.PI / 2, - }); - return; - } - - if (!leftNeighbor && !topNeighbor) { - this._basicCornerRounded({ - x, y, size, context, rotation: -Math.PI / 2, - }); - return; - } - - if (!rightNeighbor && !bottomNeighbor) { - this._basicCornerRounded({ - x, y, size, context, rotation: Math.PI / 2, - }); - return; - } - - this._basicSquare({ - x, y, size, context, rotation: 0, - }); - } - - _drawClassyRounded({ - x, y, size, context, getNeighbor, - }: DrawArgsCanvas): void { - const leftNeighbor = getNeighbor ? Number(getNeighbor(-1, 0)) : 0; - const rightNeighbor = getNeighbor ? Number(getNeighbor(1, 0)) : 0; - const topNeighbor = getNeighbor ? Number(getNeighbor(0, -1)) : 0; - const bottomNeighbor = getNeighbor ? Number(getNeighbor(0, 1)) : 0; - - const neighborsCount = leftNeighbor + rightNeighbor + topNeighbor + bottomNeighbor; - - if (neighborsCount === 0) { - this._basicCornersRounded({ - x, y, size, context, rotation: Math.PI / 2, - }); - return; - } - - if (!leftNeighbor && !topNeighbor) { - this._basicCornerExtraRounded({ - x, y, size, context, rotation: -Math.PI / 2, - }); - return; - } - - if (!rightNeighbor && !bottomNeighbor) { - this._basicCornerExtraRounded({ - x, y, size, context, rotation: Math.PI / 2, - }); - return; - } - - this._basicSquare({ - x, y, size, context, rotation: 0, - }); - } -} diff --git a/src/lib/qr-code-styling/figures/dot/svg/QRDot.ts b/src/lib/qr-code-styling/figures/dot/svg/QRDot.ts deleted file mode 100644 index 14fa51cf..00000000 --- a/src/lib/qr-code-styling/figures/dot/svg/QRDot.ts +++ /dev/null @@ -1,367 +0,0 @@ -/* eslint-disable no-underscore-dangle */ -import type { - BasicFigureDrawArgs, DotType, DrawArgs, GetNeighbor, RotateFigureArgs, -} from '../../../types'; - -import dotTypes from '../../../constants/dotTypes'; - -export default class QRDot { - _element?: SVGElement; - - _svg: SVGElement; - - _type: DotType; - - constructor({ svg, type }: { svg: SVGElement; type: DotType }) { - this._svg = svg; - this._type = type; - } - - draw(x: number, y: number, size: number, getNeighbor: GetNeighbor): void { - const type = this._type; - let drawFunction; - - switch (type) { - case dotTypes.dots: - drawFunction = this._drawDot; - break; - case dotTypes.classy: - drawFunction = this._drawClassy; - break; - case dotTypes.classyRounded: - drawFunction = this._drawClassyRounded; - break; - case dotTypes.rounded: - drawFunction = this._drawRounded; - break; - case dotTypes.extraRounded: - drawFunction = this._drawExtraRounded; - break; - case dotTypes.square: - default: - drawFunction = this._drawSquare; - } - - drawFunction.call(this, { - x, y, size, getNeighbor, - }); - } - - _rotateFigure({ - x, y, size, rotation = 0, draw, - }: RotateFigureArgs): void { - const cx = x + size / 2; - const cy = y + size / 2; - - draw(); - this._element?.setAttribute('transform', `rotate(${(180 * rotation) / Math.PI},${cx},${cy})`); - } - - _basicDot(args: BasicFigureDrawArgs): void { - const { size, x, y } = args; - - this._rotateFigure({ - ...args, - draw: () => { - this._element = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); - this._element.setAttribute('cx', String(x + size / 2)); - this._element.setAttribute('cy', String(y + size / 2)); - this._element.setAttribute('r', String(size / 2)); - }, - }); - } - - _basicSquare(args: BasicFigureDrawArgs): void { - const { size, x, y } = args; - - this._rotateFigure({ - ...args, - draw: () => { - this._element = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); - this._element.setAttribute('x', String(x)); - this._element.setAttribute('y', String(y)); - this._element.setAttribute('width', String(size)); - this._element.setAttribute('height', String(size)); - }, - }); - } - - // if rotation === 0 - right side is rounded - _basicSideRounded(args: BasicFigureDrawArgs): void { - const { size, x, y } = args; - - this._rotateFigure({ - ...args, - draw: () => { - this._element = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - this._element.setAttribute( - 'd', - `M ${x} ${y}` // go to top left position - + `v ${size}` // draw line to left bottom corner - + `h ${size / 2}` // draw line to left bottom corner + half of size right - + `a ${size / 2} ${size / 2}, 0, 0, 0, 0 ${-size}`, // draw rounded corner - ); - }, - }); - } - - // if rotation === 0 - top right corner is rounded - _basicCornerRounded(args: BasicFigureDrawArgs): void { - const { size, x, y } = args; - - this._rotateFigure({ - ...args, - draw: () => { - this._element = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - this._element.setAttribute( - 'd', - `M ${x} ${y}` // go to top left position - + `v ${size}` // draw line to left bottom corner - + `h ${size}` // draw line to right bottom corner - + `v ${-size / 2}` // draw line to right bottom corner + half of size top - + `a ${size / 2} ${size / 2}, 0, 0, 0, ${-size / 2} ${-size / 2}`, // draw rounded corner - ); - }, - }); - } - - // if rotation === 0 - top right corner is rounded - _basicCornerExtraRounded(args: BasicFigureDrawArgs): void { - const { size, x, y } = args; - - this._rotateFigure({ - ...args, - draw: () => { - this._element = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - this._element.setAttribute( - 'd', - `M ${x} ${y}` // go to top left position - + `v ${size}` // draw line to left bottom corner - + `h ${size}` // draw line to right bottom corner - + `a ${size} ${size}, 0, 0, 0, ${-size} ${-size}`, // draw rounded top right corner - ); - }, - }); - } - - // if rotation === 0 - left bottom and right top corners are rounded - _basicCornersRounded(args: BasicFigureDrawArgs): void { - const { size, x, y } = args; - - this._rotateFigure({ - ...args, - draw: () => { - this._element = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - this._element.setAttribute( - 'd', - `M ${x} ${y}` // go to left top position - + `v ${size / 2}` // draw line to left top corner + half of size bottom - + `a ${size / 2} ${size / 2}, 0, 0, 0, ${size / 2} ${size / 2}` // draw rounded left bottom corner - + `h ${size / 2}` // draw line to right bottom corner - + `v ${-size / 2}` // draw line to right bottom corner + half of size top - + `a ${size / 2} ${size / 2}, 0, 0, 0, ${-size / 2} ${-size / 2}`, // draw rounded right top corner - ); - }, - }); - } - - _drawDot({ x, y, size }: DrawArgs): void { - this._basicDot({ - x, y, size, rotation: 0, - }); - } - - _drawSquare({ x, y, size }: DrawArgs): void { - this._basicSquare({ - x, y, size, rotation: 0, - }); - } - - _drawRounded({ - x, y, size, getNeighbor, - }: DrawArgs): void { - const leftNeighbor = getNeighbor ? Number(getNeighbor(-1, 0)) : 0; - const rightNeighbor = getNeighbor ? Number(getNeighbor(1, 0)) : 0; - const topNeighbor = getNeighbor ? Number(getNeighbor(0, -1)) : 0; - const bottomNeighbor = getNeighbor ? Number(getNeighbor(0, 1)) : 0; - - const neighborsCount = leftNeighbor + rightNeighbor + topNeighbor + bottomNeighbor; - - if (neighborsCount === 0) { - this._basicDot({ - x, y, size, rotation: 0, - }); - return; - } - - if (neighborsCount > 2 || (leftNeighbor && rightNeighbor) || (topNeighbor && bottomNeighbor)) { - this._basicSquare({ - x, y, size, rotation: 0, - }); - return; - } - - if (neighborsCount === 2) { - let rotation = 0; - - if (leftNeighbor && topNeighbor) { - rotation = Math.PI / 2; - } else if (topNeighbor && rightNeighbor) { - rotation = Math.PI; - } else if (rightNeighbor && bottomNeighbor) { - rotation = -Math.PI / 2; - } - - this._basicCornerRounded({ - x, y, size, rotation, - }); - return; - } - - if (neighborsCount === 1) { - let rotation = 0; - - if (topNeighbor) { - rotation = Math.PI / 2; - } else if (rightNeighbor) { - rotation = Math.PI; - } else if (bottomNeighbor) { - rotation = -Math.PI / 2; - } - - this._basicSideRounded({ - x, y, size, rotation, - }); - } - } - - _drawExtraRounded({ - x, y, size, getNeighbor, - }: DrawArgs): void { - const leftNeighbor = getNeighbor ? Number(getNeighbor(-1, 0)) : 0; - const rightNeighbor = getNeighbor ? Number(getNeighbor(1, 0)) : 0; - const topNeighbor = getNeighbor ? Number(getNeighbor(0, -1)) : 0; - const bottomNeighbor = getNeighbor ? Number(getNeighbor(0, 1)) : 0; - - const neighborsCount = leftNeighbor + rightNeighbor + topNeighbor + bottomNeighbor; - - if (neighborsCount === 0) { - this._basicDot({ - x, y, size, rotation: 0, - }); - return; - } - - if (neighborsCount > 2 || (leftNeighbor && rightNeighbor) || (topNeighbor && bottomNeighbor)) { - this._basicSquare({ - x, y, size, rotation: 0, - }); - return; - } - - if (neighborsCount === 2) { - let rotation = 0; - - if (leftNeighbor && topNeighbor) { - rotation = Math.PI / 2; - } else if (topNeighbor && rightNeighbor) { - rotation = Math.PI; - } else if (rightNeighbor && bottomNeighbor) { - rotation = -Math.PI / 2; - } - - this._basicCornerExtraRounded({ - x, y, size, rotation, - }); - return; - } - - if (neighborsCount === 1) { - let rotation = 0; - - if (topNeighbor) { - rotation = Math.PI / 2; - } else if (rightNeighbor) { - rotation = Math.PI; - } else if (bottomNeighbor) { - rotation = -Math.PI / 2; - } - - this._basicSideRounded({ - x, y, size, rotation, - }); - } - } - - _drawClassy({ - x, y, size, getNeighbor, - }: DrawArgs): void { - const leftNeighbor = getNeighbor ? Number(getNeighbor(-1, 0)) : 0; - const rightNeighbor = getNeighbor ? Number(getNeighbor(1, 0)) : 0; - const topNeighbor = getNeighbor ? Number(getNeighbor(0, -1)) : 0; - const bottomNeighbor = getNeighbor ? Number(getNeighbor(0, 1)) : 0; - - const neighborsCount = leftNeighbor + rightNeighbor + topNeighbor + bottomNeighbor; - - if (neighborsCount === 0) { - this._basicCornersRounded({ - x, y, size, rotation: Math.PI / 2, - }); - return; - } - - if (!leftNeighbor && !topNeighbor) { - this._basicCornerRounded({ - x, y, size, rotation: -Math.PI / 2, - }); - return; - } - - if (!rightNeighbor && !bottomNeighbor) { - this._basicCornerRounded({ - x, y, size, rotation: Math.PI / 2, - }); - return; - } - - this._basicSquare({ - x, y, size, rotation: 0, - }); - } - - _drawClassyRounded({ - x, y, size, getNeighbor, - }: DrawArgs): void { - const leftNeighbor = getNeighbor ? Number(getNeighbor(-1, 0)) : 0; - const rightNeighbor = getNeighbor ? Number(getNeighbor(1, 0)) : 0; - const topNeighbor = getNeighbor ? Number(getNeighbor(0, -1)) : 0; - const bottomNeighbor = getNeighbor ? Number(getNeighbor(0, 1)) : 0; - - const neighborsCount = leftNeighbor + rightNeighbor + topNeighbor + bottomNeighbor; - - if (neighborsCount === 0) { - this._basicCornersRounded({ - x, y, size, rotation: Math.PI / 2, - }); - return; - } - - if (!leftNeighbor && !topNeighbor) { - this._basicCornerExtraRounded({ - x, y, size, rotation: -Math.PI / 2, - }); - return; - } - - if (!rightNeighbor && !bottomNeighbor) { - this._basicCornerExtraRounded({ - x, y, size, rotation: Math.PI / 2, - }); - return; - } - - this._basicSquare({ - x, y, size, rotation: 0, - }); - } -} diff --git a/src/lib/qr-code-styling/index.ts b/src/lib/qr-code-styling/index.ts deleted file mode 100644 index d56b04ef..00000000 --- a/src/lib/qr-code-styling/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import cornerDotTypes from './constants/cornerDotTypes'; -import cornerSquareTypes from './constants/cornerSquareTypes'; -import dotTypes from './constants/dotTypes'; -import drawTypes from './constants/drawTypes'; -import errorCorrectionLevels from './constants/errorCorrectionLevels'; -import errorCorrectionPercents from './constants/errorCorrectionPercents'; -import modes from './constants/modes'; -import qrTypes from './constants/qrTypes'; -import QRCodeStyling from './core/QRCodeStyling'; - -export * from './types'; - -export { - dotTypes, - cornerDotTypes, - cornerSquareTypes, - errorCorrectionLevels, - errorCorrectionPercents, - modes, - qrTypes, - drawTypes, -}; - -export default QRCodeStyling; diff --git a/src/lib/qr-code-styling/tools/calculateImageSize.ts b/src/lib/qr-code-styling/tools/calculateImageSize.ts deleted file mode 100644 index b8f29b50..00000000 --- a/src/lib/qr-code-styling/tools/calculateImageSize.ts +++ /dev/null @@ -1,70 +0,0 @@ -interface ImageSizeOptions { - originalHeight: number; - originalWidth: number; - maxHiddenDots: number; - maxHiddenAxisDots?: number; - dotSize: number; -} - -interface ImageSizeResult { - height: number; - width: number; - hideYDots: number; - hideXDots: number; -} - -export default function calculateImageSize({ - originalHeight, - originalWidth, - maxHiddenDots, - maxHiddenAxisDots, - dotSize, -}: ImageSizeOptions): ImageSizeResult { - const hideDots = { x: 0, y: 0 }; - const imageSize = { x: 0, y: 0 }; - - if (originalHeight <= 0 || originalWidth <= 0 || maxHiddenDots <= 0 || dotSize <= 0) { - return { - height: 0, - width: 0, - hideYDots: 0, - hideXDots: 0, - }; - } - - const k = originalHeight / originalWidth; - - // Getting the maximum possible axis hidden dots - hideDots.x = Math.floor(Math.sqrt(maxHiddenDots / k)); - // The count of hidden dot's can't be less than 1 - if (hideDots.x <= 0) hideDots.x = 1; - // Check the limit of the maximum allowed axis hidden dots - if (maxHiddenAxisDots && maxHiddenAxisDots < hideDots.x) hideDots.x = maxHiddenAxisDots; - // The count of dots should be odd - if (hideDots.x % 2 === 0) hideDots.x--; - imageSize.x = hideDots.x * dotSize; - // Calculate opposite axis hidden dots based on axis value. - // The value will be odd. - // We use ceil to prevent dots covering by the image. - hideDots.y = 1 + 2 * Math.ceil((hideDots.x * k - 1) / 2); - imageSize.y = Math.round(imageSize.x * k); - // If the result dots count is bigger than max - then decrease size and calculate again - if (hideDots.y * hideDots.x > maxHiddenDots || (maxHiddenAxisDots && maxHiddenAxisDots < hideDots.y)) { - if (maxHiddenAxisDots && maxHiddenAxisDots < hideDots.y) { - hideDots.y = maxHiddenAxisDots; - if (hideDots.y % 2 === 0) hideDots.x--; - } else { - hideDots.y -= 2; - } - imageSize.y = hideDots.y * dotSize; - hideDots.x = 1 + 2 * Math.ceil((hideDots.y / k - 1) / 2); - imageSize.x = Math.round(imageSize.y / k); - } - - return { - height: imageSize.y, - width: imageSize.x, - hideYDots: hideDots.y, - hideXDots: hideDots.x, - }; -} diff --git a/src/lib/qr-code-styling/tools/downloadURI.ts b/src/lib/qr-code-styling/tools/downloadURI.ts deleted file mode 100644 index 1a65bbc2..00000000 --- a/src/lib/qr-code-styling/tools/downloadURI.ts +++ /dev/null @@ -1,8 +0,0 @@ -export default function downloadURI(uri: string, name: string): void { - const link = document.createElement('a'); - link.download = name; - link.href = uri; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); -} diff --git a/src/lib/qr-code-styling/tools/getMode.ts b/src/lib/qr-code-styling/tools/getMode.ts deleted file mode 100644 index db4ffa5d..00000000 --- a/src/lib/qr-code-styling/tools/getMode.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Mode } from '../types'; - -import modes from '../constants/modes'; - -export default function getMode(data: string): Mode { - switch (true) { - case /^[0-9]*$/.test(data): - return modes.numeric; - case /^[0-9A-Z $%*+\-./:]*$/.test(data): - return modes.alphanumeric; - default: - return modes.byte; - } -} diff --git a/src/lib/qr-code-styling/tools/merge.ts b/src/lib/qr-code-styling/tools/merge.ts deleted file mode 100644 index 8c3170b4..00000000 --- a/src/lib/qr-code-styling/tools/merge.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { UnknownObject } from '../types'; - -const isObject = (obj: Record): boolean => !!obj && typeof obj === 'object' && !Array.isArray(obj); - -export default function mergeDeep(target: UnknownObject, ...sources: UnknownObject[]): UnknownObject { - if (!sources.length) return target; - const source = sources.shift(); - if (source === undefined || !isObject(target) || !isObject(source)) return target; - target = { ...target }; - Object.keys(source).forEach((key: string): void => { - const targetValue = target[key]; - const sourceValue = source[key]; - - if (Array.isArray(targetValue) && Array.isArray(sourceValue)) { - target[key] = sourceValue; - } else if (isObject(targetValue) && isObject(sourceValue)) { - target[key] = mergeDeep({ ...targetValue }, sourceValue); - } else { - target[key] = sourceValue; - } - }); - - return mergeDeep(target, ...sources); -} diff --git a/src/lib/qr-code-styling/tools/sanitizeOptions.ts b/src/lib/qr-code-styling/tools/sanitizeOptions.ts deleted file mode 100644 index c5dcec25..00000000 --- a/src/lib/qr-code-styling/tools/sanitizeOptions.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { Gradient } from '../types'; - -import type { RequiredOptions } from '../core/QROptions'; - -function sanitizeGradient(gradient: Gradient): Gradient { - const newGradient = { ...gradient }; - - if (!newGradient.colorStops || !newGradient.colorStops.length) { - throw new Error("Field 'colorStops' is required in gradient"); - } - - if (newGradient.rotation) { - newGradient.rotation = Number(newGradient.rotation); - } else { - newGradient.rotation = 0; - } - - newGradient.colorStops = newGradient.colorStops.map((colorStop: { offset: number; color: string }) => ({ - ...colorStop, - offset: Number(colorStop.offset), - })); - - return newGradient; -} - -export default function sanitizeOptions(options: RequiredOptions): RequiredOptions { - const newOptions = { ...options }; - - newOptions.width = Number(newOptions.width); - newOptions.height = Number(newOptions.height); - newOptions.margin = Number(newOptions.margin); - newOptions.imageOptions = { - ...newOptions.imageOptions, - hideBackgroundDots: Boolean(newOptions.imageOptions.hideBackgroundDots), - imageSize: Number(newOptions.imageOptions.imageSize), - margin: Number(newOptions.imageOptions.margin), - }; - - if (newOptions.margin > Math.min(newOptions.width, newOptions.height)) { - newOptions.margin = Math.min(newOptions.width, newOptions.height); - } - - newOptions.dotsOptions = { - ...newOptions.dotsOptions, - }; - if (newOptions.dotsOptions.gradient) { - newOptions.dotsOptions.gradient = sanitizeGradient(newOptions.dotsOptions.gradient); - } - - if (newOptions.cornersSquareOptions) { - newOptions.cornersSquareOptions = { - ...newOptions.cornersSquareOptions, - }; - if (newOptions.cornersSquareOptions.gradient) { - newOptions.cornersSquareOptions.gradient = sanitizeGradient(newOptions.cornersSquareOptions.gradient); - } - } - - if (newOptions.cornersDotOptions) { - newOptions.cornersDotOptions = { - ...newOptions.cornersDotOptions, - }; - if (newOptions.cornersDotOptions.gradient) { - newOptions.cornersDotOptions.gradient = sanitizeGradient(newOptions.cornersDotOptions.gradient); - } - } - - if (newOptions.backgroundOptions) { - newOptions.backgroundOptions = { - ...newOptions.backgroundOptions, - }; - if (newOptions.backgroundOptions.gradient) { - newOptions.backgroundOptions.gradient = sanitizeGradient(newOptions.backgroundOptions.gradient); - } - } - - return newOptions; -} diff --git a/src/lib/qr-code-styling/types/index.ts b/src/lib/qr-code-styling/types/index.ts deleted file mode 100644 index f4437578..00000000 --- a/src/lib/qr-code-styling/types/index.ts +++ /dev/null @@ -1,182 +0,0 @@ -export interface UnknownObject { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; -} - -export type DotType = 'dots' | 'rounded' | 'classy' | 'classy-rounded' | 'square' | 'extra-rounded'; -export type CornerDotType = 'dot' | 'square'; -export type CornerSquareType = 'dot' | 'square' | 'extra-rounded'; -export type Extension = 'svg' | 'png' | 'jpeg' | 'webp'; -export type GradientType = 'radial' | 'linear'; -export type DrawType = 'canvas' | 'svg'; - -export type Gradient = { - type: GradientType; - rotation?: number; - colorStops: { - offset: number; - color: string; - }[]; -}; - -export interface DotTypes { - [key: string]: DotType; -} - -export interface GradientTypes { - [key: string]: GradientType; -} - -export interface CornerDotTypes { - [key: string]: CornerDotType; -} - -export interface CornerSquareTypes { - [key: string]: CornerSquareType; -} - -export interface DrawTypes { - [key: string]: DrawType; -} - -export type TypeNumber = - | 0 - | 1 - | 2 - | 3 - | 4 - | 5 - | 6 - | 7 - | 8 - | 9 - | 10 - | 11 - | 12 - | 13 - | 14 - | 15 - | 16 - | 17 - | 18 - | 19 - | 20 - | 21 - | 22 - | 23 - | 24 - | 25 - | 26 - | 27 - | 28 - | 29 - | 30 - | 31 - | 32 - | 33 - | 34 - | 35 - | 36 - | 37 - | 38 - | 39 - | 40; - -export type ErrorCorrectionLevel = 'L' | 'M' | 'Q' | 'H'; -export type Mode = 'Numeric' | 'Alphanumeric' | 'Byte' | 'Kanji'; -export interface QRCode { - addData(data: string, mode?: Mode): void; - make(): void; - getModuleCount(): number; - isDark(row: number, col: number): boolean; - createImgTag(cellSize?: number, margin?: number): string; - createSvgTag(cellSize?: number, margin?: number): string; - createSvgTag(opts?: { cellSize?: number; margin?: number; scalable?: boolean }): string; - createDataURL(cellSize?: number, margin?: number): string; - createTableTag(cellSize?: number, margin?: number): string; - createASCII(cellSize?: number, margin?: number): string; - renderTo2dContext(context: CanvasRenderingContext2D, cellSize?: number): void; -} - -export type Options = { - type?: DrawType; - width?: number; - height?: number; - margin?: number; - data?: string; - image?: string; - qrOptions?: { - typeNumber?: TypeNumber; - mode?: Mode; - errorCorrectionLevel?: ErrorCorrectionLevel; - }; - imageOptions?: { - hideBackgroundDots?: boolean; - imageSize?: number; - crossOrigin?: string; - margin?: number; - }; - dotsOptions?: { - type?: DotType; - color?: string; - gradient?: Gradient; - }; - cornersSquareOptions?: { - type?: CornerSquareType; - color?: string; - gradient?: Gradient; - }; - cornersDotOptions?: { - type?: CornerDotType; - color?: string; - gradient?: Gradient; - }; - backgroundOptions?: { - color?: string; - gradient?: Gradient; - }; -}; - -export type FilterFunction = (i: number, j: number) => boolean; - -export type DownloadOptions = { - name?: string; - extension?: Extension; -}; - -export type DrawArgs = { - x: number; - y: number; - size: number; - rotation?: number; - getNeighbor?: GetNeighbor; -}; - -export type BasicFigureDrawArgs = { - x: number; - y: number; - size: number; - rotation?: number; -}; - -export type RotateFigureArgs = { - x: number; - y: number; - size: number; - rotation?: number; - draw: () => void; -}; - -export type DrawArgsCanvas = DrawArgs & { - context: CanvasRenderingContext2D; -}; - -export type BasicFigureDrawArgsCanvas = BasicFigureDrawArgs & { - context: CanvasRenderingContext2D; -}; - -export type RotateFigureArgsCanvas = RotateFigureArgs & { - context: CanvasRenderingContext2D; -}; - -export type GetNeighbor = (x: number, y: number) => boolean; diff --git a/src/lib/webextension-polyfill/browser.js b/src/lib/webextension-polyfill/browser.js deleted file mode 100644 index 33cec0b8..00000000 --- a/src/lib/webextension-polyfill/browser.js +++ /dev/null @@ -1,1269 +0,0 @@ -(function (global, factory) { - if (typeof define === "function" && define.amd) { - define("webextension-polyfill", ["module"], factory); - } else if (typeof exports !== "undefined") { - factory(module); - } else { - var mod = { - exports: {} - }; - factory(mod); - global.browser = mod.exports; - } -})(typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : this, function (module) { - /* webextension-polyfill - v0.10.0 - Fri Aug 12 2022 19:42:44 */ - - /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ - - /* vim: set sts=2 sw=2 et tw=80: */ - - /* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - "use strict"; - - if (!globalThis.chrome?.runtime?.id) { - console.error("This script should only be loaded in a browser extension."); - // throw new Error("This script should only be loaded in a browser extension."); - } - - else if (typeof globalThis.browser === "undefined" || Object.getPrototypeOf(globalThis.browser) !== Object.prototype) { - const CHROME_SEND_MESSAGE_CALLBACK_NO_RESPONSE_MESSAGE = "The message port closed before a response was received."; // Wrapping the bulk of this polyfill in a one-time-use function is a minor - // optimization for Firefox. Since Spidermonkey does not fully parse the - // contents of a function until the first time it's called, and since it will - // never actually need to be called, this allows the polyfill to be included - // in Firefox nearly for free. - - const wrapAPIs = extensionAPIs => { - // NOTE: apiMetadata is associated to the content of the api-metadata.json file - // at build time by replacing the following "include" with the content of the - // JSON file. - const apiMetadata = { - "alarms": { - "clear": { - "minArgs": 0, - "maxArgs": 1 - }, - "clearAll": { - "minArgs": 0, - "maxArgs": 0 - }, - "get": { - "minArgs": 0, - "maxArgs": 1 - }, - "getAll": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "bookmarks": { - "create": { - "minArgs": 1, - "maxArgs": 1 - }, - "get": { - "minArgs": 1, - "maxArgs": 1 - }, - "getChildren": { - "minArgs": 1, - "maxArgs": 1 - }, - "getRecent": { - "minArgs": 1, - "maxArgs": 1 - }, - "getSubTree": { - "minArgs": 1, - "maxArgs": 1 - }, - "getTree": { - "minArgs": 0, - "maxArgs": 0 - }, - "move": { - "minArgs": 2, - "maxArgs": 2 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeTree": { - "minArgs": 1, - "maxArgs": 1 - }, - "search": { - "minArgs": 1, - "maxArgs": 1 - }, - "update": { - "minArgs": 2, - "maxArgs": 2 - } - }, - "browserAction": { - "disable": { - "minArgs": 0, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "enable": { - "minArgs": 0, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "getBadgeBackgroundColor": { - "minArgs": 1, - "maxArgs": 1 - }, - "getBadgeText": { - "minArgs": 1, - "maxArgs": 1 - }, - "getPopup": { - "minArgs": 1, - "maxArgs": 1 - }, - "getTitle": { - "minArgs": 1, - "maxArgs": 1 - }, - "openPopup": { - "minArgs": 0, - "maxArgs": 0 - }, - "setBadgeBackgroundColor": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "setBadgeText": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "setIcon": { - "minArgs": 1, - "maxArgs": 1 - }, - "setPopup": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "setTitle": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - } - }, - "browsingData": { - "remove": { - "minArgs": 2, - "maxArgs": 2 - }, - "removeCache": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeCookies": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeDownloads": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeFormData": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeHistory": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeLocalStorage": { - "minArgs": 1, - "maxArgs": 1 - }, - "removePasswords": { - "minArgs": 1, - "maxArgs": 1 - }, - "removePluginData": { - "minArgs": 1, - "maxArgs": 1 - }, - "settings": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "commands": { - "getAll": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "contextMenus": { - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeAll": { - "minArgs": 0, - "maxArgs": 0 - }, - "update": { - "minArgs": 2, - "maxArgs": 2 - } - }, - "cookies": { - "get": { - "minArgs": 1, - "maxArgs": 1 - }, - "getAll": { - "minArgs": 1, - "maxArgs": 1 - }, - "getAllCookieStores": { - "minArgs": 0, - "maxArgs": 0 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "set": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "devtools": { - "inspectedWindow": { - "eval": { - "minArgs": 1, - "maxArgs": 2, - "singleCallbackArg": false - } - }, - "panels": { - "create": { - "minArgs": 3, - "maxArgs": 3, - "singleCallbackArg": true - }, - "elements": { - "createSidebarPane": { - "minArgs": 1, - "maxArgs": 1 - } - } - } - }, - "downloads": { - "cancel": { - "minArgs": 1, - "maxArgs": 1 - }, - "download": { - "minArgs": 1, - "maxArgs": 1 - }, - "erase": { - "minArgs": 1, - "maxArgs": 1 - }, - "getFileIcon": { - "minArgs": 1, - "maxArgs": 2 - }, - "open": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "pause": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeFile": { - "minArgs": 1, - "maxArgs": 1 - }, - "resume": { - "minArgs": 1, - "maxArgs": 1 - }, - "search": { - "minArgs": 1, - "maxArgs": 1 - }, - "show": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - } - }, - "extension": { - "isAllowedFileSchemeAccess": { - "minArgs": 0, - "maxArgs": 0 - }, - "isAllowedIncognitoAccess": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "history": { - "addUrl": { - "minArgs": 1, - "maxArgs": 1 - }, - "deleteAll": { - "minArgs": 0, - "maxArgs": 0 - }, - "deleteRange": { - "minArgs": 1, - "maxArgs": 1 - }, - "deleteUrl": { - "minArgs": 1, - "maxArgs": 1 - }, - "getVisits": { - "minArgs": 1, - "maxArgs": 1 - }, - "search": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "i18n": { - "detectLanguage": { - "minArgs": 1, - "maxArgs": 1 - }, - "getAcceptLanguages": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "identity": { - "launchWebAuthFlow": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "idle": { - "queryState": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "management": { - "get": { - "minArgs": 1, - "maxArgs": 1 - }, - "getAll": { - "minArgs": 0, - "maxArgs": 0 - }, - "getSelf": { - "minArgs": 0, - "maxArgs": 0 - }, - "setEnabled": { - "minArgs": 2, - "maxArgs": 2 - }, - "uninstallSelf": { - "minArgs": 0, - "maxArgs": 1 - } - }, - "notifications": { - "clear": { - "minArgs": 1, - "maxArgs": 1 - }, - "create": { - "minArgs": 1, - "maxArgs": 2 - }, - "getAll": { - "minArgs": 0, - "maxArgs": 0 - }, - "getPermissionLevel": { - "minArgs": 0, - "maxArgs": 0 - }, - "update": { - "minArgs": 2, - "maxArgs": 2 - } - }, - "pageAction": { - "getPopup": { - "minArgs": 1, - "maxArgs": 1 - }, - "getTitle": { - "minArgs": 1, - "maxArgs": 1 - }, - "hide": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "setIcon": { - "minArgs": 1, - "maxArgs": 1 - }, - "setPopup": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "setTitle": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "show": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - } - }, - "permissions": { - "contains": { - "minArgs": 1, - "maxArgs": 1 - }, - "getAll": { - "minArgs": 0, - "maxArgs": 0 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "request": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "runtime": { - "getBackgroundPage": { - "minArgs": 0, - "maxArgs": 0 - }, - "getPlatformInfo": { - "minArgs": 0, - "maxArgs": 0 - }, - "openOptionsPage": { - "minArgs": 0, - "maxArgs": 0 - }, - "requestUpdateCheck": { - "minArgs": 0, - "maxArgs": 0 - }, - "sendMessage": { - "minArgs": 1, - "maxArgs": 3 - }, - "sendNativeMessage": { - "minArgs": 2, - "maxArgs": 2 - }, - "setUninstallURL": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "sessions": { - "getDevices": { - "minArgs": 0, - "maxArgs": 1 - }, - "getRecentlyClosed": { - "minArgs": 0, - "maxArgs": 1 - }, - "restore": { - "minArgs": 0, - "maxArgs": 1 - } - }, - "storage": { - "local": { - "clear": { - "minArgs": 0, - "maxArgs": 0 - }, - "get": { - "minArgs": 0, - "maxArgs": 1 - }, - "getBytesInUse": { - "minArgs": 0, - "maxArgs": 1 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "set": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "managed": { - "get": { - "minArgs": 0, - "maxArgs": 1 - }, - "getBytesInUse": { - "minArgs": 0, - "maxArgs": 1 - } - }, - "sync": { - "clear": { - "minArgs": 0, - "maxArgs": 0 - }, - "get": { - "minArgs": 0, - "maxArgs": 1 - }, - "getBytesInUse": { - "minArgs": 0, - "maxArgs": 1 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "set": { - "minArgs": 1, - "maxArgs": 1 - } - } - }, - "tabs": { - "captureVisibleTab": { - "minArgs": 0, - "maxArgs": 2 - }, - "create": { - "minArgs": 1, - "maxArgs": 1 - }, - "detectLanguage": { - "minArgs": 0, - "maxArgs": 1 - }, - "discard": { - "minArgs": 0, - "maxArgs": 1 - }, - "duplicate": { - "minArgs": 1, - "maxArgs": 1 - }, - "executeScript": { - "minArgs": 1, - "maxArgs": 2 - }, - "get": { - "minArgs": 1, - "maxArgs": 1 - }, - "getCurrent": { - "minArgs": 0, - "maxArgs": 0 - }, - "getZoom": { - "minArgs": 0, - "maxArgs": 1 - }, - "getZoomSettings": { - "minArgs": 0, - "maxArgs": 1 - }, - "goBack": { - "minArgs": 0, - "maxArgs": 1 - }, - "goForward": { - "minArgs": 0, - "maxArgs": 1 - }, - "highlight": { - "minArgs": 1, - "maxArgs": 1 - }, - "insertCSS": { - "minArgs": 1, - "maxArgs": 2 - }, - "move": { - "minArgs": 2, - "maxArgs": 2 - }, - "query": { - "minArgs": 1, - "maxArgs": 1 - }, - "reload": { - "minArgs": 0, - "maxArgs": 2 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeCSS": { - "minArgs": 1, - "maxArgs": 2 - }, - "sendMessage": { - "minArgs": 2, - "maxArgs": 3 - }, - "setZoom": { - "minArgs": 1, - "maxArgs": 2 - }, - "setZoomSettings": { - "minArgs": 1, - "maxArgs": 2 - }, - "update": { - "minArgs": 1, - "maxArgs": 2 - } - }, - "topSites": { - "get": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "webNavigation": { - "getAllFrames": { - "minArgs": 1, - "maxArgs": 1 - }, - "getFrame": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "webRequest": { - "handlerBehaviorChanged": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "windows": { - "create": { - "minArgs": 0, - "maxArgs": 1 - }, - "get": { - "minArgs": 1, - "maxArgs": 2 - }, - "getAll": { - "minArgs": 0, - "maxArgs": 1 - }, - "getCurrent": { - "minArgs": 0, - "maxArgs": 1 - }, - "getLastFocused": { - "minArgs": 0, - "maxArgs": 1 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "update": { - "minArgs": 2, - "maxArgs": 2 - } - } - }; - - if (Object.keys(apiMetadata).length === 0) { - throw new Error("api-metadata.json has not been included in browser-polyfill"); - } - /** - * A WeakMap subclass which creates and stores a value for any key which does - * not exist when accessed, but behaves exactly as an ordinary WeakMap - * otherwise. - * - * @param {function} createItem - * A function which will be called in order to create the value for any - * key which does not exist, the first time it is accessed. The - * function receives, as its only argument, the key being created. - */ - - - class DefaultWeakMap extends WeakMap { - constructor(createItem, items = undefined) { - super(items); - this.createItem = createItem; - } - - get(key) { - if (!this.has(key)) { - this.set(key, this.createItem(key)); - } - - return super.get(key); - } - - } - /** - * Returns true if the given object is an object with a `then` method, and can - * therefore be assumed to behave as a Promise. - * - * @param {*} value The value to test. - * @returns {boolean} True if the value is thenable. - */ - - - const isThenable = value => { - return value && typeof value === "object" && typeof value.then === "function"; - }; - /** - * Creates and returns a function which, when called, will resolve or reject - * the given promise based on how it is called: - * - * - If, when called, `chrome.runtime.lastError` contains a non-null object, - * the promise is rejected with that value. - * - If the function is called with exactly one argument, the promise is - * resolved to that value. - * - Otherwise, the promise is resolved to an array containing all of the - * function's arguments. - * - * @param {object} promise - * An object containing the resolution and rejection functions of a - * promise. - * @param {function} promise.resolve - * The promise's resolution function. - * @param {function} promise.reject - * The promise's rejection function. - * @param {object} metadata - * Metadata about the wrapped method which has created the callback. - * @param {boolean} metadata.singleCallbackArg - * Whether or not the promise is resolved with only the first - * argument of the callback, alternatively an array of all the - * callback arguments is resolved. By default, if the callback - * function is invoked with only a single argument, that will be - * resolved to the promise, while all arguments will be resolved as - * an array if multiple are given. - * - * @returns {function} - * The generated callback function. - */ - - - const makeCallback = (promise, metadata) => { - return (...callbackArgs) => { - if (extensionAPIs.runtime.lastError) { - promise.reject(new Error(extensionAPIs.runtime.lastError.message)); - } else if (metadata.singleCallbackArg || callbackArgs.length <= 1 && metadata.singleCallbackArg !== false) { - promise.resolve(callbackArgs[0]); - } else { - promise.resolve(callbackArgs); - } - }; - }; - - const pluralizeArguments = numArgs => numArgs == 1 ? "argument" : "arguments"; - /** - * Creates a wrapper function for a method with the given name and metadata. - * - * @param {string} name - * The name of the method which is being wrapped. - * @param {object} metadata - * Metadata about the method being wrapped. - * @param {integer} metadata.minArgs - * The minimum number of arguments which must be passed to the - * function. If called with fewer than this number of arguments, the - * wrapper will raise an exception. - * @param {integer} metadata.maxArgs - * The maximum number of arguments which may be passed to the - * function. If called with more than this number of arguments, the - * wrapper will raise an exception. - * @param {boolean} metadata.singleCallbackArg - * Whether or not the promise is resolved with only the first - * argument of the callback, alternatively an array of all the - * callback arguments is resolved. By default, if the callback - * function is invoked with only a single argument, that will be - * resolved to the promise, while all arguments will be resolved as - * an array if multiple are given. - * - * @returns {function(object, ...*)} - * The generated wrapper function. - */ - - - const wrapAsyncFunction = (name, metadata) => { - return function asyncFunctionWrapper(target, ...args) { - if (args.length < metadata.minArgs) { - throw new Error(`Expected at least ${metadata.minArgs} ${pluralizeArguments(metadata.minArgs)} for ${name}(), got ${args.length}`); - } - - if (args.length > metadata.maxArgs) { - throw new Error(`Expected at most ${metadata.maxArgs} ${pluralizeArguments(metadata.maxArgs)} for ${name}(), got ${args.length}`); - } - - return new Promise((resolve, reject) => { - if (metadata.fallbackToNoCallback) { - // This API method has currently no callback on Chrome, but it return a promise on Firefox, - // and so the polyfill will try to call it with a callback first, and it will fallback - // to not passing the callback if the first call fails. - try { - target[name](...args, makeCallback({ - resolve, - reject - }, metadata)); - } catch (cbError) { - console.warn(`${name} API method doesn't seem to support the callback parameter, ` + "falling back to call it without a callback: ", cbError); - target[name](...args); // Update the API method metadata, so that the next API calls will not try to - // use the unsupported callback anymore. - - metadata.fallbackToNoCallback = false; - metadata.noCallback = true; - resolve(); - } - } else if (metadata.noCallback) { - target[name](...args); - resolve(); - } else { - target[name](...args, makeCallback({ - resolve, - reject - }, metadata)); - } - }); - }; - }; - /** - * Wraps an existing method of the target object, so that calls to it are - * intercepted by the given wrapper function. The wrapper function receives, - * as its first argument, the original `target` object, followed by each of - * the arguments passed to the original method. - * - * @param {object} target - * The original target object that the wrapped method belongs to. - * @param {function} method - * The method being wrapped. This is used as the target of the Proxy - * object which is created to wrap the method. - * @param {function} wrapper - * The wrapper function which is called in place of a direct invocation - * of the wrapped method. - * - * @returns {Proxy} - * A Proxy object for the given method, which invokes the given wrapper - * method in its place. - */ - - - const wrapMethod = (target, method, wrapper) => { - return new Proxy(method, { - apply(targetMethod, thisObj, args) { - return wrapper.call(thisObj, target, ...args); - } - - }); - }; - - let hasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty); - /** - * Wraps an object in a Proxy which intercepts and wraps certain methods - * based on the given `wrappers` and `metadata` objects. - * - * @param {object} target - * The target object to wrap. - * - * @param {object} [wrappers = {}] - * An object tree containing wrapper functions for special cases. Any - * function present in this object tree is called in place of the - * method in the same location in the `target` object tree. These - * wrapper methods are invoked as described in {@see wrapMethod}. - * - * @param {object} [metadata = {}] - * An object tree containing metadata used to automatically generate - * Promise-based wrapper functions for asynchronous. Any function in - * the `target` object tree which has a corresponding metadata object - * in the same location in the `metadata` tree is replaced with an - * automatically-generated wrapper function, as described in - * {@see wrapAsyncFunction} - * - * @returns {Proxy} - */ - - const wrapObject = (target, wrappers = {}, metadata = {}) => { - let cache = Object.create(null); - let handlers = { - has(proxyTarget, prop) { - return prop in target || prop in cache; - }, - - get(proxyTarget, prop, receiver) { - if (prop in cache) { - return cache[prop]; - } - - if (!(prop in target)) { - return undefined; - } - - let value = target[prop]; - - if (typeof value === "function") { - // This is a method on the underlying object. Check if we need to do - // any wrapping. - if (typeof wrappers[prop] === "function") { - // We have a special-case wrapper for this method. - value = wrapMethod(target, target[prop], wrappers[prop]); - } else if (hasOwnProperty(metadata, prop)) { - // This is an async method that we have metadata for. Create a - // Promise wrapper for it. - let wrapper = wrapAsyncFunction(prop, metadata[prop]); - value = wrapMethod(target, target[prop], wrapper); - } else { - // This is a method that we don't know or care about. Return the - // original method, bound to the underlying object. - value = value.bind(target); - } - } else if (typeof value === "object" && value !== null && (hasOwnProperty(wrappers, prop) || hasOwnProperty(metadata, prop))) { - // This is an object that we need to do some wrapping for the children - // of. Create a sub-object wrapper for it with the appropriate child - // metadata. - value = wrapObject(value, wrappers[prop], metadata[prop]); - } else if (hasOwnProperty(metadata, "*")) { - // Wrap all properties in * namespace. - value = wrapObject(value, wrappers[prop], metadata["*"]); - } else { - // We don't need to do any wrapping for this property, - // so just forward all access to the underlying object. - Object.defineProperty(cache, prop, { - configurable: true, - enumerable: true, - - get() { - return target[prop]; - }, - - set(value) { - target[prop] = value; - } - - }); - return value; - } - - cache[prop] = value; - return value; - }, - - set(proxyTarget, prop, value, receiver) { - if (prop in cache) { - cache[prop] = value; - } else { - target[prop] = value; - } - - return true; - }, - - defineProperty(proxyTarget, prop, desc) { - return Reflect.defineProperty(cache, prop, desc); - }, - - deleteProperty(proxyTarget, prop) { - return Reflect.deleteProperty(cache, prop); - } - - }; // Per contract of the Proxy API, the "get" proxy handler must return the - // original value of the target if that value is declared read-only and - // non-configurable. For this reason, we create an object with the - // prototype set to `target` instead of using `target` directly. - // Otherwise we cannot return a custom object for APIs that - // are declared read-only and non-configurable, such as `chrome.devtools`. - // - // The proxy handlers themselves will still use the original `target` - // instead of the `proxyTarget`, so that the methods and properties are - // dereferenced via the original targets. - - let proxyTarget = Object.create(target); - return new Proxy(proxyTarget, handlers); - }; - /** - * Creates a set of wrapper functions for an event object, which handles - * wrapping of listener functions that those messages are passed. - * - * A single wrapper is created for each listener function, and stored in a - * map. Subsequent calls to `addListener`, `hasListener`, or `removeListener` - * retrieve the original wrapper, so that attempts to remove a - * previously-added listener work as expected. - * - * @param {DefaultWeakMap} wrapperMap - * A DefaultWeakMap object which will create the appropriate wrapper - * for a given listener function when one does not exist, and retrieve - * an existing one when it does. - * - * @returns {object} - */ - - - const wrapEvent = wrapperMap => ({ - addListener(target, listener, ...args) { - target.addListener(wrapperMap.get(listener), ...args); - }, - - hasListener(target, listener) { - return target.hasListener(wrapperMap.get(listener)); - }, - - removeListener(target, listener) { - target.removeListener(wrapperMap.get(listener)); - } - - }); - - const onRequestFinishedWrappers = new DefaultWeakMap(listener => { - if (typeof listener !== "function") { - return listener; - } - /** - * Wraps an onRequestFinished listener function so that it will return a - * `getContent()` property which returns a `Promise` rather than using a - * callback API. - * - * @param {object} req - * The HAR entry object representing the network request. - */ - - - return function onRequestFinished(req) { - const wrappedReq = wrapObject(req, {} - /* wrappers */ - , { - getContent: { - minArgs: 0, - maxArgs: 0 - } - }); - listener(wrappedReq); - }; - }); - const onMessageWrappers = new DefaultWeakMap(listener => { - if (typeof listener !== "function") { - return listener; - } - /** - * Wraps a message listener function so that it may send responses based on - * its return value, rather than by returning a sentinel value and calling a - * callback. If the listener function returns a Promise, the response is - * sent when the promise either resolves or rejects. - * - * @param {*} message - * The message sent by the other end of the channel. - * @param {object} sender - * Details about the sender of the message. - * @param {function(*)} sendResponse - * A callback which, when called with an arbitrary argument, sends - * that value as a response. - * @returns {boolean} - * True if the wrapped listener returned a Promise, which will later - * yield a response. False otherwise. - */ - - - return function onMessage(message, sender, sendResponse) { - let didCallSendResponse = false; - let wrappedSendResponse; - let sendResponsePromise = new Promise(resolve => { - wrappedSendResponse = function (response) { - didCallSendResponse = true; - resolve(response); - }; - }); - let result; - - try { - result = listener(message, sender, wrappedSendResponse); - } catch (err) { - result = Promise.reject(err); - } - - const isResultThenable = result !== true && isThenable(result); // If the listener didn't returned true or a Promise, or called - // wrappedSendResponse synchronously, we can exit earlier - // because there will be no response sent from this listener. - - if (result !== true && !isResultThenable && !didCallSendResponse) { - return false; - } // A small helper to send the message if the promise resolves - // and an error if the promise rejects (a wrapped sendMessage has - // to translate the message into a resolved promise or a rejected - // promise). - - - const sendPromisedResult = promise => { - promise.then(msg => { - // send the message value. - sendResponse(msg); - }, error => { - // Send a JSON representation of the error if the rejected value - // is an instance of error, or the object itself otherwise. - let message; - - if (error && (error instanceof Error || typeof error.message === "string")) { - message = error.message; - } else { - message = "An unexpected error occurred"; - } - - sendResponse({ - __mozWebExtensionPolyfillReject__: true, - message - }); - }).catch(err => { - // Print an error on the console if unable to send the response. - console.error("Failed to send onMessage rejected reply", err); - }); - }; // If the listener returned a Promise, send the resolved value as a - // result, otherwise wait the promise related to the wrappedSendResponse - // callback to resolve and send it as a response. - - - if (isResultThenable) { - sendPromisedResult(result); - } else { - sendPromisedResult(sendResponsePromise); - } // Let Chrome know that the listener is replying. - - - return true; - }; - }); - - const wrappedSendMessageCallback = ({ - reject, - resolve - }, reply) => { - if (extensionAPIs.runtime.lastError) { - // Detect when none of the listeners replied to the sendMessage call and resolve - // the promise to undefined as in Firefox. - // See https://github.com/mozilla/webextension-polyfill/issues/130 - if (extensionAPIs.runtime.lastError.message === CHROME_SEND_MESSAGE_CALLBACK_NO_RESPONSE_MESSAGE) { - resolve(); - } else { - reject(new Error(extensionAPIs.runtime.lastError.message)); - } - } else if (reply && reply.__mozWebExtensionPolyfillReject__) { - // Convert back the JSON representation of the error into - // an Error instance. - reject(new Error(reply.message)); - } else { - resolve(reply); - } - }; - - const wrappedSendMessage = (name, metadata, apiNamespaceObj, ...args) => { - if (args.length < metadata.minArgs) { - throw new Error(`Expected at least ${metadata.minArgs} ${pluralizeArguments(metadata.minArgs)} for ${name}(), got ${args.length}`); - } - - if (args.length > metadata.maxArgs) { - throw new Error(`Expected at most ${metadata.maxArgs} ${pluralizeArguments(metadata.maxArgs)} for ${name}(), got ${args.length}`); - } - - return new Promise((resolve, reject) => { - const wrappedCb = wrappedSendMessageCallback.bind(null, { - resolve, - reject - }); - args.push(wrappedCb); - apiNamespaceObj.sendMessage(...args); - }); - }; - - const staticWrappers = { - devtools: { - network: { - onRequestFinished: wrapEvent(onRequestFinishedWrappers) - } - }, - runtime: { - onMessage: wrapEvent(onMessageWrappers), - onMessageExternal: wrapEvent(onMessageWrappers), - sendMessage: wrappedSendMessage.bind(null, "sendMessage", { - minArgs: 1, - maxArgs: 3 - }) - }, - tabs: { - sendMessage: wrappedSendMessage.bind(null, "sendMessage", { - minArgs: 2, - maxArgs: 3 - }) - } - }; - const settingMetadata = { - clear: { - minArgs: 1, - maxArgs: 1 - }, - get: { - minArgs: 1, - maxArgs: 1 - }, - set: { - minArgs: 1, - maxArgs: 1 - } - }; - apiMetadata.privacy = { - network: { - "*": settingMetadata - }, - services: { - "*": settingMetadata - }, - websites: { - "*": settingMetadata - } - }; - return wrapObject(extensionAPIs, staticWrappers, apiMetadata); - }; // The build process adds a UMD wrapper around this file, which makes the - // `module` variable available. - - - module.exports = wrapAPIs(chrome); - } else { - module.exports = globalThis.browser; - } -}); diff --git a/src/lib/webextension-polyfill/index.ts b/src/lib/webextension-polyfill/index.ts deleted file mode 100644 index b224a942..00000000 --- a/src/lib/webextension-polyfill/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { Browser } from 'webextension-polyfill'; - -// eslint-disable-next-line global-require -const browser = require('./browser') as Browser; - -export default browser; diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index 5c905000..988d6c39 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -162,6 +162,7 @@ --z-menu-backdrop: 199; --z-menu-bubble: 200; --z-notification: 250; + --z-tooltip: 300; html.is-ios { --layer-transition: 450ms cubic-bezier(0.33, 1, 0.68, 1); diff --git a/src/styles/brilliant-icons.css b/src/styles/brilliant-icons.css index 7654b69b..b6ad0bf2 100644 --- a/src/styles/brilliant-icons.css +++ b/src/styles/brilliant-icons.css @@ -1,7 +1,7 @@ @font-face { font-family: "brilliant-icons"; - src: url("./brilliant-icons.woff?1ba3fca65beb9674413d8a7bf95c7abd") format("woff"), -url("./brilliant-icons.woff2?1ba3fca65beb9674413d8a7bf95c7abd") format("woff2"); + src: url("./brilliant-icons.woff?85fd4716da9a2d25c4c708e31f43c267") format("woff"), +url("./brilliant-icons.woff2?85fd4716da9a2d25c4c708e31f43c267") format("woff2"); font-weight: normal; font-style: normal; } @@ -77,72 +77,81 @@ url("./brilliant-icons.woff2?1ba3fca65beb9674413d8a7bf95c7abd") format("woff2"); .icon-lock::before { content: "\f115"; } -.icon-paste::before { +.icon-params::before { content: "\f116"; } -.icon-pen::before { +.icon-paste::before { content: "\f117"; } -.icon-percent::before { +.icon-pen::before { content: "\f118"; } -.icon-plus::before { +.icon-percent::before { content: "\f119"; } -.icon-qrcode::before { +.icon-plus::before { content: "\f11a"; } -.icon-question::before { +.icon-qrcode::before { content: "\f11b"; } -.icon-receive-alt::before { +.icon-question::before { content: "\f11c"; } -.icon-receive::before { +.icon-receive-alt::before { content: "\f11d"; } -.icon-search::before { +.icon-receive::before { content: "\f11e"; } -.icon-send-alt::before { +.icon-replace::before { content: "\f11f"; } -.icon-send::before { +.icon-search::before { content: "\f120"; } -.icon-share::before { +.icon-send-alt::before { content: "\f121"; } -.icon-sort::before { +.icon-send::before { content: "\f122"; } -.icon-star-filled::before { +.icon-share::before { content: "\f123"; } -.icon-star::before { +.icon-sort::before { content: "\f124"; } -.icon-telegram::before { +.icon-star-filled::before { content: "\f125"; } -.icon-ton::before { +.icon-star::before { content: "\f126"; } -.icon-tonscan::before { +.icon-swap::before { content: "\f127"; } -.icon-trash::before { +.icon-telegram::before { content: "\f128"; } -.icon-update::before { +.icon-ton::before { content: "\f129"; } -.icon-windows-close::before { +.icon-tonscan::before { content: "\f12a"; } -.icon-windows-maximize::before { +.icon-trash::before { content: "\f12b"; } -.icon-windows-minimize::before { +.icon-update::before { content: "\f12c"; } +.icon-windows-close::before { + content: "\f12d"; +} +.icon-windows-maximize::before { + content: "\f12e"; +} +.icon-windows-minimize::before { + content: "\f12f"; +} diff --git a/src/styles/brilliant-icons.woff b/src/styles/brilliant-icons.woff index ac558552..4c6ab38a 100644 Binary files a/src/styles/brilliant-icons.woff and b/src/styles/brilliant-icons.woff differ diff --git a/src/styles/brilliant-icons.woff2 b/src/styles/brilliant-icons.woff2 index 50491723..8bdb971a 100644 Binary files a/src/styles/brilliant-icons.woff2 and b/src/styles/brilliant-icons.woff2 differ diff --git a/src/util/PostMessageConnector.ts b/src/util/PostMessageConnector.ts index 5a1d8ec8..63ca91bb 100644 --- a/src/util/PostMessageConnector.ts +++ b/src/util/PostMessageConnector.ts @@ -1,6 +1,3 @@ -import type { Runtime } from 'webextension-polyfill'; -import extension from '../lib/webextension-polyfill'; - import generateUniqueId from './generateUniqueId'; export interface CancellableCallback { @@ -91,7 +88,7 @@ class ConnectorClass { private requestStatesByCallback = new Map(); constructor( - public target: Worker | Window | Runtime.Port, + public target: Worker | Window | chrome.runtime.Port, private onUpdate?: (update: ApiUpdate) => void, private channel?: string, private targetOrigin = '*', @@ -232,7 +229,7 @@ export function createExtensionConnector( function connect() { // eslint-disable-next-line no-restricted-globals - const port = extension.runtime.connect({ name }); + const port = self.chrome.runtime.connect({ name }); port.onMessage.addListener((data: WorkerMessageData) => { connector.onMessage(data); diff --git a/src/util/account.ts b/src/util/account.ts index c9831d81..bb114d1d 100644 --- a/src/util/account.ts +++ b/src/util/account.ts @@ -1,13 +1,5 @@ import type { AccountIdParsed, ApiBlockchainKey, ApiNetwork } from '../api/types'; -export function genRelatedAccountIds(accountId: string): string[] { - const account = parseAccountId(accountId); - return [ - buildAccountId({ ...account, network: 'mainnet' }), - buildAccountId({ ...account, network: 'testnet' }), - ]; -} - export function parseAccountId(accountId: string): AccountIdParsed { const [ id, diff --git a/src/util/createPostMessageInterface.ts b/src/util/createPostMessageInterface.ts index a743db20..33b66dc1 100644 --- a/src/util/createPostMessageInterface.ts +++ b/src/util/createPostMessageInterface.ts @@ -1,5 +1,3 @@ -import extension from '../lib/webextension-polyfill'; - import { DETACHED_TAB_URL } from './ledger/tab'; import { logDebugError } from './logs'; @@ -44,7 +42,7 @@ export function createExtensionInterface( cleanUpdater?: (onUpdate: (update: ApiUpdate) => void) => void, withAutoInit = false, ) { - extension.runtime.onConnect.addListener((port) => { + chrome.runtime.onConnect.addListener((port) => { if (port.name !== portName) { return; } diff --git a/src/util/handleError.ts b/src/util/handleError.ts index bcde4f00..8589cd7b 100644 --- a/src/util/handleError.ts +++ b/src/util/handleError.ts @@ -1,12 +1,9 @@ -import { DEBUG_ALERT_MSG } from '../config'; +import { APP_ENV, DEBUG_ALERT_MSG } from '../config'; import { throttle } from './schedulers'; window.addEventListener('error', handleErrorEvent); window.addEventListener('unhandledrejection', handleErrorEvent); -// eslint-disable-next-line prefer-destructuring -const APP_ENV = process.env.APP_ENV; - function handleErrorEvent(e: ErrorEvent | PromiseRejectionEvent) { // https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded if (e instanceof ErrorEvent && e.message === 'ResizeObserver loop limit exceeded') { diff --git a/src/util/ledger/index.ts b/src/util/ledger/index.ts index a6b28630..059b6c8f 100644 --- a/src/util/ledger/index.ts +++ b/src/util/ledger/index.ts @@ -24,6 +24,7 @@ import { TON_TOKEN_SLUG } from '../../config'; import { callApi } from '../../api'; import { getWalletBalance } from '../../api/blockchains/ton'; import { TOKEN_TRANSFER_TON_AMOUNT, TOKEN_TRANSFER_TON_FORWARD_AMOUNT } from '../../api/blockchains/ton/constants'; +import { toBase64Address } from '../../api/blockchains/ton/util/tonweb'; import { ApiUserRejectsError } from '../../api/errors'; import { parseAccountId } from '../account'; import { range } from '../iteratees'; @@ -46,6 +47,18 @@ export async function importLedgerWallet(network: ApiNetwork, accountIndex: numb return callApi('importLedgerWallet', network, walletInfo); } +export async function reconnectLedger() { + try { + if (tonTransport && await tonTransport?.isAppOpen()) { + return true; + } + } catch { + // do nothing + } + + return await connectLedger() && await waitLedgerTonApp(); +} + export async function connectLedger() { try { transport = await connectHID(); @@ -132,11 +145,15 @@ export async function submitLedgerTransfer(options: ApiSubmitTransferOptions) { ]); let payload: TonPayloadFormat | undefined; + let isBounceable = Address.parseFriendly(toAddress).isBounceable; + // Force default bounceable address for `waitTxComplete` to work properly + const normalizedAddress = toBase64Address(toAddress); if (slug !== TON_TOKEN_SLUG) { ({ toAddress, amount, payload } = await buildLedgerTokenTransfer( network, slug, fromAddress!, toAddress, amount, comment, )); + isBounceable = true; } else if (comment) { if (isValidLedgerComment(comment)) { payload = { type: 'comment', text: comment }; @@ -151,7 +168,7 @@ export async function submitLedgerTransfer(options: ApiSubmitTransferOptions) { sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, seqno: seqno!, timeout: getTransferExpirationTime(), - bounce: IS_BOUNCEABLE, + bounce: isBounceable, amount: BigInt(amount), payload, }); @@ -162,7 +179,7 @@ export async function submitLedgerTransfer(options: ApiSubmitTransferOptions) { params: { amount: options.amount, fromAddress: fromAddress!, - toAddress: options.toAddress, + toAddress: normalizedAddress, comment, fee: fee!, slug, @@ -238,6 +255,7 @@ export async function signLedgerTransactions( toAddress, amount, payload, } = message; + let isBounceable = IS_BOUNCEABLE; let ledgerPayload: TonPayloadFormat | undefined; switch (payload?.type) { @@ -264,6 +282,7 @@ export async function signLedgerTransactions( forwardPayload, } = payload; + isBounceable = true; ledgerPayload = { type: 'nft-transfer', queryId: BigInt(queryId), @@ -288,6 +307,7 @@ export async function signLedgerTransactions( forwardPayload, } = payload; + isBounceable = true; ledgerPayload = { type: 'jetton-transfer', queryId: BigInt(queryId), @@ -313,7 +333,7 @@ export async function signLedgerTransactions( sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, seqno: seqno! + index, timeout: getTransferExpirationTime(), - bounce: IS_BOUNCEABLE, + bounce: isBounceable, amount: BigInt(amount), payload: ledgerPayload, }; @@ -406,6 +426,12 @@ export function getLedgerWalletAddress(index: number, isBounceable: boolean, isT }); } +export async function verifyAddress(accountId: string) { + const path = await getLedgerAccountPath(accountId); + + await tonTransport!.validateAddress(path, { bounceable: IS_BOUNCEABLE }); +} + async function getLedgerAccountPath(accountId: string) { const accountInfo = await callApi('fetchAccount', accountId); const index = accountInfo!.ledger!.index; diff --git a/src/util/ledger/tab.ts b/src/util/ledger/tab.ts index 41dcb262..bb5d75bc 100644 --- a/src/util/ledger/tab.ts +++ b/src/util/ledger/tab.ts @@ -1,5 +1,3 @@ -import extension from '../../lib/webextension-polyfill'; - export const DETACHED_TAB_URL = '#detached'; export function openLedgerTab() { @@ -7,7 +5,7 @@ export function openLedgerTab() { } export function onLedgerTabClose(id: number, onClose: () => void) { - extension.tabs.onRemoved.addListener((closedTabId: number) => { + chrome.tabs.onRemoved.addListener((closedTabId: number) => { if (closedTabId !== id) { return; } @@ -17,7 +15,7 @@ export function onLedgerTabClose(id: number, onClose: () => void) { } async function createLedgerTab() { - const tab = await extension.tabs.create({ url: `index.html${DETACHED_TAB_URL}`, active: true }); - await extension.windows.update(tab.windowId!, { focused: true }); + const tab = await chrome.tabs.create({ url: `index.html${DETACHED_TAB_URL}`, active: true }); + await chrome.windows.update(tab.windowId!, { focused: true }); return tab.id!; } diff --git a/src/util/safeNumberToString.ts b/src/util/safeNumberToString.ts new file mode 100644 index 00000000..d4e06447 --- /dev/null +++ b/src/util/safeNumberToString.ts @@ -0,0 +1,10 @@ +import { Big } from '../lib/big.js/index.js'; + +export default function safeNumberToString(value: number, decimals: number) { + const result = String(value); + if (result.includes('e-')) { + Big.NE = -decimals - 1; + return new Big(result).toString(); + } + return result; +} diff --git a/src/util/windowEnvironment.ts b/src/util/windowEnvironment.ts index 9ab59f09..7688494f 100644 --- a/src/util/windowEnvironment.ts +++ b/src/util/windowEnvironment.ts @@ -82,3 +82,5 @@ export function setPageSafeAreaProperty() { } }, SAFE_AREA_INITIALIZATION_DELAY); } + +export const REM = parseInt(getComputedStyle(document.documentElement).fontSize, 10); diff --git a/webpack.config.ts b/webpack.config.ts index 72f47db6..26382d57 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -33,6 +33,23 @@ const appRevision = !branch || branch === 'HEAD' ? gitRevisionPlugin.commithash( const STATOSCOPE_REFERENCE_URL = 'https://mytonwallet.app/build-stats.json'; let isReferenceFetched = false; +// The `connect-src` rule contains `https:` due to arbitrary requests are needed for jetton JSON configs. +// The `img-src` rule contains `https:` due to arbitrary image URLs being used as jetton logos. +// The `media-src` rule contains `data:` because of iOS sound initialization. +const CSP = ` + default-src 'none'; + manifest-src 'self'; + connect-src 'self' https:; + script-src 'self' 'wasm-unsafe-eval'; + style-src 'self' https://fonts.googleapis.com/; + img-src 'self' data: https:; + media-src 'self' data:; + object-src 'none'; + base-uri 'none'; + font-src 'self' https://fonts.gstatic.com/; + form-action 'none';` + .replace(/\s+/g, ' ').trim(); + const appVersion = require('./package.json').version; const defaultI18nFilename = path.resolve(__dirname, './src/i18n/en.json'); @@ -48,9 +65,23 @@ export default function createConfig( target: 'web', optimization: { + usedExports: true, splitChunks: { - chunks: 'initial', - maxSize: 4194304, // 4 Mb + chunks: 'all', + cacheGroups: { + extensionVendors: { + test: /[\\/]node_modules[\\/](webextension-polyfill)/, + name: 'extensionVendors', + chunks: 'all', + priority: 10, // For some reason priority is required here in order to bundle extensionVendors.js separately + }, + defaultVendors: { + test: /[\\/]node_modules[\\/]/, + name: 'vendors', + chunks: 'all', + priority: 0, + }, + }, }, }, @@ -77,6 +108,9 @@ export default function createConfig( devMiddleware: { stats: 'minimal', }, + headers: { + 'Content-Security-Policy': CSP, + }, }, watchOptions: { ignored: defaultI18nFilename }, @@ -184,6 +218,7 @@ export default function createConfig( new HtmlPlugin({ template: 'src/index.html', chunks: ['main'], + csp: CSP, }), new PreloadWebpackPlugin({ include: 'allAssets', @@ -206,7 +241,6 @@ export default function createConfig( /* eslint-disable no-null/no-null */ new EnvironmentPlugin({ APP_ENV: 'production', - APP_MOCKED_CLIENT: '', APP_NAME: null, APP_VERSION: appVersion, APP_REVISION: appRevision, @@ -244,6 +278,9 @@ export default function createConfig( transform: (content) => { const manifest = JSON.parse(content.toString()); manifest.version = appVersion; + manifest.content_security_policy = { + extension_pages: CSP, + }; if (IS_FIREFOX_EXTENSION) { manifest.background = {