From 7b946f98f521efa48c8be39e0539993e4d9c70cf Mon Sep 17 00:00:00 2001 From: mytonwalletorg Date: Mon, 21 Aug 2023 13:53:45 +0100 Subject: [PATCH] v1.15.2 --- .eslintignore | 1 - ...on-release.yml => package-and-publish.yml} | 153 +- .github/workflows/statoscope-comment.jora | 43 + .github/workflows/statoscope-comment.js | 10 + .github/workflows/statoscope.yml | 82 ++ .github/workflows/upload-main-stats.yml | 31 + .gitignore | 2 - package-lock.json | 115 +- package.json | 40 +- src/@types/global.d.ts | 3 + src/api/blockchains/ton/address.ts | 16 +- src/api/blockchains/ton/transactions.ts | 127 +- src/api/blockchains/ton/types.ts | 1 + src/api/blockchains/ton/util/tonweb.ts | 12 +- src/api/common/accounts.ts | 37 +- src/api/common/helpers.ts | 27 +- src/api/common/utils.ts | 4 +- src/api/dappMethods/types.ts | 10 - .../extension.ts | 12 +- src/api/extensionMethods/index.ts | 1 + src/api/extensionMethods/init.ts | 26 + .../legacy.ts | 12 +- .../index.ts => extensionMethods/sites.ts} | 40 +- src/api/extensionMethods/types.ts | 15 + .../window.ts | 18 +- src/api/hooks.ts | 30 + src/api/methods/accounts.ts | 9 +- src/api/methods/auth.ts | 18 +- src/api/methods/dapps.ts | 63 +- src/api/methods/index.ts | 2 - src/api/methods/init.ts | 24 +- src/api/methods/polling.ts | 26 +- src/api/methods/staking.ts | 8 +- src/api/methods/swap.ts | 31 - src/api/methods/transactions.ts | 2 +- src/api/methods/wallet.ts | 2 +- src/api/providers/direct/connector.ts | 6 +- .../extension/connectorForPageScript.ts | 16 +- .../providers/extension/pageContentProxy.ts | 7 +- .../extension/providerForContentScript.ts | 22 +- .../providers/extension/providerForPopup.ts | 15 +- src/api/providers/worker/connector.ts | 6 +- src/api/storages/extension.ts | 4 +- src/api/storages/types.ts | 1 + src/api/tonConnect/index.ts | 50 +- src/api/tonConnect/sse.ts | 59 +- src/api/types/backend.ts | 51 - src/api/types/dappUpdates.ts | 18 +- src/api/types/errors.ts | 1 + src/api/types/index.ts | 1 - src/api/types/methods.ts | 6 + src/api/types/misc.ts | 2 + src/assets/font-icons/accept.svg | 2 +- src/assets/font-icons/dot.svg | 2 +- src/assets/font-icons/params.svg | 1 + src/assets/font-icons/replace.svg | 1 + src/assets/font-icons/search.svg | 2 +- src/assets/font-icons/swap.svg | 1 + src/assets/lottie/duck_run.tgs | Bin 0 -> 29760 bytes src/assets/lottiePreview/duck_run.png | Bin 0 -> 13369 bytes src/components/App.tsx | 5 +- src/components/auth/Auth.module.scss | 76 + src/components/auth/Auth.tsx | 23 +- ...uthCreateBackup.tsx => AuthDisclaimer.tsx} | 108 +- src/components/auth/AuthStart.tsx | 2 +- src/components/dapps/Dapp.module.scss | 53 +- src/components/dapps/DappConnectModal.tsx | 42 +- src/components/ledger/LedgerConnect.tsx | 14 +- src/components/main/Main.tsx | 12 +- src/components/main/modals/LogOutModal.tsx | 19 +- .../main/modals/TransactionModal.tsx | 8 +- .../Actions/LandscapeActions.module.scss | 8 + .../sections/Actions/LandscapeActions.tsx | 169 ++- .../Actions/PortraitActions.module.scss | 5 +- .../main/sections/Actions/PortraitActions.tsx | 32 +- .../sections/Card/AccountSelector.module.scss | 2 +- .../main/sections/Card/AccountSelector.tsx | 6 +- .../main/sections/Content/Activity.tsx | 48 +- .../main/sections/Content/Content.tsx | 11 +- .../main/sections/Content/Token.tsx | 23 +- .../main/sections/Warnings/BackupWarning.tsx | 2 +- .../sections/Warnings/SecurityWarning.tsx | 2 +- src/components/receive/Content.tsx | 27 +- src/components/receive/QrModal.tsx | 2 +- .../receive/ReceiveModal.module.scss | 13 + src/components/settings/SelectTokens.tsx | 111 +- src/components/settings/Settings.module.scss | 37 +- src/components/settings/Settings.tsx | 30 +- src/components/settings/SettingsAbout.tsx | 7 +- src/components/settings/SettingsAssets.tsx | 32 +- src/components/settings/SettingsTokens.tsx | 6 +- src/components/staking/StakingInfoContent.tsx | 5 +- src/components/staking/StakingInitial.tsx | 2 +- src/components/staking/UnstakeModal.tsx | 2 +- src/components/transfer/TransferInitial.tsx | 169 +-- src/components/transfer/TransferModal.tsx | 29 +- src/components/ui/Checkbox.tsx | 4 +- src/components/ui/Dropdown.tsx | 2 +- ...odule.scss => IconWithTooltip.module.scss} | 31 +- src/components/ui/IconWithTooltip.tsx | 90 ++ src/components/ui/Input.module.scss | 12 + src/components/ui/Input.tsx | 2 +- src/components/ui/RichNumberInput.tsx | 52 +- src/components/ui/Tab.tsx | 2 +- src/components/ui/Tooltip.tsx | 37 - src/components/ui/helpers/animatedAssets.ts | 4 + src/config.ts | 17 +- src/electron/config.yml | 3 +- src/extension/contentScript.ts | 4 +- src/extension/manifest.json | 6 +- src/extension/pageScript/index.ts | 6 +- src/global/actions/api/auth.ts | 74 +- src/global/actions/api/dapps.ts | 83 +- src/global/actions/api/staking.ts | 4 +- src/global/actions/api/wallet.ts | 92 +- src/global/actions/apiUpdates/transactions.ts | 6 +- src/global/actions/ui/initial.ts | 70 +- src/global/actions/ui/misc.ts | 13 +- src/global/helpers/index.ts | 2 +- src/global/reducers/misc.ts | 23 +- src/global/reducers/staking.ts | 4 +- src/global/reducers/wallet.ts | 11 + src/global/selectors/index.ts | 5 +- src/global/types.ts | 29 +- src/i18n/en.yaml | 29 +- src/i18n/es.yaml | 33 +- src/i18n/ru.yaml | 40 +- src/i18n/zh-Hans.yaml | 30 +- src/i18n/zh-Hant.yaml | 28 +- src/index.html | 1 + src/lib/qr-code-styling/LICENSE | 21 - src/lib/qr-code-styling/README.md | 1 - .../constants/cornerDotTypes.ts | 6 - .../constants/cornerSquareTypes.ts | 7 - src/lib/qr-code-styling/constants/dotTypes.ts | 10 - .../qr-code-styling/constants/drawTypes.ts | 6 - .../constants/errorCorrectionLevels.ts | 12 - .../constants/errorCorrectionPercents.ts | 10 - .../constants/gradientTypes.ts | 6 - src/lib/qr-code-styling/constants/modes.ts | 12 - src/lib/qr-code-styling/constants/qrTypes.ts | 22 - src/lib/qr-code-styling/core/QRCanvas.ts | 475 ------ src/lib/qr-code-styling/core/QRCodeStyling.ts | 180 --- src/lib/qr-code-styling/core/QROptions.ts | 63 - src/lib/qr-code-styling/core/QRSVG.ts | 504 ------- .../figures/cornerDot/canvas/QRCornerDot.ts | 88 -- .../figures/cornerDot/svg/QRCornerDot.ts | 92 -- .../cornerSquare/canvas/QRCornerSquare.ts | 131 -- .../cornerSquare/svg/QRCornerSquare.ts | 156 -- .../figures/dot/canvas/QRDot.ts | 365 ----- .../qr-code-styling/figures/dot/svg/QRDot.ts | 367 ----- src/lib/qr-code-styling/index.ts | 24 - .../tools/calculateImageSize.ts | 70 - src/lib/qr-code-styling/tools/downloadURI.ts | 8 - src/lib/qr-code-styling/tools/getMode.ts | 14 - src/lib/qr-code-styling/tools/merge.ts | 24 - .../qr-code-styling/tools/sanitizeOptions.ts | 78 - src/lib/qr-code-styling/types/index.ts | 182 --- src/lib/webextension-polyfill/browser.js | 1269 ----------------- src/lib/webextension-polyfill/index.ts | 6 - src/styles/_variables.scss | 1 + src/styles/brilliant-icons.css | 59 +- src/styles/brilliant-icons.woff | Bin 5532 -> 5872 bytes src/styles/brilliant-icons.woff2 | Bin 4644 -> 4948 bytes src/util/PostMessageConnector.ts | 7 +- src/util/account.ts | 8 - src/util/createPostMessageInterface.ts | 4 +- src/util/handleError.ts | 5 +- src/util/ledger/index.ts | 32 +- src/util/ledger/tab.ts | 8 +- src/util/safeNumberToString.ts | 10 + src/util/windowEnvironment.ts | 2 + webpack.config.ts | 43 +- 173 files changed, 2356 insertions(+), 5350 deletions(-) rename .github/workflows/{electron-release.yml => package-and-publish.yml} (59%) create mode 100644 .github/workflows/statoscope-comment.jora create mode 100644 .github/workflows/statoscope-comment.js create mode 100644 .github/workflows/statoscope.yml create mode 100644 .github/workflows/upload-main-stats.yml delete mode 100644 src/api/dappMethods/types.ts rename src/api/{methods => extensionMethods}/extension.ts (94%) create mode 100644 src/api/extensionMethods/index.ts create mode 100644 src/api/extensionMethods/init.ts rename src/api/{dappMethods => extensionMethods}/legacy.ts (95%) rename src/api/{dappMethods/index.ts => extensionMethods/sites.ts} (61%) create mode 100644 src/api/extensionMethods/types.ts rename src/api/{dappMethods => extensionMethods}/window.ts (85%) create mode 100644 src/api/hooks.ts delete mode 100644 src/api/methods/swap.ts delete mode 100644 src/api/types/backend.ts create mode 100644 src/api/types/methods.ts create mode 100644 src/assets/font-icons/params.svg create mode 100644 src/assets/font-icons/replace.svg create mode 100644 src/assets/font-icons/swap.svg create mode 100644 src/assets/lottie/duck_run.tgs create mode 100644 src/assets/lottiePreview/duck_run.png rename src/components/auth/{AuthCreateBackup.tsx => AuthDisclaimer.tsx} (52%) rename src/components/ui/{Tooltip.module.scss => IconWithTooltip.module.scss} (61%) create mode 100644 src/components/ui/IconWithTooltip.tsx delete mode 100644 src/components/ui/Tooltip.tsx delete mode 100644 src/lib/qr-code-styling/LICENSE delete mode 100644 src/lib/qr-code-styling/README.md delete mode 100644 src/lib/qr-code-styling/constants/cornerDotTypes.ts delete mode 100644 src/lib/qr-code-styling/constants/cornerSquareTypes.ts delete mode 100644 src/lib/qr-code-styling/constants/dotTypes.ts delete mode 100644 src/lib/qr-code-styling/constants/drawTypes.ts delete mode 100644 src/lib/qr-code-styling/constants/errorCorrectionLevels.ts delete mode 100644 src/lib/qr-code-styling/constants/errorCorrectionPercents.ts delete mode 100644 src/lib/qr-code-styling/constants/gradientTypes.ts delete mode 100644 src/lib/qr-code-styling/constants/modes.ts delete mode 100644 src/lib/qr-code-styling/constants/qrTypes.ts delete mode 100644 src/lib/qr-code-styling/core/QRCanvas.ts delete mode 100644 src/lib/qr-code-styling/core/QRCodeStyling.ts delete mode 100644 src/lib/qr-code-styling/core/QROptions.ts delete mode 100644 src/lib/qr-code-styling/core/QRSVG.ts delete mode 100644 src/lib/qr-code-styling/figures/cornerDot/canvas/QRCornerDot.ts delete mode 100644 src/lib/qr-code-styling/figures/cornerDot/svg/QRCornerDot.ts delete mode 100644 src/lib/qr-code-styling/figures/cornerSquare/canvas/QRCornerSquare.ts delete mode 100644 src/lib/qr-code-styling/figures/cornerSquare/svg/QRCornerSquare.ts delete mode 100644 src/lib/qr-code-styling/figures/dot/canvas/QRDot.ts delete mode 100644 src/lib/qr-code-styling/figures/dot/svg/QRDot.ts delete mode 100644 src/lib/qr-code-styling/index.ts delete mode 100644 src/lib/qr-code-styling/tools/calculateImageSize.ts delete mode 100644 src/lib/qr-code-styling/tools/downloadURI.ts delete mode 100644 src/lib/qr-code-styling/tools/getMode.ts delete mode 100644 src/lib/qr-code-styling/tools/merge.ts delete mode 100644 src/lib/qr-code-styling/tools/sanitizeOptions.ts delete mode 100644 src/lib/qr-code-styling/types/index.ts delete mode 100644 src/lib/webextension-polyfill/browser.js delete mode 100644 src/lib/webextension-polyfill/index.ts create mode 100644 src/util/safeNumberToString.ts 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 0000000000000000000000000000000000000000..0fc5098b553f2e4d006b325369537a42dc72691c GIT binary patch literal 29760 zcmV)2K+L}%iwFP!000021MI!qjx5QQCH5vn*9rT{z!YRZ5HWq&4_UK%*Z@m1kX7UneJw`ZQHhe zUHczC{qW20KmEjRKKL^R!>6A_ z>o%W$)&BnN-^xQz|MK~tp8oCg|MT=mdGhB!|2ZG|`up#{`9WU%)1PfV{pRbx|K>aV z`yX~c{`GI=@3{H&!{2}UN$BtSA5VY&{GXqG{0cMp-PhlJ^V=U}So!7ezvBe|Mqcp` zpMHlwzSb}J%3tG)|6ATVq?9+G)nUnZ>|_4#=SABj*-IPZQMjZ_!6fNvcHhh7lIYoO-Muab@oA0TZ!<6 zEFqR5+PTN5rY$57CZ|o5jfwt-K>{ZE;vjKHEg!=s%iL<1dk`zE#8IjrT;5pYhGbtJ zg-V-@{e?4DQX%4<-4{)(tt@OLN1S zk`0uDTa(A!avfZ7n^@&Qhskp*;eqE?9(QilJ@DM}w7Iz`a|?sBxy>zP&Ml^LJU2>= z^jgG#2;vR>zkdD0ufDP(Iezu?{?kp9G~fU5iO-DXl-=-uKkH=M{lpp&?LX{&7v-DZ z$O~kPfAfFwumAIJ`7@6E?|%61+rRnd%-Fy9&8e}?5Q7XP|7Bb7(YaLdpAWX2pOxww z9zx(>P-~Zy<@Ba4F?|;_5`k%l1_TT@GYibKUUfSDS9de<`&0*^$-VMT| zc_U)h9aBVC?i}l*L<=XI0Osl#P;Z|yT?0dsK&b<)^E8pZH zl`8BgMrsz*Bg!F2^3e@d>~bp?MI^IYd0VQRGT1OO(&Ga)CY# zUIdPneQ=e(AfiO{N(VW?K}D|=jTCpZ>k{dQ@BaNPce5`;hQlBZCSov|wvh#HZ5bR* zO8so%exasdEtOpPvFB=JmO?k^Yf(2mi|H*V-tc20k!62M!dk8Dn}d3ihFEk@vha(? zE1x;QM)?-o=;Ud#k^H{U1|-UnG{dZf4HBF}c&`j-$rd43@<9>xM&?%e(TjQPz?BxC zxU_&J6`zz7vS3eV2IrJwH=IYk+&w1l>F@D$_%S)IH&Tj?C>Sah65W*l$ILH#zP+} z$_y21n~YAy49l^i<3ww_+9*1XC`qI;G&Cnu8?3L&3Pm_WW)iBhf+%XxB@I zcz~wGg@%-%xz!OP2sc@bFASn1$|i{FDfElQD<62FxG1)r4ZICt15ZUR2+;!AfJ_7R zE`kdxr!OR~n67X|=KjgLGBi3_lb*vOO0rDbn!3jZ$wq9cqxO4>4IsTF?a@idDvFOV&J#Q_Z5yQG* zgPV{G5%Wv7=*D9Gvavq3orNn)9&_YO`R$UQZ2}o0flr1=ipvfpIl_pS)yAkoMy@`q z01*DBHR9sl)<$6_CmU+&nXLsQT5PRx*Pd_@dC6fXW2wzDaK2zNF7Y=!2w5?VQ+g&j zByvtek#`@M2KTxSgq%VP`A;k?p^S_zvy}gecS3fNrp3~huR+A>+jtltYaFH^wwbUZ z{LGGctq?Z~lqq4~`pu=Ok8~o$A8z4_aM-lO!RY1vAC>Fj*7hVk_ zrbK)dgl7-DXa$x#q^wZ`UgRnda)={?Jy{V0xOn7#;fcw@4FwL2peIJO7-3*}4!`1O zwx;n??|8`stMaaj>=LcaJC%1KVJ^w!gT=|0#Zzi|^~hWN>K3aRxei6#i1wkLNnVUm z?l2K7#kXf-aq#LJmuEH%7hai&xGXnpE=>;0XjQntiLR5HMzHbF(c;WD&Ij7D19knw z?AU?k>1f+*px`y#*&EuSw@V|xZ4_78yi>Qt=IoCx97Z#QJx-xkIyogc8 z$+@XCqTa!8pSzAwg^h~n0O1;iN_65NO9C_0Ch1vwJX*z?>su8b-+*7M3NQa6M2Ws3Lh1;wx_EEBC`}S#=+F%MR{bI2$tDH4NLyOyDvT4Z_y$ zYOxHRg><|yo8c_5G+^@(;UN^uedH%`6zOtLrFLV9JQ4X3!Cz_1p6TypbGC4&q!zx1#~^hV zj{HJ!SY*y+p6gZwsn__q36369ANWzMTo~xq&|a@mg!Gc;E;S4hC8V?b*L@ zSYc3=3eXO#-T1CUI*1{S8=tZ3L1O^%Z6aHd4a zQ3H=2trNs>5E4w!hCkFKI{bS80D@HZ4i|XsF$p9kIhG(@*sUZA79x;HK`Kp@%7(+P zDQ+p}yUmGqb+96|&71k#i!b!I%OOCV&PVDaC4>Ou--gg40yZt26<*;yi}AtLk911? z_?gxMLOozHpuquc`T~C$i{C5mO9#b$|LyZXKKfB_>}s7}Jy5QlqN8aqa+-eN{W&@WDiNX0skAx5H5o*D474B$q2(?v^);%Nkd4?let*kf-6<7LQx$w zUrsEXf@B>e+vioXOi_Ld9GWaE5bNgs)wbNO{pJJh_m5A1e)_}He}DSJp7tBhXupjB zd5ADytEgY9x*gI@jEbD%#>3hBx|*5Y>BW|M@j1CD8fwZ2xh4}Xf&svQkH`hfnv{l2 zT{(?dVaT`3#`1Da=T@jzm@Vu43sVd70$;@z*E$-Hsvb`Z{stkm#CUg$Axa*vg2)3! zVsJ%`b!$anFK?=!5)+9*nvF+hqFoh`+YcMBD&O2eqq4M$pcV6ri#yV&BQusG5|rt! z41uYgA)~-;nUja4e}of*qwjDxPluua7^{V2A5Ju)aBhS7X;w5IN0M@tYbD)s5)q$3 znyuR<;Pq@>iz6yOo;v);I+(bELY@m+;r^b9_?eDGSplI*44PYW@O)S$kfM)+b@jF z$}OvokTj86QpO&$RfI_B+|=3{6j5acV#-2?&Lal}W$G41QKQ>0%Dm-Pk{ia=a6C^G zOd%A-Y!rBGQR@-q4!Y?KT8eVk8jY`B1cGoQiUz@m(Vb&|tP?3*AqP_QQDmR4`I@(% zNZfZDlnNy#DA`;Y7GweF;1bamayrW4=40Fb+*Tjl_9vP7jQC=EvYAb?ONMZA#MTsw>B9){DZIGjU!L;g|1J+lmxy*nTqlQt4!Ff1*d}3|ZP$~;RTsbrb^{H)p zY^%?0`-5=NS5zOOF7J8q!l-g0fySVoGAs_JRAF+yei3CMo*)3-)8_5bmg1>xe{8GIZQFxbEC4d)!EqcO_#Jd^D40evG-E{H zjt!gwX@x+qh+4BN4;0jOBE2onl`X!kXDksH13q_LO>0`(%_0UVt4L-Ey>geJDM*H3 zP|ItHgbzoD43BO5b6b6I+n?O(qq`@c>$3`xR<)urqN+4g>>SP!kbGev;U#9VI>FC3S})mDXORiVi}W9BaNj1PA+GP|ZdwbjSA z?YS)<+&#BL9~CL3&^M_$9Falb%Qf{5CDavPxDeVGsApsLSZEF|B-|ryKih?Ln}}S$AtXi5@D++) za4L5Z?ZL0qW4VjRw*5IA(MkdY*V5%3qy~fgV*+G&Pf(E9WB$nEM?PR}pONmChN7|u znTYUd_PodIRhvXSFsc)V$&=7fRAIA$n`%Qi)@>4hq|aefRkP`;4HNNg@RwGEx;>gy z#q}+P3IjJqLFYq`uCySh;XfX9iqDLp`}cIP~`@(bZF>$$b#h^!AlM$AbOJW z@gc)w+x{F*m!)2=Pj2SNpf94$=Zp)}+fKDm3aEnY+^(ldu*Te&f+Qn6Uu zp>qO@S6YsA71C0UyJ~M!ZcGuvJB=;?+J?%F?kBMe9qX2lg(i!B^f_!~DXQMMCNx>Y z*q+?&r&v=2e1i(zlwgNvQ=SYl@! zC{)-VB%t#u@8#-pngW}Lc}XtcmdZZG;!av_OeDQ?U-w0;bj^L zn?yDZ07yXrdr?a*pj6(Y!o8|qj?%7B2qQDE7(zHw6p9T{ib814m#inzXdzQ2+%CNt zN)9I3qD1PL97Tmt`$fn2qe2P6^Xmx(>bpOvQ6mGc44kZTM~GoX4n^!>X(34VP>4HR zEjivXz=|o!Un7~e&>4|P9JMlYp%se6gbdiSLc=b!!bjDEB8Y;r{V z!>#KDvm%!YqD6&h7_7&UyDWMzL*qLO^7o`y-4Zs%D@oIy2e?lto4E(`z;%c*R2hJqL3ndQZrJ7HnSCgcQ)& z0YRY{$^94%5kO}ZFu*&aObnsHpD`5EVXMO$Y=Pk5j^IfJZAQB$5=7ZW8@m z7X3gPsB-A41>j=50`Gc?k=(FlgXal-mNbAAHiS=es-GgUEwb1R7%j+9u{?q^^` z#~ulAS@MtF8g3ky(c#R?j3`QLY@pgr0LmL>Wg01Ym7D zhAv*--Yv8xqHinICjbE2V>CrD@XVOQBxsZXE2>Ee#85`!5B-a3_#+Ak^EEaABjg~| z1TP|Q3|DCu5X=`J<+l+5Ds-hT(vS+evY~FAih>WPQn7`NYf&*p`-T$&BB?rK6gT(; zQRP>(bBSfnjlRCWJt0NR%jqJt8=19g@`6rE7$TleodO}SP0g7$glEA6?=`HD`aI0vRzGAI5nV{WPtQy)=myr=Cm1J z5+$J8L`*-`R&FEg+0Jq2Zub1e-Rw7~J|F3=bYIG-V9}pGn|AsW4}Uy=Li1{*zXH*h zMZ}?IrkFT$fc0Dk*|4mHo zKYjj>pa1Ju_y=w8KYjJ|Z=e71=@0s0Y=GAT1N^(0c&A+lTkH026JkjDtKX9PTk_#s zZkdbid^~9`w#+ga7v`9ZH?vH}r#U7Q+$@s`Wsb?jFw10;IX9V`-g2ISpVy@SzE&9D zuqwOziT=C2sPDO-va(zUxeE17D0^ml$lY%%fx3ax3JKB4PI_IloVGqqc}+EQt*_2A zAuGx;-{b4F*UUAYdNWU+c#Dl^?~2=`yq#kv%Vg4@20`}37tCFWw@lvgIJb?qYBKW8AjV3&SKeS0l9NNomtbt*gL!M z%#2TY|HUvIGCDRR?k|(zJc= z=YnE&LVJyBdlQ;IGn+j24u8cFa9`uP?C8XnHnnu9E9clMPto_RRt&lh(Lc6v3#?L& z2zZ@0$cv;`-}A44%U2N3v99+ttO)NJ<|J*?_zB=_b?z2L=!ApTEEqfmJWUST7Lv=4?Hlzk6=8+#z!t1TsE!KccdG@ zxBfwZ_6nK|>tn|Pw%Eto+BgCP8LO{H4uxZ4*NDO(<_?u!29`A2F{*n4yJlF9VN>Wq zrGhwFVLHLQ)VBTvxibtYow`MLDOGY1RXsCncCeld86TDw6nlSs^aZh_)5mWYj_cLM0TIE}GA8c1(k?{J13 zYGHIVDslcr)-G}$Ol7KCYa52j3Zq~tsn-KURK1n9zg9bn``GGP=H=~VSKfXleI(_T zx98&RvU6<}y0*^S?W-lZnCw1Pa&U^rBdaTQ>b7+xEe=?cIn~DVM(a?lORc$L5GBWU zxHcxHm$pUbxiV^tqDmx+3JroLT%dtew8(zYcRRR+0*k=4)EIuN#N%W1i^^1A)5Fr;se5x{e~-AHk0TxOx`wI1$JRFYz=6+)6oW->212(Gbt0qPm}ER2BDq1>?HXjW|p z=#hmD`MeL!R7c}J;;O0}KyECLw8w~QQN1F8eE?M2dZd>)g|=kpod#jZ&Z*h6HHscl zbWqp=gTS{tIsg?0>7$Ozw?LTF7Zk*c*q6ywH(BXEo$|Rd^}I}N&8lU;R=mchHR}6X zQD5ua%Gj^P*siRM{c4OI);Q#=F?L+@bna(t5^xPI-p8!>_xQW99_RI&Vqa4nvrMH+ z%}|;*34rJJ{Z_h|R_H<%M=M88*ST18)CrKN5>G!K^a5P0s3rFd&q>MYEfV0%92w=) z-m=SCotECNMM~3W)6?xH&Zei5O`J_nk=jmsjh};QY|m<$IGLWYG4U;RmdW%W`?R<8 z$Z?wtzskE8k2B=E+ypvDh9W?akyEMF(GrDQF8m?a@PN=NhNZWuHyQ-uuqF1SQB?pS zw1NH~m&dJ%M60vRXkq!$PepS}s;d-VRhQAy^rMb_9_7UwDs2iLnFBPh4XSmtrw9~A znL!xnnL>YIl+&khUo-gfazkbyVv4>9S5j=QF}57EL-xHXbTJA=SVqXs@Pz@25S~f} zJ@CjoqJkEPjhI>6gcp^g=25W{G?k$@mS%5@QNN`qRZY_sMK)8rlHZN`!iqZg?Ai72 zh8|OnDjq-)Y*PW9)wYUJK|Kr5&^JIqFD8wamD*foa%COlMY1&YOJ2rVY4@QUV;p?WK7{cP=>O>|E7)?0ST@lNVKp{K{@HtMU2}~nY^-~(0U~nu5+TeL%Yh&D(-Jc2 zh1tld1OSF_@eAVQApqELH?NcmFD|)i(4;s8g)-XtGPs$J<_p-GiYR{&;bZlS1ybPub0!#6Lipc^|}FU@^ldkEJvwH=rtwkQERF>Hag-7 zqoWAz(BV1fkLhfFW%J`*H@3bkyU?&TF!QxIKT(PyTSvr>ul7zMrjXVnA(lLwygo~@ ze(IVSlS$O+EicPuio~Isj00bMJT1>KTUk4zdgGETAqksc<5qQ=-{T36mxsbQ-xU%q zM*{thWLgEJo!b*?Ol*$NLM_xG7IzJ(`KiUCF(&%pE;mJOQAt7rh%P7UBe%g#U`08T4~sLM!M~q+wCM=5w6Ky!iliVu{F-5hzk*|cy9+m z|K%?}y{2@gP7m{ zc>43_|NQjhINBEvqJ5Wm{xa&;8Ne(%V)f1R*XDhowE~IC{ExHijB7mY* z7~xy_3n?pevzFQ6GNI8&@$8p3_aJHcEs5?Gwj{QFm$%D~L{tQ%xyMKy8x6H*a-Rj} z<T}xpPm(<-O**cyby*GgVNHH zV-yPJr5zb|Q}UG`l3KQ$_8(S@w!O93%jB|!ZI+E~4SfTF#p69OfWwO|yf(eAE)w_V z^=Wo~awp2F{TZilKwL~tLcK^k*nAdw1r6l|b!Kv>l_C2u-xN<3+9N}m^^7K7+}?=B zWOn0ld%Kpqo5jNO@Po`a$NDC7yt;Zmn77#G>+~%5pkgJIhZQR^y!y>`({YIDE!YiN zqSqXTlS%)}AJ)s$YZ}R^!{D^920>^qO)a4-O+{g*$(rWn(V7=U-EQ*c9;nBUH^Jxq z-8oDiRT^7UUhjOIt)iV>UaVOQx43>$fuA<)l9kugqnivzIVj$7<1U_L8KR;*%_VIf z1#|*C_&Ko6&dkb^oh~0I%Xyfg71|RrfuEK!8{eE(=gdYIfr?Oax;(84k{WGyHdsse zBVeK|V>B*Ft;t5?aul1vWCY(w)`ni39%?~_QGs{U)8FyQ*=5Z|Jjt%rHC&n<;KL#V z$3haBoc~;$Hw)*L5UWvm zi&5y2TUU_^S87^Q*{`$;(JEW^NI^zW5?d68Bo@p3MYxhfE1R@wsHl5QB677OakBJh zq~@u$gvT2y%P65|E%Ydw1XwdWdTUQ)8QP$T zRpeSjZbM7sNaQxxOGE*`=Yz^Yk(`@8A(>C{8|2MbMfW4Hi)F3vmU*Td*5&NGi;%x9 zPx7~Hl+TWZq?sA(O#I#eOD|}8cSf&dKLqV&4Y(0z#AdBeb?D_xFw3l1l@58ED7Rr= zDZkzz|I+E&V?nmKVW;g|Q(iBc-HzF|39fa{GYke>gXUS>NJi8}o2rUD6Iksm8%Cyh zaocS!y6v&K*is3E%gpKgz84fBhX%TdYU8L$s0|)eDC{kE!dKslyx~l%z!iRZ<2@mI z0-+U)Ffm!mH%BTUZV2-jM-p1$7je6^T-4ZhR&$+^6D?bt00pVh6Jms-_P1WU^DYE* ztq$6&8*ETR!xMF=7Dpm&N>exf(6zB1_%GwYANYC6v?4g$nlvP>T@ovbwTUQQ%J`@Xm`oc`4b^J@1{CQi6m=^( zH6=2v%0eK6!blu^zqk37Xb)Q6Q-;iJlO{QID%)Md2%Suy3)McvQCrZKSVf|B`pCPfHQ+Eg{b)cgQ*FpUr$C=u_#;-+S!LF z5RqC&Nd5rC#v|4@s$)KVI@l8B1et^17A+1v<=uMl8PNmNT?{^5ytH_gE zBWPNH-`oEZaHC}1O|&Tyagm3hqOG9bF0jvu(JTfa7C~;c4g&${0+#38D*wwZ5nLsH zdr%4iDLvEskrZ7W%#)D`(Z!dL+$w%#o{a;pd#l3trC51~ z!PC{s`*N(jgXr|?%>4+?+$k9V>s9~0ulhUFD^~sfBk1>cq_04dCbkH7ZRZ1SXyNRt z2);l#f#ZAR3PqvLngm4PGGp-kaqHv;dLBYGcZ4gxf91K-WZ+oBx+g^0gx-)1C zEC4Yz>|tTGga!#Z9uq_$fLPJz1c+XXZxn4%s^ohXihx;n}N$*vK7UxhPXvN6CCxgk)GS3wuhj+5Zd*l1>hEeYcWPh-$B9sq)RmAWmTonglw|8>_?~O0L6A5}x1Vtu8-(AvF z6VHZ50d4lQ&r+p7<_ME1p!p!%iK%15(yxf6MYmV$8rUHO=S1@vP&rTkrcFAaP^WV1 zLQIQ92tLpXFL=N|+9iMJ<9%=$bHbVV(6av;huzsW2aX+ZiYXBQ6`oBGg!_P_y6dR=;A}d(N18j>@Aq6nAw{RF8 zoX9QAAZOG@%o3=djRCa#qeV-f(YjjJl3@L`r z2kq)?46@SX$nj%zXj;2mj#p|4rk=^t5AEWLkK^JJmWc)V)!IuK3Eqi`m058m9TKKd z{p-Uhvr}k|)ap9K)RKLT81ofvgj@1I4k6o$kMJr3a%jCA9qg=yuvW2leyMWkD8^v#fz(%Sxgmj2z@wOAl1C(8~Fd zIutz-3#g209Y^qN_gO>(kFDCKw0azqm-45*&lTUDQTDDl`7S@($Gvqz`I2s(7(RlE zI4Sw;T^-gteRJmY!c&_Qv)FXP8Yv+z(XQIYy=2H$wB$$fQv*cjxafZVu4@4Z+xr1-CuqEyKht) zy@ogWWRJ6~NG&2k^Eil6nEbon#R!du6(FQEDTmMcadMN;)4iO%VSFe)-;j=jh6aVO z-()Ech++@~Qt>h5$`@{Pa*HNEq2tu$cFv2=k$0AVt$n-yl5sg}94Q%ul}y!-)ry*T z^(B+%?o4&DE7s)O!#Sdn$ky4ob|t<|?1J9QXa_)A8w3d;Je=rOei?ZK z?K=WAsEu%DR$lB4RMIH^DBx8xY?4$L5;5~n>#S6ncUx9*a+5MzSm@$_*X7?~JG+~; zQGcL~{_*M0Pk(s&?@xaiYom76MsLwZGr^RCSf=r`Hi|MT;WA%_6y8)DJ=wTh)ESmV zClfBpA4jmK!K>A;pY`L^i@~23izN{t(S~}l&8Ouc@M`4-EEa^=u=H)6r`>sZi|JkM z7CPCjI}RC8krivXG(9+b0PJTP7`zs90`W3mMZC+=68qZhHm&#cqEoxtIbYsjJ5NsT z8ujhfPU+>emy}t8)bVUOb5|#95GE&C~l41?R=`DXNSm&700I67!pIoD&kBWkif^mQsFLGlev{Vsd za=?;}qiGhL309A51Bt!k=aE1SU!Df+ktTp}0HOeXx%>N-Cp+ynNBF`kSoCQ3<4`ht z!dr$i#`6Tka$-h7?^GfZfIVu8TjpASO%|PY%fIGKCtdolw(ED_{%>^R@6ZKaWu|8} zgN0e{V<`$Sqyb#*kW%mteKK#UC{(mVVvdK2`GOK^8GdEHMs^*OhA`*&yoh$-nO1>X zu5!_Y5w$z|P3rM^2lYK6&hK(sP93;*c1~<73%viNxq2SZPZ?Q?#+T(305N4M8*nA} z67fPsN=VjZ4I*v>G2E51%)P%j$4$HJ7oXmw8-D3+z58Nc{5VXw$`_j3*~@zg7W!B> zwvR&+;Q$b=yrRe7X4%6-7g-=1&2#(@nx+Xs3u~FQu*Yd=8L{z0sx3p&;b377SsHwIvKeT%i)?#cx9Y8Hh8us_hHOjXRy^2i3AcVw? z8~F=`$BW(T$c1Bdc}Zgnudi6%rFnJRY*hzRIAM2w}7f+kw^8uNexs(h|nmrG@bV4gHm9! zn)G@Uu(CE`cvPh*pp8gQSaHF+T~{*?v$FKhsNO_1%QXekx9eZ;{Nztl>E8L`r)r7s zo$K#bKR=SxzeA^ezso*>#Sk zklImJep)>mE{Af+h5BHB4h<5J>}6O<%-2>=+dVw@2x4Paj+Hckn}NfZ`%pxR7`$yv znejd+LUe zS0sVWL$TBmwEZu-AsgH)W#_^|ZiR2Rjuk15t}-ULMfB)*e)Om5(eHfm6Z5FvJJa8- zMt>-&e}^9Zj@N%yk$!(i`EKM~II(SO(w;Ny18=(zTP&6Z`#}{zPH(*7L4AKH6>saQ zu89e%DX5`m3d}c`R2~jBqO*Op?%2@7gZ()mK^MNqHQuX{_G26YyomS&2%2f&G-B`> zq{-COTANiHF_tEP$WLK8y1^Q^kgGZrHs=dLXJaCUZ}@fc)69eFqz*EKIhWX`n?rLfy0<&lrm2~gge03t>+m*;b8 z`8^Re(y<9E*73MR1VNV@2vzV7G_zN=E4{~8`@VVq8b#xbpZ4P~{EVaW%VGI*zRGt@ zfeY@+cgN6=>aPUYTCSVm(SYEwm8tw_%9I~!)SMu{YpMd~(TvuiI()44sLLy#!(&a= zx2bVm)|Wb-v|8N=$|QUG!M)M`=-#mphl>r27g_kCb2MGw%g3$Qt#?FyuX9&cPPjyM z;9Fvn1lM|&RMB*B#KN(SQ*li7v8_J0?GNg6+uegZv(1PcLKSe!Qxw$-^}kUlQk&gY z46z1%tE)}`_(lG;%Zn&duep4nNG8a~5OPZ{kJxJ@G|@@nz1XqTEOCBSq&g44;;Xvy z+^$8vZ>GOnhx+)-KI5bMLU{e0i|V~D|E%ux?il&~Ij8{pE^20ogLMzCQt@LWo-lhNBY_&ZRTsabR~JXPwLF8%)Tu?XQ}Lnr2Vu>? zzF407$xPgM^>;hrok4TGq>da`u4ShsP)A;!=T<>Kn2fl)*~iBs!<_--4|yu?(9}Qjt#Omi{&5I# zsx!m8Kl6|30*$iz07w%jrpQBU>-B-Ob&Cjr*WMhp?L6&T0Ki*CX(pJSMMLVWEtxZN zdrLZsHo&WnZpJpNrHgE5!tiNhVN?rdw$mD!m*_5%$h*pjB687=IEvBNY}uG7aJ6%c z92`U%bD1)5xB{cUpB}W>P zWp|@wa+B6ZTh`>5+xk<`_7kON`tbaFtk1+ zjm-mT_W;OXT_5}TiRlO7D+k9mS4uS%xyELM-Hp^@`cPVU#~7p|+obu+9iojm!hd9EHgoMkpDUEaCaDp z`TS3x|I@e;SzNdgS(>*IS$eBRWcRA%Ti0JWhy@cOI)}s3`}6}2?3T!#8#$-d>bJkv zZwbgP2QO~SJ=$`+phWtH;3vp%nh4}Ca3khcVg@m-kLJqo1MITvZbJuX`h4YX1 z_t!s<8ob@0@p8IX4)on(JFrbQ#4s8SivhNMgf^}vgs8}(J0%^EiY}3Ms{?G?2$SWI zz5e{3ASvG9&YaHQpK7vhQF{tFPn+wT_O(1+ifMgDwnEiRW_aKux7asdE1K!zb4!Ls zrZe=>=9fffFA{o-FC$RdG(_PZH2O`gyeo?qEUIYiDk<9U#i|KAsE-e#xwkMu%Tzu> zgGC~bBGsx36fRN(CW>hCigCqabO6+vjncA32%@Ws6)ZPu`G?@DYuj>-6R* z)_=jwD%?=8&y~+YxG?F*Oz%i%>&mS2Gpma$vo64_-mc6#KePILl37vmgwi6V8WJe! zMRKjSv$chjRi|rtHOa_USkrd)#t^#^=mTSrv}z@mynpxl^F)n00;E?tXaFtDny(`2Fszq0E5tQzVnR~L}0F|N!yKeHzL zp5>~O-A`zp9%4(#cUiW^GkHHWR#c?psNf>CWy-MUAR8D}2G{bC#heC->y%!<-67#u zs!pfsk(*~V1D3E^?P>>Hbzjkiwkr28vf#~ajZQ`UdQ8n8#MAt9w5`R`dR%UB##d)! zNiL&8$vfdS<4h6`(-grw2k52;Lfjb<z88&Cz&i7YMZH zLxj8Vpu2m>z3AeLG+De;)Lt-tu8mIoub4>mF}bCR9K2*xnrWw*El`;sAjAvE76{2R zTcBdhc{caDQ4aR=rKK;I35$B5f;=+i9Q@m*3KJShv!3|k>qXxY6)0A3PJ|3dShFCv%$!K!NF@y%g>uhL=S~GKNoSL<~7Q zChPaH-bf5UngzG1hV@#IN@g%%KQzv*8u-A;OtnGtRm);KVT&B5XXV;^2;mXz0U>a- z!&`Di9Zap@3V_T_52L5k)NqkOmnW;xo(;qCKZ^ zrYbYmuH7gC1iKEe*Ytd^{0%o3b-Mdd&@B#?5mLd^D zqlI>KaRVYNa@1Ltx=#&*R$CyyV)|ThgUEQ!pcEE?pXn+5yMm6H#OA9@3*cXdO@#h7 z18bb@vGR7*xK+EjObjkCec0{3IcK0p9ead3A;i6laNC+pE|-vjW(#niij`L6x%f@V z$OD--R(dLohD%kwb8si^6E7NLW81cEZoIK=+qP|IW81cEJKrQ5Z|vOc`#ZPloa&nH z{`AxH$5ahl^QQ&#HA4RYj^Xd0Vax+K-8Y2`M*Jc8U9Ys)Bwc`ObPwOFcr98PRluA%c!JbN)7guH+(pGu^kh?)GbwZ{w|8(;Yf7+w+nY~6Ho3P&f91{X*+B|lN-FSXq zgMRN7>MX12rwuWNy#o5A9iD-WNFlA_|N6;?cufc{5@QzQB@N>hK*HoAuNkEu=-^Po zr=Y<04i-#+VFW4~HRFGz+ep_2R2pO)CDELq;iTsp6#Wvgc{pT_AVd(XU9`Zm`^ETF z2J6aguzv}CJXn}3ahDDT^G668ecK3`HWiXgJ+tq%C43!tGYoG_8yLQu^36A2@Ip)e zyyN$&3ya7PxFiF`pwzl`6lueHOaRGN!lRJMXU6k3N&HlA>XHD#YGw{>C7p|s8V^&k z^;#QuIwt;Rq9cNtq)$H4^GyKgO{03Wo~5O@Ce~qt$$JQMC=HE~_9KvYImh_h3_vP~ zPB@BJv{9b(N!ldZJ?$A)8Uu(ANuK9H_T}J`28#E!t}a2Z$qKk%Q*aIj3J8|0Gr{oV z6dcS}f>;gY+?#!kTTiRjnoD|&;7NkFmLVQ=8Fa<=`$}rB ziTs`1PlkkHJ2p-S`2D6Ri`-d}Yem$rDN%=xd@me`lkmu|tkS=Qukpo+zkVdihML0B z(~~rCMno`RyOi(d&xQUp=1v9QeVBII?VaS%y1u$xt*^N`)-+6wHC@0FGH5xz_BQujP zbC{q$JVRvs%JlYL5Dr}V>8~~R)CWKD)b0xqMHPF!uF>mz#(2AULv!L-P`>KX>$8zL zP6pk*o@V_k*jaWDVrH}ashVA&tG|!RrN*HI0H?S?(pW3bz1qWzpWxR4$zp{11?6)Q zt`?ZKUG0p5LmUVF3s@fp9%;Zy=MX+LiYsorzq!@H{Ay1|KFG|?ULxKhY1zB;tVyjc zM6LKS*vlP|lH_|3H%>$GK1|^FRqKHJb&HXw3mgI9jht{`NobU(ZSC_~^$s37i0$O$ zEk*f4W_ZhimHUC)_8SmOeIb*`A)~ufmr(qkp|icV{WWz}iu6T%{}gb7&8VT#Mq<{& zk)-#0{F^MIE+QL6<<+m{9DV0~PVeh|b@%--td{lir6u}2t)qc9$zl8b4ejq9WUUUI znzOnAPFNJ4>Vj`r%Iw$m;&+$be{aw)ihe2WuMw>4-8o>7d8Xz}^Jw)JEPp&Fi^}i4 zd^eO|n>j70b?RiZr0iK_$db8AkOyIvB=77majmni^*0MY3EC4@!YtqfkJn7COxMk< z25E&2Qga|=reTx*-i1>Nghe+P=t=BXKGQmkf`78*oWf6jL*;DO`?JBL=Gl6`sG%eh2vkv)ewSjz&3pkuB-PFnyy1L zBm90AO}o)Jk$68_FXDL@Z|HoY-3k0A+q%3~7VX02cJ68M4-wm!={AEt zLHvpJwW(l{V%Ks>JJB}OYybs1)`=Gh+_F|yfWQ_n=0C}$t3O^Ap?ldjWc6M8!zGpn z1JrDG-Wq^Z1AI2~s7|1f{L4D&u^EgBoF9(Q25rMH6i6l+N+r`ENU{KZW<8xz5wT{c zVB%SUv^d(x&|1N^Da)*}Hn^~LZ-0G5F4Dpp>u7)oNdP3S7S34DUoef($&XY8g+gjSang~zxHf#h}p-k%2VY|Eg z)^ui7VUeWN>k=HtoT!%|p!q#x4%Wj`4HoA^zn8ZC3JGvG@8O9y7L7${bPwn`!AI&e zG$f#~pc|2b;nt*xnKiKQfHFz2(L%+!Ffn zq^?G*)SDA|pj#`*o4VJh|Rd^f6Nr%yD4+dOFmco$-7g`e8~n%oT6>SWhM zhrzUR|2(GB9K(b-T;_LF!~NlRCjYyiS1yF4Gtwt@-JNk02VN}kMP6{J09?YQj%?wg z@>O+7?|_oD)+Me9gf9IzL15p=Bej7wDo>)>LF~a?l!Jo@h2^#}#5kKKmvSCORt;jk zoY(~v!yI&RLvc7;#)C|dQ3&$J@-CraVb_Y?5rdWyN_~FAPrPD?Sm=&iPV8y4GIi9$ z;`OnMu<(nTB4kyFv|V}&>c3|bzDm+ihZBA%K;{<{{z{VRY7p7>UR4-3#>guvz!d&a z!zsX}1Ltesx;#TIEGOEyd9zT7yd9e``Ji~U_!!+%C#J*hz`R<&U$eo_{>Cu+P%dpG zFM)nPF;i-q1UQa)w%H+OpEhts%)k|UIxx&a^wb=vpk{j zFx4>rTyh4*g-D--fCpc8sT22i56bPmwK)qnq}l(u<)`0X?ETkm{e5eCKKqxL@%;Dg zk@q#}?9aWiBXc67J$b(6V3z@?GKC*cf9!Tix4i*ri1N$FzoY?;$?s7|;^LPYP*H|R+dMwR zF4JiWr7o{128;2=ig?1J|Aa)4q!%WmP|uF0Na-|-xp@2&y8h*m787@9o!fyTmUT2v z6w-48zrFbNI2<+2oVhdDDw0D!A|wB*H-WIk&w_+y^!AfVZ7_K z`UQ7_SW;jeC^7=Et7V>iA6I+&+NM54TMd0I;W3=*jCPa*>g{HJK^#zT-?4pn{Vhd? zj)lo#7uEIpA8-_@NQTI+imZb%v^)f$N-5jz@qRkD1&?G=_yFVA6;TDA(LLo29%7;x z$EM-}ka2@@2~M`w2Z+92c;l?*(*eTLWtCTfJjmGsf0L4dR#tao>F@splqE%h>Y4F4b5ORvmOv_Tg|_kJWglv$2*!V zfE=2NA*vvR!59jLZyUd(vWxwRj?)`b4UL9&?%={^b}$HoQaW3G3yvS)j&rojUMv&w zas`8s`3|bBuki)E*UOLuf9)|htz);bj)qe*W^8W~*i#-Nt?>=I$3D=P)Bj#5v2c!n z>)&wo2B`dvw$n{ufxB=rbIs`l;5C$&2dC=b9vzLrzb+(JHm)T?Vb#^M!gUTXWS`^l z+wYF$W!hQ~xr?;8{9rW{lz)2_bRc(I6YlDc$}!>pMRt&=|+^?=j<<`n?M?8 zu(GmX6ExxFibh9^V-?IHa^}Mxr3@o91>GmrR(OEHD(#DWQh+v)z?dgggsm>;7@Avg zWAyP3?UTY@YIka_MX!33B{9}a86Et&+l5NZK7Z<;$GmZbX*)G`5N1aRcJFVwonsFz zh)dJc8-yW*pi){jNBan|DNLB)s~=H63M7l>9v`FzDe8Q~roJg9$CUIDF?MYlxi`d4 zYq)pXEQn_{Eh+x45 zM6X71mI#17b+aed#pQ|btN@^Euu^M~xn|{tEkv3ER9q88eA;755b?HRQL2q4oHx`L zNuq8l#`0!&u=e%2`h}yGVx^~{(Fi>~F_*XMWEYwpwMy%<0wgA78IJaz-v&iQDd^&; zsLNwg``h}A2L85oLTi(>E9?+$LJf5-#9E?)4wCd`I!j>U_k;BR`fzu|476qkFrs2> z2nN5ZX~8g<9eKb;?xtA@L;y`K_l$7ypF%}eT*DSizoGBTg$n%QOXO`tb?q16xmO3E zAo|Y~6DYUuD2!C+@QeXYACTgn)JcDTmlj*{JxL_kP_@BG_sZ)8SX;^cnlbc4ZkwTc ze`pXXMp#qM;{OBN;`75i=HNzwd;zRSTS;TdoQM9A>NJpew(hz` zJht6uWI4Gnr=ybc^Y5B#VU22@Si$5#BVr>ZL7BKwInZyZvH1o@GwEB}06x&B$ceKeK6m2aO$ zjcdS@CqrIzcrNF(hd?QV)lUuFvx?FI{WoH-VJ+h&o3?o#WGzwfqtg~Aim#isy@GN# zN`#21&&p98+E%TiFZ@euH=HV#QwXlKVh=gT zhWa2~1N9)-ySE z<%n`_5@(3-0B5~UQYKDBPb*XFkl2t(Fv7%Nd^;88w0Vg0Q=jh&EWh5e3l2orX!FZu zcrnxqsE~0JW`ihip~Huf{^!kYF!_v!s8TBuc`+&#-Vdva{-Y zF{ZLb7&_)=ev|RpZ|!X3#-n&mV?OgDv<=LA`N~x%T1Q!r+=HzpM#{I8mx<+dvDm)~ z?89gMzGfV^oOUL%Kiq#x0;AlVI0|ocU_zcsK|l|&x`sKEY8X_B3lz4#9XvaoI9?h7 zs*V^Jw)fFUYteIah8-_O0$CtZZ6mJ{pRZ&p_!jJEk2Wx-N$)PVzb3!_s`))EpL|c* zSX|7@`@H=_*!a0wNToZ`Jlfw~jqwf#y%(Qdx@nL`zwBw!meovEzXd@Y9U>CFg+DUA zquu)5kN{?nNpOoic=75xS~Zy5CIAbD1yJ78D|B8Yy7j%oP^6FQ&JJ56Ds^gHD57Up!gb$y;aZs;&#nUu!= zo)8h2Ze2x$9B-WIHnb%xQH?d&#V zjzWPqn{~PAQ(;h$@XxQ)S3u+QSCPYOO6oLumeQbhFJeqo#Y^+UhYo8y&yqIvpG+J+J1&UG=32effh*`%(JAs#Px|j}+32AQq9Xchhnz?z}ig^=AQTD%dFLNmkL1`0>uZke=E0W5){b3G5*YS?%IeJw~ z3JPuMAQ_zH!thYtJ5BW+B)2-@)617g|GV1c<}<9QP~Xs^d#7% zN3>ERFIa?fsnpvDG0vA^h3ZxqzM%tquEkTE|K=$=xt}+xBI686_vcctBvgGa%OiK& z;$vZ$K87l!jY_(cN^MV*Q;>sHLfolwi8T#DkYd!rPd23Ir5x_^kdJ)1|BVWBB}9cj z5u`{P3R$e~sowupHkCKaDB#re5g*;l$i%)Dx?5?I>^|RuEN@ow2t7-|MNp2Zf)q@j zGzkKMq=K%Y)LUiJ)6F77k+&1MnUkwyl1jxm#c)M-Lx|!SoW5n0G$^B!ksndcCDL*+ zZc?tjAv07=(@@2HP8$Ds6|aOgg^Sll@X*n8wR|;M&)v<4cQx%(5cEculo@DZsnP;Y zkRvT)c`J0^2T07UVvx`l1BlalT%LFostn&76rJ#wQfMZS<)Ufr$A<;DXl2Z-ar5jI z`f84KP;cN6r!!^}RDb}wxV#K~LoWjg=YvOl=dQEsA1J|i&7AmPfoIos z-YY7j%&^M5yuTRTFYKpY#_Xjj-2%w~ z8h#gaKjL*_q%~}VQQ!^VqL(b{Q5C3CrTL2V!~w`wJ>E2C$dg0;96iYorU6ERAeLB) zB!p*_`rv6sD!6}2CA4TQU&%p$QIpb`4~<1_oZ}twr{@anN%0a8AKdZL>~l>#%Jge- zYT2lxG@RlJ$Jw!taDUql7gdUebuS1m>5xdd?cM4+F>!F^UkZBM zye&Zl1w?(#7JiL2)@%GgGsM?T2MmKQ4Yflm^c7?jYOi6>weM+$YjK)Er4rAW5I zEU@la&Fg@%IGHq*}E5Hw;Z_iFV13;5O2bL4*UQ|-VmT(i$PSEK}-`ZS{K)sZYFEA zcgF(t0&HWsKQ&qDo9w)#KQ%cSne1SFOD)s?VIzzGVQK6CVI{l&VIIeSn4|h}!|-uq zU79Y@1I<&+wW~og%ibhMP&33Vp*6W68aqr-X4vxGrYT0yOq7DwJ$H?9x!w##vl!Wh zj9L?QU^}>S3xX#YnS%*s2Y_k;+LvUUGDf9474dPSjRWijLYuoW7k5M;=s%3(Xbo%$A}dK86Tv`(k*;@Vkk%e{mhOw;`EFCwD1v?oZKXk>J~BE#!~;o_e60t{ zg*&Peahw1lt2BhOyX1dM_>X zc?|1$7C?$iXT)6={B`nNFhY%Uh!NKo*FtEu!<#NFBGHJlm(EG(&Hjx=!!@^oL_ zw*cS@&o->6C&Tco@ScF-f~ub?tWpWKx(2`TOggn!f!E*n1!b5`k z+C_2?Dpc9JbyDCX!4wT|t69;hY-j2<2KE z{%%=&BUWp@xMT72L#skD9_MIJSF^V23v+<*@CAfb(r$L3Mk z-@n2E=6QSTtgcO3h#v_;yLo4F%R`p&MjKqWJfyqO+L)p}o)_kpdLx!jEnwBioA~d9 zQP}eUTzBD&VLeY{-0tFTK7Ye!L&g_i3d9F#+r6!hIJdLB=Okm{Wf) zv}{3sT}gb|BI7$}m@seq3D_|2OfhF%1?RA5Tv=puW?V^TeQWfY-x@qu?v$*Zq#7>h z$5Yhv*%fT`v&R8vqtzIDx~5;tu8jq@5BCrg_<0eF?g1^1>UJ^o_I-&5{40yEx7VyX z(XkuD?-srJJ+v=weFT5{x{pqn{b6_)5?4;|8`71AQ+P?#lW5(E2igAA%iJB)VeNV( z&n!)=f&(b6^?Y*?O~|omQR~YETqX1Mk{PzuF8vhsMRg6!ZN+(3XUcu9j3};DM}+X> za=N^P8>_YSm1gJOei0X8Uz5dRVjuTz22Nbl3{L_SR}%LWMz|e*o&Zsu7_fmU4Xpss zo_VX5$}?-B^sPQ#G@SVsG*uYf;l$>8OVr}+pBnW1;$aX4nM54x&NE@jaY#CLxSKeI z=Qc#_1`XTo>O$uAY|c!~&=pyAeVOy}KRl~9mKLGcv2b5P3Vr(x4^j(kBAu*@YQaRP z_(Zr~1iysU%BtUev548snXLXWMB*wxQXnRhnEB;iI1*EP1GNxq#27!x@Io?o&FI{_ zC=`dd6u82<6=dbIQBg-)N=hua-UrP19|@e1mAz)MgBh! z=kOnB%B-^+zO!gH*bP0ovmER}o5g5M{H&J9Q+2o_KPag=IIec@siAM=a74+9S`a@4 zO)c{mvI7gRF`Twg>yA?+lypynaK`|I=;ScggWvSY6#Wd0OX4v9!_%-{!B{sppArii zq*_Dtu;Dg2%fu0yMzxd;g!en;It_Z(ecKXmx_^WKOmwebF)R2cV#_+wCk(F0cE+Xw zvR(G;_1}rf|FK_L>i72k9n}14(_dMUz zKw=AbUrXFvs0nvhHdwvqw##8^(kIgS-0=vJ1`Zo!r@_O+=I5uJVun^QnNdM^Vhg}I zAFZP)*}u?Q{*6_$Hg-PD9Uqe{Dm3Eh1R{02Vc*eaN-myL`8vD@KP>+4^ORYh;G_x+ zCu-fO+?hEJVL7$T9OcrZoVJIi0V9i4fYMD`#F5sZL6bkI0GX#^S?B}9gAut9-6usp zDUz7a*Vt#dFH)n;PCXYJXYOE68qG-NBI2Y(!c{P&qATXR-}|$GqlR zZg{l7J`xEoj|0yN{-YF^U2wI8vU=KGoC!u=2vB0j&#-pO3K;b|WcJEe@zEkHuvfmz z4*tDWiZz{4ljf@C4c8eE*zq2!OH5;P zX^v4!G@IYpG^~QC#+7LNaP%}Z`vHbHb8^Y1N8t+i-9!&!h8i2P@C2S4PgHg{OahFX za~N5(qcIw}IzjGa0%h1X(I}LIO_eX39uf10=!3Rs&fN$qA*#oI;%uZIp*?~S_S}c0 zL-+Em&vKq+OREsFLsnB`t^Ziv1~fMB?e?M`k(}7xtN<6Nz%-U`H`QUILQIFb zl{-p4$!2u!>@@U?T&P6s;oSYwoiPU`GHAs zYe@tDttCx+es#TmI5Dd`9K0WSw{eXXi3H}U1Ony8I{$rLkD_EBP>;C4H@1wQ%!Sq< z+B2ihqE(DSc0|@8sTO1VjDKedlovBYEh0=rLfn_ZzF&{2HkYf}_OUJ{Zpa74?hlLP z=vYic$ZQb8v=l$w{S&Uca+t1Pmdo~=5IiH?FqSK!)PMXi0J0~(E(teVP%img1u7gd zs)Tv^s-7Q;9B+%K!FRBFK9~cG_f{R%t zTatY}=h$UG4(C~g@zj|CS{^#|oeD&aupBEAw4z0wM#@Ilf_}O)X_76jFPaL+X8z^CESDhdN)A}MEd9RcpOgyleVm_s zXZwXr|Fp8?A3wZunjQBYXNCr*j51&yYH&G;VCPZdtKFmDnJ-hE@S`Fz4Hy*Olj{Op zvL!>DGY)x4{FvNkHp3 z;qhu+@~%@JQ$Qd--Z@i^mb5U+&Ok-KNQ2 zCeQub#TaBwf%{}WG-raPBBcdd#w=|YmHy)QGm^O4!YlAf09jVkg!Dr-&<6}hO(_Uy zvC;#A5M1Y7I%y~FQm*WF_y8yc?&^-6% z0IfC8s3#-h?lDuu0@?~_BvRz>y<_Ej=2oP`L>)*5mJRu zr_)V!U1&ccL~^Mp6k!?7v#lvNj!X%y8ZNfIpF;WbWg{7@wxe9OntoJaJ+V0M8F)c_ zC@P=BQ_FbVk=cOX4a2{JHzaqMn7VR}OgJpccf-rRnh~sJvwTH&sf?-u8iaT&zhl^y zm=I0boox+e3vqvHnOSrnB2N;`<+$R+sne^$K?Ii4?slV>C*qINx_Z08jS_dhdL(u| zBWN}`^DcLH-Ucwbzrl%CHuUa$gJr}Kx>S6I_-B`jZUk&O4kz>-6INs$->r)mk5Q>b zp~L zBF7RXWb7B^k4(B@SNHH#ny@0(J(iWRaSI4F*ggI*N5?dBX_-hMjLIAbShRw9=P*lq z1)@;n1`&URb0hSZR8`mf=%WEn5BAHD`j;%RDCbd_<@AfrwkjT^2nn3hguqhZr){PDYnwN?5ks?T5fHikWgF?j-lcsfS1*TSNS(vj8R) zX&ZKq(vE<&)3c&hF^6@kxm!+*(lpd8#8EvCw{S7Yd2~e5M7zo7IP6kub~m;zlgF+n zZW9N5s&v(+G{!|R1n{(jq+PKnG$ITN8*s4g=hUIQfne49g8>A4_*Cfp1z#Uh`+4^7 z2T7f^+PoN*UbzR?Cl|Il+4^IiOtmKOsQVQ3pD5fq^mOHsFn*!&u%w z(t&40>AuqiUTgM792CM~S@U^((QD}31qd?SA7ACC$JT`|QYH!~?BLM2wj4*{=w6NO z?99UqJ#SVo2PfU&(?Wi@9(g#BKcC{Z+)Cp{zaFRczHT1u{9dx`dODc20;nWc%rHdQ zS|Yu>*(mxg^FuBRH9#>7&pguP6s*h5%jJNmZXC*mSpEd6 zg-y)5jN7afAT>(dRmUS$6`cCez#;Mzn{w+{VvHr}rMYv$2WiJWd0FZoVZ656-4?)b zw6gXY3fB@oux`TZnVNGF)17sO6?U-tjdCHxJ0cK0^q zF0BsnR(z#jn1l472icEtj%O8LN$~I@6X1w}AIMx{#Q%p1cnB3Xt2hXMa&vP@rL8M$ zu<;!*47DX&`VOtal%cB0N1&$s|8(UqbvZWgk0gtI&8VE2`i7nJv<{i_48uenvGMaG z5T-0yRUR7c|8`%zP2NI85GU&x5;A)fCHoYPmYP9VE)Y|pWt}W5Do+gjA1&|+T4?rq zH1&Qk%7e7MNBRDe5A5*!rJQ+&p$$|#dA9fEpm0;`=lha38R$3B`+f)5t@ZPHc}t2p zTtsQ|S7R|FQT{JQ>R%mt6$Wm_f*(5u`U=DHl}5i?@qi!daea@``JXHc zC~0g)kCvYS>IBPVO>ExzxI;4Zb39{hGazPR>*}ALjfcaHXf_;`9CnlVG8AEGsWpD)iveNG zIei6Zv@=O`BdA#CTd0NCP#U1T?CnAB^MsCj5JjPbKj9>~sFR@Kr13YaZswBe7yj(rqFF z9htr9hOO_D3lu`jVU@5-m}N}TCm2%vx|(JD)iMF3UdGHJvx81aZc~!}Xdb!Fj^H1mrX+)1f%J+X$W-h} zzb3;}41cZ~`3F}3O@mgOtHg1ND|S4kq)+}AlQnt9FgDSIh!nG*a^m>e9njeDYKxOh zvgHh6GfIjP^=Ma!Q6yZBkt)mZMgi#^KPk#NaKO@|A-$CM@68FOef!w;p}{~5vLh=> z)56lr`BKyLJ{69DD7QYer1a!AsAA<%p?`;HhH1)#gwNgUgN*`m6gD?5GQsF46Ph6k zE|4W8K33<0%|a??L8~U{BE^(qxFgI|3Fk0J?b4D(w?Ym`f7(yamn5093M@J0m^CUH zy)X*Rc3|Cx@Lo#%tmDk_+1rsTSuz=WJNu`^pQEi3MmKDU{s`?W2RtGj~rhO~W=YDHXwq2yg zO&{#SAM$l;u(IkBKiOxdx@e*zYMA?oPHpjq-W0~aL6o$`!4XSUByAd41ewMF$AWyo z3hL#L@t{75{#|tjg)n;zp#wJa2+k;7e%Jy?HQD2O$&%cTEk&o3@VOU(Bf7TR(#_7L zA(ifq&lweDGcNO~HFZ8mJ5yE&MLWgGJT8o<(KEAT(O8~*DwmXC1Q&@}w=%2`YT*IU zz^cNLu+1&;yF_2VUQ{EWD zED91Me|Q$#u+~)U(j0Zrf&~Z{r(!rMoMd)N8+VY6J;zg72#Ti=rfkf;c#)R>r2eTRpwAISXdiq`q8g=TO+yP+pSaiWfzOk{yZCS zFQb&I)k3aa@06rz;sEqKD&Io&881v}56 z=IFut!=o47WV#*(%Sm z-IV9Gb{9?Zc2=`kUAea9b-0+;kWI*}NrjQPgAq1FitsxO{fHkVC_(^tiZ(Y1wR*dhY4CQ;{=y}gBy!vp8 z^<t@L=FV!-a-{Y3G2UVW43^fnlIQcGagu zaeba0ogCSd%~~4&RpQ#4n;U;4tuiNgTpwQ^2jkjz6*+$vLlr{Z?bbEC|>(WZ7^^E}@@$IbohpZCx8=U?MmaCfyb&37{OMCz#n6@@2& zXdvjWV$1P=W~3&MrhMXTVq4e7&ym@Kxi8!Mx`VGb{&nl3vW+UYHMh{!_MThn%HE&2 zJFceE&d973pvoVni^M}Oc>ewU58In5-Q2w^Q@Z^3=Wh>BSF7rI;Xmj<^q=;}7oNGl zfBX7Y7s!Tk*IaN6)ZUO;XFOcS(_5EN>-$JN9pmXvfT#2A@c5NyO#qZXO#Nj%em+w+ zvE6m{@L0xUJsw8Wv+^Vqk0+P$3>VI>kHF)%Z4U&Hw{02EFX6PI0$aB(P{xxRJ)SnA z)O#N5=ab_3M>{+`fB9--?rGxj_-)%|_ih$olvbnnE|4FM0&v4HY{ zK`tFnLM`{43g|Q9$>r0w4lcfq8*L3`atZzxaD1MD125ZbJx_;4ua~J0KxEJ++CQ zACE5ukHWldbFmE266X$#NAtvp?iE9b`KRiHXbi#Hy;mWAFa4~SwSE8Y= zJlxMm)rc9-YeFFyjWFkK31Q+_8xe&MkPN+U^E5)k+VFF)JpD+*aN-d?hu{o#<=NpO zzw+#FM{;g}&yTqG(#y&PjlV~FBmg6LWITH%tT@Bkwo%VLPtFZQ0L^;jJMNi`gW~`d z3<(9uaoqD{JQ+_ofgm+t(V#5Xh^jvF*x6}=0&!*tHm@gia4P{w@E9MS@X8cMGXgv_ zGRutT4GtFkSc0r9av;l%q_Wn<%}nXplsHLMiAV|sqTJu!`D^&qhYE=cxB9#Ra+kjZ zJmFPosCc-%^1MGOfDns&z;bJrFX%cq}KYhoWqjx~S(w z)2WoE0Jre?L4buj?q)zwTH+D`Xxw<>w{j_P26t3Vh?kBB0CkPBv`YWS-W@>Lj-`13 ze->Oo0RjjH0USU80R)3!5I~kiFbEbwBNzldy+I?p+KSOuthQo3D`+FevtqRo>lxA7 zh*5g7qpetNH{GuD?fHL69l6!7yuS0Qrc$rⅇ)lZ`B>8-~0K#^PO|+`qU32#c84N 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 P00000NkvXXu0mjfLC}QV literal 0 HcmV?d00001 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 ac5585529052d6f63fa3a0259c1655c578f7723e..4c6ab38a40bd360264f21c745c8a6f41ed5af8e7 100644 GIT binary patch delta 5401 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 aCLRJ>w>_80%?J3GhV=hoNn(960RRAU!o4#9 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 = {