diff --git a/.env.example b/.env.example index f32d3801..19ff981c 100644 --- a/.env.example +++ b/.env.example @@ -1,11 +1,14 @@ NODE_ENV=development -TONHTTPAPI_MAINNET_URL=https://tonhttpapi.mytonwallet.org/jsonRPC +TONHTTPAPI_MAINNET_URL=https://tonhttpapi.mytonwallet.org/api/v2/jsonRPC TONHTTPAPI_MAINNET_API_KEY= -TONHTTPAPI_TESTNET_URL=https://tonhttpapi-testnet.mytonwallet.org/jsonRPC +TONHTTPAPI_TESTNET_URL=https://tonhttpapi-testnet.mytonwallet.org/api/v2/jsonRPC TONHTTPAPI_TESTNET_API_KEY= +TONINDEXER_MAINNET_URL=https://tonhttpapi.mytonwallet.org/api/v3 +TONINDEXER_TESTNET_URL=https://tonhttpapi-testnet.mytonwallet.org/api/v3 TONAPIIO_MAINNET_URL=https://tonapiio.mytonwallet.org TONAPIIO_TESTNET_URL=https://tonapiio-testnet.mytonwallet.org PROXY_HOSTS="tonproxy.io:38080 tonproxy.io:38081 tonproxy.io:38082" BRILLIANT_API_BASE_URL= STAKING_POOLS= +CSP_CONNECT_SRC_EXTRA_URL= diff --git a/.eslintignore b/.eslintignore index ed061118..26c54681 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,10 +1,12 @@ dev public +mobile src/lib/big.js/ src/lib/rlottie/rlottie-wasm.js src/lib/aes-js/index.js src/lib/noble-ed25519/index.js +src/lib/dexie/ jest.config.js playwright.config.ts postcss.config.js @@ -15,3 +17,4 @@ trash deploy dist +dist-electron diff --git a/.eslintrc b/.eslintrc index 0c96d448..78b98429 100644 --- a/.eslintrc +++ b/.eslintrc @@ -156,6 +156,7 @@ "^react", "^ton", "^tonweb(/.*|$)", + "^qr-code-styling(/.*|$)", "^@?\\w", "dist(/.*|$)", "^(\\.+/)+(lib/teact)(/.*|$)", diff --git a/.github/workflows/package-and-publish.yml b/.github/workflows/package-and-publish.yml index 2de78b79..711f63a3 100644 --- a/.github/workflows/package-and-publish.yml +++ b/.github/workflows/package-and-publish.yml @@ -13,6 +13,11 @@ on: push: branches: - master + - mobile-release + +concurrency: + group: ${{ github.ref }} + cancel-in-progress: true env: APP_NAME: MyTonWallet @@ -22,6 +27,7 @@ jobs: electron-release: name: Build, package and publish Electron runs-on: macos-latest + timeout-minutes: 30 steps: - name: Checkout uses: actions/checkout@v3 @@ -59,6 +65,10 @@ 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: Get branch name for current workflow run + id: branch-name + uses: tj-actions/branch-names@v7 + - name: Build, package and publish env: TONHTTPAPI_MAINNET_URL: ${{ vars.TONHTTPAPI_MAINNET_URL }} @@ -74,6 +84,8 @@ jobs: PUBLISH_REPO: ${{ vars.PUBLISH_REPO }} GH_TOKEN: ${{ secrets.GH_TOKEN }} + BASE_URL: ${{ vars.BASE_URL }} + IS_PREVIEW: ${{ steps.branch-name.outputs.current_branch != 'master' }} run: | if [ -z "$PUBLISH_REPO" ]; then npm run electron:package:staging @@ -109,6 +121,7 @@ jobs: name: Sign and re-publish Windows package needs: electron-release runs-on: windows-latest + timeout-minutes: 10 if: vars.PUBLISH_REPO != '' env: GH_TOKEN: ${{ secrets.GH_TOKEN }} @@ -200,6 +213,7 @@ jobs: extensions-package: name: Build and package extensions runs-on: ubuntu-latest + timeout-minutes: 10 steps: - name: Checkout uses: actions/checkout@v3 @@ -257,6 +271,7 @@ jobs: name: Publish extensions needs: extensions-package runs-on: ubuntu-latest + timeout-minutes: 5 if: vars.PUBLISH_REPO != '' steps: - name: Checkout @@ -292,33 +307,33 @@ jobs: 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 }} -# # App 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 }} -# if: ${{ env.WEB_EXT_API_KEY != '' }} -# run: | -# npm i jsonwebtoken@9 web-ext-submit@7 -# UNZIP_DIR=/tmp/${{ env.APP_NAME }}-firefox -# mkdir $UNZIP_DIR -# unzip ${{ env.FIREFOX_FILE_NAME }} -d $UNZIP_DIR -# web-ext-submit --source-dir=$UNZIP_DIR/dist -# echo "APP_NAME=\"${APP_NAME}\" -# TONHTTPAPI_MAINNET_URL=\"${TONHTTPAPI_MAINNET_URL}\" -# TONHTTPAPI_TESTNET_URL=\"${TONHTTPAPI_TESTNET_URL}\" -# TONAPIIO_MAINNET_URL=\"${TONAPIIO_MAINNET_URL}\" -# TONAPIIO_TESTNET_URL=\"${TONAPIIO_TESTNET_URL}\" -# PROXY_HOSTS=\"${PROXY_HOSTS}\" -# STAKING_POOLS=\"${STAKING_POOLS}\"" >.env -# bash deploy/firefox_pack_sources.sh -# node deploy/firefoxPatchVersion.js + - name: Publish to Firefox addons + env: + WEB_EXT_API_KEY: ${{ secrets.FIREFOX_API_KEY }} + WEB_EXT_API_SECRET: ${{ secrets.FIREFOX_API_SECRET }} + # App 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 }} + if: ${{ env.WEB_EXT_API_KEY != '' }} + run: | + npm i jsonwebtoken@9 web-ext-submit@7 + UNZIP_DIR=/tmp/${{ env.APP_NAME }}-firefox + mkdir $UNZIP_DIR + unzip ${{ env.FIREFOX_FILE_NAME }} -d $UNZIP_DIR + web-ext-submit --source-dir=$UNZIP_DIR/dist + echo "APP_NAME=\"${APP_NAME}\" + TONHTTPAPI_MAINNET_URL=\"${TONHTTPAPI_MAINNET_URL}\" + TONHTTPAPI_TESTNET_URL=\"${TONHTTPAPI_TESTNET_URL}\" + TONAPIIO_MAINNET_URL=\"${TONAPIIO_MAINNET_URL}\" + TONAPIIO_TESTNET_URL=\"${TONAPIIO_TESTNET_URL}\" + PROXY_HOSTS=\"${PROXY_HOSTS}\" + STAKING_POOLS=\"${STAKING_POOLS}\"" >.env + bash deploy/firefox_pack_sources.sh + node deploy/firefoxPatchVersion.js calculate-hash: name: Calculate sha256 hashes @@ -343,3 +358,121 @@ jobs: with: name: ${{ env.HASH_FILENAME }} path: ${{ env.HASH_FILENAME }} + + mobile-release: + name: Build, package and publish mobile apps + runs-on: macos-latest + timeout-minutes: 30 + if: vars.PUBLISH_REPO == '' && github.event_name != 'workflow_dispatch' + 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 sync mobile projects + 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 [ "$GITHUB_REF_NAME" == "mobile-release" ]; then + npm run mobile:build:production + else + npm run mobile:build:staging + fi + + - name: Use Ruby and install dependencies + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2 + bundler-cache: true + working-directory: mobile + + - name: Install the Apple certificate and provisioning profile + env: + IOS_CERTIFICATE_BASE64: ${{ secrets.IOS_CERTIFICATE_BASE64 }} + IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} + IOS_PROVISION_PROFILE_BASE64: ${{ secrets.IOS_PROVISION_PROFILE_BASE64 }} + IOS_KEYCHAIN_PASSWORD: ${{ secrets.IOS_KEYCHAIN_PASSWORD }} + run: | + # create variables + CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 + PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + + # import certificate and provisioning profile from secrets + echo -n "$IOS_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH + echo -n "$IOS_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH + + # create temporary keychain + security create-keychain -p "$IOS_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$IOS_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + # import certificate to keychain + security import $CERTIFICATE_PATH -P "$IOS_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security list-keychain -d user -s $KEYCHAIN_PATH + + # apply provisioning profile + mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles + cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles + + - name: "iOS: Package and publish" + env: + IOS_AUTH_KEY_BASE64: ${{ secrets.IOS_AUTH_KEY_BASE64 }} + run: | + cd mobile/ios/App + echo -n "$IOS_AUTH_KEY_BASE64" | base64 --decode -o ./AuthKey.p8 + if [ "$GITHUB_REF_NAME" == "mobile-release" ]; then + bundle exec fastlane release + else + bundle exec fastlane beta + fi + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: "Android: Package and publish" + env: + ANDROID_API_KEY_BASE64: ${{ secrets.ANDROID_API_KEY_BASE64 }} + ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + run: | + cd mobile/android + + echo -n "$ANDROID_API_KEY_BASE64" | base64 --decode -o ./api-key.json + echo -n "$ANDROID_KEYSTORE_BASE64" | base64 --decode -o ./android.keystore + + if [ "$GITHUB_REF_NAME" == "mobile-release" ]; then + bundle exec fastlane release + else + bundle exec fastlane beta + fi diff --git a/.github/workflows/statoscope-upload-reference-statistics.yml b/.github/workflows/statoscope-upload-reference-statistics.yml index 3309c335..4df6b092 100644 --- a/.github/workflows/statoscope-upload-reference-statistics.yml +++ b/.github/workflows/statoscope-upload-reference-statistics.yml @@ -24,7 +24,7 @@ jobs: - name: Install run: npm ci - name: Build - run: npm run build:production; cp ./public/statoscope-build-statistics.json ./statoscope-reference.json + run: npm run build:production; mv ./public/statoscope-build-statistics.json ./statoscope-reference.json - uses: actions/upload-artifact@v3 with: name: statoscope-reference diff --git a/.github/workflows/statoscope.yml b/.github/workflows/statoscope.yml index 8dd428ee..34d02145 100644 --- a/.github/workflows/statoscope.yml +++ b/.github/workflows/statoscope.yml @@ -67,7 +67,7 @@ jobs: path: ./ continue-on-error: true - name: Prepare statoscope input - run: cp public/statoscope-build-statistics.json input.json; mv statoscope-reference.json reference.json + run: mv public/statoscope-build-statistics.json input.json; mv statoscope-reference.json reference.json - name: Validate run: npm run statoscope:validate-diff - name: Query stats diff --git a/.gitignore b/.gitignore index 965fc6a6..164cd4d1 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ trash/ coverage/ src/i18n/en.json notarization-error.log +.patch-version diff --git a/.stylelintrc.json b/.stylelintrc.json index 0cbd610b..d9b45534 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -6,7 +6,9 @@ "ignoreFiles": [ "dist/*.css", "src/styles/brilliant-icons.css", - "coverage/**" + "coverage/**", + "dist-electron/*.css", + "mobile/**/public/*.css" ], "plugins": [ "stylelint-declaration-block-no-ignored-properties", diff --git a/capacitor.config.ts b/capacitor.config.ts new file mode 100644 index 00000000..09ba03d5 --- /dev/null +++ b/capacitor.config.ts @@ -0,0 +1,36 @@ +import type { CapacitorConfig } from '@capacitor/cli'; + +const config: CapacitorConfig = { + appId: 'org.mytonwallet.app', + appName: 'MyTonWallet', + webDir: 'dist', + server: { + androidScheme: 'https', + hostname: 'mytonwallet.local', + }, + android: { + path: 'mobile/android', + includePlugins: [ + '@capacitor-mlkit/barcode-scanning', + '@capacitor/app', + '@capacitor/dialog', + '@capacitor/haptics', + '@capacitor/status-bar', + '@capgo/capacitor-native-biometric', + '@mauricewegner/capacitor-navigation-bar', + 'capacitor-plugin-safe-area', + 'native-bottom-sheet', + ], + }, + ios: { + path: 'mobile/ios', + scheme: 'MyTonWallet', + }, + plugins: { + SplashScreen: { + launchAutoHide: false, + }, + }, +}; + +export default config; diff --git a/changelogs/.gitkeep b/changelogs/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/changelogs/1.17.0.txt b/changelogs/1.17.0.txt new file mode 100644 index 00000000..5f2164e4 --- /dev/null +++ b/changelogs/1.17.0.txt @@ -0,0 +1 @@ +Some hotfixes diff --git a/deploy/update_version.js b/deploy/update_version.js new file mode 100644 index 00000000..81ac7919 --- /dev/null +++ b/deploy/update_version.js @@ -0,0 +1,21 @@ +const path = require('path'); +const fs = require('fs'); + +const ROOT_PATH = `${path.dirname(__filename)}/..`; +const PATCH_VERSION_PATH = `${ROOT_PATH}/.patch-version`; +const PACKAGE_JSON_PATH = `${ROOT_PATH}/package.json`; +const VERSION_TXT_PATH = `${ROOT_PATH}/public/version.txt`; + +// This patch value is used to override the one from package.json +const currentPatch = fs.existsSync(PATCH_VERSION_PATH) ? Number(fs.readFileSync(PATCH_VERSION_PATH, 'utf-8')) : -1; +const packageJsonContent = fs.readFileSync(PACKAGE_JSON_PATH, 'utf-8'); +const currentVersion = JSON.parse(packageJsonContent).version; +const [major, minor] = currentVersion.split('.'); + +const newPatch = currentPatch + 1; +const newVersion = [major, minor, newPatch].join('.'); +const newPackageJsonContent = packageJsonContent.replace(`"version": "${currentVersion}"`, `"version": "${newVersion}"`); + +fs.writeFileSync(PATCH_VERSION_PATH, String(newPatch), 'utf-8'); +fs.writeFileSync(PACKAGE_JSON_PATH, newPackageJsonContent, 'utf-8'); +fs.writeFileSync(VERSION_TXT_PATH, newVersion, 'utf-8'); diff --git a/mobile/Gemfile b/mobile/Gemfile new file mode 100644 index 00000000..5c376f04 --- /dev/null +++ b/mobile/Gemfile @@ -0,0 +1,6 @@ +source "https://rubygems.org" + +gem "fastlane" + +plugins_path = File.join(File.dirname(__FILE__), 'android', 'fastlane', 'Pluginfile') +eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/mobile/Gemfile.lock b/mobile/Gemfile.lock new file mode 100644 index 00000000..eca0b673 --- /dev/null +++ b/mobile/Gemfile.lock @@ -0,0 +1,218 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.6) + rexml + addressable (2.8.5) + public_suffix (>= 2.0.2, < 6.0) + artifactory (3.0.15) + atomos (0.1.3) + aws-eventstream (1.2.0) + aws-partitions (1.853.0) + aws-sdk-core (3.187.0) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.5) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.72.0) + aws-sdk-core (~> 3, >= 3.184.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.137.0) + aws-sdk-core (~> 3, >= 3.181.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.6) + aws-sigv4 (1.6.1) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + declarative (0.0.20) + digest-crc (0.6.5) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20231109) + dotenv (2.8.1) + emoji_regex (3.2.3) + excon (0.104.0) + faraday (1.10.3) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.4) + multipart-post (~> 2) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.0) + faraday (~> 1.0) + fastimage (2.2.7) + fastlane (2.217.0) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored + commander (~> 4.6) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + naturally (~> 2.2) + optparse (~> 0.1.1) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.3) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 3) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3) + fastlane-plugin-versioning_android (0.1.1) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.52.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.2) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + webrick + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.29.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.6.0) + google-cloud-env (~> 1.0) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.3.1) + google-cloud-storage (1.45.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.29.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.5) + domain_name (~> 0.5) + httpclient (2.8.3) + jmespath (1.6.2) + json (2.6.3) + jwt (2.7.1) + mini_magick (4.12.0) + mini_mime (1.1.5) + multi_json (1.15.0) + multipart-post (2.3.0) + nanaimo (0.3.0) + naturally (2.2.1) + optparse (0.1.1) + os (1.1.4) + plist (3.7.0) + public_suffix (5.0.4) + rake (13.1.0) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.2.6) + rouge (2.0.7) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + security (0.1.3) + signet (0.18.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.1) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unicode-display_width (2.5.0) + webrick (1.8.1) + word_wrap (1.0.0) + xcodeproj (1.23.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.3.0) + rexml (~> 3.2.4) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + x86_64-darwin-20 + x86_64-darwin-21 + x86_64-darwin-22 + x86_64-darwin-23 + +DEPENDENCIES + fastlane + fastlane-plugin-versioning_android + +BUNDLED WITH + 2.4.21 diff --git a/mobile/android/.gitignore b/mobile/android/.gitignore new file mode 100644 index 00000000..8522b3ef --- /dev/null +++ b/mobile/android/.gitignore @@ -0,0 +1,109 @@ +# Custom +app/release/ +app/debug/ +api-key.json +android.keystore +fastlane/README.md +fastlane/report.xml + +# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore + +# Built application files +*.apk +*.aar +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +# Android Profiling +*.hprof + +# Cordova plugins for Capacitor +capacitor-cordova-android-plugins + +# Copied web assets +app/src/main/assets/public + +# Generated Config files +app/src/main/assets/capacitor.config.json +app/src/main/assets/capacitor.plugins.json +app/src/main/res/xml/config.xml diff --git a/mobile/android/app/.gitignore b/mobile/android/app/.gitignore new file mode 100644 index 00000000..043df802 --- /dev/null +++ b/mobile/android/app/.gitignore @@ -0,0 +1,2 @@ +/build/* +!/build/.npmkeep diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle new file mode 100644 index 00000000..9258dead --- /dev/null +++ b/mobile/android/app/build.gradle @@ -0,0 +1,55 @@ +apply plugin: 'com.android.application' + +android { + namespace "org.mytonwallet.app" + compileSdkVersion rootProject.ext.compileSdkVersion + defaultConfig { + applicationId "org.mytonwallet.app" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + aaptOptions { + // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. + // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61 + ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +repositories { + flatDir{ + dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs' + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" + implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" + implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" + implementation project(':capacitor-android') + testImplementation "junit:junit:$junitVersion" + androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" + implementation project(':capacitor-cordova-android-plugins') + implementation "com.google.mlkit:barcode-scanning:$mlkitBarcodeScanningVersion" +} + +apply from: 'capacitor.build.gradle' + +try { + def servicesJSON = file('google-services.json') + if (servicesJSON.text) { + apply plugin: 'com.google.gms.google-services' + } +} catch(Exception e) { + logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work") +} diff --git a/mobile/android/app/capacitor.build.gradle b/mobile/android/app/capacitor.build.gradle new file mode 100644 index 00000000..037cb0ff --- /dev/null +++ b/mobile/android/app/capacitor.build.gradle @@ -0,0 +1,27 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN + +android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } +} + +apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" +dependencies { + implementation project(':capacitor-mlkit-barcode-scanning') + implementation project(':capacitor-app') + implementation project(':capacitor-dialog') + implementation project(':capacitor-haptics') + implementation project(':capacitor-status-bar') + implementation project(':capgo-capacitor-native-biometric') + implementation project(':mauricewegner-capacitor-navigation-bar') + implementation project(':capacitor-plugin-safe-area') + implementation project(':native-bottom-sheet') + +} + + +if (hasProperty('postBuildExtras')) { + postBuildExtras() +} diff --git a/mobile/android/app/proguard-rules.pro b/mobile/android/app/proguard-rules.pro new file mode 100644 index 00000000..f1b42451 --- /dev/null +++ b/mobile/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/mobile/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java b/mobile/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java new file mode 100644 index 00000000..f2c2217e --- /dev/null +++ b/mobile/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import android.content.Context; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("com.getcapacitor.app", appContext.getPackageName()); + } +} diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..0787a7b3 --- /dev/null +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/app/src/main/ic_launcher-playstore.png b/mobile/android/app/src/main/ic_launcher-playstore.png new file mode 100644 index 00000000..c2608f1d Binary files /dev/null and b/mobile/android/app/src/main/ic_launcher-playstore.png differ diff --git a/mobile/android/app/src/main/java/org/mytonwallet/app/MainActivity.java b/mobile/android/app/src/main/java/org/mytonwallet/app/MainActivity.java new file mode 100644 index 00000000..691604f4 --- /dev/null +++ b/mobile/android/app/src/main/java/org/mytonwallet/app/MainActivity.java @@ -0,0 +1,49 @@ +package org.mytonwallet.app; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.os.Bundle; +import android.os.Handler; +import android.view.View; +import androidx.core.splashscreen.SplashScreen; +import androidx.interpolator.view.animation.FastOutSlowInInterpolator; +import com.getcapacitor.BridgeActivity; + +public class MainActivity extends BridgeActivity { + private boolean keep = true; + private final int DELAY = 1000; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + SplashScreen splashScreen = SplashScreen.installSplashScreen(this); + splashScreen.setKeepOnScreenCondition(() -> keep); + splashScreen.setOnExitAnimationListener(splashScreenView -> { + AnimatorSet animationSet = new AnimatorSet(); + + View view = splashScreenView.getView(); + ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, View.SCALE_Y, 4f); + ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, View.SCALE_X, 4f); + ObjectAnimator opacity = ObjectAnimator.ofFloat(view, View.ALPHA, 0.0f); + + animationSet.setInterpolator(new FastOutSlowInInterpolator()); + animationSet.setDuration(350L); + animationSet.playTogether(scaleX, scaleY, opacity); + + animationSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + splashScreenView.remove(); + } + }); + + animationSet.start(); + }); + + Handler handler = new Handler(); + handler.postDelayed(() -> keep = false, DELAY); + } +} diff --git a/mobile/android/app/src/main/res/drawable-hdpi/splash.9.png b/mobile/android/app/src/main/res/drawable-hdpi/splash.9.png new file mode 100644 index 00000000..325fe87e Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-hdpi/splash.9.png differ diff --git a/mobile/android/app/src/main/res/drawable-ldpi/splash.9.png b/mobile/android/app/src/main/res/drawable-ldpi/splash.9.png new file mode 100644 index 00000000..42b607fc Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-ldpi/splash.9.png differ diff --git a/mobile/android/app/src/main/res/drawable-mdpi/splash.9.png b/mobile/android/app/src/main/res/drawable-mdpi/splash.9.png new file mode 100644 index 00000000..792c79ba Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-mdpi/splash.9.png differ diff --git a/mobile/android/app/src/main/res/drawable-night-hdpi/splash.9.png b/mobile/android/app/src/main/res/drawable-night-hdpi/splash.9.png new file mode 100644 index 00000000..405a44f5 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-night-hdpi/splash.9.png differ diff --git a/mobile/android/app/src/main/res/drawable-night-ldpi/splash.9.png b/mobile/android/app/src/main/res/drawable-night-ldpi/splash.9.png new file mode 100644 index 00000000..3e458038 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-night-ldpi/splash.9.png differ diff --git a/mobile/android/app/src/main/res/drawable-night-mdpi/splash.9.png b/mobile/android/app/src/main/res/drawable-night-mdpi/splash.9.png new file mode 100644 index 00000000..88056e3a Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-night-mdpi/splash.9.png differ diff --git a/mobile/android/app/src/main/res/drawable-night-xhdpi/splash.9.png b/mobile/android/app/src/main/res/drawable-night-xhdpi/splash.9.png new file mode 100644 index 00000000..c53c2e16 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-night-xhdpi/splash.9.png differ diff --git a/mobile/android/app/src/main/res/drawable-night-xxhdpi/splash.9.png b/mobile/android/app/src/main/res/drawable-night-xxhdpi/splash.9.png new file mode 100644 index 00000000..2eb9d37c Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-night-xxhdpi/splash.9.png differ diff --git a/mobile/android/app/src/main/res/drawable-night-xxxhdpi/splash.9.png b/mobile/android/app/src/main/res/drawable-night-xxxhdpi/splash.9.png new file mode 100644 index 00000000..08b8ace9 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-night-xxxhdpi/splash.9.png differ diff --git a/mobile/android/app/src/main/res/drawable-night/splash.9.png b/mobile/android/app/src/main/res/drawable-night/splash.9.png new file mode 100644 index 00000000..b12b8bec Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-night/splash.9.png differ diff --git a/mobile/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/mobile/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..c7bd21db --- /dev/null +++ b/mobile/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/splash.9.png b/mobile/android/app/src/main/res/drawable-xhdpi/splash.9.png new file mode 100644 index 00000000..7e1da899 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xhdpi/splash.9.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/splash.9.png b/mobile/android/app/src/main/res/drawable-xxhdpi/splash.9.png new file mode 100644 index 00000000..8a3fe604 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxhdpi/splash.9.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/splash.9.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/splash.9.png new file mode 100644 index 00000000..69d18a9d Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxxhdpi/splash.9.png differ diff --git a/mobile/android/app/src/main/res/drawable/ic_launcher_background.xml b/mobile/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..d5fccc53 --- /dev/null +++ b/mobile/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/app/src/main/res/drawable/icon_svg.xml b/mobile/android/app/src/main/res/drawable/icon_svg.xml new file mode 100644 index 00000000..e01a862b --- /dev/null +++ b/mobile/android/app/src/main/res/drawable/icon_svg.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + diff --git a/mobile/android/app/src/main/res/drawable/launch_splash.xml b/mobile/android/app/src/main/res/drawable/launch_splash.xml new file mode 100644 index 00000000..26c89c78 --- /dev/null +++ b/mobile/android/app/src/main/res/drawable/launch_splash.xml @@ -0,0 +1,6 @@ + + diff --git a/mobile/android/app/src/main/res/drawable/splash.9.png b/mobile/android/app/src/main/res/drawable/splash.9.png new file mode 100644 index 00000000..288a149b Binary files /dev/null and b/mobile/android/app/src/main/res/drawable/splash.9.png differ diff --git a/mobile/android/app/src/main/res/layout/activity_main.xml b/mobile/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..b5ad1387 --- /dev/null +++ b/mobile/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..4ae7d123 --- /dev/null +++ b/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..4ae7d123 --- /dev/null +++ b/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 00000000..c5a38260 Binary files /dev/null and b/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp b/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp new file mode 100644 index 00000000..91fd907d Binary files /dev/null and b/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp differ diff --git a/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 00000000..dcd9e4c7 Binary files /dev/null and b/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 00000000..dedf6905 Binary files /dev/null and b/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 00000000..cd91d494 Binary files /dev/null and b/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp b/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp new file mode 100644 index 00000000..7f9fb401 Binary files /dev/null and b/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp differ diff --git a/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 00000000..2fa6d7c1 Binary files /dev/null and b/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 00000000..f5197783 Binary files /dev/null and b/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 00000000..f3cb39a3 Binary files /dev/null and b/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp b/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp new file mode 100644 index 00000000..09d4751a Binary files /dev/null and b/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp differ diff --git a/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 00000000..d5392d5e Binary files /dev/null and b/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..34cdc233 Binary files /dev/null and b/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 00000000..946ebb0c Binary files /dev/null and b/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp b/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp new file mode 100644 index 00000000..2430b97d Binary files /dev/null and b/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp differ diff --git a/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 00000000..5af97cea Binary files /dev/null and b/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..0a4dcce1 Binary files /dev/null and b/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 00000000..06f9cfa3 Binary files /dev/null and b/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp b/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp new file mode 100644 index 00000000..27c3ed57 Binary files /dev/null and b/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp differ diff --git a/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 00000000..ec2e27e3 Binary files /dev/null and b/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..d6ef051b Binary files /dev/null and b/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/mobile/android/app/src/main/res/values-v31/styles.xml b/mobile/android/app/src/main/res/values-v31/styles.xml new file mode 100644 index 00000000..6f91d8b9 --- /dev/null +++ b/mobile/android/app/src/main/res/values-v31/styles.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/mobile/android/app/src/main/res/values/ic_launcher_background.xml b/mobile/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 00000000..c5d5899f --- /dev/null +++ b/mobile/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/mobile/android/app/src/main/res/values/strings.xml b/mobile/android/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..42dac8fa --- /dev/null +++ b/mobile/android/app/src/main/res/values/strings.xml @@ -0,0 +1,8 @@ + + + MyTonWallet + MyTonWallet + org.mytonwallet.app + ton + tc + diff --git a/mobile/android/app/src/main/res/values/styles.xml b/mobile/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..be874e54 --- /dev/null +++ b/mobile/android/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/mobile/android/app/src/main/res/xml/file_paths.xml b/mobile/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 00000000..bd0c4d80 --- /dev/null +++ b/mobile/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mobile/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java b/mobile/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java new file mode 100644 index 00000000..02973278 --- /dev/null +++ b/mobile/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java @@ -0,0 +1,18 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} diff --git a/mobile/android/build.gradle b/mobile/android/build.gradle new file mode 100644 index 00000000..9cc72cb6 --- /dev/null +++ b/mobile/android/build.gradle @@ -0,0 +1,29 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.0.0' + classpath 'com.google.gms:google-services:4.3.15' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +apply from: "variables.gradle" + +allprojects { + repositories { + google() + mavenCentral() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/mobile/android/capacitor.settings.gradle b/mobile/android/capacitor.settings.gradle new file mode 100644 index 00000000..9242b899 --- /dev/null +++ b/mobile/android/capacitor.settings.gradle @@ -0,0 +1,30 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN +include ':capacitor-android' +project(':capacitor-android').projectDir = new File('../../node_modules/@capacitor/android/capacitor') + +include ':capacitor-mlkit-barcode-scanning' +project(':capacitor-mlkit-barcode-scanning').projectDir = new File('../../node_modules/@capacitor-mlkit/barcode-scanning/android') + +include ':capacitor-app' +project(':capacitor-app').projectDir = new File('../../node_modules/@capacitor/app/android') + +include ':capacitor-dialog' +project(':capacitor-dialog').projectDir = new File('../../node_modules/@capacitor/dialog/android') + +include ':capacitor-haptics' +project(':capacitor-haptics').projectDir = new File('../../node_modules/@capacitor/haptics/android') + +include ':capacitor-status-bar' +project(':capacitor-status-bar').projectDir = new File('../../node_modules/@capacitor/status-bar/android') + +include ':capgo-capacitor-native-biometric' +project(':capgo-capacitor-native-biometric').projectDir = new File('../../node_modules/@capgo/capacitor-native-biometric/android') + +include ':mauricewegner-capacitor-navigation-bar' +project(':mauricewegner-capacitor-navigation-bar').projectDir = new File('../../node_modules/@mauricewegner/capacitor-navigation-bar/android') + +include ':capacitor-plugin-safe-area' +project(':capacitor-plugin-safe-area').projectDir = new File('../../node_modules/capacitor-plugin-safe-area/android') + +include ':native-bottom-sheet' +project(':native-bottom-sheet').projectDir = new File('../plugins/native-bottom-sheet/android') diff --git a/mobile/android/fastlane/Appfile b/mobile/android/fastlane/Appfile new file mode 100644 index 00000000..29fba405 --- /dev/null +++ b/mobile/android/fastlane/Appfile @@ -0,0 +1,2 @@ +json_key_file("./api-key.json") +package_name("org.mytonwallet.app") diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile new file mode 100644 index 00000000..b026c738 --- /dev/null +++ b/mobile/android/fastlane/Fastfile @@ -0,0 +1,78 @@ +default_platform(:android) + +def update_version_and_build_number + beta_build_number = google_play_track_version_codes( + package_name: CredentialsManager::AppfileConfig.try_fetch_value(:package_name), + track: "beta", + json_key: CredentialsManager::AppfileConfig.try_fetch_value(:json_key_file), + )[0] + + production_build_number = google_play_track_version_codes( + package_name: CredentialsManager::AppfileConfig.try_fetch_value(:package_name), + track: "production", + json_key: CredentialsManager::AppfileConfig.try_fetch_value(:json_key_file), + )[0] + + build_number = [beta_build_number, production_build_number].max() + 1 + version = JSON.parse(File.read("../../../package.json"))["version"] + + android_set_version_name(version_name: version) + android_set_version_code(version_code: build_number) +end + +platform :android do + desc "Deploy a new Beta Build to the Google Play" + + $gradle_data = nil + + before_all do |lane| + $gradle_data = File.read("../app/build.gradle") + end + + lane :beta do + update_version_and_build_number + + keystore_path = Dir.pwd + "/../android.keystore" + + gradle( + task: "clean bundle", + build_type: "Release", + properties: { + "android.injected.signing.store.file" => keystore_path, + "android.injected.signing.store.password" => ENV["ANDROID_KEYSTORE_PASSWORD"], + "android.injected.signing.key.alias" => "key0", + "android.injected.signing.key.password" => ENV["ANDROID_KEYSTORE_PASSWORD"] + } + ) + + upload_to_play_store(track: "beta") + end + + desc "Release a new version to the Google Play" + lane :release do + update_version_and_build_number + + keystore_path = Dir.pwd + "/../android.keystore" + + gradle( + task: "clean bundle", + build_type: "Release", + properties: { + "android.injected.signing.store.file" => keystore_path, + "android.injected.signing.store.password" => ENV["ANDROID_KEYSTORE_PASSWORD"], + "android.injected.signing.key.alias" => "key0", + "android.injected.signing.key.password" => ENV["ANDROID_KEYSTORE_PASSWORD"] + } + ) + + upload_to_play_store(track: "production") + end + + after_all do |lane| + File.write("../app/build.gradle", $gradle_data) + end + + error do |lane, exception| + File.write("../app/build.gradle", $gradle_data) + end +end diff --git a/mobile/android/fastlane/Pluginfile b/mobile/android/fastlane/Pluginfile new file mode 100644 index 00000000..052b0905 --- /dev/null +++ b/mobile/android/fastlane/Pluginfile @@ -0,0 +1,5 @@ +# Autogenerated by fastlane +# +# Ensure this file is checked in to source control! + +gem 'fastlane-plugin-versioning_android' diff --git a/mobile/android/gradle.properties b/mobile/android/gradle.properties new file mode 100644 index 00000000..2e87c52f --- /dev/null +++ b/mobile/android/gradle.properties @@ -0,0 +1,22 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true diff --git a/mobile/android/gradle/wrapper/gradle-wrapper.jar b/mobile/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..ccebba77 Binary files /dev/null and b/mobile/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/mobile/android/gradle/wrapper/gradle-wrapper.properties b/mobile/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..761b8f08 --- /dev/null +++ b/mobile/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/mobile/android/gradlew b/mobile/android/gradlew new file mode 100755 index 00000000..79a61d42 --- /dev/null +++ b/mobile/android/gradlew @@ -0,0 +1,244 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/mobile/android/gradlew.bat b/mobile/android/gradlew.bat new file mode 100644 index 00000000..93e3f59f --- /dev/null +++ b/mobile/android/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mobile/android/settings.gradle b/mobile/android/settings.gradle new file mode 100644 index 00000000..3b4431d7 --- /dev/null +++ b/mobile/android/settings.gradle @@ -0,0 +1,5 @@ +include ':app' +include ':capacitor-cordova-android-plugins' +project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') + +apply from: 'capacitor.settings.gradle' \ No newline at end of file diff --git a/mobile/android/variables.gradle b/mobile/android/variables.gradle new file mode 100644 index 00000000..409c2575 --- /dev/null +++ b/mobile/android/variables.gradle @@ -0,0 +1,17 @@ +ext { + minSdkVersion = 22 + compileSdkVersion = 33 + targetSdkVersion = 33 + androidxActivityVersion = '1.7.0' + androidxAppCompatVersion = '1.6.1' + androidxCoordinatorLayoutVersion = '1.2.0' + androidxCoreVersion = '1.10.0' + androidxFragmentVersion = '1.5.6' + coreSplashScreenVersion = '1.0.0' + androidxWebkitVersion = '1.6.1' + junitVersion = '4.13.2' + androidxJunitVersion = '1.1.5' + androidxEspressoCoreVersion = '3.5.1' + cordovaAndroidVersion = '10.1.1' + mlkitBarcodeScanningVersion = '17.2.0' +} diff --git a/mobile/ios/.gitignore b/mobile/ios/.gitignore new file mode 100644 index 00000000..4e9639a4 --- /dev/null +++ b/mobile/ios/.gitignore @@ -0,0 +1,20 @@ +App/build +App/Pods +App/output +App/App/public +DerivedData +xcuserdata + +# Cordova plugins for Capacitor +capacitor-cordova-ios-plugins + +# Generated Config files +App/App/capacitor.config.json +App/App/config.xml + +# fastlane files +App/*.ipa +App/*.zip +App/*.p8 +App/fastlane/report.xml +App/fastlane/README.md diff --git a/mobile/ios/App/App.xcodeproj/project.pbxproj b/mobile/ios/App/App.xcodeproj/project.pbxproj new file mode 100644 index 00000000..53c2f5fc --- /dev/null +++ b/mobile/ios/App/App.xcodeproj/project.pbxproj @@ -0,0 +1,425 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 48; + objects = { + +/* Begin PBXBuildFile section */ + 0EB354256D6B4E26B50364CD /* Pods_MyTonWallet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1BABB2300AAC81FD0F3E47EB /* Pods_MyTonWallet.framework */; }; + 2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; }; + 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; }; + 504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; }; + 504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; }; + 504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; }; + 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; }; + 50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 1BABB2300AAC81FD0F3E47EB /* Pods_MyTonWallet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MyTonWallet.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = ""; }; + 50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = ""; }; + 504EC3041FED79650016851F /* MyTonWallet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MyTonWallet.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = ""; }; + 65D4981FD7A0308A5E60DEDC /* Pods-MyTonWallet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MyTonWallet.debug.xcconfig"; path = "Pods/Target Support Files/Pods-MyTonWallet/Pods-MyTonWallet.debug.xcconfig"; sourceTree = ""; }; + 66B73F8B8E24A3D107892F83 /* Pods-MyTonWallet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MyTonWallet.release.xcconfig"; path = "Pods/Target Support Files/Pods-MyTonWallet/Pods-MyTonWallet.release.xcconfig"; sourceTree = ""; }; + AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = ""; }; + CE138AE22AC7209B00BE5802 /* MyTonWallet.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MyTonWallet.entitlements; sourceTree = ""; }; + FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 504EC3011FED79650016851F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0EB354256D6B4E26B50364CD /* Pods_MyTonWallet.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = { + isa = PBXGroup; + children = ( + 1BABB2300AAC81FD0F3E47EB /* Pods_MyTonWallet.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 504EC2FB1FED79650016851F = { + isa = PBXGroup; + children = ( + CE138AE22AC7209B00BE5802 /* MyTonWallet.entitlements */, + 504EC3061FED79650016851F /* App */, + 504EC3051FED79650016851F /* Products */, + 7F8756D8B27F46E3366F6CEA /* Pods */, + 27E2DDA53C4D2A4D1A88CE4A /* Frameworks */, + ); + sourceTree = ""; + }; + 504EC3051FED79650016851F /* Products */ = { + isa = PBXGroup; + children = ( + 504EC3041FED79650016851F /* MyTonWallet.app */, + ); + name = Products; + sourceTree = ""; + }; + 504EC3061FED79650016851F /* App */ = { + isa = PBXGroup; + children = ( + 50379B222058CBB4000EE86E /* capacitor.config.json */, + 504EC3071FED79650016851F /* AppDelegate.swift */, + 504EC30B1FED79650016851F /* Main.storyboard */, + 504EC30E1FED79650016851F /* Assets.xcassets */, + 504EC3101FED79650016851F /* LaunchScreen.storyboard */, + 504EC3131FED79650016851F /* Info.plist */, + 2FAD9762203C412B000D30F8 /* config.xml */, + 50B271D01FEDC1A000F3C39B /* public */, + ); + path = App; + sourceTree = ""; + }; + 7F8756D8B27F46E3366F6CEA /* Pods */ = { + isa = PBXGroup; + children = ( + FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */, + AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */, + 65D4981FD7A0308A5E60DEDC /* Pods-MyTonWallet.debug.xcconfig */, + 66B73F8B8E24A3D107892F83 /* Pods-MyTonWallet.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 504EC3031FED79650016851F /* MyTonWallet */ = { + isa = PBXNativeTarget; + buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "MyTonWallet" */; + buildPhases = ( + 6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */, + 504EC3001FED79650016851F /* Sources */, + 504EC3011FED79650016851F /* Frameworks */, + 504EC3021FED79650016851F /* Resources */, + 9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MyTonWallet; + productName = App; + productReference = 504EC3041FED79650016851F /* MyTonWallet.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 504EC2FC1FED79650016851F /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 920; + LastUpgradeCheck = 920; + TargetAttributes = { + 504EC3031FED79650016851F = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */; + compatibilityVersion = "Xcode 8.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 504EC2FB1FED79650016851F; + productRefGroup = 504EC3051FED79650016851F /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 504EC3031FED79650016851F /* MyTonWallet */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 504EC3021FED79650016851F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */, + 50B271D11FEDC1A000F3C39B /* public in Resources */, + 504EC30F1FED79650016851F /* Assets.xcassets in Resources */, + 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */, + 504EC30D1FED79650016851F /* Main.storyboard in Resources */, + 2FAD9763203C412B000D30F8 /* config.xml in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-MyTonWallet-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-MyTonWallet/Pods-MyTonWallet-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 504EC3001FED79650016851F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 504EC3081FED79650016851F /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 504EC30B1FED79650016851F /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 504EC30C1FED79650016851F /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 504EC3101FED79650016851F /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 504EC3111FED79650016851F /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 504EC3141FED79650016851F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 504EC3151FED79650016851F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 504EC3171FED79650016851F /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 65D4981FD7A0308A5E60DEDC /* Pods-MyTonWallet.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; + CODE_SIGN_ENTITLEMENTS = MyTonWallet.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = Y54Z4K69Z9; + INFOPLIST_FILE = App/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MARKETING_VERSION = 1.16.1; + OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; + PRODUCT_BUNDLE_IDENTIFIER = org.mytonwallet.app; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 504EC3181FED79650016851F /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 66B73F8B8E24A3D107892F83 /* Pods-MyTonWallet.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; + CODE_SIGN_ENTITLEMENTS = MyTonWallet.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = Y54Z4K69Z9; + INFOPLIST_FILE = App/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MARKETING_VERSION = 1.16.1; + PRODUCT_BUNDLE_IDENTIFIER = org.mytonwallet.app; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "MyTonWallet Production profile"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 504EC3141FED79650016851F /* Debug */, + 504EC3151FED79650016851F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "MyTonWallet" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 504EC3171FED79650016851F /* Debug */, + 504EC3181FED79650016851F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 504EC2FC1FED79650016851F /* Project object */; +} diff --git a/mobile/ios/App/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/mobile/ios/App/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..2db664d7 --- /dev/null +++ b/mobile/ios/App/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/mobile/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/mobile/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/mobile/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/mobile/ios/App/App.xcodeproj/xcshareddata/xcschemes/MyTonWallet.xcscheme b/mobile/ios/App/App.xcodeproj/xcshareddata/xcschemes/MyTonWallet.xcscheme new file mode 100644 index 00000000..2691e580 --- /dev/null +++ b/mobile/ios/App/App.xcodeproj/xcshareddata/xcschemes/MyTonWallet.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/App/App.xcworkspace/contents.xcworkspacedata b/mobile/ios/App/App.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..b301e824 --- /dev/null +++ b/mobile/ios/App/App.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/mobile/ios/App/App.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/mobile/ios/App/App.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/mobile/ios/App/App.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/mobile/ios/App/App.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/mobile/ios/App/App.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..0c67376e --- /dev/null +++ b/mobile/ios/App/App.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/mobile/ios/App/App/AppDelegate.swift b/mobile/ios/App/App/AppDelegate.swift new file mode 100644 index 00000000..c3cd83b5 --- /dev/null +++ b/mobile/ios/App/App/AppDelegate.swift @@ -0,0 +1,49 @@ +import UIKit +import Capacitor + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. + } + + func applicationDidEnterBackground(_ application: UIApplication) { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + } + + func applicationWillEnterForeground(_ application: UIApplication) { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + // Called when the app was launched with a url. Feel free to add additional processing here, + // but if you want the App API to support tracking app url opens, make sure to keep this call + return ApplicationDelegateProxy.shared.application(app, open: url, options: options) + } + + func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { + // Called when the app was launched with an activity, including Universal Links. + // Feel free to add additional processing here, but if you want the App API to support + // tracking app url opens, make sure to keep this call + return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler) + } + +} diff --git a/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png new file mode 100644 index 00000000..bd28e34c Binary files /dev/null and b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png differ diff --git a/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png new file mode 100644 index 00000000..e6503aec Binary files /dev/null and b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png differ diff --git a/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png new file mode 100644 index 00000000..e6503aec Binary files /dev/null and b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png differ diff --git a/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@3x.png b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@3x.png new file mode 100644 index 00000000..80b8848a Binary files /dev/null and b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@3x.png differ diff --git a/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png new file mode 100644 index 00000000..f526fef8 Binary files /dev/null and b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png differ diff --git a/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png new file mode 100644 index 00000000..5f344e13 Binary files /dev/null and b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png differ diff --git a/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png new file mode 100644 index 00000000..5f344e13 Binary files /dev/null and b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png differ diff --git a/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png new file mode 100644 index 00000000..be2840f3 Binary files /dev/null and b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png differ diff --git a/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png new file mode 100644 index 00000000..e6503aec Binary files /dev/null and b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png differ diff --git a/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x-1.png b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x-1.png new file mode 100644 index 00000000..8c5f270a Binary files /dev/null and b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x-1.png differ diff --git a/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png new file mode 100644 index 00000000..8c5f270a Binary files /dev/null and b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png differ diff --git a/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png new file mode 100644 index 00000000..14822a1a Binary files /dev/null and b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png differ diff --git a/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png new file mode 100644 index 00000000..0f3151e3 Binary files /dev/null and b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png differ diff --git a/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png new file mode 100644 index 00000000..14822a1a Binary files /dev/null and b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png differ diff --git a/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png new file mode 100644 index 00000000..c22cdacc Binary files /dev/null and b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png differ diff --git a/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png new file mode 100644 index 00000000..4ce87ad4 Binary files /dev/null and b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png differ diff --git a/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png new file mode 100644 index 00000000..bf952a3a Binary files /dev/null and b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png differ diff --git a/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png new file mode 100644 index 00000000..ae6f99ea Binary files /dev/null and b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png differ diff --git a/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..7fc32260 --- /dev/null +++ b/mobile/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images": [ + { + "filename": "AppIcon-512@2x.png", + "idiom": "universal", + "platform": "ios", + "size": "1024x1024" + }, + { + "idiom": "ipad", + "size": "20x20", + "scale": "1x", + "filename": "AppIcon-20x20@1x.png" + }, + { + "idiom": "iphone", + "size": "20x20", + "scale": "2x", + "filename": "AppIcon-20x20@2x.png" + }, + { + "idiom": "ipad", + "size": "20x20", + "scale": "2x", + "filename": "AppIcon-20x20@2x-1.png" + }, + { + "idiom": "iphone", + "size": "20x20", + "scale": "3x", + "filename": "AppIcon-20x20@3x.png" + }, + { + "idiom": "ipad", + "size": "29x29", + "scale": "1x", + "filename": "AppIcon-29x29@1x.png" + }, + { + "idiom": "iphone", + "size": "29x29", + "scale": "2x", + "filename": "AppIcon-29x29@2x.png" + }, + { + "idiom": "ipad", + "size": "29x29", + "scale": "2x", + "filename": "AppIcon-29x29@2x-1.png" + }, + { + "idiom": "iphone", + "size": "29x29", + "scale": "3x", + "filename": "AppIcon-29x29@3x.png" + }, + { + "idiom": "ipad", + "size": "40x40", + "scale": "1x", + "filename": "AppIcon-40x40@1x.png" + }, + { + "idiom": "iphone", + "size": "40x40", + "scale": "2x", + "filename": "AppIcon-40x40@2x.png" + }, + { + "idiom": "ipad", + "size": "40x40", + "scale": "2x", + "filename": "AppIcon-40x40@2x-1.png" + }, + { + "idiom": "iphone", + "size": "40x40", + "scale": "3x", + "filename": "AppIcon-40x40@3x.png" + }, + { + "idiom": "iphone", + "size": "60x60", + "scale": "2x", + "filename": "AppIcon-60x60@2x.png" + }, + { + "idiom": "iphone", + "size": "60x60", + "scale": "3x", + "filename": "AppIcon-60x60@3x.png" + }, + { + "idiom": "ipad", + "size": "76x76", + "scale": "1x", + "filename": "AppIcon-76x76@1x.png" + }, + { + "idiom": "ipad", + "size": "76x76", + "scale": "2x", + "filename": "AppIcon-76x76@2x.png" + }, + { + "idiom": "ipad", + "size": "83.5x83.5", + "scale": "2x", + "filename": "AppIcon-83.5x83.5@2x.png" + }, + { + "idiom": "ios-marketing", + "size": "1024x1024", + "scale": "1x", + "filename": "AppIcon-512@2x.png" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/mobile/ios/App/App/Assets.xcassets/Contents.json b/mobile/ios/App/App/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/mobile/ios/App/App/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json b/mobile/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json new file mode 100644 index 00000000..bb5a7a69 --- /dev/null +++ b/mobile/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json @@ -0,0 +1,56 @@ +{ + "images" : [ + { + "filename" : "Default@1x~universal~anyany.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Default@1x~universal~anyany-dark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Default@2x~universal~anyany.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Default@2x~universal~anyany-dark.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Default@3x~universal~anyany.png", + "idiom" : "universal", + "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Default@3x~universal~anyany-dark.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mobile/ios/App/App/Assets.xcassets/Splash.imageset/Default@1x~universal~anyany-dark.png b/mobile/ios/App/App/Assets.xcassets/Splash.imageset/Default@1x~universal~anyany-dark.png new file mode 100644 index 00000000..67db81af Binary files /dev/null and b/mobile/ios/App/App/Assets.xcassets/Splash.imageset/Default@1x~universal~anyany-dark.png differ diff --git a/mobile/ios/App/App/Assets.xcassets/Splash.imageset/Default@1x~universal~anyany.png b/mobile/ios/App/App/Assets.xcassets/Splash.imageset/Default@1x~universal~anyany.png new file mode 100644 index 00000000..842a0c78 Binary files /dev/null and b/mobile/ios/App/App/Assets.xcassets/Splash.imageset/Default@1x~universal~anyany.png differ diff --git a/mobile/ios/App/App/Assets.xcassets/Splash.imageset/Default@2x~universal~anyany-dark.png b/mobile/ios/App/App/Assets.xcassets/Splash.imageset/Default@2x~universal~anyany-dark.png new file mode 100644 index 00000000..640696b6 Binary files /dev/null and b/mobile/ios/App/App/Assets.xcassets/Splash.imageset/Default@2x~universal~anyany-dark.png differ diff --git a/mobile/ios/App/App/Assets.xcassets/Splash.imageset/Default@2x~universal~anyany.png b/mobile/ios/App/App/Assets.xcassets/Splash.imageset/Default@2x~universal~anyany.png new file mode 100644 index 00000000..ed7cae3d Binary files /dev/null and b/mobile/ios/App/App/Assets.xcassets/Splash.imageset/Default@2x~universal~anyany.png differ diff --git a/mobile/ios/App/App/Assets.xcassets/Splash.imageset/Default@3x~universal~anyany-dark.png b/mobile/ios/App/App/Assets.xcassets/Splash.imageset/Default@3x~universal~anyany-dark.png new file mode 100644 index 00000000..8b661202 Binary files /dev/null and b/mobile/ios/App/App/Assets.xcassets/Splash.imageset/Default@3x~universal~anyany-dark.png differ diff --git a/mobile/ios/App/App/Assets.xcassets/Splash.imageset/Default@3x~universal~anyany.png b/mobile/ios/App/App/Assets.xcassets/Splash.imageset/Default@3x~universal~anyany.png new file mode 100644 index 00000000..2ca08c27 Binary files /dev/null and b/mobile/ios/App/App/Assets.xcassets/Splash.imageset/Default@3x~universal~anyany.png differ diff --git a/mobile/ios/App/App/Base.lproj/LaunchScreen.storyboard b/mobile/ios/App/App/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..e7ae5d78 --- /dev/null +++ b/mobile/ios/App/App/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/App/App/Base.lproj/Main.storyboard b/mobile/ios/App/App/Base.lproj/Main.storyboard new file mode 100644 index 00000000..b44df7be --- /dev/null +++ b/mobile/ios/App/App/Base.lproj/Main.storyboard @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/App/App/Info.plist b/mobile/ios/App/App/Info.plist new file mode 100644 index 00000000..de86f1d9 --- /dev/null +++ b/mobile/ios/App/App/Info.plist @@ -0,0 +1,67 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + MyTonWallet + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleURLTypes + + + CFBundleURLName + org.mytonwallet.app + CFBundleURLSchemes + + ton + tc + + + + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + ITSAppUsesNonExemptEncryption + + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen.storyboard + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UIRequiresFullScreen + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + + UIViewControllerBasedStatusBarAppearance + + NSCameraUsageDescription + The app enables the scanning of various barcodes. + NSFaceIDUsageDescription + For an easier and faster operation verification. + + diff --git a/mobile/ios/App/MyTonWallet.entitlements b/mobile/ios/App/MyTonWallet.entitlements new file mode 100644 index 00000000..e997e85e --- /dev/null +++ b/mobile/ios/App/MyTonWallet.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.associated-domains + + applinks:connect.mytonwallet.org + + + diff --git a/mobile/ios/App/Podfile b/mobile/ios/App/Podfile new file mode 100644 index 00000000..8424c7cc --- /dev/null +++ b/mobile/ios/App/Podfile @@ -0,0 +1,44 @@ +def assertDeploymentTarget(installer) + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + # ensure IPHONEOS_DEPLOYMENT_TARGET is at least 13.0 + deployment_target = config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'].to_f + should_upgrade = deployment_target < 13.0 && deployment_target != 0.0 + if should_upgrade + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' + end + end + end +end + +platform :ios, '13.0' +use_frameworks! + +# workaround to avoid Xcode caching of Pods that requires +# Product -> Clean Build Folder after new Cordova plugins installed +# Requires CocoaPods 1.6 or newer +install! 'cocoapods', :disable_input_output_paths => true + +def capacitor_pods + pod 'Capacitor', :path => '../../../node_modules/@capacitor/ios' + pod 'CapacitorCordova', :path => '../../../node_modules/@capacitor/ios' + pod 'CapacitorMlkitBarcodeScanning', :path => '../../../node_modules/@capacitor-mlkit/barcode-scanning' + pod 'CapacitorApp', :path => '../../../node_modules/@capacitor/app' + pod 'CapacitorDialog', :path => '../../../node_modules/@capacitor/dialog' + pod 'CapacitorHaptics', :path => '../../../node_modules/@capacitor/haptics' + pod 'CapacitorStatusBar', :path => '../../../node_modules/@capacitor/status-bar' + pod 'CapgoCapacitorNativeBiometric', :path => '../../../node_modules/@capgo/capacitor-native-biometric' + pod 'MauricewegnerCapacitorNavigationBar', :path => '../../../node_modules/@mauricewegner/capacitor-navigation-bar' + pod 'CapacitorPluginSafeArea', :path => '../../../node_modules/capacitor-plugin-safe-area' + pod 'CapacitorSplashScreen', :path => '../../plugins/capacitor-splash-screen' + pod 'NativeBottomSheet', :path => '../../plugins/native-bottom-sheet' +end + +target 'MyTonWallet' do + capacitor_pods + # Add your Pods here +end + +post_install do |installer| + assertDeploymentTarget(installer) +end diff --git a/mobile/ios/App/Podfile.lock b/mobile/ios/App/Podfile.lock new file mode 100644 index 00000000..c17de57d --- /dev/null +++ b/mobile/ios/App/Podfile.lock @@ -0,0 +1,168 @@ +PODS: + - Capacitor (5.5.1): + - CapacitorCordova + - CapacitorApp (5.0.6): + - Capacitor + - CapacitorCordova (5.5.1) + - CapacitorDialog (5.0.6): + - Capacitor + - CapacitorHaptics (5.0.6): + - Capacitor + - CapacitorMlkitBarcodeScanning (5.3.0): + - Capacitor + - GoogleMLKit/BarcodeScanning (= 4.0.0) + - CapacitorPluginSafeArea (2.0.5): + - Capacitor + - CapacitorSplashScreen (5.0.6.1): + - Capacitor + - CapacitorStatusBar (5.0.6): + - Capacitor + - CapgoCapacitorNativeBiometric (0.0.1): + - Capacitor + - FloatingPanel (2.8.0) + - GoogleDataTransport (9.2.5): + - GoogleUtilities/Environment (~> 7.7) + - nanopb (< 2.30910.0, >= 2.30908.0) + - PromisesObjC (< 3.0, >= 1.2) + - GoogleMLKit/BarcodeScanning (4.0.0): + - GoogleMLKit/MLKitCore + - MLKitBarcodeScanning (~> 3.0.0) + - GoogleMLKit/MLKitCore (4.0.0): + - MLKitCommon (~> 9.0.0) + - GoogleToolboxForMac/DebugUtils (2.3.2): + - GoogleToolboxForMac/Defines (= 2.3.2) + - GoogleToolboxForMac/Defines (2.3.2) + - GoogleToolboxForMac/Logger (2.3.2): + - GoogleToolboxForMac/Defines (= 2.3.2) + - "GoogleToolboxForMac/NSData+zlib (2.3.2)": + - GoogleToolboxForMac/Defines (= 2.3.2) + - "GoogleToolboxForMac/NSDictionary+URLArguments (2.3.2)": + - GoogleToolboxForMac/DebugUtils (= 2.3.2) + - GoogleToolboxForMac/Defines (= 2.3.2) + - "GoogleToolboxForMac/NSString+URLArguments (= 2.3.2)" + - "GoogleToolboxForMac/NSString+URLArguments (2.3.2)" + - GoogleUtilities/Environment (7.11.5): + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/Logger (7.11.5): + - GoogleUtilities/Environment + - GoogleUtilities/UserDefaults (7.11.5): + - GoogleUtilities/Logger + - GoogleUtilitiesComponents (1.1.0): + - GoogleUtilities/Logger + - GTMSessionFetcher/Core (2.3.0) + - MauricewegnerCapacitorNavigationBar (2.0.3): + - Capacitor + - MLImage (1.0.0-beta4) + - MLKitBarcodeScanning (3.0.0): + - MLKitCommon (~> 9.0) + - MLKitVision (~> 5.0) + - MLKitCommon (9.0.0): + - GoogleDataTransport (~> 9.0) + - GoogleToolboxForMac/Logger (~> 2.1) + - "GoogleToolboxForMac/NSData+zlib (~> 2.1)" + - "GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1)" + - GoogleUtilities/UserDefaults (~> 7.0) + - GoogleUtilitiesComponents (~> 1.0) + - GTMSessionFetcher/Core (< 3.0, >= 1.1) + - MLKitVision (5.0.0): + - GoogleToolboxForMac/Logger (~> 2.1) + - "GoogleToolboxForMac/NSData+zlib (~> 2.1)" + - GTMSessionFetcher/Core (< 3.0, >= 1.1) + - MLImage (= 1.0.0-beta4) + - MLKitCommon (~> 9.0) + - nanopb (2.30909.0): + - nanopb/decode (= 2.30909.0) + - nanopb/encode (= 2.30909.0) + - nanopb/decode (2.30909.0) + - nanopb/encode (2.30909.0) + - NativeBottomSheet (0.0.1): + - Capacitor + - FloatingPanel + - PromisesObjC (2.3.1) + +DEPENDENCIES: + - "Capacitor (from `../../../node_modules/@capacitor/ios`)" + - "CapacitorApp (from `../../../node_modules/@capacitor/app`)" + - "CapacitorCordova (from `../../../node_modules/@capacitor/ios`)" + - "CapacitorDialog (from `../../../node_modules/@capacitor/dialog`)" + - "CapacitorHaptics (from `../../../node_modules/@capacitor/haptics`)" + - "CapacitorMlkitBarcodeScanning (from `../../../node_modules/@capacitor-mlkit/barcode-scanning`)" + - CapacitorPluginSafeArea (from `../../../node_modules/capacitor-plugin-safe-area`) + - CapacitorSplashScreen (from `../../plugins/capacitor-splash-screen`) + - "CapacitorStatusBar (from `../../../node_modules/@capacitor/status-bar`)" + - "CapgoCapacitorNativeBiometric (from `../../../node_modules/@capgo/capacitor-native-biometric`)" + - "MauricewegnerCapacitorNavigationBar (from `../../../node_modules/@mauricewegner/capacitor-navigation-bar`)" + - NativeBottomSheet (from `../../plugins/native-bottom-sheet`) + +SPEC REPOS: + trunk: + - FloatingPanel + - GoogleDataTransport + - GoogleMLKit + - GoogleToolboxForMac + - GoogleUtilities + - GoogleUtilitiesComponents + - GTMSessionFetcher + - MLImage + - MLKitBarcodeScanning + - MLKitCommon + - MLKitVision + - nanopb + - PromisesObjC + +EXTERNAL SOURCES: + Capacitor: + :path: "../../../node_modules/@capacitor/ios" + CapacitorApp: + :path: "../../../node_modules/@capacitor/app" + CapacitorCordova: + :path: "../../../node_modules/@capacitor/ios" + CapacitorDialog: + :path: "../../../node_modules/@capacitor/dialog" + CapacitorHaptics: + :path: "../../../node_modules/@capacitor/haptics" + CapacitorMlkitBarcodeScanning: + :path: "../../../node_modules/@capacitor-mlkit/barcode-scanning" + CapacitorPluginSafeArea: + :path: "../../../node_modules/capacitor-plugin-safe-area" + CapacitorSplashScreen: + :path: "../../plugins/capacitor-splash-screen" + CapacitorStatusBar: + :path: "../../../node_modules/@capacitor/status-bar" + CapgoCapacitorNativeBiometric: + :path: "../../../node_modules/@capgo/capacitor-native-biometric" + MauricewegnerCapacitorNavigationBar: + :path: "../../../node_modules/@mauricewegner/capacitor-navigation-bar" + NativeBottomSheet: + :path: "../../plugins/native-bottom-sheet" + +SPEC CHECKSUMS: + Capacitor: 9da0a2415e3b6098511f8b5ffdb578d91ee79f8f + CapacitorApp: 024e1b1bea5f883d79f6330d309bc441c88ad04a + CapacitorCordova: e128cc7688c070ca0bfa439898a5f609da8dbcfe + CapacitorDialog: 0f3c15dfe9414b83bc64aef4078f1b92bcfead26 + CapacitorHaptics: 1fffc1217c7e64a472d7845be50fb0c2f7d4204c + CapacitorMlkitBarcodeScanning: ea08ef246e5d3511d5a231a59fae36b16ad9acb3 + CapacitorPluginSafeArea: bfdd714827dbd89fb44fea286beec996e1b0c5c4 + CapacitorSplashScreen: 6fce4269c6f7dc7591cc28760d1f85255e5edb1b + CapacitorStatusBar: 565c0a1ebd79bb40d797606a8992b4a105885309 + CapgoCapacitorNativeBiometric: 44b0bb31118f6ed5171087a77a856a80a0cfa250 + FloatingPanel: f585be005983e66f8f4932f93ca46bf9f09dae3a + GoogleDataTransport: 54dee9d48d14580407f8f5fbf2f496e92437a2f2 + GoogleMLKit: 2bd0dc6253c4d4f227aad460f69215a504b2980e + GoogleToolboxForMac: 8bef7c7c5cf7291c687cf5354f39f9db6399ad34 + GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084 + GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe + GTMSessionFetcher: 3a63d75eecd6aa32c2fc79f578064e1214dfdec2 + MauricewegnerCapacitorNavigationBar: 37f1308a961a8d8cd00f6362e30bad03a73728f7 + MLImage: 7bb7c4264164ade9bf64f679b40fb29c8f33ee9b + MLKitBarcodeScanning: 04e264482c5f3810cb89ebc134ef6b61e67db505 + MLKitCommon: c1b791c3e667091918d91bda4bba69a91011e390 + MLKitVision: 8baa5f46ee3352614169b85250574fde38c36f49 + nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 + NativeBottomSheet: edfc1f70d3517ea92e70392db8c2de5ea3e6f15e + PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 + +PODFILE CHECKSUM: f2e7c24dfc22bd949d0c502101768c1e911d066f + +COCOAPODS: 1.14.3 diff --git a/mobile/ios/App/fastlane/Appfile b/mobile/ios/App/fastlane/Appfile new file mode 100644 index 00000000..01add11a --- /dev/null +++ b/mobile/ios/App/fastlane/Appfile @@ -0,0 +1,8 @@ +app_identifier("org.mytonwallet.app") # The bundle identifier of your app +apple_id("troman.dev@icloud.com") # Your Apple Developer Portal username + +itc_team_id("126235213") # App Store Connect Team ID +team_id("Y54Z4K69Z9") # Developer Portal Team ID + +# For more information about the Appfile, see: +# https://docs.fastlane.tools/advanced/#appfile diff --git a/mobile/ios/App/fastlane/Fastfile b/mobile/ios/App/fastlane/Fastfile new file mode 100644 index 00000000..369a968b --- /dev/null +++ b/mobile/ios/App/fastlane/Fastfile @@ -0,0 +1,107 @@ +default_platform(:ios) + +def update_version_and_build_number + version = JSON.parse(File.read("../../../../package.json"))["version"] + production_build_number = app_store_build_number(version: version, initial_build_number: 0, live: true).to_i + beta_build_number = latest_testflight_build_number(version: version, initial_build_number: 0) + build_number = [production_build_number, beta_build_number].max() + 1 + + increment_version_number(version_number: version) + increment_build_number(skip_info_plist: true, build_number: build_number) + + return version, build_number +end + +platform :ios do + desc "Push a new beta build to TestFlight" + + $project_data = nil + $plist_data = nil + + before_all do |lane| + $project_data = File.read("../App.xcodeproj/project.pbxproj") + $plist_data = File.read("../App/Info.plist") + + app_store_connect_api_key( + key_id: "TL49GJ73DP", + issuer_id: "519080b4-bc5f-4d06-a889-a69254108348", + key_filepath: "./AuthKey.p8", + duration: 1200, + in_house: false + ) + + update_code_signing_settings( + path: "App.xcodeproj", + use_automatic_signing: false, + profile_name: "MyTonWallet Production profile", + build_configurations: ["Debug"], + sdk: "iphoneos*", + team_id: "Y54Z4K69Z9", + code_sign_identity: "iPhone Distribution" + ) + end + + lane :beta do + update_version_and_build_number + + build_app( + workspace: "App.xcworkspace", + scheme: "MyTonWallet", + export_method: "app-store", + export_options: { + provisioningProfiles: { + "org.mytonwallet.app" => "MyTonWallet Production profile", + } + } + ) + + changelog_from_git_commits(merge_commit_filtering: "exclude_merges", commits_count: 3) + upload_to_testflight(distribute_external: true, groups: "MTW external group") + end + + lane :release do + version, build_number = update_version_and_build_number + + changelog_path = "../../../../changelogs/" + version + ".txt" + if !File.exist?(changelog_path) + raise "There is no changelog for version " + version + end + + changelog = File.read(changelog_path) + + build_app( + workspace: "App.xcworkspace", + scheme: "MyTonWallet", + export_method: "app-store", + export_options: { + provisioningProfiles: { + "org.mytonwallet.app" => "MyTonWallet Production profile", + } + } + ) + + upload_to_app_store( + skip_screenshots: true, + skip_metadata: true, + precheck_include_in_app_purchases: false, + submit_for_review: true, + submission_information: { + add_id_info_uses_idfa: false + }, + release_notes: { + "default" => changelog + } + ) + end + + after_all do |lane| + File.write("../App.xcodeproj/project.pbxproj", $project_data) + File.write("../App/Info.plist", $plist_data) + end + + error do |lane, exception| + File.write("../App.xcodeproj/project.pbxproj", $project_data) + File.write("../App/Info.plist", $plist_data) + end + +end diff --git a/mobile/plugins/capacitor-splash-screen/.eslintignore b/mobile/plugins/capacitor-splash-screen/.eslintignore new file mode 100644 index 00000000..9d0b71a3 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/.eslintignore @@ -0,0 +1,2 @@ +build +dist diff --git a/mobile/plugins/capacitor-splash-screen/.gitignore b/mobile/plugins/capacitor-splash-screen/.gitignore new file mode 100644 index 00000000..d4c3b664 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/.gitignore @@ -0,0 +1,61 @@ +# node files +!dist +node_modules + +# iOS files +Pods +Podfile.lock +Build +xcuserdata + +# macOS files +.DS_Store + + + +# Based on Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore + +# Built application files +*.apk +*.ap_ + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin +gen +out + +# Gradle files +.gradle +build + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation + +# Android Studio captures folder +captures + +# IntelliJ +*.iml +.idea + +# Keystore files +# Uncomment the following line if you do not want to check your keystore files in. +#*.jks + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild diff --git a/mobile/plugins/capacitor-splash-screen/.prettierignore b/mobile/plugins/capacitor-splash-screen/.prettierignore new file mode 100644 index 00000000..9d0b71a3 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/.prettierignore @@ -0,0 +1,2 @@ +build +dist diff --git a/mobile/plugins/capacitor-splash-screen/CHANGELOG.md b/mobile/plugins/capacitor-splash-screen/CHANGELOG.md new file mode 100644 index 00000000..dd46eb11 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/CHANGELOG.md @@ -0,0 +1,488 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [5.0.6](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@5.0.5...@capacitor/splash-screen@5.0.6) (2023-07-12) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +## [5.0.5](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@5.0.4...@capacitor/splash-screen@5.0.5) (2023-06-29) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +## [5.0.4](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@5.0.3...@capacitor/splash-screen@5.0.4) (2023-06-08) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +## [5.0.3](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@5.0.2...@capacitor/splash-screen@5.0.3) (2023-06-08) + + +### Bug Fixes + +* **splash-screen:** Avoid crash when splash resources not found ([#1642](https://github.com/ionic-team/capacitor-plugins/issues/1642)) ([f94773c](https://github.com/ionic-team/capacitor-plugins/commit/f94773c914b5e7b2664f2c10a3ff1c4ac996b70e)) + + + + + +## [5.0.2](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@5.0.1...@capacitor/splash-screen@5.0.2) (2023-05-09) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +## [5.0.1](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@5.0.0...@capacitor/splash-screen@5.0.1) (2023-05-05) + + +### Bug Fixes + +* Use Capacitor 5 final ([#1574](https://github.com/ionic-team/capacitor-plugins/issues/1574)) ([139c18b](https://github.com/ionic-team/capacitor-plugins/commit/139c18b86a11d31246e952d1a74335ff8ce5dbc2)) + + + + + +# [5.0.0](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@5.0.0-beta.1...@capacitor/splash-screen@5.0.0) (2023-05-03) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +# [5.0.0-beta.1](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@5.0.0-beta.0...@capacitor/splash-screen@5.0.0-beta.1) (2023-04-21) + + +### Features + +* Update gradle to 8.0.2 and gradle plugin to 8.0.0 ([#1542](https://github.com/ionic-team/capacitor-plugins/issues/1542)) ([e7210b4](https://github.com/ionic-team/capacitor-plugins/commit/e7210b47867644f5983e37acdbf0247214ec232d)) + + + + + +# [5.0.0-beta.0](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@5.0.0-alpha.1...@capacitor/splash-screen@5.0.0-beta.0) (2023-03-31) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +# [5.0.0-alpha.1](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@4.2.0...@capacitor/splash-screen@5.0.0-alpha.1) (2023-03-16) + + +### Features + +* **android:** Removing enableJetifier ([d66f9cb](https://github.com/ionic-team/capacitor-plugins/commit/d66f9cbd9da7e3b1d8c64ca6a5b45156867d4a04)) + + + + + +# [4.2.0](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@4.1.4...@capacitor/splash-screen@4.2.0) (2023-02-22) + + +### Features + +* **splash-screen:** Add launchFadeOutDuration configuration option ([#1393](https://github.com/ionic-team/capacitor-plugins/issues/1393)) ([66929c2](https://github.com/ionic-team/capacitor-plugins/commit/66929c2177009f65758ccbacdc166ae95294ef37)) + + + + + +## [4.1.4](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@4.1.3...@capacitor/splash-screen@4.1.4) (2023-02-03) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +## [4.1.3](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@4.1.2...@capacitor/splash-screen@4.1.3) (2023-01-17) + + +### Bug Fixes + +* **splash-screen:** Don't show if WebView is older than supported ([#1317](https://github.com/ionic-team/capacitor-plugins/issues/1317)) ([c884c38](https://github.com/ionic-team/capacitor-plugins/commit/c884c38cb2d24105e4667e32ffb6bbe59c97b9b4)) + + + + + +## [4.1.2](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@4.1.1...@capacitor/splash-screen@4.1.2) (2022-11-16) + + +### Bug Fixes + +* **splash-screen:** Remove extension from storyboard name ([#1281](https://github.com/ionic-team/capacitor-plugins/issues/1281)) ([86ecc4f](https://github.com/ionic-team/capacitor-plugins/commit/86ecc4f0c45799b2a5a02700c18a3d5f0de00615)) + + + + + +## [4.1.1](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@4.1.0...@capacitor/splash-screen@4.1.1) (2022-10-21) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +# [4.1.0](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@1.2.2...@capacitor/splash-screen@4.1.0) (2022-09-29) + + + + + +## [4.0.1](https://github.com/ionic-team/capacitor-plugins/compare/4.0.0...4.0.1) (2022-07-28) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +# [4.0.0](https://github.com/ionic-team/capacitor-plugins/compare/4.0.0-beta.2...4.0.0) (2022-07-27) + + +### Features + +* **splash-screen:** Use Android 12 Splash Screen API ([#1011](https://github.com/ionic-team/capacitor-plugins/issues/1011)) ([79185ad](https://github.com/ionic-team/capacitor-plugins/commit/79185adf76bc4ff4bae1be5ec5b5881cfbe748b1)) + + + + + +# [4.0.0-beta.2](https://github.com/ionic-team/capacitor-plugins/compare/4.0.0-beta.0...4.0.0-beta.2) (2022-07-08) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +# 4.0.0-beta.0 (2022-06-27) + + +### Bug Fixes + +* **splash-screen:** avoid conditional downcast warning ([#776](https://github.com/ionic-team/capacitor-plugins/issues/776)) ([87ed912](https://github.com/ionic-team/capacitor-plugins/commit/87ed9128f43f0be498601f0ba89cadeba7a339b4)) +* **splash-screen:** pick first window when there is no key window ([#730](https://github.com/ionic-team/capacitor-plugins/issues/730)) ([0e335ad](https://github.com/ionic-team/capacitor-plugins/commit/0e335ad386da8ceadfdfaa3be982840547fc41b6)) +* **splash-screen:** Use Locale.ROOT on toLowerCase ([#813](https://github.com/ionic-team/capacitor-plugins/issues/813)) ([ecc55e1](https://github.com/ionic-team/capacitor-plugins/commit/ecc55e172acfe066977a4aac5018a55beef64f53)) +* add es2017 lib to tsconfig ([#180](https://github.com/ionic-team/capacitor-plugins/issues/180)) ([2c3776c](https://github.com/ionic-team/capacitor-plugins/commit/2c3776c38ca025c5ee965dec10ccf1cdb6c02e2f)) +* correct addListeners links ([#655](https://github.com/ionic-team/capacitor-plugins/issues/655)) ([f9871e7](https://github.com/ionic-team/capacitor-plugins/commit/f9871e7bd53478addb21155e148829f550c0e457)) +* export all TS definitions ([6cd2996](https://github.com/ionic-team/capacitor-plugins/commit/6cd299660fdeb27382ec7f45f0b3a55224cd0ad1)) +* inline source code in esm map files ([#760](https://github.com/ionic-team/capacitor-plugins/issues/760)) ([a960489](https://github.com/ionic-team/capacitor-plugins/commit/a960489a19db0182b90d187a50deff9dfbe51038)) +* remove postpublish scripts ([#656](https://github.com/ionic-team/capacitor-plugins/issues/656)) ([ed6ac49](https://github.com/ionic-team/capacitor-plugins/commit/ed6ac499ebf4a47525071ccbfc36c27503e11f60)) +* **splash-screen:** launchAutoHide not working on iOS ([#319](https://github.com/ionic-team/capacitor-plugins/issues/319)) ([2a83fcb](https://github.com/ionic-team/capacitor-plugins/commit/2a83fcb536cdfc5b601f363212353201de40ca5b)) +* **splash-screen:** Use configured storyboard instead of hardcoded value ([#548](https://github.com/ionic-team/capacitor-plugins/issues/548)) ([67dd67f](https://github.com/ionic-team/capacitor-plugins/commit/67dd67fc443ea5494e8482fd4346c5275a42841b)) +* support deprecated types from Capacitor 2 ([#139](https://github.com/ionic-team/capacitor-plugins/issues/139)) ([2d7127a](https://github.com/ionic-team/capacitor-plugins/commit/2d7127a488e26f0287951921a6db47c49d817336)) + + +### Features + +* add commonjs output format ([#179](https://github.com/ionic-team/capacitor-plugins/issues/179)) ([8e9e098](https://github.com/ionic-team/capacitor-plugins/commit/8e9e09862064b3f6771d7facbc4008e995d9b463)) +* set targetSDK default value to 31 ([#824](https://github.com/ionic-team/capacitor-plugins/issues/824)) ([3ee10de](https://github.com/ionic-team/capacitor-plugins/commit/3ee10de98067984c1a4e75295d001c5a895c47f4)) +* set targetSDK default value to 32 ([#970](https://github.com/ionic-team/capacitor-plugins/issues/970)) ([fa70d96](https://github.com/ionic-team/capacitor-plugins/commit/fa70d96f141af751aae53ceb5642c46b204f5958)) +* SplashScreen plugin ([#149](https://github.com/ionic-team/capacitor-plugins/issues/149)) ([c5f44be](https://github.com/ionic-team/capacitor-plugins/commit/c5f44bee46d06bd9a2623cd907862633ee5331eb)) +* Upgrade gradle to 7.4 ([#826](https://github.com/ionic-team/capacitor-plugins/issues/826)) ([5db0906](https://github.com/ionic-team/capacitor-plugins/commit/5db0906f6264287c4f8e69dbaecf19d4d387824b)) +* Use java 11 ([#910](https://github.com/ionic-team/capacitor-plugins/issues/910)) ([5acb2a2](https://github.com/ionic-team/capacitor-plugins/commit/5acb2a288a413492b163e4e97da46a085d9e4be0)) +* **splash-screen:** add useDialog and layoutName options for Android ([#519](https://github.com/ionic-team/capacitor-plugins/issues/519)) ([f48733f](https://github.com/ionic-team/capacitor-plugins/commit/f48733fd42a49d718a70c2fd36d28355a64b7a88)) +* **splash-screen:** Make splash work in apps that use scenes ([#631](https://github.com/ionic-team/capacitor-plugins/issues/631)) ([cf0d214](https://github.com/ionic-team/capacitor-plugins/commit/cf0d2143c225336984a6bc8fa7ef814a18b02bd1)) +* **splash-screen:** Use Launch Storyboard for splash ([#516](https://github.com/ionic-team/capacitor-plugins/issues/516)) ([0292dab](https://github.com/ionic-team/capacitor-plugins/commit/0292dab65ac9c0f81e632eaf711b13b051f4da92)) + + + + + +## [1.2.2](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@1.2.1...@capacitor/splash-screen@1.2.2) (2022-02-10) + + +### Bug Fixes + +* **splash-screen:** avoid conditional downcast warning ([#776](https://github.com/ionic-team/capacitor-plugins/issues/776)) ([87ed912](https://github.com/ionic-team/capacitor-plugins/commit/87ed9128f43f0be498601f0ba89cadeba7a339b4)) +* **splash-screen:** Use Locale.ROOT on toLowerCase ([#813](https://github.com/ionic-team/capacitor-plugins/issues/813)) ([ecc55e1](https://github.com/ionic-team/capacitor-plugins/commit/ecc55e172acfe066977a4aac5018a55beef64f53)) + + + + + +## [1.2.1](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@1.2.0...@capacitor/splash-screen@1.2.1) (2022-01-19) + + +### Bug Fixes + +* inline source code in esm map files ([#760](https://github.com/ionic-team/capacitor-plugins/issues/760)) ([a960489](https://github.com/ionic-team/capacitor-plugins/commit/a960489a19db0182b90d187a50deff9dfbe51038)) + + + + + +# [1.2.0](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@1.1.6...@capacitor/splash-screen@1.2.0) (2021-12-08) + + +### Bug Fixes + +* **splash-screen:** pick first window when there is no key window ([#730](https://github.com/ionic-team/capacitor-plugins/issues/730)) ([0e335ad](https://github.com/ionic-team/capacitor-plugins/commit/0e335ad386da8ceadfdfaa3be982840547fc41b6)) + + +### Features + +* **splash-screen:** Make splash work in apps that use scenes ([#631](https://github.com/ionic-team/capacitor-plugins/issues/631)) ([cf0d214](https://github.com/ionic-team/capacitor-plugins/commit/cf0d2143c225336984a6bc8fa7ef814a18b02bd1)) + + + + + +## [1.1.6](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@1.1.5...@capacitor/splash-screen@1.1.6) (2021-11-03) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +## [1.1.5](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@1.1.4...@capacitor/splash-screen@1.1.5) (2021-10-14) + + +### Bug Fixes + +* remove postpublish scripts ([#656](https://github.com/ionic-team/capacitor-plugins/issues/656)) ([ed6ac49](https://github.com/ionic-team/capacitor-plugins/commit/ed6ac499ebf4a47525071ccbfc36c27503e11f60)) + + + + + +## [1.1.4](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@1.1.3...@capacitor/splash-screen@1.1.4) (2021-10-13) + + +### Bug Fixes + +* correct addListeners links ([#655](https://github.com/ionic-team/capacitor-plugins/issues/655)) ([f9871e7](https://github.com/ionic-team/capacitor-plugins/commit/f9871e7bd53478addb21155e148829f550c0e457)) + + + + + +## [1.1.3](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@1.1.2...@capacitor/splash-screen@1.1.3) (2021-09-27) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +## [1.1.2](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@1.1.1...@capacitor/splash-screen@1.1.2) (2021-09-01) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +## [1.1.1](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@1.1.0...@capacitor/splash-screen@1.1.1) (2021-08-18) + + +### Bug Fixes + +* **splash-screen:** Use configured storyboard instead of hardcoded value ([#548](https://github.com/ionic-team/capacitor-plugins/issues/548)) ([67dd67f](https://github.com/ionic-team/capacitor-plugins/commit/67dd67fc443ea5494e8482fd4346c5275a42841b)) + + + + + +# [1.1.0](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@1.0.2...@capacitor/splash-screen@1.1.0) (2021-07-21) + + +### Features + +* **splash-screen:** add useDialog and layoutName options for Android ([#519](https://github.com/ionic-team/capacitor-plugins/issues/519)) ([f48733f](https://github.com/ionic-team/capacitor-plugins/commit/f48733fd42a49d718a70c2fd36d28355a64b7a88)) +* **splash-screen:** Use Launch Storyboard for splash ([#516](https://github.com/ionic-team/capacitor-plugins/issues/516)) ([0292dab](https://github.com/ionic-team/capacitor-plugins/commit/0292dab65ac9c0f81e632eaf711b13b051f4da92)) + + + + + +## [1.0.2](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@1.0.1...@capacitor/splash-screen@1.0.2) (2021-06-23) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +## [1.0.1](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@1.0.0...@capacitor/splash-screen@1.0.1) (2021-06-09) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +# [1.0.0](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@0.3.10...@capacitor/splash-screen@1.0.0) (2021-05-19) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +## [0.3.10](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@0.3.9...@capacitor/splash-screen@0.3.10) (2021-05-11) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +## [0.3.9](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@0.3.8...@capacitor/splash-screen@0.3.9) (2021-05-10) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +## [0.3.8](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@0.3.7...@capacitor/splash-screen@0.3.8) (2021-05-07) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +## [0.3.7](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@0.3.6...@capacitor/splash-screen@0.3.7) (2021-04-29) + + +### Bug Fixes + +* **splash-screen:** launchAutoHide not working on iOS ([#319](https://github.com/ionic-team/capacitor-plugins/issues/319)) ([2a83fcb](https://github.com/ionic-team/capacitor-plugins/commit/2a83fcb536cdfc5b601f363212353201de40ca5b)) + + + + + +## [0.3.6](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@0.3.5...@capacitor/splash-screen@0.3.6) (2021-03-10) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +## [0.3.5](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@0.3.4...@capacitor/splash-screen@0.3.5) (2021-03-02) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +## [0.3.4](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@0.3.3...@capacitor/splash-screen@0.3.4) (2021-02-27) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +## [0.3.3](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@0.3.2...@capacitor/splash-screen@0.3.3) (2021-02-10) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +## [0.3.2](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@0.3.1...@capacitor/splash-screen@0.3.2) (2021-02-05) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +## [0.3.1](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@0.3.0...@capacitor/splash-screen@0.3.1) (2021-01-26) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +# [0.3.0](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@0.2.0...@capacitor/splash-screen@0.3.0) (2021-01-14) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +# [0.2.0](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@0.1.3...@capacitor/splash-screen@0.2.0) (2021-01-13) + + +### Bug Fixes + +* add es2017 lib to tsconfig ([#180](https://github.com/ionic-team/capacitor-plugins/issues/180)) ([2c3776c](https://github.com/ionic-team/capacitor-plugins/commit/2c3776c38ca025c5ee965dec10ccf1cdb6c02e2f)) +* export all TS definitions ([6cd2996](https://github.com/ionic-team/capacitor-plugins/commit/6cd299660fdeb27382ec7f45f0b3a55224cd0ad1)) + + +### Features + +* add commonjs output format ([#179](https://github.com/ionic-team/capacitor-plugins/issues/179)) ([8e9e098](https://github.com/ionic-team/capacitor-plugins/commit/8e9e09862064b3f6771d7facbc4008e995d9b463)) + + + + + +## [0.1.3](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@0.1.2...@capacitor/splash-screen@0.1.3) (2021-01-13) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +## [0.1.2](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@0.1.1...@capacitor/splash-screen@0.1.2) (2021-01-08) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +## [0.1.1](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/splash-screen@0.1.0...@capacitor/splash-screen@0.1.1) (2020-12-27) + +**Note:** Version bump only for package @capacitor/splash-screen + + + + + +# 0.1.0 (2020-12-20) + + +### Bug Fixes + +* support deprecated types from Capacitor 2 ([#139](https://github.com/ionic-team/capacitor-plugins/issues/139)) ([2d7127a](https://github.com/ionic-team/capacitor-plugins/commit/2d7127a488e26f0287951921a6db47c49d817336)) + + +### Features + +* SplashScreen plugin ([#149](https://github.com/ionic-team/capacitor-plugins/issues/149)) ([c5f44be](https://github.com/ionic-team/capacitor-plugins/commit/c5f44bee46d06bd9a2623cd907862633ee5331eb)) diff --git a/mobile/plugins/capacitor-splash-screen/CapacitorSplashScreen.podspec b/mobile/plugins/capacitor-splash-screen/CapacitorSplashScreen.podspec new file mode 100644 index 00000000..a86dda7f --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/CapacitorSplashScreen.podspec @@ -0,0 +1,17 @@ +require 'json' + +package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) + +Pod::Spec.new do |s| + s.name = 'CapacitorSplashScreen' + s.version = package['version'] + s.summary = package['description'] + s.license = package['license'] + s.homepage = 'https://capacitorjs.com' + s.author = package['author'] + s.source = { :git => 'https://github.com/ionic-team/capacitor-plugins.git', :tag => package['name'] + '@' + package['version'] } + s.source_files = 'ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}', 'splash-screen/ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}' + s.ios.deployment_target = '13.0' + s.dependency 'Capacitor' + s.swift_version = '5.1' +end diff --git a/mobile/plugins/capacitor-splash-screen/LICENSE b/mobile/plugins/capacitor-splash-screen/LICENSE new file mode 100644 index 00000000..6652495c --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/LICENSE @@ -0,0 +1,23 @@ +Copyright 2020-present Ionic +https://ionic.io + +MIT License + +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/mobile/plugins/capacitor-splash-screen/README.md b/mobile/plugins/capacitor-splash-screen/README.md new file mode 100644 index 00000000..ebad66dc --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/README.md @@ -0,0 +1,262 @@ +# @capacitor/splash-screen + +The Splash Screen API provides methods for showing or hiding a Splash image. + +## Install + +```bash +npm install @capacitor/splash-screen +npx cap sync +``` + +### Android 12 Splash Screen API + +_**This only affects the launch splash screen and is not used when utilizing the programmatic `show()` method.**_ + +Capacitor 4 uses the **[Android 12 Splash Screen API](https://developer.android.com/guide/topics/ui/splash-screen)** and the `androidx.core:core-splashscreen` compatibility library to make it work on Android 11 and below. + +The compatibility library can be disabled by changing the parent of `AppTheme.NoActionBarLaunch` from `Theme.SplashScreen` to `AppTheme.NoActionBar` in `android/app/src/main/res/values/styles.xml`. +The Android 12 Splash Screen API can't be disabled on Android 12+ as it's part of the Android OS. + +```xml + +``` + +**NOTE**: On Android 12 and Android 12L devices the Splash Screen image is not showing when launched from third party launchers such as Nova Launcher, MIUI, Realme Launcher, OPPO Launcher, etc., from app info in Settings App, or from IDEs such as Android Studio. +**[Google Issue Tracker](https://issuetracker.google.com/issues/205021357)** +**[Google Issue Tracker](https://issuetracker.google.com/issues/207386164)** +Google have fixed those problems on Android 13 but they won't be backport the fixes to Android 12 and Android 12L. +Launcher related issues might get fixed by a launcher update. +If you still find issues related to the Splash Screen on Android 13, please, report them to [Google](https://issuetracker.google.com/). + +## Example + +```typescript +import { SplashScreen } from '@capacitor/splash-screen'; + +// Hide the splash (you should do this on app launch) +await SplashScreen.hide(); + +// Show the splash for an indefinite amount of time: +await SplashScreen.show({ + autoHide: false, +}); + +// Show the splash for two seconds and then automatically hide it: +await SplashScreen.show({ + showDuration: 2000, + autoHide: true, +}); +``` + +## Hiding the Splash Screen + +By default, the Splash Screen is set to automatically hide after 500 ms. + +If you want to be sure the splash screen never disappears before your app is ready, set `launchAutoHide` to `false`; the splash screen will then stay visible until manually hidden. For the best user experience, your app should call `hide()` as soon as possible. + +If, instead, you want to show the splash screen for a fixed amount of time, set `launchShowDuration` in your [Capacitor configuration file](https://capacitorjs.com/docs/config). + +## Background Color + +In certain conditions, especially if the splash screen does not fully cover the device screen, it might happen that the app screen is visible around the corners (due to transparency). Instead of showing a transparent color, you can set a `backgroundColor` to cover those areas. + +Possible values for `backgroundColor` are either `#RRGGBB` or `#RRGGBBAA`. + +## Spinner + +If you want to show a spinner on top of the splash screen, set `showSpinner` to `true` in your [Capacitor configuration file](https://capacitorjs.com/docs/config). + +You can customize the appearance of the spinner with the following configuration. + +For Android, `androidSpinnerStyle` has the following options: + +- `horizontal` +- `small` +- `large` (default) +- `inverse` +- `smallInverse` +- `largeInverse` + +For iOS, `iosSpinnerStyle` has the following options: + +- `large` (default) +- `small` + +To set the color of the spinner use `spinnerColor`, values are either `#RRGGBB` or `#RRGGBBAA`. + +## Configuration + + + + +These config values are available: + +| Prop | Type | Description | Default | Since | +| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ----- | +| **`launchShowDuration`** | number | How long to show the launch splash screen when autoHide is enabled (in ms) | 500 | 1.0.0 | +| **`launchAutoHide`** | boolean | Whether to auto hide the splash after launchShowDuration. | true | 1.0.0 | +| **`launchFadeOutDuration`** | number | Duration for the fade out animation of the launch splash screen (in ms) Only available for Android, when using the Android 12 Splash Screen API. | 200 | 4.2.0 | +| **`backgroundColor`** | string | Color of the background of the Splash Screen in hex format, #RRGGBB or #RRGGBBAA. Doesn't work if `useDialog` is true or on launch when using the Android 12 API. | | 1.0.0 | +| **`androidSplashResourceName`** | string | Name of the resource to be used as Splash Screen. Doesn't work on launch when using the Android 12 API. Only available on Android. | splash | 1.0.0 | +| **`androidScaleType`** | 'CENTER' \| 'CENTER_CROP' \| 'CENTER_INSIDE' \| 'FIT_CENTER' \| 'FIT_END' \| 'FIT_START' \| 'FIT_XY' \| 'MATRIX' | The [ImageView.ScaleType](https://developer.android.com/reference/android/widget/ImageView.ScaleType) used to scale the Splash Screen image. Doesn't work if `useDialog` is true or on launch when using the Android 12 API. Only available on Android. | FIT_XY | 1.0.0 | +| **`showSpinner`** | boolean | Show a loading spinner on the Splash Screen. Doesn't work if `useDialog` is true or on launch when using the Android 12 API. | | 1.0.0 | +| **`androidSpinnerStyle`** | 'horizontal' \| 'small' \| 'large' \| 'inverse' \| 'smallInverse' \| 'largeInverse' | Style of the Android spinner. Doesn't work if `useDialog` is true or on launch when using the Android 12 API. | large | 1.0.0 | +| **`iosSpinnerStyle`** | 'small' \| 'large' | Style of the iOS spinner. Doesn't work if `useDialog` is true. Only available on iOS. | large | 1.0.0 | +| **`spinnerColor`** | string | Color of the spinner in hex format, #RRGGBB or #RRGGBBAA. Doesn't work if `useDialog` is true or on launch when using the Android 12 API. | | 1.0.0 | +| **`splashFullScreen`** | boolean | Hide the status bar on the Splash Screen. Doesn't work on launch when using the Android 12 API. Only available on Android. | | 1.0.0 | +| **`splashImmersive`** | boolean | Hide the status bar and the software navigation buttons on the Splash Screen. Doesn't work on launch when using the Android 12 API. Only available on Android. | | 1.0.0 | +| **`layoutName`** | string | If `useDialog` is set to true, configure the Dialog layout. If `useDialog` is not set or false, use a layout instead of the ImageView. Doesn't work on launch when using the Android 12 API. Only available on Android. | | 1.1.0 | +| **`useDialog`** | boolean | Use a Dialog instead of an ImageView. If `layoutName` is not configured, it will use a layout that uses the splash image as background. Doesn't work on launch when using the Android 12 API. Only available on Android. | | 1.1.0 | + +### Examples + +In `capacitor.config.json`: + +```json +{ + "plugins": { + "SplashScreen": { + "launchShowDuration": 3000, + "launchAutoHide": true, + "launchFadeOutDuration": 3000, + "backgroundColor": "#ffffffff", + "androidSplashResourceName": "splash", + "androidScaleType": "CENTER_CROP", + "showSpinner": true, + "androidSpinnerStyle": "large", + "iosSpinnerStyle": "small", + "spinnerColor": "#999999", + "splashFullScreen": true, + "splashImmersive": true, + "layoutName": "launch_screen", + "useDialog": true + } + } +} +``` + +In `capacitor.config.ts`: + +```ts +/// + +import { CapacitorConfig } from '@capacitor/cli'; + +const config: CapacitorConfig = { + plugins: { + SplashScreen: { + launchShowDuration: 3000, + launchAutoHide: true, + launchFadeOutDuration: 3000, + backgroundColor: "#ffffffff", + androidSplashResourceName: "splash", + androidScaleType: "CENTER_CROP", + showSpinner: true, + androidSpinnerStyle: "large", + iosSpinnerStyle: "small", + spinnerColor: "#999999", + splashFullScreen: true, + splashImmersive: true, + layoutName: "launch_screen", + useDialog: true, + }, + }, +}; + +export default config; +``` + + + +### Android + +To use splash screen images named something other than `splash.png`, set `androidSplashResourceName` to the new resource name. Additionally, in `android/app/src/main/res/values/styles.xml`, change the resource name in the following block: + +```xml + +``` + +### Variables + +This plugin will use the following project variables (defined in your app's `variables.gradle` file): + +- `coreSplashScreenVersion` version of `androidx.core:core-splashscreen` (default: `1.0.1`) + +## Example Guides + +[Adding Your Own Icons and Splash Screen Images ›](https://www.joshmorony.com/adding-icons-splash-screens-launch-images-to-capacitor-projects/) + +[Creating a Dynamic/Adaptable Splash Screen for Capacitor (Android) ›](https://www.joshmorony.com/creating-a-dynamic-universal-splash-screen-for-capacitor-android/) + +## API + + + +* [`show(...)`](#show) +* [`hide(...)`](#hide) +* [Interfaces](#interfaces) + + + + + + +### show(...) + +```typescript +show(options?: ShowOptions | undefined) => Promise +``` + +Show the splash screen + +| Param | Type | +| ------------- | --------------------------------------------------- | +| **`options`** | ShowOptions | + +**Since:** 1.0.0 + +-------------------- + + +### hide(...) + +```typescript +hide(options?: HideOptions | undefined) => Promise +``` + +Hide the splash screen + +| Param | Type | +| ------------- | --------------------------------------------------- | +| **`options`** | HideOptions | + +**Since:** 1.0.0 + +-------------------- + + +### Interfaces + + +#### ShowOptions + +| Prop | Type | Description | Default | Since | +| --------------------- | -------------------- | ------------------------------------------------------------------- | ----------------- | ----- | +| **`autoHide`** | boolean | Whether to auto hide the splash after showDuration | | 1.0.0 | +| **`fadeInDuration`** | number | How long (in ms) to fade in. | 200 | 1.0.0 | +| **`fadeOutDuration`** | number | How long (in ms) to fade out. | 200 | 1.0.0 | +| **`showDuration`** | number | How long to show the splash screen when autoHide is enabled (in ms) | 3000 | 1.0.0 | + + +#### HideOptions + +| Prop | Type | Description | Default | Since | +| --------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ----- | +| **`fadeOutDuration`** | number | How long (in ms) to fade out. On Android, if using the Android 12 Splash Screen API, it's not being used. Use launchFadeOutDuration configuration option instead. | 200 | 1.0.0 | + + diff --git a/mobile/plugins/capacitor-splash-screen/android/.gitignore b/mobile/plugins/capacitor-splash-screen/android/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/android/.gitignore @@ -0,0 +1 @@ +/build diff --git a/mobile/plugins/capacitor-splash-screen/android/build.gradle b/mobile/plugins/capacitor-splash-screen/android/build.gradle new file mode 100644 index 00000000..91ea7a34 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/android/build.gradle @@ -0,0 +1,81 @@ +ext { + capacitorVersion = System.getenv('CAPACITOR_VERSION') + junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2' + androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.6.1' + androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.1.5' + androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.5.1' + coreSplashScreenVersion = project.hasProperty('coreSplashScreenVersion') ? rootProject.ext.coreSplashScreenVersion : '1.0.1' +} + +buildscript { + repositories { + google() + mavenCentral() + maven { + url "https://plugins.gradle.org/m2/" + } + } + dependencies { + classpath 'com.android.tools.build:gradle:8.1.1' + if (System.getenv("CAP_PLUGIN_PUBLISH") == "true") { + classpath 'io.github.gradle-nexus:publish-plugin:1.3.0' + } + } +} + +apply plugin: 'com.android.library' +if (System.getenv("CAP_PLUGIN_PUBLISH") == "true") { + apply plugin: 'io.github.gradle-nexus.publish-plugin' + apply from: file('../../scripts/android/publish-root.gradle') + apply from: file('../../scripts/android/publish-module.gradle') +} + +android { + namespace "com.capacitorjs.plugins.splashscreen" + compileSdkVersion project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 34 + defaultConfig { + minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 22 + targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 34 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + lintOptions { + abortOnError false + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + publishing { + singleVariant("release") + } +} + +repositories { + google() + mavenCentral() +} + + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + if (System.getenv("CAP_PLUGIN_PUBLISH") == "true") { + implementation "com.capacitorjs:core:$capacitorVersion" + } else { + implementation project(':capacitor-android') + } + + implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" + testImplementation "junit:junit:$junitVersion" + androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" + implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" +} diff --git a/mobile/plugins/capacitor-splash-screen/android/gradle.properties b/mobile/plugins/capacitor-splash-screen/android/gradle.properties new file mode 100644 index 00000000..2e87c52f --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/android/gradle.properties @@ -0,0 +1,22 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true diff --git a/mobile/plugins/capacitor-splash-screen/android/gradle/wrapper/gradle-wrapper.jar b/mobile/plugins/capacitor-splash-screen/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..c1962a79 Binary files /dev/null and b/mobile/plugins/capacitor-splash-screen/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/mobile/plugins/capacitor-splash-screen/android/gradle/wrapper/gradle-wrapper.properties b/mobile/plugins/capacitor-splash-screen/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..8707e8b5 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/mobile/plugins/capacitor-splash-screen/android/gradlew b/mobile/plugins/capacitor-splash-screen/android/gradlew new file mode 100755 index 00000000..aeb74cbb --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/android/gradlew @@ -0,0 +1,245 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/mobile/plugins/capacitor-splash-screen/android/gradlew.bat b/mobile/plugins/capacitor-splash-screen/android/gradlew.bat new file mode 100644 index 00000000..93e3f59f --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/android/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mobile/plugins/capacitor-splash-screen/android/proguard-rules.pro b/mobile/plugins/capacitor-splash-screen/android/proguard-rules.pro new file mode 100644 index 00000000..f1b42451 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/android/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/mobile/plugins/capacitor-splash-screen/android/settings.gradle b/mobile/plugins/capacitor-splash-screen/android/settings.gradle new file mode 100644 index 00000000..1e5b8431 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/android/settings.gradle @@ -0,0 +1,2 @@ +include ':capacitor-android' +project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') \ No newline at end of file diff --git a/mobile/plugins/capacitor-splash-screen/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java b/mobile/plugins/capacitor-splash-screen/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java new file mode 100644 index 00000000..58020e16 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.getcapacitor.android; + +import static org.junit.Assert.*; + +import android.content.Context; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("com.getcapacitor.android", appContext.getPackageName()); + } +} diff --git a/mobile/plugins/capacitor-splash-screen/android/src/main/AndroidManifest.xml b/mobile/plugins/capacitor-splash-screen/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a2f47b60 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/android/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/mobile/plugins/capacitor-splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashListener.java b/mobile/plugins/capacitor-splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashListener.java new file mode 100644 index 00000000..9271b679 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashListener.java @@ -0,0 +1,6 @@ +package com.capacitorjs.plugins.splashscreen; + +public interface SplashListener { + void completed(); + void error(); +} diff --git a/mobile/plugins/capacitor-splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashScreen.java b/mobile/plugins/capacitor-splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashScreen.java new file mode 100644 index 00000000..d2e14848 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashScreen.java @@ -0,0 +1,694 @@ +package com.capacitorjs.plugins.splashscreen; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.graphics.PixelFormat; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.os.Build; +import android.os.Handler; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.ViewTreeObserver.OnPreDrawListener; +import android.view.Window; +import android.view.WindowInsetsController; +import android.view.WindowManager; +import android.view.animation.LinearInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.view.WindowCompat; +import androidx.core.view.WindowInsetsCompat; +import com.getcapacitor.Logger; + +/** + * A Splash Screen service for showing and hiding a splash screen in the app. + */ +public class SplashScreen { + + private Dialog dialog; + private View splashImage; + private ProgressBar spinnerBar; + private WindowManager windowManager; + private boolean isVisible = false; + private boolean isHiding = false; + private Context context; + private View content; + private SplashScreenConfig config; + private OnPreDrawListener onPreDrawListener; + + SplashScreen(Context context, SplashScreenConfig config) { + this.context = context; + this.config = config; + } + + /** + * Show the splash screen on launch without fading in + * + * @param activity + */ + public void showOnLaunch(final AppCompatActivity activity) { + if (config.getLaunchShowDuration() == 0) { + return; + } + SplashScreenSettings settings = new SplashScreenSettings(); + settings.setShowDuration(config.getLaunchShowDuration()); + settings.setAutoHide(config.isLaunchAutoHide()); + + // Method can fail if styles are incorrectly set... + // If it fails, log error & fallback to old method + try { + showWithAndroid12API(activity, settings); + return; + } catch (Exception e) { + Logger.warn("Android 12 Splash API failed... using previous method."); + this.onPreDrawListener = null; + } + + settings.setFadeInDuration(config.getLaunchFadeInDuration()); + if (config.isUsingDialog()) { + showDialog(activity, settings, null, true); + } else { + show(activity, settings, null, true); + } + } + + /** + * Show the Splash Screen using the Android 12 API (31+) + * Uses Compat Library for backwards compatibility + * + * @param activity + * @param settings Settings used to show the Splash Screen + */ + private void showWithAndroid12API(final AppCompatActivity activity, final SplashScreenSettings settings) { + if (activity == null || activity.isFinishing()) return; + + activity.runOnUiThread( + () -> { + androidx.core.splashscreen.SplashScreen windowSplashScreen = androidx.core.splashscreen.SplashScreen.installSplashScreen( + activity + ); + windowSplashScreen.setKeepOnScreenCondition(() -> isVisible || isHiding); + + if (config.getLaunchFadeOutDuration() > 0) { + // Set Fade Out Animation + windowSplashScreen.setOnExitAnimationListener( + windowSplashScreenView -> { + final ObjectAnimator fadeAnimator = ObjectAnimator.ofFloat( + windowSplashScreenView.getView(), + View.ALPHA, + 1f, + 0f + ); + fadeAnimator.setInterpolator(new LinearInterpolator()); + fadeAnimator.setDuration(config.getLaunchFadeOutDuration()); + + fadeAnimator.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + isHiding = false; + windowSplashScreenView.remove(); + } + } + ); + + fadeAnimator.start(); + + isHiding = true; + isVisible = false; + } + ); + } else { + windowSplashScreen.setOnExitAnimationListener( + windowSplashScreenView -> { + isHiding = false; + isVisible = false; + windowSplashScreenView.remove(); + } + ); + } + + // Set Pre Draw Listener & Delay Drawing Until Duration Elapses + content = activity.findViewById(android.R.id.content); + + this.onPreDrawListener = + new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + // Start Timer On First Run + if (!isVisible && !isHiding) { + isVisible = true; + + new Handler(context.getMainLooper()) + .postDelayed( + () -> { + // Splash screen is done... start drawing content. + if (settings.isAutoHide()) { + isVisible = false; + onPreDrawListener = null; + content.getViewTreeObserver().removeOnPreDrawListener(this); + } + }, + settings.getShowDuration() + ); + } + + // Not ready to dismiss splash screen + return false; + } + }; + + content.getViewTreeObserver().addOnPreDrawListener(this.onPreDrawListener); + } + ); + } + + /** + * Show the Splash Screen + * + * @param activity + * @param settings Settings used to show the Splash Screen + * @param splashListener A listener to handle the finish of the animation (if any) + */ + public void show(final AppCompatActivity activity, final SplashScreenSettings settings, final SplashListener splashListener) { + if (config.isUsingDialog()) { + showDialog(activity, settings, splashListener, false); + } else { + show(activity, settings, splashListener, false); + } + } + + private void showDialog( + final AppCompatActivity activity, + final SplashScreenSettings settings, + final SplashListener splashListener, + final boolean isLaunchSplash + ) { + if (activity == null || activity.isFinishing()) return; + + if (isVisible) { + splashListener.completed(); + return; + } + + activity.runOnUiThread( + () -> { + if (config.isImmersive()) { + dialog = new Dialog(activity, R.style.capacitor_immersive_style); + } else if (config.isFullScreen()) { + dialog = new Dialog(activity, R.style.capacitor_full_screen_style); + } else { + dialog = new Dialog(activity, R.style.capacitor_default_style); + } + int splashId = 0; + if (config.getLayoutName() != null) { + splashId = context.getResources().getIdentifier(config.getLayoutName(), "layout", context.getPackageName()); + if (splashId == 0) { + Logger.warn("Layout not found, using default"); + } + } + if (splashId != 0) { + dialog.setContentView(splashId); + } else { + Drawable splash = getSplashDrawable(); + LinearLayout parent = new LinearLayout(context); + parent.setLayoutParams( + new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT) + ); + parent.setOrientation(LinearLayout.VERTICAL); + if (splash != null) { + parent.setBackground(splash); + } + dialog.setContentView(parent); + } + + dialog.setCancelable(false); + if (!dialog.isShowing()) { + dialog.show(); + } + isVisible = true; + + if (settings.isAutoHide()) { + new Handler(context.getMainLooper()) + .postDelayed( + () -> { + hideDialog(activity, isLaunchSplash); + + if (splashListener != null) { + splashListener.completed(); + } + }, + settings.getShowDuration() + ); + } else { + // If no autoHide, call complete + if (splashListener != null) { + splashListener.completed(); + } + } + } + ); + } + + /** + * Hide the Splash Screen + * + * @param settings Settings used to hide the Splash Screen + */ + public void hide(SplashScreenSettings settings) { + hide(settings.getFadeOutDuration(), false); + } + + /** + * Hide the Splash Screen when showing it as a dialog + * + * @param activity the activity showing the dialog + */ + public void hideDialog(final AppCompatActivity activity) { + hideDialog(activity, false); + } + + public void onPause() { + tearDown(true); + } + + public void onDestroy() { + tearDown(true); + } + + private void buildViews() { + if (splashImage == null) { + int splashId = 0; + Drawable splash; + + if (config.getLayoutName() != null) { + splashId = context.getResources().getIdentifier(config.getLayoutName(), "layout", context.getPackageName()); + if (splashId == 0) { + Logger.warn("Layout not found, defaulting to ImageView"); + } + } + + if (splashId != 0) { + Activity activity = (Activity) context; + LayoutInflater inflator = activity.getLayoutInflater(); + ViewGroup root = new FrameLayout(context); + root.setLayoutParams( + new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + ); + splashImage = inflator.inflate(splashId, root, false); + } else { + splash = getSplashDrawable(); + if (splash != null) { + if (splash instanceof Animatable) { + ((Animatable) splash).start(); + } + + if (splash instanceof LayerDrawable) { + LayerDrawable layeredSplash = (LayerDrawable) splash; + + for (int i = 0; i < layeredSplash.getNumberOfLayers(); i++) { + Drawable layerDrawable = layeredSplash.getDrawable(i); + + if (layerDrawable instanceof Animatable) { + ((Animatable) layerDrawable).start(); + } + } + } + + splashImage = new ImageView(context); + // Stops flickers dead in their tracks + // https://stackoverflow.com/a/21847579/32140 + ImageView imageView = (ImageView) splashImage; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + imageView.setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } else { + legacyStopFlickers(imageView); + } + imageView.setScaleType(config.getScaleType()); + imageView.setImageDrawable(splash); + } else { + return; + } + } + + splashImage.setFitsSystemWindows(true); + + if (config.getBackgroundColor() != null) { + splashImage.setBackgroundColor(config.getBackgroundColor()); + } + } + + if (spinnerBar == null) { + if (config.getSpinnerStyle() != null) { + int spinnerBarStyle = config.getSpinnerStyle(); + spinnerBar = new ProgressBar(context, null, spinnerBarStyle); + } else { + spinnerBar = new ProgressBar(context); + } + spinnerBar.setIndeterminate(true); + + Integer spinnerBarColor = config.getSpinnerColor(); + if (spinnerBarColor != null) { + int[][] states = new int[][] { + new int[] { android.R.attr.state_enabled }, // enabled + new int[] { -android.R.attr.state_enabled }, // disabled + new int[] { -android.R.attr.state_checked }, // unchecked + new int[] { android.R.attr.state_pressed } // pressed + }; + int[] colors = new int[] { spinnerBarColor, spinnerBarColor, spinnerBarColor, spinnerBarColor }; + ColorStateList colorStateList = new ColorStateList(states, colors); + spinnerBar.setIndeterminateTintList(colorStateList); + } + } + } + + @SuppressWarnings("deprecation") + private void legacyStopFlickers(ImageView imageView) { + imageView.setDrawingCacheEnabled(true); + } + + private Drawable getSplashDrawable() { + int splashId = context.getResources().getIdentifier(config.getResourceName(), "drawable", context.getPackageName()); + try { + Drawable drawable = context.getResources().getDrawable(splashId, context.getTheme()); + return drawable; + } catch (Resources.NotFoundException ex) { + Logger.warn("No splash screen found, not displaying"); + return null; + } + } + + private void show( + final AppCompatActivity activity, + final SplashScreenSettings settings, + final SplashListener splashListener, + final boolean isLaunchSplash + ) { + windowManager = (WindowManager) activity.getSystemService(Context.WINDOW_SERVICE); + + if (activity.isFinishing()) { + return; + } + + buildViews(); + + if (isVisible) { + splashListener.completed(); + return; + } + + final Animator.AnimatorListener listener = new Animator.AnimatorListener() { + @Override + public void onAnimationEnd(Animator animator) { + isVisible = true; + + if (settings.isAutoHide()) { + new Handler(context.getMainLooper()) + .postDelayed( + () -> { + hide(settings.getFadeOutDuration(), isLaunchSplash); + + if (splashListener != null) { + splashListener.completed(); + } + }, + settings.getShowDuration() + ); + } else { + // If no autoHide, call complete + if (splashListener != null) { + splashListener.completed(); + } + } + } + + @Override + public void onAnimationCancel(Animator animator) {} + + @Override + public void onAnimationRepeat(Animator animator) {} + + @Override + public void onAnimationStart(Animator animator) {} + }; + + Handler mainHandler = new Handler(context.getMainLooper()); + + mainHandler.post( + () -> { + WindowManager.LayoutParams params = new WindowManager.LayoutParams(); + params.gravity = Gravity.CENTER; + params.flags = activity.getWindow().getAttributes().flags; + + // Required to enable the view to actually fade + params.format = PixelFormat.TRANSLUCENT; + + try { + windowManager.addView(splashImage, params); + } catch (IllegalStateException | IllegalArgumentException ex) { + Logger.debug("Could not add splash view"); + return; + } + + if (config.isImmersive()) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + activity.runOnUiThread( + () -> { + Window window = activity.getWindow(); + WindowCompat.setDecorFitsSystemWindows(window, false); + WindowInsetsController controller = splashImage.getWindowInsetsController(); + controller.hide(WindowInsetsCompat.Type.systemBars()); + controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + } + ); + } else { + legacyImmersive(); + } + } else if (config.isFullScreen()) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + activity.runOnUiThread( + () -> { + Window window = activity.getWindow(); + WindowCompat.setDecorFitsSystemWindows(window, false); + WindowInsetsController controller = splashImage.getWindowInsetsController(); + controller.hide(WindowInsetsCompat.Type.statusBars()); + } + ); + } else { + legacyFullscreen(); + } + } + + splashImage.setAlpha(0f); + + splashImage + .animate() + .alpha(1f) + .setInterpolator(new LinearInterpolator()) + .setDuration(settings.getFadeInDuration()) + .setListener(listener) + .start(); + + splashImage.setVisibility(View.VISIBLE); + + if (spinnerBar != null) { + spinnerBar.setVisibility(View.INVISIBLE); + + if (spinnerBar.getParent() != null) { + windowManager.removeView(spinnerBar); + } + + params.height = WindowManager.LayoutParams.WRAP_CONTENT; + params.width = WindowManager.LayoutParams.WRAP_CONTENT; + + windowManager.addView(spinnerBar, params); + + if (config.isShowSpinner()) { + spinnerBar.setAlpha(0f); + + spinnerBar + .animate() + .alpha(1f) + .setInterpolator(new LinearInterpolator()) + .setDuration(settings.getFadeInDuration()) + .start(); + + spinnerBar.setVisibility(View.VISIBLE); + } + } + } + ); + } + + @SuppressWarnings("deprecation") + private void legacyImmersive() { + final int flags = + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_FULLSCREEN | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + splashImage.setSystemUiVisibility(flags); + } + + @SuppressWarnings("deprecation") + private void legacyFullscreen() { + splashImage.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN); + } + + private void hide(final int fadeOutDuration, boolean isLaunchSplash) { + // Warn the user if the splash was hidden automatically, which means they could be experiencing an app + // that feels slower than it actually is. + if (isLaunchSplash && isVisible) { + Logger.debug( + "SplashScreen was automatically hidden after the launch timeout. " + + "You should call `SplashScreen.hide()` as soon as your web app is loaded (or increase the timeout)." + + "Read more at https://capacitorjs.com/docs/apis/splash-screen#hiding-the-splash-screen" + ); + } + + if (isHiding) { + return; + } + + // Hide with Android 12 API + if (null != this.onPreDrawListener) { + if (fadeOutDuration != 200) { + Logger.warn( + "fadeOutDuration parameter doesn't work on initial splash screen, use launchFadeOutDuration configuration option" + ); + } + this.isVisible = false; + if (null != content) { + content.getViewTreeObserver().removeOnPreDrawListener(this.onPreDrawListener); + } + this.onPreDrawListener = null; + return; + } + + if (splashImage == null || splashImage.getParent() == null) { + return; + } + + isHiding = true; + + final Animator.AnimatorListener listener = new Animator.AnimatorListener() { + @Override + public void onAnimationEnd(Animator animator) { + tearDown(false); + } + + @Override + public void onAnimationCancel(Animator animator) { + tearDown(false); + } + + @Override + public void onAnimationStart(Animator animator) {} + + @Override + public void onAnimationRepeat(Animator animator) {} + }; + + Handler mainHandler = new Handler(context.getMainLooper()); + + mainHandler.post( + () -> { + if (spinnerBar != null) { + spinnerBar.setAlpha(1f); + + spinnerBar.animate().alpha(0).setInterpolator(new LinearInterpolator()).setDuration(fadeOutDuration).start(); + } + + splashImage.setAlpha(1f); + + splashImage + .animate() + .alpha(0) + .setInterpolator(new LinearInterpolator()) + .setDuration(fadeOutDuration) + .setListener(listener) + .start(); + } + ); + } + + private void hideDialog(final AppCompatActivity activity, boolean isLaunchSplash) { + // Warn the user if the splash was hidden automatically, which means they could be experiencing an app + // that feels slower than it actually is. + if (isLaunchSplash && isVisible) { + Logger.debug( + "SplashScreen was automatically hidden after the launch timeout. " + + "You should call `SplashScreen.hide()` as soon as your web app is loaded (or increase the timeout)." + + "Read more at https://capacitorjs.com/docs/apis/splash-screen#hiding-the-splash-screen" + ); + } + + if (isHiding) { + return; + } + + // Hide with Android 12 API + if (null != this.onPreDrawListener) { + this.isVisible = false; + if (null != content) { + content.getViewTreeObserver().removeOnPreDrawListener(this.onPreDrawListener); + } + this.onPreDrawListener = null; + return; + } + + isHiding = true; + + activity.runOnUiThread( + () -> { + if (dialog != null && dialog.isShowing()) { + if (!activity.isFinishing() && !activity.isDestroyed()) { + dialog.dismiss(); + } + dialog = null; + isHiding = false; + isVisible = false; + } + } + ); + } + + private void tearDown(boolean removeSpinner) { + if (spinnerBar != null && spinnerBar.getParent() != null) { + spinnerBar.setVisibility(View.INVISIBLE); + + if (removeSpinner) { + windowManager.removeView(spinnerBar); + } + } + + if (splashImage != null && splashImage.getParent() != null) { + splashImage.setVisibility(View.INVISIBLE); + + windowManager.removeView(splashImage); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && config.isFullScreen() || config.isImmersive()) { + // Exit fullscreen mode + Window window = ((Activity) context).getWindow(); + WindowCompat.setDecorFitsSystemWindows(window, true); + } + isHiding = false; + isVisible = false; + } +} diff --git a/mobile/plugins/capacitor-splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashScreenConfig.java b/mobile/plugins/capacitor-splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashScreenConfig.java new file mode 100644 index 00000000..a8dd69dd --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashScreenConfig.java @@ -0,0 +1,129 @@ +package com.capacitorjs.plugins.splashscreen; + +import android.widget.ImageView.ScaleType; + +public class SplashScreenConfig { + + private Integer backgroundColor; + private Integer spinnerStyle; + private Integer spinnerColor; + private boolean showSpinner = false; + private Integer launchShowDuration = 500; + private boolean launchAutoHide = true; + private Integer launchFadeInDuration = 0; + private Integer launchFadeOutDuration = 200; + private String resourceName = "splash"; + private boolean immersive = false; + private boolean fullScreen = false; + private ScaleType scaleType = ScaleType.FIT_XY; + private boolean usingDialog = false; + private String layoutName; + + public Integer getBackgroundColor() { + return backgroundColor; + } + + public void setBackgroundColor(Integer backgroundColor) { + this.backgroundColor = backgroundColor; + } + + public Integer getSpinnerStyle() { + return spinnerStyle; + } + + public void setSpinnerStyle(Integer spinnerStyle) { + this.spinnerStyle = spinnerStyle; + } + + public Integer getSpinnerColor() { + return spinnerColor; + } + + public void setSpinnerColor(Integer spinnerColor) { + this.spinnerColor = spinnerColor; + } + + public boolean isShowSpinner() { + return showSpinner; + } + + public void setShowSpinner(boolean showSpinner) { + this.showSpinner = showSpinner; + } + + public Integer getLaunchShowDuration() { + return launchShowDuration; + } + + public void setLaunchShowDuration(Integer launchShowDuration) { + this.launchShowDuration = launchShowDuration; + } + + public boolean isLaunchAutoHide() { + return launchAutoHide; + } + + public void setLaunchAutoHide(boolean launchAutoHide) { + this.launchAutoHide = launchAutoHide; + } + + public Integer getLaunchFadeInDuration() { + return launchFadeInDuration; + } + + public String getResourceName() { + return resourceName; + } + + public void setResourceName(String resourceName) { + this.resourceName = resourceName; + } + + public boolean isImmersive() { + return immersive; + } + + public void setImmersive(boolean immersive) { + this.immersive = immersive; + } + + public boolean isFullScreen() { + return fullScreen; + } + + public void setFullScreen(boolean fullScreen) { + this.fullScreen = fullScreen; + } + + public ScaleType getScaleType() { + return scaleType; + } + + public void setScaleType(ScaleType scaleType) { + this.scaleType = scaleType; + } + + public boolean isUsingDialog() { + return usingDialog; + } + + public void setUsingDialog(boolean usingDialog) { + this.usingDialog = usingDialog; + } + + public String getLayoutName() { + return layoutName; + } + + public void setLayoutName(String layoutName) { + this.layoutName = layoutName; + } + + public Integer getLaunchFadeOutDuration() { + return launchFadeOutDuration; + } + + public void setLaunchFadeOutDuration(Integer launchFadeOutDuration) { + this.launchFadeOutDuration = launchFadeOutDuration; + } +} diff --git a/mobile/plugins/capacitor-splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashScreenPlugin.java b/mobile/plugins/capacitor-splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashScreenPlugin.java new file mode 100644 index 00000000..7d803501 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashScreenPlugin.java @@ -0,0 +1,166 @@ +package com.capacitorjs.plugins.splashscreen; + +import android.widget.ImageView; +import com.getcapacitor.Logger; +import com.getcapacitor.Plugin; +import com.getcapacitor.PluginCall; +import com.getcapacitor.PluginMethod; +import com.getcapacitor.annotation.CapacitorPlugin; +import com.getcapacitor.util.WebColor; +import java.util.Locale; + +@CapacitorPlugin(name = "SplashScreen") +public class SplashScreenPlugin extends Plugin { + + private SplashScreen splashScreen; + private SplashScreenConfig config; + + public void load() { + config = getSplashScreenConfig(); + splashScreen = new SplashScreen(getContext(), config); + if (!bridge.isMinimumWebViewInstalled() && bridge.getConfig().getErrorPath() != null && !config.isLaunchAutoHide()) { + return; + } else { + splashScreen.showOnLaunch(getActivity()); + } + } + + @PluginMethod + public void show(final PluginCall call) { + splashScreen.show( + getActivity(), + getSettings(call), + new SplashListener() { + @Override + public void completed() { + call.resolve(); + } + + @Override + public void error() { + call.reject("An error occurred while showing splash"); + } + } + ); + } + + @PluginMethod + public void hide(PluginCall call) { + if (config.isUsingDialog()) { + splashScreen.hideDialog(getActivity()); + } else { + splashScreen.hide(getSettings(call)); + } + call.resolve(); + } + + @Override + protected void handleOnPause() { + splashScreen.onPause(); + } + + @Override + protected void handleOnDestroy() { + splashScreen.onDestroy(); + } + + private SplashScreenSettings getSettings(PluginCall call) { + SplashScreenSettings settings = new SplashScreenSettings(); + if (call.getInt("showDuration") != null) { + settings.setShowDuration(call.getInt("showDuration")); + } + if (call.getInt("fadeInDuration") != null) { + settings.setFadeInDuration(call.getInt("fadeInDuration")); + } + if (call.getInt("fadeOutDuration") != null) { + settings.setFadeOutDuration(call.getInt("fadeOutDuration")); + } + if (call.getBoolean("autoHide") != null) { + settings.setAutoHide(call.getBoolean("autoHide")); + } + return settings; + } + + private SplashScreenConfig getSplashScreenConfig() { + SplashScreenConfig config = new SplashScreenConfig(); + String backgroundColor = getConfig().getString("backgroundColor"); + if (backgroundColor != null) { + try { + config.setBackgroundColor(WebColor.parseColor(backgroundColor)); + } catch (IllegalArgumentException ex) { + Logger.debug("Background color not applied"); + } + } + Integer duration = getConfig().getInt("launchShowDuration", config.getLaunchShowDuration()); + config.setLaunchShowDuration(duration); + Integer fadeOutDuration = getConfig().getInt("launchFadeOutDuration", config.getLaunchFadeOutDuration()); + config.setLaunchFadeOutDuration(fadeOutDuration); + Boolean autohide = getConfig().getBoolean("launchAutoHide", config.isLaunchAutoHide()); + config.setLaunchAutoHide(autohide); + if (getConfig().getString("androidSplashResourceName") != null) { + config.setResourceName(getConfig().getString("androidSplashResourceName")); + } + Boolean immersive = getConfig().getBoolean("splashImmersive", config.isImmersive()); + config.setImmersive(immersive); + + Boolean fullScreen = getConfig().getBoolean("splashFullScreen", config.isFullScreen()); + config.setFullScreen(fullScreen); + + String spinnerStyle = getConfig().getString("androidSpinnerStyle"); + if (spinnerStyle != null) { + int spinnerBarStyle = android.R.attr.progressBarStyleLarge; + + switch (spinnerStyle.toLowerCase(Locale.ROOT)) { + case "horizontal": + spinnerBarStyle = android.R.attr.progressBarStyleHorizontal; + break; + case "small": + spinnerBarStyle = android.R.attr.progressBarStyleSmall; + break; + case "large": + spinnerBarStyle = android.R.attr.progressBarStyleLarge; + break; + case "inverse": + spinnerBarStyle = android.R.attr.progressBarStyleInverse; + break; + case "smallinverse": + spinnerBarStyle = android.R.attr.progressBarStyleSmallInverse; + break; + case "largeinverse": + spinnerBarStyle = android.R.attr.progressBarStyleLargeInverse; + break; + } + config.setSpinnerStyle(spinnerBarStyle); + } + String spinnerColor = getConfig().getString("spinnerColor"); + if (spinnerColor != null) { + try { + config.setSpinnerColor(WebColor.parseColor(spinnerColor)); + } catch (IllegalArgumentException ex) { + Logger.debug("Spinner color not applied"); + } + } + String scaleTypeName = getConfig().getString("androidScaleType"); + if (scaleTypeName != null) { + ImageView.ScaleType scaleType = null; + try { + scaleType = ImageView.ScaleType.valueOf(scaleTypeName); + } catch (IllegalArgumentException ex) { + scaleType = ImageView.ScaleType.FIT_XY; + } + config.setScaleType(scaleType); + } + + Boolean showSpinner = getConfig().getBoolean("showSpinner", config.isShowSpinner()); + config.setShowSpinner(showSpinner); + + Boolean useDialog = getConfig().getBoolean("useDialog", config.isUsingDialog()); + config.setUsingDialog(useDialog); + + if (getConfig().getString("layoutName") != null) { + config.setLayoutName(getConfig().getString("layoutName")); + } + + return config; + } +} diff --git a/mobile/plugins/capacitor-splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashScreenSettings.java b/mobile/plugins/capacitor-splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashScreenSettings.java new file mode 100644 index 00000000..ca35dad7 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashScreenSettings.java @@ -0,0 +1,41 @@ +package com.capacitorjs.plugins.splashscreen; + +public class SplashScreenSettings { + + private Integer showDuration = 3000; + private Integer fadeInDuration = 200; + private Integer fadeOutDuration = 200; + private boolean autoHide = true; + + public Integer getShowDuration() { + return showDuration; + } + + public void setShowDuration(Integer showDuration) { + this.showDuration = showDuration; + } + + public Integer getFadeInDuration() { + return fadeInDuration; + } + + public void setFadeInDuration(Integer fadeInDuration) { + this.fadeInDuration = fadeInDuration; + } + + public Integer getFadeOutDuration() { + return fadeOutDuration; + } + + public void setFadeOutDuration(Integer fadeOutDuration) { + this.fadeOutDuration = fadeOutDuration; + } + + public boolean isAutoHide() { + return autoHide; + } + + public void setAutoHide(boolean autoHide) { + this.autoHide = autoHide; + } +} diff --git a/mobile/plugins/capacitor-splash-screen/android/src/main/res/.gitkeep b/mobile/plugins/capacitor-splash-screen/android/src/main/res/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/mobile/plugins/capacitor-splash-screen/android/src/main/res/values/styles.xml b/mobile/plugins/capacitor-splash-screen/android/src/main/res/values/styles.xml new file mode 100644 index 00000000..3534cf91 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/android/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/mobile/plugins/capacitor-splash-screen/android/src/test/java/com/getcapacitor/ExampleUnitTest.java b/mobile/plugins/capacitor-splash-screen/android/src/test/java/com/getcapacitor/ExampleUnitTest.java new file mode 100644 index 00000000..a0fed0cf --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/android/src/test/java/com/getcapacitor/ExampleUnitTest.java @@ -0,0 +1,18 @@ +package com.getcapacitor; + +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} diff --git a/mobile/plugins/capacitor-splash-screen/dist/docs.json b/mobile/plugins/capacitor-splash-screen/dist/docs.json new file mode 100644 index 00000000..92adf93a --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/dist/docs.json @@ -0,0 +1,416 @@ +{ + "api": { + "name": "SplashScreenPlugin", + "slug": "splashscreenplugin", + "docs": "", + "tags": [], + "methods": [ + { + "name": "show", + "signature": "(options?: ShowOptions | undefined) => Promise", + "parameters": [ + { + "name": "options", + "docs": "", + "type": "ShowOptions | undefined" + } + ], + "returns": "Promise", + "tags": [ + { + "name": "since", + "text": "1.0.0" + } + ], + "docs": "Show the splash screen", + "complexTypes": [ + "ShowOptions" + ], + "slug": "show" + }, + { + "name": "hide", + "signature": "(options?: HideOptions | undefined) => Promise", + "parameters": [ + { + "name": "options", + "docs": "", + "type": "HideOptions | undefined" + } + ], + "returns": "Promise", + "tags": [ + { + "name": "since", + "text": "1.0.0" + } + ], + "docs": "Hide the splash screen", + "complexTypes": [ + "HideOptions" + ], + "slug": "hide" + } + ], + "properties": [] + }, + "interfaces": [ + { + "name": "ShowOptions", + "slug": "showoptions", + "docs": "", + "tags": [], + "methods": [], + "properties": [ + { + "name": "autoHide", + "tags": [ + { + "text": "1.0.0", + "name": "since" + } + ], + "docs": "Whether to auto hide the splash after showDuration", + "complexTypes": [], + "type": "boolean | undefined" + }, + { + "name": "fadeInDuration", + "tags": [ + { + "text": "1.0.0", + "name": "since" + }, + { + "text": "200", + "name": "default" + } + ], + "docs": "How long (in ms) to fade in.", + "complexTypes": [], + "type": "number | undefined" + }, + { + "name": "fadeOutDuration", + "tags": [ + { + "text": "1.0.0", + "name": "since" + }, + { + "text": "200", + "name": "default" + } + ], + "docs": "How long (in ms) to fade out.", + "complexTypes": [], + "type": "number | undefined" + }, + { + "name": "showDuration", + "tags": [ + { + "text": "1.0.0", + "name": "since" + }, + { + "text": "3000", + "name": "default" + } + ], + "docs": "How long to show the splash screen when autoHide is enabled (in ms)", + "complexTypes": [], + "type": "number | undefined" + } + ] + }, + { + "name": "HideOptions", + "slug": "hideoptions", + "docs": "", + "tags": [], + "methods": [], + "properties": [ + { + "name": "fadeOutDuration", + "tags": [ + { + "text": "1.0.0", + "name": "since" + }, + { + "text": "200", + "name": "default" + } + ], + "docs": "How long (in ms) to fade out.\n\nOn Android, if using the Android 12 Splash Screen API, it's not being used.\nUse launchFadeOutDuration configuration option instead.", + "complexTypes": [], + "type": "number | undefined" + } + ] + } + ], + "enums": [], + "typeAliases": [], + "pluginConfigs": [ + { + "name": "SplashScreen", + "slug": "splashscreen", + "properties": [ + { + "name": "launchShowDuration", + "tags": [ + { + "text": "1.0.0", + "name": "since" + }, + { + "text": "500", + "name": "default" + }, + { + "text": "3000", + "name": "example" + } + ], + "docs": "How long to show the launch splash screen when autoHide is enabled (in ms)", + "complexTypes": [], + "type": "number | undefined" + }, + { + "name": "launchAutoHide", + "tags": [ + { + "text": "1.0.0", + "name": "since" + }, + { + "text": "true", + "name": "default" + }, + { + "text": "true", + "name": "example" + } + ], + "docs": "Whether to auto hide the splash after launchShowDuration.", + "complexTypes": [], + "type": "boolean | undefined" + }, + { + "name": "launchFadeOutDuration", + "tags": [ + { + "text": "4.2.0", + "name": "since" + }, + { + "text": "200", + "name": "default" + }, + { + "text": "3000", + "name": "example" + } + ], + "docs": "Duration for the fade out animation of the launch splash screen (in ms)\n\nOnly available for Android, when using the Android 12 Splash Screen API.", + "complexTypes": [], + "type": "number | undefined" + }, + { + "name": "backgroundColor", + "tags": [ + { + "text": "1.0.0", + "name": "since" + }, + { + "text": "\"#ffffffff\"", + "name": "example" + } + ], + "docs": "Color of the background of the Splash Screen in hex format, #RRGGBB or #RRGGBBAA.\nDoesn't work if `useDialog` is true or on launch when using the Android 12 API.", + "complexTypes": [], + "type": "string | undefined" + }, + { + "name": "androidSplashResourceName", + "tags": [ + { + "text": "1.0.0", + "name": "since" + }, + { + "text": "splash", + "name": "default" + }, + { + "text": "\"splash\"", + "name": "example" + } + ], + "docs": "Name of the resource to be used as Splash Screen.\n\nDoesn't work on launch when using the Android 12 API.\n\nOnly available on Android.", + "complexTypes": [], + "type": "string | undefined" + }, + { + "name": "androidScaleType", + "tags": [ + { + "text": "1.0.0", + "name": "since" + }, + { + "text": "FIT_XY", + "name": "default" + }, + { + "text": "\"CENTER_CROP\"", + "name": "example" + } + ], + "docs": "The [ImageView.ScaleType](https://developer.android.com/reference/android/widget/ImageView.ScaleType) used to scale\nthe Splash Screen image.\nDoesn't work if `useDialog` is true or on launch when using the Android 12 API.\n\nOnly available on Android.", + "complexTypes": [], + "type": "'CENTER' | 'CENTER_CROP' | 'CENTER_INSIDE' | 'FIT_CENTER' | 'FIT_END' | 'FIT_START' | 'FIT_XY' | 'MATRIX' | undefined" + }, + { + "name": "showSpinner", + "tags": [ + { + "text": "1.0.0", + "name": "since" + }, + { + "text": "true", + "name": "example" + } + ], + "docs": "Show a loading spinner on the Splash Screen.\nDoesn't work if `useDialog` is true or on launch when using the Android 12 API.", + "complexTypes": [], + "type": "boolean | undefined" + }, + { + "name": "androidSpinnerStyle", + "tags": [ + { + "text": "1.0.0", + "name": "since" + }, + { + "text": "large", + "name": "default" + }, + { + "text": "\"large\"", + "name": "example" + } + ], + "docs": "Style of the Android spinner.\nDoesn't work if `useDialog` is true or on launch when using the Android 12 API.", + "complexTypes": [], + "type": "'horizontal' | 'small' | 'large' | 'inverse' | 'smallInverse' | 'largeInverse' | undefined" + }, + { + "name": "iosSpinnerStyle", + "tags": [ + { + "text": "1.0.0", + "name": "since" + }, + { + "text": "large", + "name": "default" + }, + { + "text": "\"small\"", + "name": "example" + } + ], + "docs": "Style of the iOS spinner.\nDoesn't work if `useDialog` is true.\n\nOnly available on iOS.", + "complexTypes": [], + "type": "'small' | 'large' | undefined" + }, + { + "name": "spinnerColor", + "tags": [ + { + "text": "1.0.0", + "name": "since" + }, + { + "text": "\"#999999\"", + "name": "example" + } + ], + "docs": "Color of the spinner in hex format, #RRGGBB or #RRGGBBAA.\nDoesn't work if `useDialog` is true or on launch when using the Android 12 API.", + "complexTypes": [], + "type": "string | undefined" + }, + { + "name": "splashFullScreen", + "tags": [ + { + "text": "1.0.0", + "name": "since" + }, + { + "text": "true", + "name": "example" + } + ], + "docs": "Hide the status bar on the Splash Screen.\n\nDoesn't work on launch when using the Android 12 API.\n\nOnly available on Android.", + "complexTypes": [], + "type": "boolean | undefined" + }, + { + "name": "splashImmersive", + "tags": [ + { + "text": "1.0.0", + "name": "since" + }, + { + "text": "true", + "name": "example" + } + ], + "docs": "Hide the status bar and the software navigation buttons on the Splash Screen.\n\nDoesn't work on launch when using the Android 12 API.\n\nOnly available on Android.", + "complexTypes": [], + "type": "boolean | undefined" + }, + { + "name": "layoutName", + "tags": [ + { + "text": "1.1.0", + "name": "since" + }, + { + "text": "\"launch_screen\"", + "name": "example" + } + ], + "docs": "If `useDialog` is set to true, configure the Dialog layout.\nIf `useDialog` is not set or false, use a layout instead of the ImageView.\n\nDoesn't work on launch when using the Android 12 API.\n\nOnly available on Android.", + "complexTypes": [], + "type": "string | undefined" + }, + { + "name": "useDialog", + "tags": [ + { + "text": "1.1.0", + "name": "since" + }, + { + "text": "true", + "name": "example" + } + ], + "docs": "Use a Dialog instead of an ImageView.\nIf `layoutName` is not configured, it will use\na layout that uses the splash image as background.\n\nDoesn't work on launch when using the Android 12 API.\n\nOnly available on Android.", + "complexTypes": [], + "type": "boolean | undefined" + } + ], + "docs": "These config values are available:" + } + ] +} \ No newline at end of file diff --git a/mobile/plugins/capacitor-splash-screen/dist/esm/definitions.d.ts b/mobile/plugins/capacitor-splash-screen/dist/esm/definitions.d.ts new file mode 100644 index 00000000..ae757415 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/dist/esm/definitions.d.ts @@ -0,0 +1,215 @@ +declare module '@capacitor/cli' { + interface PluginsConfig { + /** + * These config values are available: + */ + SplashScreen?: { + /** + * How long to show the launch splash screen when autoHide is enabled (in ms) + * + * @since 1.0.0 + * @default 500 + * @example 3000 + */ + launchShowDuration?: number; + /** + * Whether to auto hide the splash after launchShowDuration. + * + * @since 1.0.0 + * @default true + * @example true + */ + launchAutoHide?: boolean; + /** + * Duration for the fade out animation of the launch splash screen (in ms) + * + * Only available for Android, when using the Android 12 Splash Screen API. + * + * @since 4.2.0 + * @default 200 + * @example 3000 + */ + launchFadeOutDuration?: number; + /** + * Color of the background of the Splash Screen in hex format, #RRGGBB or #RRGGBBAA. + * Doesn't work if `useDialog` is true or on launch when using the Android 12 API. + * + * @since 1.0.0 + * @example "#ffffffff" + */ + backgroundColor?: string; + /** + * Name of the resource to be used as Splash Screen. + * + * Doesn't work on launch when using the Android 12 API. + * + * Only available on Android. + * + * @since 1.0.0 + * @default splash + * @example "splash" + */ + androidSplashResourceName?: string; + /** + * The [ImageView.ScaleType](https://developer.android.com/reference/android/widget/ImageView.ScaleType) used to scale + * the Splash Screen image. + * Doesn't work if `useDialog` is true or on launch when using the Android 12 API. + * + * Only available on Android. + * + * @since 1.0.0 + * @default FIT_XY + * @example "CENTER_CROP" + */ + androidScaleType?: 'CENTER' | 'CENTER_CROP' | 'CENTER_INSIDE' | 'FIT_CENTER' | 'FIT_END' | 'FIT_START' | 'FIT_XY' | 'MATRIX'; + /** + * Show a loading spinner on the Splash Screen. + * Doesn't work if `useDialog` is true or on launch when using the Android 12 API. + * + * @since 1.0.0 + * @example true + */ + showSpinner?: boolean; + /** + * Style of the Android spinner. + * Doesn't work if `useDialog` is true or on launch when using the Android 12 API. + * + * @since 1.0.0 + * @default large + * @example "large" + */ + androidSpinnerStyle?: 'horizontal' | 'small' | 'large' | 'inverse' | 'smallInverse' | 'largeInverse'; + /** + * Style of the iOS spinner. + * Doesn't work if `useDialog` is true. + * + * Only available on iOS. + * + * @since 1.0.0 + * @default large + * @example "small" + */ + iosSpinnerStyle?: 'large' | 'small'; + /** + * Color of the spinner in hex format, #RRGGBB or #RRGGBBAA. + * Doesn't work if `useDialog` is true or on launch when using the Android 12 API. + * + * @since 1.0.0 + * @example "#999999" + */ + spinnerColor?: string; + /** + * Hide the status bar on the Splash Screen. + * + * Doesn't work on launch when using the Android 12 API. + * + * Only available on Android. + * + * @since 1.0.0 + * @example true + */ + splashFullScreen?: boolean; + /** + * Hide the status bar and the software navigation buttons on the Splash Screen. + * + * Doesn't work on launch when using the Android 12 API. + * + * Only available on Android. + * + * @since 1.0.0 + * @example true + */ + splashImmersive?: boolean; + /** + * If `useDialog` is set to true, configure the Dialog layout. + * If `useDialog` is not set or false, use a layout instead of the ImageView. + * + * Doesn't work on launch when using the Android 12 API. + * + * Only available on Android. + * + * @since 1.1.0 + * @example "launch_screen" + */ + layoutName?: string; + /** + * Use a Dialog instead of an ImageView. + * If `layoutName` is not configured, it will use + * a layout that uses the splash image as background. + * + * Doesn't work on launch when using the Android 12 API. + * + * Only available on Android. + * + * @since 1.1.0 + * @example true + */ + useDialog?: boolean; + }; + } +} +export interface ShowOptions { + /** + * Whether to auto hide the splash after showDuration + * + * @since 1.0.0 + */ + autoHide?: boolean; + /** + * How long (in ms) to fade in. + * + * @since 1.0.0 + * @default 200 + */ + fadeInDuration?: number; + /** + * How long (in ms) to fade out. + * + * @since 1.0.0 + * @default 200 + */ + fadeOutDuration?: number; + /** + * How long to show the splash screen when autoHide is enabled (in ms) + * + * @since 1.0.0 + * @default 3000 + */ + showDuration?: number; +} +export interface HideOptions { + /** + * How long (in ms) to fade out. + * + * On Android, if using the Android 12 Splash Screen API, it's not being used. + * Use launchFadeOutDuration configuration option instead. + * + * @since 1.0.0 + * @default 200 + */ + fadeOutDuration?: number; +} +export interface SplashScreenPlugin { + /** + * Show the splash screen + * + * @since 1.0.0 + */ + show(options?: ShowOptions): Promise; + /** + * Hide the splash screen + * + * @since 1.0.0 + */ + hide(options?: HideOptions): Promise; +} +/** + * @deprecated Use `ShowOptions`. + * @since 1.0.0 + */ +export declare type SplashScreenShowOptions = ShowOptions; +/** + * @deprecated Use `HideOptions`. + * @since 1.0.0 + */ +export declare type SplashScreenHideOptions = HideOptions; diff --git a/mobile/plugins/capacitor-splash-screen/dist/esm/definitions.js b/mobile/plugins/capacitor-splash-screen/dist/esm/definitions.js new file mode 100644 index 00000000..7e3939b1 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/dist/esm/definitions.js @@ -0,0 +1,3 @@ +/// +export {}; +//# sourceMappingURL=definitions.js.map \ No newline at end of file diff --git a/mobile/plugins/capacitor-splash-screen/dist/esm/definitions.js.map b/mobile/plugins/capacitor-splash-screen/dist/esm/definitions.js.map new file mode 100644 index 00000000..63827acd --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/dist/esm/definitions.js.map @@ -0,0 +1 @@ +{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"AAAA,wCAAwC","sourcesContent":["/// \n\ndeclare module '@capacitor/cli' {\n export interface PluginsConfig {\n /**\n * These config values are available:\n */\n SplashScreen?: {\n /**\n * How long to show the launch splash screen when autoHide is enabled (in ms)\n *\n * @since 1.0.0\n * @default 500\n * @example 3000\n */\n launchShowDuration?: number;\n\n /**\n * Whether to auto hide the splash after launchShowDuration.\n *\n * @since 1.0.0\n * @default true\n * @example true\n */\n launchAutoHide?: boolean;\n\n /**\n * Duration for the fade out animation of the launch splash screen (in ms)\n *\n * Only available for Android, when using the Android 12 Splash Screen API.\n *\n * @since 4.2.0\n * @default 200\n * @example 3000\n */\n launchFadeOutDuration?: number;\n\n /**\n * Color of the background of the Splash Screen in hex format, #RRGGBB or #RRGGBBAA.\n * Doesn't work if `useDialog` is true or on launch when using the Android 12 API.\n *\n * @since 1.0.0\n * @example \"#ffffffff\"\n */\n backgroundColor?: string;\n\n /**\n * Name of the resource to be used as Splash Screen.\n *\n * Doesn't work on launch when using the Android 12 API.\n *\n * Only available on Android.\n *\n * @since 1.0.0\n * @default splash\n * @example \"splash\"\n */\n androidSplashResourceName?: string;\n\n /**\n * The [ImageView.ScaleType](https://developer.android.com/reference/android/widget/ImageView.ScaleType) used to scale\n * the Splash Screen image.\n * Doesn't work if `useDialog` is true or on launch when using the Android 12 API.\n *\n * Only available on Android.\n *\n * @since 1.0.0\n * @default FIT_XY\n * @example \"CENTER_CROP\"\n */\n androidScaleType?:\n | 'CENTER'\n | 'CENTER_CROP'\n | 'CENTER_INSIDE'\n | 'FIT_CENTER'\n | 'FIT_END'\n | 'FIT_START'\n | 'FIT_XY'\n | 'MATRIX';\n\n /**\n * Show a loading spinner on the Splash Screen.\n * Doesn't work if `useDialog` is true or on launch when using the Android 12 API.\n *\n * @since 1.0.0\n * @example true\n */\n showSpinner?: boolean;\n\n /**\n * Style of the Android spinner.\n * Doesn't work if `useDialog` is true or on launch when using the Android 12 API.\n *\n * @since 1.0.0\n * @default large\n * @example \"large\"\n */\n androidSpinnerStyle?:\n | 'horizontal'\n | 'small'\n | 'large'\n | 'inverse'\n | 'smallInverse'\n | 'largeInverse';\n\n /**\n * Style of the iOS spinner.\n * Doesn't work if `useDialog` is true.\n *\n * Only available on iOS.\n *\n * @since 1.0.0\n * @default large\n * @example \"small\"\n */\n iosSpinnerStyle?: 'large' | 'small';\n\n /**\n * Color of the spinner in hex format, #RRGGBB or #RRGGBBAA.\n * Doesn't work if `useDialog` is true or on launch when using the Android 12 API.\n *\n * @since 1.0.0\n * @example \"#999999\"\n */\n spinnerColor?: string;\n\n /**\n * Hide the status bar on the Splash Screen.\n *\n * Doesn't work on launch when using the Android 12 API.\n *\n * Only available on Android.\n *\n * @since 1.0.0\n * @example true\n */\n splashFullScreen?: boolean;\n\n /**\n * Hide the status bar and the software navigation buttons on the Splash Screen.\n *\n * Doesn't work on launch when using the Android 12 API.\n *\n * Only available on Android.\n *\n * @since 1.0.0\n * @example true\n */\n splashImmersive?: boolean;\n\n /**\n * If `useDialog` is set to true, configure the Dialog layout.\n * If `useDialog` is not set or false, use a layout instead of the ImageView.\n *\n * Doesn't work on launch when using the Android 12 API.\n *\n * Only available on Android.\n *\n * @since 1.1.0\n * @example \"launch_screen\"\n */\n layoutName?: string;\n\n /**\n * Use a Dialog instead of an ImageView.\n * If `layoutName` is not configured, it will use\n * a layout that uses the splash image as background.\n *\n * Doesn't work on launch when using the Android 12 API.\n *\n * Only available on Android.\n *\n * @since 1.1.0\n * @example true\n */\n useDialog?: boolean;\n };\n }\n}\n\nexport interface ShowOptions {\n /**\n * Whether to auto hide the splash after showDuration\n *\n * @since 1.0.0\n */\n autoHide?: boolean;\n /**\n * How long (in ms) to fade in.\n *\n * @since 1.0.0\n * @default 200\n */\n fadeInDuration?: number;\n /**\n * How long (in ms) to fade out.\n *\n * @since 1.0.0\n * @default 200\n */\n fadeOutDuration?: number;\n /**\n * How long to show the splash screen when autoHide is enabled (in ms)\n *\n * @since 1.0.0\n * @default 3000\n */\n showDuration?: number;\n}\n\nexport interface HideOptions {\n /**\n * How long (in ms) to fade out.\n *\n * On Android, if using the Android 12 Splash Screen API, it's not being used.\n * Use launchFadeOutDuration configuration option instead.\n *\n * @since 1.0.0\n * @default 200\n */\n fadeOutDuration?: number;\n}\n\nexport interface SplashScreenPlugin {\n /**\n * Show the splash screen\n *\n * @since 1.0.0\n */\n show(options?: ShowOptions): Promise;\n /**\n * Hide the splash screen\n *\n * @since 1.0.0\n */\n hide(options?: HideOptions): Promise;\n}\n\n/**\n * @deprecated Use `ShowOptions`.\n * @since 1.0.0\n */\nexport type SplashScreenShowOptions = ShowOptions;\n\n/**\n * @deprecated Use `HideOptions`.\n * @since 1.0.0\n */\nexport type SplashScreenHideOptions = HideOptions;\n"]} \ No newline at end of file diff --git a/mobile/plugins/capacitor-splash-screen/dist/esm/index.d.ts b/mobile/plugins/capacitor-splash-screen/dist/esm/index.d.ts new file mode 100644 index 00000000..aad8c0d8 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/dist/esm/index.d.ts @@ -0,0 +1,4 @@ +import type { SplashScreenPlugin } from './definitions'; +declare const SplashScreen: SplashScreenPlugin; +export * from './definitions'; +export { SplashScreen }; diff --git a/mobile/plugins/capacitor-splash-screen/dist/esm/index.js b/mobile/plugins/capacitor-splash-screen/dist/esm/index.js new file mode 100644 index 00000000..2034a1a1 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/dist/esm/index.js @@ -0,0 +1,7 @@ +import { registerPlugin } from '@capacitor/core'; +const SplashScreen = registerPlugin('SplashScreen', { + web: () => import('./web').then(m => new m.SplashScreenWeb()), +}); +export * from './definitions'; +export { SplashScreen }; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/mobile/plugins/capacitor-splash-screen/dist/esm/index.js.map b/mobile/plugins/capacitor-splash-screen/dist/esm/index.js.map new file mode 100644 index 00000000..e7bd2907 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/dist/esm/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAIjD,MAAM,YAAY,GAAG,cAAc,CAAqB,cAAc,EAAE;IACtE,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,eAAe,EAAE,CAAC;CAC9D,CAAC,CAAC;AAEH,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,YAAY,EAAE,CAAC","sourcesContent":["import { registerPlugin } from '@capacitor/core';\n\nimport type { SplashScreenPlugin } from './definitions';\n\nconst SplashScreen = registerPlugin('SplashScreen', {\n web: () => import('./web').then(m => new m.SplashScreenWeb()),\n});\n\nexport * from './definitions';\nexport { SplashScreen };\n"]} \ No newline at end of file diff --git a/mobile/plugins/capacitor-splash-screen/dist/esm/web.d.ts b/mobile/plugins/capacitor-splash-screen/dist/esm/web.d.ts new file mode 100644 index 00000000..881de436 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/dist/esm/web.d.ts @@ -0,0 +1,6 @@ +import { WebPlugin } from '@capacitor/core'; +import type { HideOptions, ShowOptions, SplashScreenPlugin } from './definitions'; +export declare class SplashScreenWeb extends WebPlugin implements SplashScreenPlugin { + show(_options?: ShowOptions): Promise; + hide(_options?: HideOptions): Promise; +} diff --git a/mobile/plugins/capacitor-splash-screen/dist/esm/web.js b/mobile/plugins/capacitor-splash-screen/dist/esm/web.js new file mode 100644 index 00000000..bee33275 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/dist/esm/web.js @@ -0,0 +1,10 @@ +import { WebPlugin } from '@capacitor/core'; +export class SplashScreenWeb extends WebPlugin { + async show(_options) { + return undefined; + } + async hide(_options) { + return undefined; + } +} +//# sourceMappingURL=web.js.map \ No newline at end of file diff --git a/mobile/plugins/capacitor-splash-screen/dist/esm/web.js.map b/mobile/plugins/capacitor-splash-screen/dist/esm/web.js.map new file mode 100644 index 00000000..5caa39c4 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/dist/esm/web.js.map @@ -0,0 +1 @@ +{"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAQ5C,MAAM,OAAO,eAAgB,SAAQ,SAAS;IAC5C,KAAK,CAAC,IAAI,CAAC,QAAsB;QAC/B,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,QAAsB;QAC/B,OAAO,SAAS,CAAC;IACnB,CAAC;CACF","sourcesContent":["import { WebPlugin } from '@capacitor/core';\n\nimport type {\n HideOptions,\n ShowOptions,\n SplashScreenPlugin,\n} from './definitions';\n\nexport class SplashScreenWeb extends WebPlugin implements SplashScreenPlugin {\n async show(_options?: ShowOptions): Promise {\n return undefined;\n }\n\n async hide(_options?: HideOptions): Promise {\n return undefined;\n }\n}\n"]} \ No newline at end of file diff --git a/mobile/plugins/capacitor-splash-screen/ios/Plugin.xcodeproj/project.pbxproj b/mobile/plugins/capacitor-splash-screen/ios/Plugin.xcodeproj/project.pbxproj new file mode 100644 index 00000000..01ea34e2 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/ios/Plugin.xcodeproj/project.pbxproj @@ -0,0 +1,579 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 48; + objects = { + +/* Begin PBXBuildFile section */ + 03FC29A292ACC40490383A1F /* Pods_Plugin.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B2A61DA5A1F2DD4F959604D /* Pods_Plugin.framework */; }; + 20C0B05DCFC8E3958A738AF2 /* Pods_PluginTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F6753A823D3815DB436415E3 /* Pods_PluginTests.framework */; }; + 2F94DB20258B85A6003E0C43 /* SplashScreenSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F94DB1F258B85A6003E0C43 /* SplashScreenSettings.swift */; }; + 2F94DB24258B85BE003E0C43 /* SplashScreenConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F94DB23258B85BE003E0C43 /* SplashScreenConfig.swift */; }; + 2F98D68224C9AAE500613A4C /* SplashScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F98D68124C9AAE400613A4C /* SplashScreen.swift */; }; + 50ADFF92201F53D600D50D53 /* Plugin.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50ADFF88201F53D600D50D53 /* Plugin.framework */; }; + 50ADFF97201F53D600D50D53 /* SplashScreenPluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50ADFF96201F53D600D50D53 /* SplashScreenPluginTests.swift */; }; + 50ADFF99201F53D600D50D53 /* SplashScreenPlugin.h in Headers */ = {isa = PBXBuildFile; fileRef = 50ADFF8B201F53D600D50D53 /* SplashScreenPlugin.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 50ADFFA42020D75100D50D53 /* Capacitor.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50ADFFA52020D75100D50D53 /* Capacitor.framework */; }; + 50ADFFA82020EE4F00D50D53 /* SplashScreenPlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = 50ADFFA72020EE4F00D50D53 /* SplashScreenPlugin.m */; }; + 50E1A94820377CB70090CE1A /* SplashScreenPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E1A94720377CB70090CE1A /* SplashScreenPlugin.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 50ADFF93201F53D600D50D53 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 50ADFF7F201F53D600D50D53 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 50ADFF87201F53D600D50D53; + remoteInfo = Plugin; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 2F94DB1F258B85A6003E0C43 /* SplashScreenSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenSettings.swift; sourceTree = ""; }; + 2F94DB23258B85BE003E0C43 /* SplashScreenConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenConfig.swift; sourceTree = ""; }; + 2F98D68124C9AAE400613A4C /* SplashScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashScreen.swift; sourceTree = ""; }; + 3B2A61DA5A1F2DD4F959604D /* Pods_Plugin.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Plugin.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 50ADFF88201F53D600D50D53 /* Plugin.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Plugin.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 50ADFF8B201F53D600D50D53 /* SplashScreenPlugin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SplashScreenPlugin.h; sourceTree = ""; }; + 50ADFF8C201F53D600D50D53 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 50ADFF91201F53D600D50D53 /* PluginTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PluginTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 50ADFF96201F53D600D50D53 /* SplashScreenPluginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenPluginTests.swift; sourceTree = ""; }; + 50ADFF98201F53D600D50D53 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 50ADFFA52020D75100D50D53 /* Capacitor.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Capacitor.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 50ADFFA72020EE4F00D50D53 /* SplashScreenPlugin.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SplashScreenPlugin.m; sourceTree = ""; }; + 50E1A94720377CB70090CE1A /* SplashScreenPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashScreenPlugin.swift; sourceTree = ""; }; + 5E23F77F099397094342571A /* Pods-Plugin.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Plugin.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Plugin/Pods-Plugin.debug.xcconfig"; sourceTree = ""; }; + 91781294A431A2A7CC6EB714 /* Pods-Plugin.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Plugin.release.xcconfig"; path = "Pods/Target Support Files/Pods-Plugin/Pods-Plugin.release.xcconfig"; sourceTree = ""; }; + 96ED1B6440D6672E406C8D19 /* Pods-PluginTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PluginTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.debug.xcconfig"; sourceTree = ""; }; + F65BB2953ECE002E1EF3E424 /* Pods-PluginTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PluginTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.release.xcconfig"; sourceTree = ""; }; + F6753A823D3815DB436415E3 /* Pods_PluginTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PluginTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 50ADFF84201F53D600D50D53 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 50ADFFA42020D75100D50D53 /* Capacitor.framework in Frameworks */, + 03FC29A292ACC40490383A1F /* Pods_Plugin.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 50ADFF8E201F53D600D50D53 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 50ADFF92201F53D600D50D53 /* Plugin.framework in Frameworks */, + 20C0B05DCFC8E3958A738AF2 /* Pods_PluginTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 50ADFF7E201F53D600D50D53 = { + isa = PBXGroup; + children = ( + 50ADFF8A201F53D600D50D53 /* Plugin */, + 50ADFF95201F53D600D50D53 /* PluginTests */, + 50ADFF89201F53D600D50D53 /* Products */, + 8C8E7744173064A9F6D438E3 /* Pods */, + A797B9EFA3DCEFEA1FBB66A9 /* Frameworks */, + ); + sourceTree = ""; + }; + 50ADFF89201F53D600D50D53 /* Products */ = { + isa = PBXGroup; + children = ( + 50ADFF88201F53D600D50D53 /* Plugin.framework */, + 50ADFF91201F53D600D50D53 /* PluginTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 50ADFF8A201F53D600D50D53 /* Plugin */ = { + isa = PBXGroup; + children = ( + 2F94DB23258B85BE003E0C43 /* SplashScreenConfig.swift */, + 2F94DB1F258B85A6003E0C43 /* SplashScreenSettings.swift */, + 50E1A94720377CB70090CE1A /* SplashScreenPlugin.swift */, + 2F98D68124C9AAE400613A4C /* SplashScreen.swift */, + 50ADFF8B201F53D600D50D53 /* SplashScreenPlugin.h */, + 50ADFFA72020EE4F00D50D53 /* SplashScreenPlugin.m */, + 50ADFF8C201F53D600D50D53 /* Info.plist */, + ); + path = Plugin; + sourceTree = ""; + }; + 50ADFF95201F53D600D50D53 /* PluginTests */ = { + isa = PBXGroup; + children = ( + 50ADFF96201F53D600D50D53 /* SplashScreenPluginTests.swift */, + 50ADFF98201F53D600D50D53 /* Info.plist */, + ); + path = PluginTests; + sourceTree = ""; + }; + 8C8E7744173064A9F6D438E3 /* Pods */ = { + isa = PBXGroup; + children = ( + 5E23F77F099397094342571A /* Pods-Plugin.debug.xcconfig */, + 91781294A431A2A7CC6EB714 /* Pods-Plugin.release.xcconfig */, + 96ED1B6440D6672E406C8D19 /* Pods-PluginTests.debug.xcconfig */, + F65BB2953ECE002E1EF3E424 /* Pods-PluginTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + A797B9EFA3DCEFEA1FBB66A9 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 50ADFFA52020D75100D50D53 /* Capacitor.framework */, + 3B2A61DA5A1F2DD4F959604D /* Pods_Plugin.framework */, + F6753A823D3815DB436415E3 /* Pods_PluginTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 50ADFF85201F53D600D50D53 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 50ADFF99201F53D600D50D53 /* SplashScreenPlugin.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 50ADFF87201F53D600D50D53 /* Plugin */ = { + isa = PBXNativeTarget; + buildConfigurationList = 50ADFF9C201F53D600D50D53 /* Build configuration list for PBXNativeTarget "Plugin" */; + buildPhases = ( + AB5B3E54B4E897F32C2279DA /* [CP] Check Pods Manifest.lock */, + 50ADFF83201F53D600D50D53 /* Sources */, + 50ADFF84201F53D600D50D53 /* Frameworks */, + 50ADFF85201F53D600D50D53 /* Headers */, + 50ADFF86201F53D600D50D53 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Plugin; + productName = Plugin; + productReference = 50ADFF88201F53D600D50D53 /* Plugin.framework */; + productType = "com.apple.product-type.framework"; + }; + 50ADFF90201F53D600D50D53 /* PluginTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 50ADFF9F201F53D600D50D53 /* Build configuration list for PBXNativeTarget "PluginTests" */; + buildPhases = ( + 0596884F929ED6F1DE134961 /* [CP] Check Pods Manifest.lock */, + 50ADFF8D201F53D600D50D53 /* Sources */, + 50ADFF8E201F53D600D50D53 /* Frameworks */, + 50ADFF8F201F53D600D50D53 /* Resources */, + 8E97F58B69A94C6503FC9C85 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 50ADFF94201F53D600D50D53 /* PBXTargetDependency */, + ); + name = PluginTests; + productName = PluginTests; + productReference = 50ADFF91201F53D600D50D53 /* PluginTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 50ADFF7F201F53D600D50D53 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1160; + ORGANIZATIONNAME = "Max Lynch"; + TargetAttributes = { + 50ADFF87201F53D600D50D53 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + }; + 50ADFF90201F53D600D50D53 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 50ADFF82201F53D600D50D53 /* Build configuration list for PBXProject "Plugin" */; + compatibilityVersion = "Xcode 8.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 50ADFF7E201F53D600D50D53; + productRefGroup = 50ADFF89201F53D600D50D53 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 50ADFF87201F53D600D50D53 /* Plugin */, + 50ADFF90201F53D600D50D53 /* PluginTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 50ADFF86201F53D600D50D53 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 50ADFF8F201F53D600D50D53 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0596884F929ED6F1DE134961 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-PluginTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 8E97F58B69A94C6503FC9C85 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-PluginTests/Pods-PluginTests-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/Capacitor/Capacitor.framework", + "${BUILT_PRODUCTS_DIR}/CapacitorCordova/Cordova.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Capacitor.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Cordova.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-PluginTests/Pods-PluginTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + AB5B3E54B4E897F32C2279DA /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Plugin-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 50ADFF83201F53D600D50D53 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 50E1A94820377CB70090CE1A /* SplashScreenPlugin.swift in Sources */, + 2F94DB20258B85A6003E0C43 /* SplashScreenSettings.swift in Sources */, + 2F94DB24258B85BE003E0C43 /* SplashScreenConfig.swift in Sources */, + 2F98D68224C9AAE500613A4C /* SplashScreen.swift in Sources */, + 50ADFFA82020EE4F00D50D53 /* SplashScreenPlugin.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 50ADFF8D201F53D600D50D53 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 50ADFF97201F53D600D50D53 /* SplashScreenPluginTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 50ADFF94201F53D600D50D53 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 50ADFF87201F53D600D50D53 /* Plugin */; + targetProxy = 50ADFF93201F53D600D50D53 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 50ADFF9A201F53D600D50D53 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + FRAMEWORK_SEARCH_PATHS = ( + "\"${BUILT_PRODUCTS_DIR}/Capacitor\"", + "\"${BUILT_PRODUCTS_DIR}/CapacitorCordova\"", + ); + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 50ADFF9B201F53D600D50D53 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + FRAMEWORK_SEARCH_PATHS = ( + "\"${BUILT_PRODUCTS_DIR}/Capacitor\"", + "\"${BUILT_PRODUCTS_DIR}/CapacitorCordova\"", + ); + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 50ADFF9D201F53D600D50D53 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5E23F77F099397094342571A /* Pods-Plugin.debug.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Plugin/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks $(FRAMEWORK_SEARCH_PATHS)\n$(FRAMEWORK_SEARCH_PATHS)\n$(FRAMEWORK_SEARCH_PATHS)"; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.getcapacitor.Plugin; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 50ADFF9E201F53D600D50D53 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 91781294A431A2A7CC6EB714 /* Pods-Plugin.release.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Plugin/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks $(FRAMEWORK_SEARCH_PATHS)"; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = com.getcapacitor.Plugin; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 50ADFFA0201F53D600D50D53 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 96ED1B6440D6672E406C8D19 /* Pods-PluginTests.debug.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = PluginTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.getcapacitor.PluginTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 50ADFFA1201F53D600D50D53 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F65BB2953ECE002E1EF3E424 /* Pods-PluginTests.release.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = PluginTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.getcapacitor.PluginTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 50ADFF82201F53D600D50D53 /* Build configuration list for PBXProject "Plugin" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 50ADFF9A201F53D600D50D53 /* Debug */, + 50ADFF9B201F53D600D50D53 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 50ADFF9C201F53D600D50D53 /* Build configuration list for PBXNativeTarget "Plugin" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 50ADFF9D201F53D600D50D53 /* Debug */, + 50ADFF9E201F53D600D50D53 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 50ADFF9F201F53D600D50D53 /* Build configuration list for PBXNativeTarget "PluginTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 50ADFFA0201F53D600D50D53 /* Debug */, + 50ADFFA1201F53D600D50D53 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 50ADFF7F201F53D600D50D53 /* Project object */; +} diff --git a/mobile/plugins/capacitor-splash-screen/ios/Plugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/mobile/plugins/capacitor-splash-screen/ios/Plugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/ios/Plugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/mobile/plugins/capacitor-splash-screen/ios/Plugin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/mobile/plugins/capacitor-splash-screen/ios/Plugin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/ios/Plugin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/mobile/plugins/capacitor-splash-screen/ios/Plugin.xcodeproj/xcshareddata/xcschemes/Plugin.xcscheme b/mobile/plugins/capacitor-splash-screen/ios/Plugin.xcodeproj/xcshareddata/xcschemes/Plugin.xcscheme new file mode 100644 index 00000000..303f2621 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/ios/Plugin.xcodeproj/xcshareddata/xcschemes/Plugin.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/plugins/capacitor-splash-screen/ios/Plugin.xcodeproj/xcshareddata/xcschemes/PluginTests.xcscheme b/mobile/plugins/capacitor-splash-screen/ios/Plugin.xcodeproj/xcshareddata/xcschemes/PluginTests.xcscheme new file mode 100644 index 00000000..3d8c88d2 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/ios/Plugin.xcodeproj/xcshareddata/xcschemes/PluginTests.xcscheme @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/plugins/capacitor-splash-screen/ios/Plugin.xcworkspace/contents.xcworkspacedata b/mobile/plugins/capacitor-splash-screen/ios/Plugin.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..afad624e --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/ios/Plugin.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/mobile/plugins/capacitor-splash-screen/ios/Plugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/mobile/plugins/capacitor-splash-screen/ios/Plugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/ios/Plugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/mobile/plugins/capacitor-splash-screen/ios/Plugin/Info.plist b/mobile/plugins/capacitor-splash-screen/ios/Plugin/Info.plist new file mode 100644 index 00000000..1007fd9d --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/ios/Plugin/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/mobile/plugins/capacitor-splash-screen/ios/Plugin/SplashScreen.swift b/mobile/plugins/capacitor-splash-screen/ios/Plugin/SplashScreen.swift new file mode 100644 index 00000000..b50b7c63 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/ios/Plugin/SplashScreen.swift @@ -0,0 +1,161 @@ +import Foundation +import Capacitor + +@objc public class SplashScreen: NSObject { + + var parentView: UIView + var viewController = UIViewController() + var spinner = UIActivityIndicatorView() + var config: SplashScreenConfig = SplashScreenConfig() + var hideTask: Any? + var isVisible: Bool = false + + init(parentView: UIView, config: SplashScreenConfig) { + self.parentView = parentView + self.config = config + } + + public func showOnLaunch() { + buildViews() + if self.config.launchShowDuration == 0 { + return + } + var settings = SplashScreenSettings() + settings.showDuration = config.launchShowDuration + settings.fadeInDuration = config.launchFadeInDuration + settings.autoHide = config.launchAutoHide + showSplash(settings: settings, completion: {}, isLaunchSplash: true) + } + + public func show(settings: SplashScreenSettings, completion: @escaping () -> Void) { + self.showSplash(settings: settings, completion: completion, isLaunchSplash: false) + } + + public func hide(settings: SplashScreenSettings) { + hideSplash(fadeOutDuration: settings.fadeOutDuration, isLaunchSplash: false) + } + + private func showSplash(settings: SplashScreenSettings, completion: @escaping () -> Void, isLaunchSplash: Bool) { + DispatchQueue.main.async { [weak self] in + guard let strongSelf = self else { + return + } + if let backgroundColor = strongSelf.config.backgroundColor { + strongSelf.viewController.view.backgroundColor = backgroundColor + } + + if strongSelf.config.showSpinner { + if let style = strongSelf.config.spinnerStyle { + strongSelf.spinner.style = style + } + + if let spinnerColor = strongSelf.config.spinnerColor { + strongSelf.spinner.color = spinnerColor + } + } + + strongSelf.parentView.addSubview(strongSelf.viewController.view) + + if strongSelf.config.showSpinner { + strongSelf.parentView.addSubview(strongSelf.spinner) + strongSelf.spinner.centerXAnchor.constraint(equalTo: strongSelf.parentView.centerXAnchor).isActive = true + strongSelf.spinner.centerYAnchor.constraint(equalTo: strongSelf.parentView.centerYAnchor).isActive = true + } + + strongSelf.parentView.isUserInteractionEnabled = false + + UIView.transition(with: strongSelf.viewController.view, duration: TimeInterval(Double(settings.fadeInDuration) / 1000), options: .curveLinear, animations: { + strongSelf.viewController.view.alpha = 1 + + if strongSelf.config.showSpinner { + strongSelf.spinner.alpha = 1 + } + }) { (_: Bool) in + strongSelf.isVisible = true + + if settings.autoHide { + strongSelf.hideTask = DispatchQueue.main.asyncAfter( + deadline: DispatchTime.now() + (Double(settings.showDuration) / 1000) + ) { + strongSelf.hideSplash(fadeOutDuration: settings.fadeOutDuration, isLaunchSplash: isLaunchSplash) + completion() + } + } else { + completion() + } + } + } + } + + private func buildViews() { + let storyboardName = Bundle.main.infoDictionary?["UILaunchStoryboardName"] as? String ?? "LaunchScreen" + if let vc = UIStoryboard(name: storyboardName.replacingOccurrences(of: ".storyboard", with: ""), bundle: nil).instantiateInitialViewController() { + viewController = vc + } + + // Observe for changes on frame and bounds to handle rotation resizing + parentView.addObserver(self, forKeyPath: "frame", options: .new, context: nil) + parentView.addObserver(self, forKeyPath: "bounds", options: .new, context: nil) + + updateSplashImageBounds() + if config.showSpinner { + spinner.translatesAutoresizingMaskIntoConstraints = false + spinner.startAnimating() + } + } + + private func tearDown() { + isVisible = false + parentView.isUserInteractionEnabled = true + viewController.view.removeFromSuperview() + + if config.showSpinner { + spinner.removeFromSuperview() + } + } + + // Update the bounds for the splash image. This will also be called when + // the parent view observers fire + private func updateSplashImageBounds() { + var window: UIWindow? = UIApplication.shared.delegate?.window ?? nil + + if window == nil { + let scene: UIWindowScene? = UIApplication.shared.connectedScenes.first as? UIWindowScene + window = scene?.windows.filter({$0.isKeyWindow}).first + if window == nil { + window = scene?.windows.first + } + } + + if let unwrappedWindow = window { + viewController.view.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: unwrappedWindow.bounds.size) + } else { + CAPLog.print("Unable to find root window object for SplashScreen bounds. Please file an issue") + } + } + + override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change _: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { + updateSplashImageBounds() + } + + private func hideSplash(fadeOutDuration: Int, isLaunchSplash: Bool) { + if isLaunchSplash, isVisible { + CAPLog.print("SplashScreen.hideSplash: SplashScreen was automatically hidden after default timeout. " + + "You should call `SplashScreen.hide()` as soon as your web app is loaded (or increase the timeout). " + + "Read more at https://capacitorjs.com/docs/apis/splash-screen#hiding-the-splash-screen") + } + if !isVisible { return } + DispatchQueue.main.async { + UIView.transition(with: self.viewController.view, duration: TimeInterval(Double(fadeOutDuration) / 1000), options: .curveEaseInOut, animations: { + self.viewController.view.alpha = 0 + self.viewController.view.transform = CGAffineTransform.identity.scaledBy(x: 8, y: 8) + + if self.config.showSpinner { + self.spinner.alpha = 0 + } + }) { (_: Bool) in + self.tearDown() + } + } + } +} diff --git a/mobile/plugins/capacitor-splash-screen/ios/Plugin/SplashScreenConfig.swift b/mobile/plugins/capacitor-splash-screen/ios/Plugin/SplashScreenConfig.swift new file mode 100644 index 00000000..6a5d5f57 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/ios/Plugin/SplashScreenConfig.swift @@ -0,0 +1,11 @@ +import UIKit + +public struct SplashScreenConfig { + var backgroundColor: UIColor? + var spinnerStyle: UIActivityIndicatorView.Style? + var spinnerColor: UIColor? + var showSpinner = false + var launchShowDuration = 500 + var launchAutoHide = true + let launchFadeInDuration = 0 +} diff --git a/mobile/plugins/capacitor-splash-screen/ios/Plugin/SplashScreenPlugin.h b/mobile/plugins/capacitor-splash-screen/ios/Plugin/SplashScreenPlugin.h new file mode 100644 index 00000000..f2bd9e0b --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/ios/Plugin/SplashScreenPlugin.h @@ -0,0 +1,10 @@ +#import + +//! Project version number for Plugin. +FOUNDATION_EXPORT double PluginVersionNumber; + +//! Project version string for Plugin. +FOUNDATION_EXPORT const unsigned char PluginVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + diff --git a/mobile/plugins/capacitor-splash-screen/ios/Plugin/SplashScreenPlugin.m b/mobile/plugins/capacitor-splash-screen/ios/Plugin/SplashScreenPlugin.m new file mode 100644 index 00000000..01a7e894 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/ios/Plugin/SplashScreenPlugin.m @@ -0,0 +1,9 @@ +#import +#import + +// Define the plugin using the CAP_PLUGIN Macro, and +// each method the plugin supports using the CAP_PLUGIN_METHOD macro. +CAP_PLUGIN(SplashScreenPlugin, "SplashScreen", + CAP_PLUGIN_METHOD(show, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(hide, CAPPluginReturnPromise); +) diff --git a/mobile/plugins/capacitor-splash-screen/ios/Plugin/SplashScreenPlugin.swift b/mobile/plugins/capacitor-splash-screen/ios/Plugin/SplashScreenPlugin.swift new file mode 100644 index 00000000..8c0e31a6 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/ios/Plugin/SplashScreenPlugin.swift @@ -0,0 +1,82 @@ +import Foundation +import Capacitor + +@objc(SplashScreenPlugin) +public class SplashScreenPlugin: CAPPlugin { + private var splashScreen: SplashScreen? + + override public func load() { + if let view = bridge?.viewController?.view { + splashScreen = SplashScreen(parentView: view, config: splashScreenConfig()) + splashScreen?.showOnLaunch() + } + } + + // Show the splash screen + @objc public func show(_ call: CAPPluginCall) { + if let splash = splashScreen { + let settings = splashScreenSettings(from: call) + splash.show(settings: settings, + completion: { + call.resolve() + }) + } else { + call.reject("Unable to show Splash Screen") + } + + } + + // Hide the splash screen + @objc public func hide(_ call: CAPPluginCall) { + if let splash = splashScreen { + let settings = splashScreenSettings(from: call) + splash.hide(settings: settings) + call.resolve() + } else { + call.reject("Unable to hide Splash Screen") + } + } + + private func splashScreenSettings(from call: CAPPluginCall) -> SplashScreenSettings { + var settings = SplashScreenSettings() + + if let showDuration = call.getInt("showDuration") { + settings.showDuration = showDuration + } + if let fadeInDuration = call.getInt("fadeInDuration") { + settings.fadeInDuration = fadeInDuration + } + if let fadeOutDuration = call.getInt("fadeOutDuration") { + settings.fadeOutDuration = fadeOutDuration + } + if let autoHide = call.getBool("autoHide") { + settings.autoHide = autoHide + } + return settings + } + + private func splashScreenConfig() -> SplashScreenConfig { + var config = SplashScreenConfig() + + if let backgroundColor = getConfig().getString("backgroundColor") { + config.backgroundColor = UIColor.capacitor.color(fromHex: backgroundColor) + } + if let spinnerStyle = getConfig().getString("iosSpinnerStyle") { + switch spinnerStyle.lowercased() { + case "small": + config.spinnerStyle = .medium + default: + config.spinnerStyle = .large + } + } + if let spinnerColor = getConfig().getString("spinnerColor") { + config.spinnerColor = UIColor.capacitor.color(fromHex: spinnerColor) + } + config.showSpinner = getConfig().getBoolean("showSpinner", config.showSpinner) + + config.launchShowDuration = getConfig().getInt("launchShowDuration", config.launchShowDuration) + config.launchAutoHide = getConfig().getBoolean("launchAutoHide", config.launchAutoHide) + return config + } + +} diff --git a/mobile/plugins/capacitor-splash-screen/ios/Plugin/SplashScreenSettings.swift b/mobile/plugins/capacitor-splash-screen/ios/Plugin/SplashScreenSettings.swift new file mode 100644 index 00000000..483bdfbd --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/ios/Plugin/SplashScreenSettings.swift @@ -0,0 +1,8 @@ +import UIKit + +public struct SplashScreenSettings { + var showDuration = 3000 + var fadeInDuration = 200 + var fadeOutDuration = 200 + var autoHide = true +} diff --git a/mobile/plugins/capacitor-splash-screen/ios/PluginTests/Info.plist b/mobile/plugins/capacitor-splash-screen/ios/PluginTests/Info.plist new file mode 100644 index 00000000..6c40a6cd --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/ios/PluginTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/mobile/plugins/capacitor-splash-screen/ios/PluginTests/SplashScreenPluginTests.swift b/mobile/plugins/capacitor-splash-screen/ios/PluginTests/SplashScreenPluginTests.swift new file mode 100644 index 00000000..e40584a3 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/ios/PluginTests/SplashScreenPluginTests.swift @@ -0,0 +1,14 @@ +import XCTest +@testable import Plugin + +class SplashScreenTests: XCTestCase { + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } +} diff --git a/mobile/plugins/capacitor-splash-screen/ios/Podfile b/mobile/plugins/capacitor-splash-screen/ios/Podfile new file mode 100644 index 00000000..dee40960 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/ios/Podfile @@ -0,0 +1,16 @@ +platform :ios, '13.0' + +def capacitor_pods + # Comment the next line if you're not using Swift and don't want to use dynamic frameworks + use_frameworks! + pod 'Capacitor', :path => '../node_modules/@capacitor/ios' + pod 'CapacitorCordova', :path => '../node_modules/@capacitor/ios' +end + +target 'Plugin' do + capacitor_pods +end + +target 'PluginTests' do + capacitor_pods +end diff --git a/mobile/plugins/capacitor-splash-screen/package-lock.json b/mobile/plugins/capacitor-splash-screen/package-lock.json new file mode 100644 index 00000000..99a8c14c --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/package-lock.json @@ -0,0 +1,4152 @@ +{ + "name": "capacitor-splash-screen", + "version": "5.0.6.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "capacitor-splash-screen", + "version": "5.0.6.1", + "license": "MIT", + "devDependencies": { + "@capacitor/android": "^5.0.0", + "@capacitor/cli": "^5.0.0", + "@capacitor/core": "^5.0.0", + "@capacitor/docgen": "0.2.0", + "@capacitor/ios": "^5.0.0", + "@ionic/eslint-config": "^0.3.0", + "@ionic/prettier-config": "~1.0.1", + "@ionic/swiftlint-config": "^1.1.2", + "eslint": "^7.11.0", + "prettier": "~2.3.0", + "prettier-plugin-java": "~1.0.2", + "rimraf": "^3.0.2", + "rollup": "^2.32.0", + "swiftlint": "^1.0.1", + "typescript": "~4.1.5" + }, + "peerDependencies": { + "@capacitor/core": "^5.0.0" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@capacitor/android": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-5.5.0.tgz", + "integrity": "sha512-ipJijb3M0FA6DvotS9zrbJ8p/mTEVg9EVtBmvUexogm8g5se1mc7i1gvOr3MQ/iTZ3PnNrRC/P7kHxa2R55iqg==", + "dev": true, + "peerDependencies": { + "@capacitor/core": "^5.5.0" + } + }, + "node_modules/@capacitor/cli": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-5.5.0.tgz", + "integrity": "sha512-JkF7p+EV1mEFObp3e/3snKZiiDPbHTAXlch9jKcvvuCjm92Be7ka8sG4M3fH8BPajSE3jRNPZa/xt7bITDvAAA==", + "dev": true, + "dependencies": { + "@ionic/cli-framework-output": "^2.2.5", + "@ionic/utils-fs": "^3.1.6", + "@ionic/utils-subprocess": "^2.1.11", + "@ionic/utils-terminal": "^2.3.3", + "commander": "^9.3.0", + "debug": "^4.3.4", + "env-paths": "^2.2.0", + "kleur": "^4.1.4", + "native-run": "^1.7.3", + "open": "^8.4.0", + "plist": "^3.0.5", + "prompts": "^2.4.2", + "rimraf": "^4.4.1", + "semver": "^7.3.7", + "tar": "^6.1.11", + "tslib": "^2.4.0", + "xml2js": "^0.5.0" + }, + "bin": { + "cap": "bin/capacitor", + "capacitor": "bin/capacitor" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@capacitor/cli/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@capacitor/cli/node_modules/glob": { + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", + "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "minimatch": "^8.0.2", + "minipass": "^4.2.4", + "path-scurry": "^1.6.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@capacitor/cli/node_modules/minimatch": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", + "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@capacitor/cli/node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@capacitor/cli/node_modules/rimraf": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.4.1.tgz", + "integrity": "sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og==", + "dev": true, + "dependencies": { + "glob": "^9.2.0" + }, + "bin": { + "rimraf": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@capacitor/core": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-5.5.0.tgz", + "integrity": "sha512-w59io0ctwnb7JRng7yO2H0YLHG8uz7XARUugRfp5aYTNiG55FqdSmSMOOqGCMPRg4sEnKjJTvAa4ImCYh3Kk1w==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@capacitor/docgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@capacitor/docgen/-/docgen-0.2.0.tgz", + "integrity": "sha512-JoKuEx4Ep9LVnTY1I4zaoQPlxy+AWVMwwOSou/U9f/nXsL391aBRH7hC12lKNZGetiComnt369vQ+/r7n9iXfQ==", + "dev": true, + "dependencies": { + "@types/node": "^14.18.0", + "colorette": "^2.0.16", + "github-slugger": "^1.4.0", + "minimist": "^1.2.5", + "typescript": "~4.2.4" + }, + "bin": { + "docgen": "bin/docgen" + }, + "engines": { + "node": ">=14.5.0" + } + }, + "node_modules/@capacitor/docgen/node_modules/typescript": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", + "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/@capacitor/ios": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-5.5.0.tgz", + "integrity": "sha512-kApjblUOlLY91+1OrWIx+vaVfEN1bl1kh1jSgK1/IdGfS9kFs1hxUE/okRoLJGT6tYeSOa6GA/19MLOs64wb6A==", + "dev": true, + "peerDependencies": { + "@capacitor/core": "^5.5.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@ionic/cli-framework-output": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.6.tgz", + "integrity": "sha512-YLPRwnk5Lw0XQ9pKWG+p2KoR5HjMBigZ6yv+/XtL3TGOnCS1+oAz56ABbAORCjTWhSJQisr8APNFiELAecY6QA==", + "dev": true, + "dependencies": { + "@ionic/utils-terminal": "2.3.4", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/eslint-config": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@ionic/eslint-config/-/eslint-config-0.3.0.tgz", + "integrity": "sha512-Uf1hS2YIoHlcvXPF5LnsPM6auMewEdChQhR117Rt3sVEAutbyKMpFP4slNC2a6up3a5Q34zepqlf61Qgkf9XeQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "^4.1.0", + "@typescript-eslint/parser": "^4.1.0", + "eslint-config-prettier": "^6.11.0", + "eslint-plugin-import": "^2.22.0" + }, + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/@ionic/prettier-config": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@ionic/prettier-config/-/prettier-config-1.0.1.tgz", + "integrity": "sha512-/v8UOW7rxkw/hvrRe/QfjlQsdjkm3sfAHoE3uqffO5BoNGijQMARrT32JT9Ei0g6KySXPyxxW+7LzPHrQmfzCw==", + "dev": true, + "peerDependencies": { + "prettier": "^2.0.0" + } + }, + "node_modules/@ionic/swiftlint-config": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ionic/swiftlint-config/-/swiftlint-config-1.1.2.tgz", + "integrity": "sha512-UbE1AIlTowt9uR7fMzRtbQX4URcyuok7mcpdJfFDHAIGM6nDjohYMke+6xOr6ZYlLnEyVmBGNEg0+grEYRgcVg==", + "dev": true + }, + "node_modules/@ionic/utils-array": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz", + "integrity": "sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==", + "dev": true, + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-fs": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.7.tgz", + "integrity": "sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==", + "dev": true, + "dependencies": { + "@types/fs-extra": "^8.0.0", + "debug": "^4.0.0", + "fs-extra": "^9.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-object": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.6.tgz", + "integrity": "sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==", + "dev": true, + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-process": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.11.tgz", + "integrity": "sha512-Uavxn+x8j3rDlZEk1X7YnaN6wCgbCwYQOeIjv/m94i1dzslqWhqIHEqxEyeE8HsT5Negboagg7GtQiABy+BLbA==", + "dev": true, + "dependencies": { + "@ionic/utils-object": "2.1.6", + "@ionic/utils-terminal": "2.3.4", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-stream": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.6.tgz", + "integrity": "sha512-4+Kitey1lTA1yGtnigeYNhV/0tggI3lWBMjC7tBs1K9GXa/q7q4CtOISppdh8QgtOhrhAXS2Igp8rbko/Cj+lA==", + "dev": true, + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-subprocess": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-2.1.12.tgz", + "integrity": "sha512-N05Y+dIXBHofKWJTheCMzVqmgY9wFmZcRv/LdNnfXaaA/mxLTyGxQYeig8fvQXTtDafb/siZXcrTkmQ+y6n3Yg==", + "dev": true, + "dependencies": { + "@ionic/utils-array": "2.1.6", + "@ionic/utils-fs": "3.1.7", + "@ionic/utils-process": "2.1.11", + "@ionic/utils-stream": "3.1.6", + "@ionic/utils-terminal": "2.3.4", + "cross-spawn": "^7.0.3", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-terminal": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.4.tgz", + "integrity": "sha512-cEiMFl3jklE0sW60r8JHH3ijFTwh/jkdEKWbylSyExQwZ8pPuwoXz7gpkWoJRLuoRHHSvg+wzNYyPJazIHfoJA==", + "dev": true, + "dependencies": { + "@types/slice-ansi": "^4.0.0", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "slice-ansi": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "tslib": "^2.0.1", + "untildify": "^4.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/fs-extra": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.3.tgz", + "integrity": "sha512-7IdV01N0u/CaVO0fuY1YmEg14HQN3+EW8mpNgg6NEfxEl/lzCa5OxlBu3iFsCAdamnYOcTQ7oEi43Xc/67Rgzw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", + "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "dev": true + }, + "node_modules/@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, + "node_modules/@types/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz", + "integrity": "sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==", + "dev": true, + "dependencies": { + "@typescript-eslint/experimental-utils": "4.33.0", + "@typescript-eslint/scope-manager": "4.33.0", + "debug": "^4.3.1", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.1.8", + "regexpp": "^3.1.0", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^4.0.0", + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/experimental-utils": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz", + "integrity": "sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.7", + "@typescript-eslint/scope-manager": "4.33.0", + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/typescript-estree": "4.33.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz", + "integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "4.33.0", + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/typescript-estree": "4.33.0", + "debug": "^4.3.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz", + "integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/visitor-keys": "4.33.0" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz", + "integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==", + "dev": true, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz", + "integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/visitor-keys": "4.33.0", + "debug": "^4.3.1", + "globby": "^11.0.3", + "is-glob": "^4.0.1", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz", + "integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "4.33.0", + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", + "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/bplist-parser": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", + "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==", + "dev": true, + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chevrotain": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-6.5.0.tgz", + "integrity": "sha512-BwqQ/AgmKJ8jcMEjaSnfMybnKMgGTrtDKowfTP3pX4jwVy0kNjRsT/AP6h+wC3+3NC+X8X15VWBnTCQlX+wQFg==", + "dev": true, + "dependencies": { + "regexp-to-ast": "0.4.0" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/elementtree": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz", + "integrity": "sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==", + "dev": true, + "dependencies": { + "sax": "1.1.4" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.2.tgz", + "integrity": "sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.1", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.12", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.11" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", + "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz", + "integrity": "sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw==", + "dev": true, + "dependencies": { + "get-stdin": "^6.0.0" + }, + "bin": { + "eslint-config-prettier-check": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=3.14.1" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.28.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz", + "integrity": "sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.findlastindex": "^1.2.2", + "array.prototype.flat": "^1.3.1", + "array.prototype.flatmap": "^1.3.1", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.7", + "eslint-module-utils": "^2.8.0", + "has": "^1.0.3", + "is-core-module": "^2.13.0", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.6", + "object.groupby": "^1.0.0", + "object.values": "^1.1.6", + "semver": "^6.3.1", + "tsconfig-paths": "^3.14.2" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint/node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "dependencies": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat-cache": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", + "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/github-slugger": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", + "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==", + "dev": true + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", + "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-3.0.1.tgz", + "integrity": "sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==", + "dev": true, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/internal-slot": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.11" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/java-parser": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/java-parser/-/java-parser-1.0.2.tgz", + "integrity": "sha512-lBXc+F62ds2W83eH5MwGnzuWdb6kgGBV0x0R7w0B4JKGDrJzolMUEhRMzzzlIX68HvRU7XtfPon22YaB+dVg+A==", + "dev": true, + "dependencies": { + "chevrotain": "6.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true + }, + "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, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/native-run": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/native-run/-/native-run-1.7.3.tgz", + "integrity": "sha512-vEw8X3Yu8TAbP4/uCJV3nCsCrhfHgUecRRDc69ZU9EK0QXHHc7YDzmIeI7SfA08ywzPlC9YcpITcB6bgMbrtwQ==", + "dev": true, + "dependencies": { + "@ionic/utils-fs": "^3.1.6", + "@ionic/utils-terminal": "^2.3.3", + "bplist-parser": "^0.3.2", + "debug": "^4.3.4", + "elementtree": "^0.1.7", + "ini": "^3.0.1", + "plist": "^3.0.6", + "split2": "^4.1.0", + "through2": "^4.0.2", + "tslib": "^2.4.0", + "yauzl": "^2.10.0" + }, + "bin": { + "native-run": "bin/native-run" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/object-inspect": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.0.tgz", + "integrity": "sha512-HQ4J+ic8hKrgIt3mqk6cVOVrW2ozL4KdvHlqpBv9vDYWx9ysAgENAdvy4FoGF+KFdhR7nQTNm5J0ctAeOwn+3g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1" + } + }, + "node_modules/object.values": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", + "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz", + "integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/prettier-plugin-java": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/prettier-plugin-java/-/prettier-plugin-java-1.0.2.tgz", + "integrity": "sha512-YgcN1WGZlrH0E+bHdqtIYtfDp6k2PHBnIaGjzdff/7t/NyDWAA6ypAmnD7YQVG2OuoIaXYkC37HN7cz68lLWLg==", + "dev": true, + "dependencies": { + "java-parser": "1.0.2", + "lodash": "4.17.21", + "prettier": "2.2.1" + } + }, + "node_modules/prettier-plugin-java/node_modules/prettier": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz", + "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/regexp-to-ast": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.4.0.tgz", + "integrity": "sha512-4qf/7IsIKfSNHQXSwial1IFmfM1Cc/whNBQqRwe0V2stPe7KmN1U0tWQiIx6JiirgSrisjE0eECdNf7Tav1Ntw==", + "dev": true + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "2.79.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", + "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sax": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz", + "integrity": "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==", + "dev": true + }, + "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, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swiftlint": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/swiftlint/-/swiftlint-1.0.2.tgz", + "integrity": "sha512-YhcS0N3vkwBatnZf/iJPg89LGQk7MbFnz67Cg3EZ6Ppqm2H8y6x7A1t6KMQ0jYVQpea9wQiFiFRFhkoChaQ29Q==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@ionic/utils-fs": "3.1.6", + "@ionic/utils-subprocess": "2.1.11", + "cosmiconfig": "^6.0.0" + }, + "bin": { + "node-swiftlint": "bin.js" + } + }, + "node_modules/swiftlint/node_modules/@ionic/utils-array": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.5.tgz", + "integrity": "sha512-HD72a71IQVBmQckDwmA8RxNVMTbxnaLbgFOl+dO5tbvW9CkkSFCv41h6fUuNsSEVgngfkn0i98HDuZC8mk+lTA==", + "dev": true, + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=10.3.0" + } + }, + "node_modules/swiftlint/node_modules/@ionic/utils-fs": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.6.tgz", + "integrity": "sha512-eikrNkK89CfGPmexjTfSWl4EYqsPSBh0Ka7by4F0PLc1hJZYtJxUZV3X4r5ecA8ikjicUmcbU7zJmAjmqutG/w==", + "dev": true, + "dependencies": { + "@types/fs-extra": "^8.0.0", + "debug": "^4.0.0", + "fs-extra": "^9.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=10.3.0" + } + }, + "node_modules/swiftlint/node_modules/@ionic/utils-object": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.5.tgz", + "integrity": "sha512-XnYNSwfewUqxq+yjER1hxTKggftpNjFLJH0s37jcrNDwbzmbpFTQTVAp4ikNK4rd9DOebX/jbeZb8jfD86IYxw==", + "dev": true, + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=10.3.0" + } + }, + "node_modules/swiftlint/node_modules/@ionic/utils-process": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.10.tgz", + "integrity": "sha512-mZ7JEowcuGQK+SKsJXi0liYTcXd2bNMR3nE0CyTROpMECUpJeAvvaBaPGZf5ERQUPeWBVuwqAqjUmIdxhz5bxw==", + "dev": true, + "dependencies": { + "@ionic/utils-object": "2.1.5", + "@ionic/utils-terminal": "2.3.3", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=10.3.0" + } + }, + "node_modules/swiftlint/node_modules/@ionic/utils-stream": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.5.tgz", + "integrity": "sha512-hkm46uHvEC05X/8PHgdJi4l4zv9VQDELZTM+Kz69odtO9zZYfnt8DkfXHJqJ+PxmtiE5mk/ehJWLnn/XAczTUw==", + "dev": true, + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=10.3.0" + } + }, + "node_modules/swiftlint/node_modules/@ionic/utils-subprocess": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-2.1.11.tgz", + "integrity": "sha512-6zCDixNmZCbMCy5np8klSxOZF85kuDyzZSTTQKQP90ZtYNCcPYmuFSzaqDwApJT4r5L3MY3JrqK1gLkc6xiUPw==", + "dev": true, + "dependencies": { + "@ionic/utils-array": "2.1.5", + "@ionic/utils-fs": "3.1.6", + "@ionic/utils-process": "2.1.10", + "@ionic/utils-stream": "3.1.5", + "@ionic/utils-terminal": "2.3.3", + "cross-spawn": "^7.0.3", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=10.3.0" + } + }, + "node_modules/swiftlint/node_modules/@ionic/utils-terminal": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.3.tgz", + "integrity": "sha512-RnuSfNZ5fLEyX3R5mtcMY97cGD1A0NVBbarsSQ6yMMfRJ5YHU7hHVyUfvZeClbqkBC/pAqI/rYJuXKCT9YeMCQ==", + "dev": true, + "dependencies": { + "@types/slice-ansi": "^4.0.0", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "slice-ansi": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "tslib": "^2.0.1", + "untildify": "^4.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.3.0" + } + }, + "node_modules/table": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", + "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/tar": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.6.tgz", + "integrity": "sha512-pxnwLxeb/Z5SP80JDRzVjh58KsM6jZHRAOtTpS7sXLS4ogXNKC9ANxHHZqLLeVHZN35jCtI4JdmLLbLiC1kBow==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/v8-compile-cache": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", + "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==", + "dev": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", + "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "engines": { + "node": ">=8.0" + } + }, + "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 + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/mobile/plugins/capacitor-splash-screen/package.json b/mobile/plugins/capacitor-splash-screen/package.json new file mode 100644 index 00000000..add762df --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/package.json @@ -0,0 +1,80 @@ +{ + "name": "capacitor-splash-screen", + "version": "5.0.6.1", + "description": "The Splash Screen API provides methods for showing or hiding a Splash image.", + "main": "dist/plugin.cjs.js", + "module": "dist/esm/index.js", + "types": "dist/esm/index.d.ts", + "unpkg": "dist/plugin.js", + "files": [ + "android/src/main/", + "android/build.gradle", + "dist/", + "ios/Plugin/", + "CapacitorSplashScreen.podspec" + ], + "author": "Ionic ", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/korenskoy/capacitor-splash-screen.git" + }, + "bugs": { + "url": "https://github.com/ionic-team/capacitor-plugins/issues" + }, + "keywords": [ + "capacitor", + "plugin", + "native" + ], + "scripts": { + "verify": "npm run verify:ios && npm run verify:android && npm run verify:web", + "verify:ios": "cd ios && pod install && xcodebuild -workspace Plugin.xcworkspace -scheme Plugin -destination generic/platform=iOS && cd ..", + "verify:android": "cd android && ./gradlew clean build test && cd ..", + "verify:web": "npm run build", + "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint", + "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format", + "eslint": "eslint . --ext ts", + "prettier": "prettier \"**/*.{css,html,ts,js,java}\"", + "swiftlint": "node-swiftlint", + "docgen": "docgen --api SplashScreenPlugin --output-readme README.md --output-json dist/docs.json", + "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.js", + "clean": "rimraf ./dist", + "watch": "tsc --watch", + "prepublishOnly": "npm run build", + "publish:cocoapod": "pod trunk push ./CapacitorSplashScreen.podspec --allow-warnings" + }, + "devDependencies": { + "@capacitor/android": "^5.0.0", + "@capacitor/cli": "^5.0.0", + "@capacitor/core": "^5.0.0", + "@capacitor/docgen": "0.2.0", + "@capacitor/ios": "^5.0.0", + "@ionic/eslint-config": "^0.3.0", + "@ionic/prettier-config": "~1.0.1", + "@ionic/swiftlint-config": "^1.1.2", + "eslint": "^7.11.0", + "prettier": "~2.3.0", + "prettier-plugin-java": "~1.0.2", + "rimraf": "^3.0.2", + "rollup": "^2.32.0", + "swiftlint": "^1.0.1", + "typescript": "~4.1.5" + }, + "peerDependencies": { + "@capacitor/core": "^5.0.0" + }, + "prettier": "@ionic/prettier-config", + "swiftlint": "@ionic/swiftlint-config", + "eslintConfig": { + "extends": "@ionic/eslint-config/recommended" + }, + "capacitor": { + "ios": { + "src": "ios" + }, + "android": { + "src": "android" + } + } +} diff --git a/mobile/plugins/capacitor-splash-screen/rollup.config.js b/mobile/plugins/capacitor-splash-screen/rollup.config.js new file mode 100644 index 00000000..a50ed81f --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/rollup.config.js @@ -0,0 +1,22 @@ +export default { + input: 'dist/esm/index.js', + output: [ + { + file: 'dist/plugin.js', + format: 'iife', + name: 'capacitorSplashScreen', + globals: { + '@capacitor/core': 'capacitorExports', + }, + sourcemap: true, + inlineDynamicImports: true, + }, + { + file: 'dist/plugin.cjs.js', + format: 'cjs', + sourcemap: true, + inlineDynamicImports: true, + }, + ], + external: ['@capacitor/core'], +}; diff --git a/mobile/plugins/capacitor-splash-screen/src/definitions.ts b/mobile/plugins/capacitor-splash-screen/src/definitions.ts new file mode 100644 index 00000000..2b0ab5ad --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/src/definitions.ts @@ -0,0 +1,249 @@ +/// + +declare module '@capacitor/cli' { + export interface PluginsConfig { + /** + * These config values are available: + */ + SplashScreen?: { + /** + * How long to show the launch splash screen when autoHide is enabled (in ms) + * + * @since 1.0.0 + * @default 500 + * @example 3000 + */ + launchShowDuration?: number; + + /** + * Whether to auto hide the splash after launchShowDuration. + * + * @since 1.0.0 + * @default true + * @example true + */ + launchAutoHide?: boolean; + + /** + * Duration for the fade out animation of the launch splash screen (in ms) + * + * Only available for Android, when using the Android 12 Splash Screen API. + * + * @since 4.2.0 + * @default 200 + * @example 3000 + */ + launchFadeOutDuration?: number; + + /** + * Color of the background of the Splash Screen in hex format, #RRGGBB or #RRGGBBAA. + * Doesn't work if `useDialog` is true or on launch when using the Android 12 API. + * + * @since 1.0.0 + * @example "#ffffffff" + */ + backgroundColor?: string; + + /** + * Name of the resource to be used as Splash Screen. + * + * Doesn't work on launch when using the Android 12 API. + * + * Only available on Android. + * + * @since 1.0.0 + * @default splash + * @example "splash" + */ + androidSplashResourceName?: string; + + /** + * The [ImageView.ScaleType](https://developer.android.com/reference/android/widget/ImageView.ScaleType) used to scale + * the Splash Screen image. + * Doesn't work if `useDialog` is true or on launch when using the Android 12 API. + * + * Only available on Android. + * + * @since 1.0.0 + * @default FIT_XY + * @example "CENTER_CROP" + */ + androidScaleType?: + | 'CENTER' + | 'CENTER_CROP' + | 'CENTER_INSIDE' + | 'FIT_CENTER' + | 'FIT_END' + | 'FIT_START' + | 'FIT_XY' + | 'MATRIX'; + + /** + * Show a loading spinner on the Splash Screen. + * Doesn't work if `useDialog` is true or on launch when using the Android 12 API. + * + * @since 1.0.0 + * @example true + */ + showSpinner?: boolean; + + /** + * Style of the Android spinner. + * Doesn't work if `useDialog` is true or on launch when using the Android 12 API. + * + * @since 1.0.0 + * @default large + * @example "large" + */ + androidSpinnerStyle?: + | 'horizontal' + | 'small' + | 'large' + | 'inverse' + | 'smallInverse' + | 'largeInverse'; + + /** + * Style of the iOS spinner. + * Doesn't work if `useDialog` is true. + * + * Only available on iOS. + * + * @since 1.0.0 + * @default large + * @example "small" + */ + iosSpinnerStyle?: 'large' | 'small'; + + /** + * Color of the spinner in hex format, #RRGGBB or #RRGGBBAA. + * Doesn't work if `useDialog` is true or on launch when using the Android 12 API. + * + * @since 1.0.0 + * @example "#999999" + */ + spinnerColor?: string; + + /** + * Hide the status bar on the Splash Screen. + * + * Doesn't work on launch when using the Android 12 API. + * + * Only available on Android. + * + * @since 1.0.0 + * @example true + */ + splashFullScreen?: boolean; + + /** + * Hide the status bar and the software navigation buttons on the Splash Screen. + * + * Doesn't work on launch when using the Android 12 API. + * + * Only available on Android. + * + * @since 1.0.0 + * @example true + */ + splashImmersive?: boolean; + + /** + * If `useDialog` is set to true, configure the Dialog layout. + * If `useDialog` is not set or false, use a layout instead of the ImageView. + * + * Doesn't work on launch when using the Android 12 API. + * + * Only available on Android. + * + * @since 1.1.0 + * @example "launch_screen" + */ + layoutName?: string; + + /** + * Use a Dialog instead of an ImageView. + * If `layoutName` is not configured, it will use + * a layout that uses the splash image as background. + * + * Doesn't work on launch when using the Android 12 API. + * + * Only available on Android. + * + * @since 1.1.0 + * @example true + */ + useDialog?: boolean; + }; + } +} + +export interface ShowOptions { + /** + * Whether to auto hide the splash after showDuration + * + * @since 1.0.0 + */ + autoHide?: boolean; + /** + * How long (in ms) to fade in. + * + * @since 1.0.0 + * @default 200 + */ + fadeInDuration?: number; + /** + * How long (in ms) to fade out. + * + * @since 1.0.0 + * @default 200 + */ + fadeOutDuration?: number; + /** + * How long to show the splash screen when autoHide is enabled (in ms) + * + * @since 1.0.0 + * @default 3000 + */ + showDuration?: number; +} + +export interface HideOptions { + /** + * How long (in ms) to fade out. + * + * On Android, if using the Android 12 Splash Screen API, it's not being used. + * Use launchFadeOutDuration configuration option instead. + * + * @since 1.0.0 + * @default 200 + */ + fadeOutDuration?: number; +} + +export interface SplashScreenPlugin { + /** + * Show the splash screen + * + * @since 1.0.0 + */ + show(options?: ShowOptions): Promise; + /** + * Hide the splash screen + * + * @since 1.0.0 + */ + hide(options?: HideOptions): Promise; +} + +/** + * @deprecated Use `ShowOptions`. + * @since 1.0.0 + */ +export type SplashScreenShowOptions = ShowOptions; + +/** + * @deprecated Use `HideOptions`. + * @since 1.0.0 + */ +export type SplashScreenHideOptions = HideOptions; diff --git a/mobile/plugins/capacitor-splash-screen/src/index.ts b/mobile/plugins/capacitor-splash-screen/src/index.ts new file mode 100644 index 00000000..98b918eb --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/src/index.ts @@ -0,0 +1,10 @@ +import { registerPlugin } from '@capacitor/core'; + +import type { SplashScreenPlugin } from './definitions'; + +const SplashScreen = registerPlugin('SplashScreen', { + web: () => import('./web').then(m => new m.SplashScreenWeb()), +}); + +export * from './definitions'; +export { SplashScreen }; diff --git a/mobile/plugins/capacitor-splash-screen/src/web.ts b/mobile/plugins/capacitor-splash-screen/src/web.ts new file mode 100644 index 00000000..bf9a9f66 --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/src/web.ts @@ -0,0 +1,17 @@ +import { WebPlugin } from '@capacitor/core'; + +import type { + HideOptions, + ShowOptions, + SplashScreenPlugin, +} from './definitions'; + +export class SplashScreenWeb extends WebPlugin implements SplashScreenPlugin { + async show(_options?: ShowOptions): Promise { + return undefined; + } + + async hide(_options?: HideOptions): Promise { + return undefined; + } +} diff --git a/mobile/plugins/capacitor-splash-screen/tsconfig.json b/mobile/plugins/capacitor-splash-screen/tsconfig.json new file mode 100644 index 00000000..f2e88e6a --- /dev/null +++ b/mobile/plugins/capacitor-splash-screen/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "allowUnreachableCode": false, + "declaration": true, + "esModuleInterop": true, + "inlineSources": true, + "lib": ["dom", "es2017"], + "module": "esnext", + "moduleResolution": "node", + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "outDir": "dist/esm", + "pretty": true, + "sourceMap": true, + "strict": true, + "target": "es2017" + }, + "files": ["src/index.ts"] +} diff --git a/mobile/plugins/native-bottom-sheet/.eslintignore b/mobile/plugins/native-bottom-sheet/.eslintignore new file mode 100644 index 00000000..9d0b71a3 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/.eslintignore @@ -0,0 +1,2 @@ +build +dist diff --git a/mobile/plugins/native-bottom-sheet/.gitignore b/mobile/plugins/native-bottom-sheet/.gitignore new file mode 100644 index 00000000..d4c3b664 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/.gitignore @@ -0,0 +1,61 @@ +# node files +!dist +node_modules + +# iOS files +Pods +Podfile.lock +Build +xcuserdata + +# macOS files +.DS_Store + + + +# Based on Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore + +# Built application files +*.apk +*.ap_ + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin +gen +out + +# Gradle files +.gradle +build + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation + +# Android Studio captures folder +captures + +# IntelliJ +*.iml +.idea + +# Keystore files +# Uncomment the following line if you do not want to check your keystore files in. +#*.jks + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild diff --git a/mobile/plugins/native-bottom-sheet/.prettierignore b/mobile/plugins/native-bottom-sheet/.prettierignore new file mode 100644 index 00000000..9d0b71a3 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/.prettierignore @@ -0,0 +1,2 @@ +build +dist diff --git a/mobile/plugins/native-bottom-sheet/CONTRIBUTING.md b/mobile/plugins/native-bottom-sheet/CONTRIBUTING.md new file mode 100644 index 00000000..3f875180 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/CONTRIBUTING.md @@ -0,0 +1,52 @@ +# Contributing + +This guide provides instructions for contributing to this Capacitor plugin. + +## Developing + +### Local Setup + +1. Fork and clone the repo. +1. Install the dependencies. + + ```shell + npm install + ``` + +1. Install SwiftLint if you're on macOS. + + ```shell + brew install swiftlint + ``` + +### Scripts + +#### `npm run build` + +Build the plugin web assets and generate plugin API documentation using [`@capacitor/docgen`](https://github.com/ionic-team/capacitor-docgen). + +It will compile the TypeScript code from `src/` into ESM JavaScript in `dist/esm/`. These files are used in apps with bundlers when your plugin is imported. + +Then, Rollup will bundle the code into a single file at `dist/plugin.js`. This file is used in apps without bundlers by including it as a script in `index.html`. + +#### `npm run verify` + +Build and validate the web and native projects. + +This is useful to run in CI to verify that the plugin builds for all platforms. + +#### `npm run lint` / `npm run fmt` + +Check formatting and code quality, autoformat/autofix if possible. + +This template is integrated with ESLint, Prettier, and SwiftLint. Using these tools is completely optional, but the [Capacitor Community](https://github.com/capacitor-community/) strives to have consistent code style and structure for easier cooperation. + +## Publishing + +There is a `prepublishOnly` hook in `package.json` which prepares the plugin before publishing, so all you need to do is run: + +```shell +npm publish +``` + +> **Note**: The [`files`](https://docs.npmjs.com/cli/v7/configuring-npm/package-json#files) array in `package.json` specifies which files get published. If you rename files/directories or add files elsewhere, you may need to update it. diff --git a/mobile/plugins/native-bottom-sheet/NativeBottomSheet.podspec b/mobile/plugins/native-bottom-sheet/NativeBottomSheet.podspec new file mode 100644 index 00000000..f2fafeb3 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/NativeBottomSheet.podspec @@ -0,0 +1,18 @@ +require 'json' + +package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) + +Pod::Spec.new do |s| + s.name = 'NativeBottomSheet' + s.version = package['version'] + s.summary = package['description'] + s.license = package['license'] + s.homepage = package['repository']['url'] + s.author = package['author'] + s.source = { :git => package['repository']['url'], :tag => s.version.to_s } + s.source_files = 'ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}' + s.ios.deployment_target = '13.0' + s.dependency 'Capacitor' + s.dependency 'FloatingPanel' + s.swift_version = '5.1' +end diff --git a/mobile/plugins/native-bottom-sheet/README.md b/mobile/plugins/native-bottom-sheet/README.md new file mode 100644 index 00000000..6b92d0d0 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/README.md @@ -0,0 +1,248 @@ +# native-bottom-sheet + +Allows to open a native BottomSheet/FloatingPanel on iOS + +## Install + +```bash +npm install native-bottom-sheet +npx cap sync +``` + +## API + + + +* [`prepare()`](#prepare) +* [`delegate(...)`](#delegate) +* [`release(...)`](#release) +* [`openSelf(...)`](#openself) +* [`closeSelf(...)`](#closeself) +* [`setSelfSize(...)`](#setselfsize) +* [`callActionInMain(...)`](#callactioninmain) +* [`callActionInNative(...)`](#callactioninnative) +* [`openInMain(...)`](#openinmain) +* [`addListener('delegate', ...)`](#addlistenerdelegate) +* [`addListener('move', ...)`](#addlistenermove) +* [`addListener('callActionInMain', ...)`](#addlistenercallactioninmain) +* [`addListener('callActionInNative', ...)`](#addlistenercallactioninnative) +* [`addListener('openInMain', ...)`](#addlisteneropeninmain) +* [Interfaces](#interfaces) +* [Type Aliases](#type-aliases) + + + + + + +### prepare() + +```typescript +prepare() => Promise +``` + +-------------------- + + +### delegate(...) + +```typescript +delegate(options: { key: BottomSheetKeys; globalJson: string; }) => Promise +``` + +| Param | Type | +| ------------- | ----------------------------------------------------------------------------------------- | +| **`options`** | { key: BottomSheetKeys; globalJson: string; } | + +-------------------- + + +### release(...) + +```typescript +release(options: { key: BottomSheetKeys | '*'; }) => Promise +``` + +| Param | Type | +| ------------- | ---------------------------------------------------------------------------- | +| **`options`** | { key: BottomSheetKeys \| '*'; } | + +-------------------- + + +### openSelf(...) + +```typescript +openSelf(options: { key: BottomSheetKeys; height: string; backgroundColor: string; }) => Promise +``` + +| Param | Type | +| ------------- | -------------------------------------------------------------------------------------------------------------- | +| **`options`** | { key: BottomSheetKeys; height: string; backgroundColor: string; } | + +-------------------- + + +### closeSelf(...) + +```typescript +closeSelf(options: { key: BottomSheetKeys; }) => Promise +``` + +| Param | Type | +| ------------- | --------------------------------------------------------------------- | +| **`options`** | { key: BottomSheetKeys; } | + +-------------------- + + +### setSelfSize(...) + +```typescript +setSelfSize(options: { size: 'half' | 'full'; }) => Promise +``` + +| Param | Type | +| ------------- | ---------------------------------------- | +| **`options`** | { size: 'half' \| 'full'; } | + +-------------------- + + +### callActionInMain(...) + +```typescript +callActionInMain(options: { name: string; optionsJson: string; }) => Promise +``` + +| Param | Type | +| ------------- | --------------------------------------------------- | +| **`options`** | { name: string; optionsJson: string; } | + +-------------------- + + +### callActionInNative(...) + +```typescript +callActionInNative(options: { name: string; optionsJson: string; }) => Promise +``` + +| Param | Type | +| ------------- | --------------------------------------------------- | +| **`options`** | { name: string; optionsJson: string; } | + +-------------------- + + +### openInMain(...) + +```typescript +openInMain(options: { key: BottomSheetKeys; }) => Promise +``` + +| Param | Type | +| ------------- | --------------------------------------------------------------------- | +| **`options`** | { key: BottomSheetKeys; } | + +-------------------- + + +### addListener('delegate', ...) + +```typescript +addListener(eventName: 'delegate', handler: (options: { key: BottomSheetKeys; globalJson: string; }) => void) => Promise & PluginListenerHandle +``` + +| Param | Type | +| --------------- | --------------------------------------------------------------------------------------------------------------- | +| **`eventName`** | 'delegate' | +| **`handler`** | (options: { key: BottomSheetKeys; globalJson: string; }) => void | + +**Returns:** Promise<PluginListenerHandle> & PluginListenerHandle + +-------------------- + + +### addListener('move', ...) + +```typescript +addListener(eventName: 'move', handler: () => void) => Promise & PluginListenerHandle +``` + +| Param | Type | +| --------------- | -------------------------- | +| **`eventName`** | 'move' | +| **`handler`** | () => void | + +**Returns:** Promise<PluginListenerHandle> & PluginListenerHandle + +-------------------- + + +### addListener('callActionInMain', ...) + +```typescript +addListener(eventName: 'callActionInMain', handler: (options: { name: string; optionsJson: string; }) => void) => Promise & PluginListenerHandle +``` + +| Param | Type | +| --------------- | ------------------------------------------------------------------------- | +| **`eventName`** | 'callActionInMain' | +| **`handler`** | (options: { name: string; optionsJson: string; }) => void | + +**Returns:** Promise<PluginListenerHandle> & PluginListenerHandle + +-------------------- + + +### addListener('callActionInNative', ...) + +```typescript +addListener(eventName: 'callActionInNative', handler: (options: { name: string; optionsJson: string; }) => void) => Promise & PluginListenerHandle +``` + +| Param | Type | +| --------------- | ------------------------------------------------------------------------- | +| **`eventName`** | 'callActionInNative' | +| **`handler`** | (options: { name: string; optionsJson: string; }) => void | + +**Returns:** Promise<PluginListenerHandle> & PluginListenerHandle + +-------------------- + + +### addListener('openInMain', ...) + +```typescript +addListener(eventName: 'openInMain', handler: (options: { key: BottomSheetKeys; }) => void) => Promise & PluginListenerHandle +``` + +| Param | Type | +| --------------- | ------------------------------------------------------------------------------------------- | +| **`eventName`** | 'openInMain' | +| **`handler`** | (options: { key: BottomSheetKeys; }) => void | + +**Returns:** Promise<PluginListenerHandle> & PluginListenerHandle + +-------------------- + + +### Interfaces + + +#### PluginListenerHandle + +| Prop | Type | +| ------------ | ----------------------------------------- | +| **`remove`** | () => Promise<void> | + + +### Type Aliases + + +#### BottomSheetKeys + +'initial' | 'receive' | 'invoice' | 'transfer' | 'swap' | 'stake' | 'unstake' | 'staking-info' | 'transaction-info' | 'swap-activity' | 'backup' | 'add-account' | 'settings' | 'qr-scanner' | 'dapp-connect' | 'dapp-transaction' | 'disclaimer' | 'backup-warning' + + diff --git a/mobile/plugins/native-bottom-sheet/android/.gitignore b/mobile/plugins/native-bottom-sheet/android/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/android/.gitignore @@ -0,0 +1 @@ +/build diff --git a/mobile/plugins/native-bottom-sheet/android/build.gradle b/mobile/plugins/native-bottom-sheet/android/build.gradle new file mode 100644 index 00000000..d27d08bd --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/android/build.gradle @@ -0,0 +1,58 @@ +ext { + junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2' + androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.6.1' + androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.1.5' + androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.5.1' +} + +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.0.0' + } +} + +apply plugin: 'com.android.library' + +android { + namespace "org.mytonwallet.plugins.nativebottomsheet" + compileSdkVersion project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 33 + defaultConfig { + minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 22 + targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 33 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + lintOptions { + abortOnError false + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } +} + +repositories { + google() + mavenCentral() +} + + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation project(':capacitor-android') + implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" + testImplementation "junit:junit:$junitVersion" + androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" +} diff --git a/mobile/plugins/native-bottom-sheet/android/gradle.properties b/mobile/plugins/native-bottom-sheet/android/gradle.properties new file mode 100644 index 00000000..2e87c52f --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/android/gradle.properties @@ -0,0 +1,22 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true diff --git a/mobile/plugins/native-bottom-sheet/android/gradle/wrapper/gradle-wrapper.jar b/mobile/plugins/native-bottom-sheet/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..ccebba77 Binary files /dev/null and b/mobile/plugins/native-bottom-sheet/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/mobile/plugins/native-bottom-sheet/android/gradle/wrapper/gradle-wrapper.properties b/mobile/plugins/native-bottom-sheet/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..761b8f08 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/mobile/plugins/native-bottom-sheet/android/gradlew b/mobile/plugins/native-bottom-sheet/android/gradlew new file mode 100755 index 00000000..79a61d42 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/android/gradlew @@ -0,0 +1,244 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/mobile/plugins/native-bottom-sheet/android/gradlew.bat b/mobile/plugins/native-bottom-sheet/android/gradlew.bat new file mode 100644 index 00000000..93e3f59f --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/android/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mobile/plugins/native-bottom-sheet/android/proguard-rules.pro b/mobile/plugins/native-bottom-sheet/android/proguard-rules.pro new file mode 100644 index 00000000..f1b42451 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/android/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/mobile/plugins/native-bottom-sheet/android/settings.gradle b/mobile/plugins/native-bottom-sheet/android/settings.gradle new file mode 100644 index 00000000..1e5b8431 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/android/settings.gradle @@ -0,0 +1,2 @@ +include ':capacitor-android' +project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') \ No newline at end of file diff --git a/mobile/plugins/native-bottom-sheet/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java b/mobile/plugins/native-bottom-sheet/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java new file mode 100644 index 00000000..58020e16 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.getcapacitor.android; + +import static org.junit.Assert.*; + +import android.content.Context; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("com.getcapacitor.android", appContext.getPackageName()); + } +} diff --git a/mobile/plugins/native-bottom-sheet/android/src/main/AndroidManifest.xml b/mobile/plugins/native-bottom-sheet/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a2f47b60 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/android/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/mobile/plugins/native-bottom-sheet/android/src/main/java/org/mytonwallet/plugins/nativebottomsheet/BottomSheet.java b/mobile/plugins/native-bottom-sheet/android/src/main/java/org/mytonwallet/plugins/nativebottomsheet/BottomSheet.java new file mode 100644 index 00000000..ae8b697c --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/android/src/main/java/org/mytonwallet/plugins/nativebottomsheet/BottomSheet.java @@ -0,0 +1,11 @@ +package org.mytonwallet.plugins.nativebottomsheet; + +import android.util.Log; + +public class BottomSheet { + + public String echo(String value) { + Log.i("Echo", value); + return value; + } +} diff --git a/mobile/plugins/native-bottom-sheet/android/src/main/java/org/mytonwallet/plugins/nativebottomsheet/BottomSheetPlugin.java b/mobile/plugins/native-bottom-sheet/android/src/main/java/org/mytonwallet/plugins/nativebottomsheet/BottomSheetPlugin.java new file mode 100644 index 00000000..701bc663 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/android/src/main/java/org/mytonwallet/plugins/nativebottomsheet/BottomSheetPlugin.java @@ -0,0 +1,22 @@ +package org.mytonwallet.plugins.nativebottomsheet; + +import com.getcapacitor.JSObject; +import com.getcapacitor.Plugin; +import com.getcapacitor.PluginCall; +import com.getcapacitor.PluginMethod; +import com.getcapacitor.annotation.CapacitorPlugin; + +@CapacitorPlugin(name = "BottomSheet") +public class BottomSheetPlugin extends Plugin { + + private BottomSheet implementation = new BottomSheet(); + + @PluginMethod + public void echo(PluginCall call) { + String value = call.getString("value"); + + JSObject ret = new JSObject(); + ret.put("value", implementation.echo(value)); + call.resolve(ret); + } +} diff --git a/mobile/plugins/native-bottom-sheet/android/src/main/res/.gitkeep b/mobile/plugins/native-bottom-sheet/android/src/main/res/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/mobile/plugins/native-bottom-sheet/android/src/test/java/com/getcapacitor/ExampleUnitTest.java b/mobile/plugins/native-bottom-sheet/android/src/test/java/com/getcapacitor/ExampleUnitTest.java new file mode 100644 index 00000000..a0fed0cf --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/android/src/test/java/com/getcapacitor/ExampleUnitTest.java @@ -0,0 +1,18 @@ +package com.getcapacitor; + +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} diff --git a/mobile/plugins/native-bottom-sheet/dist/docs.json b/mobile/plugins/native-bottom-sheet/dist/docs.json new file mode 100644 index 00000000..a0595e64 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/dist/docs.json @@ -0,0 +1,377 @@ +{ + "api": { + "name": "BottomSheetPlugin", + "slug": "bottomsheetplugin", + "docs": "", + "tags": [], + "methods": [ + { + "name": "prepare", + "signature": "() => Promise", + "parameters": [], + "returns": "Promise", + "tags": [], + "docs": "", + "complexTypes": [], + "slug": "prepare" + }, + { + "name": "delegate", + "signature": "(options: { key: BottomSheetKeys; globalJson: string; }) => Promise", + "parameters": [ + { + "name": "options", + "docs": "", + "type": "{ key: BottomSheetKeys; globalJson: string; }" + } + ], + "returns": "Promise", + "tags": [], + "docs": "", + "complexTypes": [ + "BottomSheetKeys" + ], + "slug": "delegate" + }, + { + "name": "release", + "signature": "(options: { key: BottomSheetKeys | '*'; }) => Promise", + "parameters": [ + { + "name": "options", + "docs": "", + "type": "{ key: BottomSheetKeys | '*'; }" + } + ], + "returns": "Promise", + "tags": [], + "docs": "", + "complexTypes": [ + "BottomSheetKeys" + ], + "slug": "release" + }, + { + "name": "openSelf", + "signature": "(options: { key: BottomSheetKeys; height: string; backgroundColor: string; }) => Promise", + "parameters": [ + { + "name": "options", + "docs": "", + "type": "{ key: BottomSheetKeys; height: string; backgroundColor: string; }" + } + ], + "returns": "Promise", + "tags": [], + "docs": "", + "complexTypes": [ + "BottomSheetKeys" + ], + "slug": "openself" + }, + { + "name": "closeSelf", + "signature": "(options: { key: BottomSheetKeys; }) => Promise", + "parameters": [ + { + "name": "options", + "docs": "", + "type": "{ key: BottomSheetKeys; }" + } + ], + "returns": "Promise", + "tags": [], + "docs": "", + "complexTypes": [ + "BottomSheetKeys" + ], + "slug": "closeself" + }, + { + "name": "setSelfSize", + "signature": "(options: { size: 'half' | 'full'; }) => Promise", + "parameters": [ + { + "name": "options", + "docs": "", + "type": "{ size: 'half' | 'full'; }" + } + ], + "returns": "Promise", + "tags": [], + "docs": "", + "complexTypes": [], + "slug": "setselfsize" + }, + { + "name": "callActionInMain", + "signature": "(options: { name: string; optionsJson: string; }) => Promise", + "parameters": [ + { + "name": "options", + "docs": "", + "type": "{ name: string; optionsJson: string; }" + } + ], + "returns": "Promise", + "tags": [], + "docs": "", + "complexTypes": [], + "slug": "callactioninmain" + }, + { + "name": "callActionInNative", + "signature": "(options: { name: string; optionsJson: string; }) => Promise", + "parameters": [ + { + "name": "options", + "docs": "", + "type": "{ name: string; optionsJson: string; }" + } + ], + "returns": "Promise", + "tags": [], + "docs": "", + "complexTypes": [], + "slug": "callactioninnative" + }, + { + "name": "openInMain", + "signature": "(options: { key: BottomSheetKeys; }) => Promise", + "parameters": [ + { + "name": "options", + "docs": "", + "type": "{ key: BottomSheetKeys; }" + } + ], + "returns": "Promise", + "tags": [], + "docs": "", + "complexTypes": [ + "BottomSheetKeys" + ], + "slug": "openinmain" + }, + { + "name": "addListener", + "signature": "(eventName: 'delegate', handler: (options: { key: BottomSheetKeys; globalJson: string; }) => void) => Promise & PluginListenerHandle", + "parameters": [ + { + "name": "eventName", + "docs": "", + "type": "'delegate'" + }, + { + "name": "handler", + "docs": "", + "type": "(options: { key: BottomSheetKeys; globalJson: string; }) => void" + } + ], + "returns": "Promise & PluginListenerHandle", + "tags": [], + "docs": "", + "complexTypes": [ + "PluginListenerHandle", + "BottomSheetKeys" + ], + "slug": "addlistenerdelegate" + }, + { + "name": "addListener", + "signature": "(eventName: 'move', handler: () => void) => Promise & PluginListenerHandle", + "parameters": [ + { + "name": "eventName", + "docs": "", + "type": "'move'" + }, + { + "name": "handler", + "docs": "", + "type": "() => void" + } + ], + "returns": "Promise & PluginListenerHandle", + "tags": [], + "docs": "", + "complexTypes": [ + "PluginListenerHandle" + ], + "slug": "addlistenermove" + }, + { + "name": "addListener", + "signature": "(eventName: 'callActionInMain', handler: (options: { name: string; optionsJson: string; }) => void) => Promise & PluginListenerHandle", + "parameters": [ + { + "name": "eventName", + "docs": "", + "type": "'callActionInMain'" + }, + { + "name": "handler", + "docs": "", + "type": "(options: { name: string; optionsJson: string; }) => void" + } + ], + "returns": "Promise & PluginListenerHandle", + "tags": [], + "docs": "", + "complexTypes": [ + "PluginListenerHandle" + ], + "slug": "addlistenercallactioninmain" + }, + { + "name": "addListener", + "signature": "(eventName: 'callActionInNative', handler: (options: { name: string; optionsJson: string; }) => void) => Promise & PluginListenerHandle", + "parameters": [ + { + "name": "eventName", + "docs": "", + "type": "'callActionInNative'" + }, + { + "name": "handler", + "docs": "", + "type": "(options: { name: string; optionsJson: string; }) => void" + } + ], + "returns": "Promise & PluginListenerHandle", + "tags": [], + "docs": "", + "complexTypes": [ + "PluginListenerHandle" + ], + "slug": "addlistenercallactioninnative" + }, + { + "name": "addListener", + "signature": "(eventName: 'openInMain', handler: (options: { key: BottomSheetKeys; }) => void) => Promise & PluginListenerHandle", + "parameters": [ + { + "name": "eventName", + "docs": "", + "type": "'openInMain'" + }, + { + "name": "handler", + "docs": "", + "type": "(options: { key: BottomSheetKeys; }) => void" + } + ], + "returns": "Promise & PluginListenerHandle", + "tags": [], + "docs": "", + "complexTypes": [ + "PluginListenerHandle", + "BottomSheetKeys" + ], + "slug": "addlisteneropeninmain" + } + ], + "properties": [] + }, + "interfaces": [ + { + "name": "PluginListenerHandle", + "slug": "pluginlistenerhandle", + "docs": "", + "tags": [], + "methods": [], + "properties": [ + { + "name": "remove", + "tags": [], + "docs": "", + "complexTypes": [], + "type": "() => Promise" + } + ] + } + ], + "enums": [], + "typeAliases": [ + { + "name": "BottomSheetKeys", + "slug": "bottomsheetkeys", + "docs": "", + "types": [ + { + "text": "'initial'", + "complexTypes": [] + }, + { + "text": "'receive'", + "complexTypes": [] + }, + { + "text": "'invoice'", + "complexTypes": [] + }, + { + "text": "'transfer'", + "complexTypes": [] + }, + { + "text": "'swap'", + "complexTypes": [] + }, + { + "text": "'stake'", + "complexTypes": [] + }, + { + "text": "'unstake'", + "complexTypes": [] + }, + { + "text": "'staking-info'", + "complexTypes": [] + }, + { + "text": "'transaction-info'", + "complexTypes": [] + }, + { + "text": "'swap-activity'", + "complexTypes": [] + }, + { + "text": "'backup'", + "complexTypes": [] + }, + { + "text": "'add-account'", + "complexTypes": [] + }, + { + "text": "'settings'", + "complexTypes": [] + }, + { + "text": "'qr-scanner'", + "complexTypes": [] + }, + { + "text": "'dapp-connect'", + "complexTypes": [] + }, + { + "text": "'dapp-transaction'", + "complexTypes": [] + }, + { + "text": "'disclaimer'", + "complexTypes": [] + }, + { + "text": "'backup-warning'", + "complexTypes": [] + } + ] + } + ], + "pluginConfigs": [] +} \ No newline at end of file diff --git a/mobile/plugins/native-bottom-sheet/dist/esm/definitions.d.ts b/mobile/plugins/native-bottom-sheet/dist/esm/definitions.d.ts new file mode 100644 index 00000000..9af22629 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/dist/esm/definitions.d.ts @@ -0,0 +1,50 @@ +import { PluginListenerHandle } from '@capacitor/core'; +export declare type BottomSheetKeys = 'initial' | 'receive' | 'invoice' | 'transfer' | 'swap' | 'stake' | 'unstake' | 'staking-info' | 'transaction-info' | 'swap-activity' | 'backup' | 'add-account' | 'settings' | 'qr-scanner' | 'dapp-connect' | 'dapp-transaction' | 'disclaimer' | 'backup-warning'; +export interface BottomSheetPlugin { + prepare(): Promise; + delegate(options: { + key: BottomSheetKeys; + globalJson: string; + }): Promise; + release(options: { + key: BottomSheetKeys | '*'; + }): Promise; + openSelf(options: { + key: BottomSheetKeys; + height: string; + backgroundColor: string; + }): Promise; + closeSelf(options: { + key: BottomSheetKeys; + }): Promise; + setSelfSize(options: { + size: 'half' | 'full'; + }): Promise; + callActionInMain(options: { + name: string; + optionsJson: string; + }): Promise; + callActionInNative(options: { + name: string; + optionsJson: string; + }): Promise; + openInMain(options: { + key: BottomSheetKeys; + }): Promise; + addListener(eventName: 'delegate', handler: (options: { + key: BottomSheetKeys; + globalJson: string; + }) => void): Promise & PluginListenerHandle; + addListener(eventName: 'move', handler: () => void): Promise & PluginListenerHandle; + addListener(eventName: 'callActionInMain', handler: (options: { + name: string; + optionsJson: string; + }) => void): Promise & PluginListenerHandle; + addListener(eventName: 'callActionInNative', handler: (options: { + name: string; + optionsJson: string; + }) => void): Promise & PluginListenerHandle; + addListener(eventName: 'openInMain', handler: (options: { + key: BottomSheetKeys; + }) => void): Promise & PluginListenerHandle; +} diff --git a/mobile/plugins/native-bottom-sheet/dist/esm/definitions.js b/mobile/plugins/native-bottom-sheet/dist/esm/definitions.js new file mode 100644 index 00000000..497acb52 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/dist/esm/definitions.js @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=definitions.js.map \ No newline at end of file diff --git a/mobile/plugins/native-bottom-sheet/dist/esm/definitions.js.map b/mobile/plugins/native-bottom-sheet/dist/esm/definitions.js.map new file mode 100644 index 00000000..105a052d --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/dist/esm/definitions.js.map @@ -0,0 +1 @@ +{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import { PluginListenerHandle } from '@capacitor/core';\n\nexport type BottomSheetKeys =\n 'initial'\n | 'receive'\n | 'invoice'\n | 'transfer'\n | 'swap'\n | 'stake'\n | 'unstake'\n | 'staking-info'\n | 'transaction-info'\n | 'swap-activity'\n | 'backup'\n | 'add-account'\n | 'settings'\n | 'qr-scanner'\n | 'dapp-connect'\n | 'dapp-transaction'\n | 'disclaimer'\n | 'backup-warning';\n\nexport interface BottomSheetPlugin {\n prepare(): Promise;\n\n delegate(options: { key: BottomSheetKeys, globalJson: string }): Promise;\n\n release(options: { key: BottomSheetKeys | '*' }): Promise;\n\n openSelf(options: { key: BottomSheetKeys, height: string, backgroundColor: string }): Promise;\n\n closeSelf(options: { key: BottomSheetKeys }): Promise;\n\n setSelfSize(options: { size: 'half' | 'full' }): Promise;\n\n callActionInMain(options: { name: string, optionsJson: string }): Promise;\n\n callActionInNative(options: { name: string, optionsJson: string }): Promise;\n\n openInMain(options: { key: BottomSheetKeys }): Promise;\n\n addListener(\n eventName: 'delegate',\n handler: (options: { key: BottomSheetKeys, globalJson: string }) => void,\n ): Promise & PluginListenerHandle;\n\n addListener(\n eventName: 'move',\n handler: () => void,\n ): Promise & PluginListenerHandle;\n\n addListener(\n eventName: 'callActionInMain',\n handler: (options: { name: string, optionsJson: string }) => void,\n ): Promise & PluginListenerHandle;\n\n addListener(\n eventName: 'callActionInNative',\n handler: (options: { name: string, optionsJson: string }) => void,\n ): Promise & PluginListenerHandle;\n\n addListener(\n eventName: 'openInMain',\n handler: (options: { key: BottomSheetKeys }) => void,\n ): Promise & PluginListenerHandle;\n}\n"]} \ No newline at end of file diff --git a/mobile/plugins/native-bottom-sheet/dist/esm/index.d.ts b/mobile/plugins/native-bottom-sheet/dist/esm/index.d.ts new file mode 100644 index 00000000..beb13107 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/dist/esm/index.d.ts @@ -0,0 +1,4 @@ +import type { BottomSheetPlugin } from './definitions'; +declare const BottomSheet: BottomSheetPlugin; +export * from './definitions'; +export { BottomSheet }; diff --git a/mobile/plugins/native-bottom-sheet/dist/esm/index.js b/mobile/plugins/native-bottom-sheet/dist/esm/index.js new file mode 100644 index 00000000..0b0286c7 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/dist/esm/index.js @@ -0,0 +1,5 @@ +import { registerPlugin } from '@capacitor/core'; +const BottomSheet = registerPlugin('BottomSheet'); +export * from './definitions'; +export { BottomSheet }; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/mobile/plugins/native-bottom-sheet/dist/esm/index.js.map b/mobile/plugins/native-bottom-sheet/dist/esm/index.js.map new file mode 100644 index 00000000..78558ebe --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/dist/esm/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAIjD,MAAM,WAAW,GAAG,cAAc,CAAoB,aAAa,CAAC,CAAC;AAErE,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,WAAW,EAAE,CAAC","sourcesContent":["import { registerPlugin } from '@capacitor/core';\n\nimport type { BottomSheetPlugin } from './definitions';\n\nconst BottomSheet = registerPlugin('BottomSheet');\n\nexport * from './definitions';\nexport { BottomSheet };\n"]} \ No newline at end of file diff --git a/mobile/plugins/native-bottom-sheet/ios/Plugin.xcodeproj/project.pbxproj b/mobile/plugins/native-bottom-sheet/ios/Plugin.xcodeproj/project.pbxproj new file mode 100644 index 00000000..93cbe6c9 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/ios/Plugin.xcodeproj/project.pbxproj @@ -0,0 +1,573 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 48; + objects = { + +/* Begin PBXBuildFile section */ + 03FC29A292ACC40490383A1F /* Pods_Plugin.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B2A61DA5A1F2DD4F959604D /* Pods_Plugin.framework */; }; + 20C0B05DCFC8E3958A738AF2 /* Pods_PluginTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F6753A823D3815DB436415E3 /* Pods_PluginTests.framework */; }; + 2F98D68224C9AAE500613A4C /* BottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F98D68124C9AAE400613A4C /* BottomSheet.swift */; }; + 50ADFF92201F53D600D50D53 /* Plugin.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50ADFF88201F53D600D50D53 /* Plugin.framework */; }; + 50ADFF97201F53D600D50D53 /* BottomSheetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50ADFF96201F53D600D50D53 /* BottomSheetTests.swift */; }; + 50ADFF99201F53D600D50D53 /* BottomSheetPlugin.h in Headers */ = {isa = PBXBuildFile; fileRef = 50ADFF8B201F53D600D50D53 /* BottomSheetPlugin.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 50ADFFA42020D75100D50D53 /* Capacitor.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50ADFFA52020D75100D50D53 /* Capacitor.framework */; }; + 50ADFFA82020EE4F00D50D53 /* BottomSheetPlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = 50ADFFA72020EE4F00D50D53 /* BottomSheetPlugin.m */; }; + 50E1A94820377CB70090CE1A /* BottomSheetPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E1A94720377CB70090CE1A /* BottomSheetPlugin.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 50ADFF93201F53D600D50D53 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 50ADFF7F201F53D600D50D53 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 50ADFF87201F53D600D50D53; + remoteInfo = Plugin; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 2F98D68124C9AAE400613A4C /* BottomSheet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BottomSheet.swift; sourceTree = ""; }; + 3B2A61DA5A1F2DD4F959604D /* Pods_Plugin.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Plugin.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 50ADFF88201F53D600D50D53 /* Plugin.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Plugin.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 50ADFF8B201F53D600D50D53 /* BottomSheetPlugin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BottomSheetPlugin.h; sourceTree = ""; }; + 50ADFF8C201F53D600D50D53 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 50ADFF91201F53D600D50D53 /* PluginTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PluginTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 50ADFF96201F53D600D50D53 /* BottomSheetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetTests.swift; sourceTree = ""; }; + 50ADFF98201F53D600D50D53 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 50ADFFA52020D75100D50D53 /* Capacitor.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Capacitor.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 50ADFFA72020EE4F00D50D53 /* BottomSheetPlugin.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BottomSheetPlugin.m; sourceTree = ""; }; + 50E1A94720377CB70090CE1A /* BottomSheetPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BottomSheetPlugin.swift; sourceTree = ""; }; + 5E23F77F099397094342571A /* Pods-Plugin.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Plugin.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Plugin/Pods-Plugin.debug.xcconfig"; sourceTree = ""; }; + 91781294A431A2A7CC6EB714 /* Pods-Plugin.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Plugin.release.xcconfig"; path = "Pods/Target Support Files/Pods-Plugin/Pods-Plugin.release.xcconfig"; sourceTree = ""; }; + 96ED1B6440D6672E406C8D19 /* Pods-PluginTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PluginTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.debug.xcconfig"; sourceTree = ""; }; + F65BB2953ECE002E1EF3E424 /* Pods-PluginTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PluginTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.release.xcconfig"; sourceTree = ""; }; + F6753A823D3815DB436415E3 /* Pods_PluginTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PluginTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 50ADFF84201F53D600D50D53 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 50ADFFA42020D75100D50D53 /* Capacitor.framework in Frameworks */, + 03FC29A292ACC40490383A1F /* Pods_Plugin.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 50ADFF8E201F53D600D50D53 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 50ADFF92201F53D600D50D53 /* Plugin.framework in Frameworks */, + 20C0B05DCFC8E3958A738AF2 /* Pods_PluginTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 50ADFF7E201F53D600D50D53 = { + isa = PBXGroup; + children = ( + 50ADFF8A201F53D600D50D53 /* Plugin */, + 50ADFF95201F53D600D50D53 /* PluginTests */, + 50ADFF89201F53D600D50D53 /* Products */, + 8C8E7744173064A9F6D438E3 /* Pods */, + A797B9EFA3DCEFEA1FBB66A9 /* Frameworks */, + ); + sourceTree = ""; + }; + 50ADFF89201F53D600D50D53 /* Products */ = { + isa = PBXGroup; + children = ( + 50ADFF88201F53D600D50D53 /* Plugin.framework */, + 50ADFF91201F53D600D50D53 /* PluginTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 50ADFF8A201F53D600D50D53 /* Plugin */ = { + isa = PBXGroup; + children = ( + 50E1A94720377CB70090CE1A /* BottomSheetPlugin.swift */, + 2F98D68124C9AAE400613A4C /* BottomSheet.swift */, + 50ADFF8B201F53D600D50D53 /* BottomSheetPlugin.h */, + 50ADFFA72020EE4F00D50D53 /* BottomSheetPlugin.m */, + 50ADFF8C201F53D600D50D53 /* Info.plist */, + ); + path = Plugin; + sourceTree = ""; + }; + 50ADFF95201F53D600D50D53 /* PluginTests */ = { + isa = PBXGroup; + children = ( + 50ADFF96201F53D600D50D53 /* BottomSheetTests.swift */, + 50ADFF98201F53D600D50D53 /* Info.plist */, + ); + path = PluginTests; + sourceTree = ""; + }; + 8C8E7744173064A9F6D438E3 /* Pods */ = { + isa = PBXGroup; + children = ( + 5E23F77F099397094342571A /* Pods-Plugin.debug.xcconfig */, + 91781294A431A2A7CC6EB714 /* Pods-Plugin.release.xcconfig */, + 96ED1B6440D6672E406C8D19 /* Pods-PluginTests.debug.xcconfig */, + F65BB2953ECE002E1EF3E424 /* Pods-PluginTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + A797B9EFA3DCEFEA1FBB66A9 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 50ADFFA52020D75100D50D53 /* Capacitor.framework */, + 3B2A61DA5A1F2DD4F959604D /* Pods_Plugin.framework */, + F6753A823D3815DB436415E3 /* Pods_PluginTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 50ADFF85201F53D600D50D53 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 50ADFF99201F53D600D50D53 /* BottomSheetPlugin.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 50ADFF87201F53D600D50D53 /* Plugin */ = { + isa = PBXNativeTarget; + buildConfigurationList = 50ADFF9C201F53D600D50D53 /* Build configuration list for PBXNativeTarget "Plugin" */; + buildPhases = ( + AB5B3E54B4E897F32C2279DA /* [CP] Check Pods Manifest.lock */, + 50ADFF83201F53D600D50D53 /* Sources */, + 50ADFF84201F53D600D50D53 /* Frameworks */, + 50ADFF85201F53D600D50D53 /* Headers */, + 50ADFF86201F53D600D50D53 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Plugin; + productName = Plugin; + productReference = 50ADFF88201F53D600D50D53 /* Plugin.framework */; + productType = "com.apple.product-type.framework"; + }; + 50ADFF90201F53D600D50D53 /* PluginTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 50ADFF9F201F53D600D50D53 /* Build configuration list for PBXNativeTarget "PluginTests" */; + buildPhases = ( + 0596884F929ED6F1DE134961 /* [CP] Check Pods Manifest.lock */, + 50ADFF8D201F53D600D50D53 /* Sources */, + 50ADFF8E201F53D600D50D53 /* Frameworks */, + 50ADFF8F201F53D600D50D53 /* Resources */, + 8E97F58B69A94C6503FC9C85 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 50ADFF94201F53D600D50D53 /* PBXTargetDependency */, + ); + name = PluginTests; + productName = PluginTests; + productReference = 50ADFF91201F53D600D50D53 /* PluginTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 50ADFF7F201F53D600D50D53 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1160; + ORGANIZATIONNAME = "Max Lynch"; + TargetAttributes = { + 50ADFF87201F53D600D50D53 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + }; + 50ADFF90201F53D600D50D53 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 50ADFF82201F53D600D50D53 /* Build configuration list for PBXProject "Plugin" */; + compatibilityVersion = "Xcode 8.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 50ADFF7E201F53D600D50D53; + productRefGroup = 50ADFF89201F53D600D50D53 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 50ADFF87201F53D600D50D53 /* Plugin */, + 50ADFF90201F53D600D50D53 /* PluginTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 50ADFF86201F53D600D50D53 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 50ADFF8F201F53D600D50D53 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0596884F929ED6F1DE134961 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-PluginTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 8E97F58B69A94C6503FC9C85 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-PluginTests/Pods-PluginTests-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/Capacitor/Capacitor.framework", + "${BUILT_PRODUCTS_DIR}/CapacitorCordova/Cordova.framework", + "${BUILT_PRODUCTS_DIR}/FloatingPanel/FloatingPanel.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Capacitor.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Cordova.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FloatingPanel.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-PluginTests/Pods-PluginTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + AB5B3E54B4E897F32C2279DA /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Plugin-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 50ADFF83201F53D600D50D53 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 50E1A94820377CB70090CE1A /* BottomSheetPlugin.swift in Sources */, + 2F98D68224C9AAE500613A4C /* BottomSheet.swift in Sources */, + 50ADFFA82020EE4F00D50D53 /* BottomSheetPlugin.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 50ADFF8D201F53D600D50D53 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 50ADFF97201F53D600D50D53 /* BottomSheetTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 50ADFF94201F53D600D50D53 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 50ADFF87201F53D600D50D53 /* Plugin */; + targetProxy = 50ADFF93201F53D600D50D53 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 50ADFF9A201F53D600D50D53 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + FRAMEWORK_SEARCH_PATHS = ( + "\"${BUILT_PRODUCTS_DIR}/Capacitor\"", + "\"${BUILT_PRODUCTS_DIR}/CapacitorCordova\"", + ); + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 50ADFF9B201F53D600D50D53 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + FRAMEWORK_SEARCH_PATHS = ( + "\"${BUILT_PRODUCTS_DIR}/Capacitor\"", + "\"${BUILT_PRODUCTS_DIR}/CapacitorCordova\"", + ); + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 50ADFF9D201F53D600D50D53 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5E23F77F099397094342571A /* Pods-Plugin.debug.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Plugin/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks $(FRAMEWORK_SEARCH_PATHS)\n$(FRAMEWORK_SEARCH_PATHS)\n$(FRAMEWORK_SEARCH_PATHS)"; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.getcapacitor.Plugin; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 50ADFF9E201F53D600D50D53 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 91781294A431A2A7CC6EB714 /* Pods-Plugin.release.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Plugin/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks $(FRAMEWORK_SEARCH_PATHS)"; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = com.getcapacitor.Plugin; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 50ADFFA0201F53D600D50D53 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 96ED1B6440D6672E406C8D19 /* Pods-PluginTests.debug.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = PluginTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.getcapacitor.PluginTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 50ADFFA1201F53D600D50D53 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F65BB2953ECE002E1EF3E424 /* Pods-PluginTests.release.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = PluginTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.getcapacitor.PluginTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 50ADFF82201F53D600D50D53 /* Build configuration list for PBXProject "Plugin" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 50ADFF9A201F53D600D50D53 /* Debug */, + 50ADFF9B201F53D600D50D53 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 50ADFF9C201F53D600D50D53 /* Build configuration list for PBXNativeTarget "Plugin" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 50ADFF9D201F53D600D50D53 /* Debug */, + 50ADFF9E201F53D600D50D53 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 50ADFF9F201F53D600D50D53 /* Build configuration list for PBXNativeTarget "PluginTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 50ADFFA0201F53D600D50D53 /* Debug */, + 50ADFFA1201F53D600D50D53 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 50ADFF7F201F53D600D50D53 /* Project object */; +} diff --git a/mobile/plugins/native-bottom-sheet/ios/Plugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/mobile/plugins/native-bottom-sheet/ios/Plugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/ios/Plugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/mobile/plugins/native-bottom-sheet/ios/Plugin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/mobile/plugins/native-bottom-sheet/ios/Plugin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/ios/Plugin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/mobile/plugins/native-bottom-sheet/ios/Plugin.xcodeproj/xcshareddata/xcschemes/Plugin.xcscheme b/mobile/plugins/native-bottom-sheet/ios/Plugin.xcodeproj/xcshareddata/xcschemes/Plugin.xcscheme new file mode 100644 index 00000000..303f2621 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/ios/Plugin.xcodeproj/xcshareddata/xcschemes/Plugin.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/plugins/native-bottom-sheet/ios/Plugin.xcodeproj/xcshareddata/xcschemes/PluginTests.xcscheme b/mobile/plugins/native-bottom-sheet/ios/Plugin.xcodeproj/xcshareddata/xcschemes/PluginTests.xcscheme new file mode 100644 index 00000000..3d8c88d2 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/ios/Plugin.xcodeproj/xcshareddata/xcschemes/PluginTests.xcscheme @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/plugins/native-bottom-sheet/ios/Plugin.xcworkspace/contents.xcworkspacedata b/mobile/plugins/native-bottom-sheet/ios/Plugin.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..afad624e --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/ios/Plugin.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/mobile/plugins/native-bottom-sheet/ios/Plugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/mobile/plugins/native-bottom-sheet/ios/Plugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/ios/Plugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/mobile/plugins/native-bottom-sheet/ios/Plugin/BottomSheetPlugin.h b/mobile/plugins/native-bottom-sheet/ios/Plugin/BottomSheetPlugin.h new file mode 100644 index 00000000..f2bd9e0b --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/ios/Plugin/BottomSheetPlugin.h @@ -0,0 +1,10 @@ +#import + +//! Project version number for Plugin. +FOUNDATION_EXPORT double PluginVersionNumber; + +//! Project version string for Plugin. +FOUNDATION_EXPORT const unsigned char PluginVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + diff --git a/mobile/plugins/native-bottom-sheet/ios/Plugin/BottomSheetPlugin.m b/mobile/plugins/native-bottom-sheet/ios/Plugin/BottomSheetPlugin.m new file mode 100644 index 00000000..3823ad5c --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/ios/Plugin/BottomSheetPlugin.m @@ -0,0 +1,16 @@ +#import +#import + +// Define the plugin using the CAP_PLUGIN Macro, and +// each method the plugin supports using the CAP_PLUGIN_METHOD macro. +CAP_PLUGIN(BottomSheetPlugin, "BottomSheet", + CAP_PLUGIN_METHOD(prepare, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(delegate, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(release, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(openSelf, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(closeSelf, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(setSelfSize, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(callActionInMain, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(callActionInNative, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(openInMain, CAPPluginReturnPromise); +) diff --git a/mobile/plugins/native-bottom-sheet/ios/Plugin/BottomSheetPlugin.swift b/mobile/plugins/native-bottom-sheet/ios/Plugin/BottomSheetPlugin.swift new file mode 100644 index 00000000..5cd829ae --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/ios/Plugin/BottomSheetPlugin.swift @@ -0,0 +1,394 @@ +import Capacitor +import FloatingPanel +import UIKit +import WebKit + +private let CORNER_RADIUS = 16.0 +private let EASING_1 = CGPoint(x: 0.16, y: 1) +private let EASING_2 = CGPoint(x: 0.3, y: 1) +private let ANIMATION_DURATION = 0.600 +private let HALF_FRACTIONAL_INSET = 0.5 +private let MAX_HALF_INSET = 0.85 +private let FULL_INSET = 8.0 + +@objc(BottomSheetPlugin) +public class BottomSheetPlugin: CAPPlugin, FloatingPanelControllerDelegate { + let timingParameters = UICubicTimingParameters(controlPoint1: EASING_1, controlPoint2: EASING_2) + let fpc = FloatingPanelController() + + var capVc: CAPBridgeViewControllerForBottomSheet? + var isPrepared = false + var currentDelegateCall: CAPPluginCall? + public var currentOpenSelfCall: CAPPluginCall? + var currentHalfY: CGFloat? + var prevStatusBarStyle: UIStatusBarStyle? + + @objc func prepare(_ call: CAPPluginCall) { + ensureLocalOrigin() + + isPrepared = true + + DispatchQueue.main.async { [self] in + fpc.layout = MyPanelLayout() + fpc.delegate = self + fpc.isRemovalInteractionEnabled = false + fpc.backdropView.dismissalTapGestureRecognizer.isEnabled = false + fpc.surfaceView.appearance.cornerRadius = CORNER_RADIUS + fpc.surfaceView.grabberHandle.isHidden = true + + capVc = CAPBridgeViewControllerForBottomSheet() + fpc.set(contentViewController: capVc) + fpc.track(scrollView: capVc!.webView!.scrollView) + // Fix redunant scroll offsets + fpc.contentInsetAdjustmentBehavior = .never + + setupScrollReducers() + + let topVc = bridge!.viewController! + topVc.view.clipsToBounds = true + topVc.present(fpc, animated: false) { + call.resolve() + } + } + } + + @objc func delegate(_ call: CAPPluginCall) { + ensureLocalOrigin() + ensureDelegating() + + resolveOpenCalls() + currentDelegateCall = call + + DispatchQueue.main.async { [self] in + capVc!.bridge?.plugin(withName: "BottomSheet")!.notifyListeners("delegate", data: [ + "key": call.getString("key")!, + "globalJson": call.getString("globalJson")! + ]) + + let screenHeight = bridge!.viewController!.view!.superview!.frame.height + currentHalfY = screenHeight - screenHeight * HALF_FRACTIONAL_INSET + } + } + + @objc func release(_ call: CAPPluginCall) { + ensureLocalOrigin() + ensureDelegating() + + DispatchQueue.main.async { [self] in + let releaseKey = call.getString("key") + if releaseKey == currentDelegateCall?.getString("key") || releaseKey == "*" { + doClose() + } + + call.resolve() + } + } + + @objc func openSelf(_ call: CAPPluginCall) { + ensureLocalOrigin() + ensureDelegated() + + currentOpenSelfCall = call + + DispatchQueue.main.async { [self] in + let parentFpc = bridge!.viewController!.parent! as! FloatingPanelController + parentFpc.surfaceView.backgroundColor = UIColor(hexString: call.getString("backgroundColor")!) + + let topVc = parentFpc.presentingViewController as! CAPBridgeViewController + let topBottomSheetPlugin = topVc.bridge!.plugin(withName: "BottomSheet") as! BottomSheetPlugin + + topBottomSheetPlugin.doOpen( + height: CGFloat(Float(call.getString("height")!)!) + ) + + bridge!.webView!.becomeFirstResponder() + } + } + + @objc func closeSelf(_ call: CAPPluginCall) { + ensureLocalOrigin() + ensureDelegated() + + call.resolve() + + DispatchQueue.main.async { [self] in + if currentOpenSelfCall == nil || currentOpenSelfCall!.getString("key") != call.getString("key") { + return + } + + let topVc = bridge!.viewController!.parent!.presentingViewController as! CAPBridgeViewController + let topBottomSheetPlugin = topVc.bridge!.plugin(withName: "BottomSheet") as! BottomSheetPlugin + topBottomSheetPlugin.doClose() + } + } + + @objc func setSelfSize(_ call: CAPPluginCall) { + ensureLocalOrigin() + ensureDelegated() + + call.resolve() + + DispatchQueue.main.async { [self] in + if currentOpenSelfCall == nil { + return + } + + let topVc = bridge!.viewController!.parent!.presentingViewController as! CAPBridgeViewController + let topBottomSheetPlugin = topVc.bridge!.plugin(withName: "BottomSheet") as! BottomSheetPlugin + let toFull = call.getString("size") == "full" + let layout = topBottomSheetPlugin.fpc.layout as! MyPanelLayout + + if toFull && layout.anchors[.full] == nil { + layout.anchors[.full] = layout.fullAnchor + } + + topBottomSheetPlugin.animateTo(to: toFull ? .full : .half) + } + } + + @objc func callActionInMain(_ call: CAPPluginCall) { + ensureLocalOrigin() + ensureDelegated() + + call.resolve() + + DispatchQueue.main.async { [self] in + let topVc = bridge!.viewController!.parent!.presentingViewController as! CAPBridgeViewController + let topBottomSheetPlugin = topVc.bridge!.plugin(withName: "BottomSheet") as! BottomSheetPlugin + topBottomSheetPlugin.notifyListeners("callActionInMain", data: [ + "name": call.getString("name")!, + "optionsJson": call.getString("optionsJson")! + ]) + } + } + + @objc func openInMain(_ call: CAPPluginCall) { + ensureLocalOrigin() + ensureDelegated() + + call.resolve() + + DispatchQueue.main.async { [self] in + let topVc = bridge!.viewController!.parent!.presentingViewController as! CAPBridgeViewController + let topBottomSheetPlugin = topVc.bridge!.plugin(withName: "BottomSheet") as! BottomSheetPlugin + topBottomSheetPlugin.notifyListeners("openInMain", data: [ + "key": call.getString("key")! + ]) + } + } + + @objc func callActionInNative(_ call: CAPPluginCall) { + ensureLocalOrigin() + ensureDelegating() + + call.resolve() + + DispatchQueue.main.async { [self] in + capVc!.bridge?.plugin(withName: "BottomSheet")!.notifyListeners("callActionInNative", data: [ + "name": call.getString("name")!, + "optionsJson": call.getString("optionsJson")! + ]) + } + } + + // Extra security level, potentially redundant + private func ensureLocalOrigin() { + DispatchQueue.main.sync { [self] in + precondition(bridge!.webView!.url!.absoluteString.hasPrefix(bridge!.config.serverURL.absoluteString)) + } + } + + private func ensureDelegating() { + precondition(isPrepared) + } + + private func ensureDelegated() { + precondition(!isPrepared) + } + + public func doOpen(height: CGFloat) { + let screenHeight = bridge!.viewController!.view!.superview!.frame.height + let newFractionalInset = min(height / screenHeight, MAX_HALF_INSET) + let layout = fpc.layout as! MyPanelLayout + + if newFractionalInset < MAX_HALF_INSET { + layout.anchors[.half] = FloatingPanelLayoutAnchor(fractionalInset: newFractionalInset, edge: .bottom, referenceGuide: .superview) + layout.anchors[.full] = nil + currentHalfY = screenHeight - screenHeight * newFractionalInset + toggleExtraScroll(false) + animateTo(to: .half) + } else { + layout.anchors[.half] = nil + layout.anchors[.full] = layout.fullAnchor + currentHalfY = screenHeight - screenHeight * HALF_FRACTIONAL_INSET + toggleExtraScroll(true) + animateTo(to: .full) + } + } + + public func doClose() { + resolveOpenCalls() + + if fpc.state == .hidden { + return + } + + animateTo(to: .hidden) + } + + private func resolveOpenCalls() { + currentDelegateCall?.resolve() + currentDelegateCall = nil + + let childBottomSheetPlugin = capVc!.bridge!.plugin(withName: "BottomSheet") as! BottomSheetPlugin + childBottomSheetPlugin.currentOpenSelfCall?.resolve() + childBottomSheetPlugin.currentOpenSelfCall = nil + } + + private func animateTo(to: FloatingPanelState) { + if to == .half && fpc.layout.anchors[.half] == nil { + return + } + + let timing = UICubicTimingParameters(controlPoint1: EASING_1, controlPoint2: EASING_2) + let animator = UIViewPropertyAnimator(duration: ANIMATION_DURATION, timingParameters: timing) + + animator.addAnimations { [self] in + fpc.move(to: to, animated: false) + } + + animator.startAnimation() + } + + public func floatingPanelDidMove(_ fpc: FloatingPanelController) { + if currentHalfY == nil { + return + } + + let view = bridge!.viewController!.view! + let y = fpc.surfaceView.frame.origin.y + let offsetTop = view.safeAreaInsets.top + FULL_INSET + let currentOffsetY = y - offsetTop + let currentHalfOffsetY = currentHalfY! - offsetTop + let progress = 1 - currentOffsetY / currentHalfOffsetY + + let maxMainHeight = view.superview!.frame.height + let minMainHeight = maxMainHeight - (view.safeAreaInsets.top * 2) + let maxScaleFactor = 1 - minMainHeight / maxMainHeight + + let scale = 1 - maxScaleFactor * max(progress, 0) + + view.transform = CGAffineTransform(scaleX: scale, y: scale) + + let topVc = bridge!.viewController! + topVc.view.layer.cornerRadius = scale < 1 ? CORNER_RADIUS : 0.0 + + let childBottomSheetPlugin = capVc!.bridge!.plugin(withName: "BottomSheet") as! BottomSheetPlugin + childBottomSheetPlugin.notifyListeners("move", data: nil) + } + + public func floatingPanelDidChangeState(_ fpc: FloatingPanelController) { + let inEnteringFull = fpc.state == .full && prevStatusBarStyle == nil + let isLeavingFull = fpc.state != .full && prevStatusBarStyle != nil + + if inEnteringFull { + prevStatusBarStyle = bridge!.statusBarStyle + bridge!.statusBarStyle = .lightContent + } else if isLeavingFull { + bridge!.statusBarStyle = prevStatusBarStyle! + prevStatusBarStyle = nil + } + + if fpc.state == .hidden { + resolveOpenCalls() + bridge!.webView!.becomeFirstResponder() + } + } + + // This is a multi-purpose hack: + // 1. For some reason, we need an extra pixel on container for the scroll tracking to work. + // 2. However, it breaks the rubber-band effect, so we remove it when not needed. + // 3. Also, there are some weird defaults by Bottom Sheet Plugin which break focusing, so we override them. + private func toggleExtraScroll(_ withExtraScroll: Bool = false) { + capVc!.webView!.scrollView.contentInset = withExtraScroll + ? .init(top: 0, left: 0, bottom: 1, right: 0) + : .zero + } +} + +// To be extracted to separate "Reduce Scroll Angle" plugin +extension BottomSheetPlugin: UIGestureRecognizerDelegate { + private static let REDUCED_ANGLE = 45.0 + + private func setupScrollReducers() { + let mainGestureRecognizer = UIPanGestureRecognizer() + mainGestureRecognizer.delegate = self + bridge!.webView!.scrollView.addGestureRecognizer(mainGestureRecognizer) + } + + @objc public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer { + let velocity = panGestureRecognizer.velocity(in: nil) + let angle = atan2(abs(velocity.y), abs(velocity.x)) * 180 / .pi + + if angle < Self.REDUCED_ANGLE { + return true + } + } + + return false + } + + @objc public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + + otherGestureRecognizer.require(toFail: gestureRecognizer) + + return true + } +} + +class CAPBridgeViewControllerForBottomSheet: CAPBridgeViewController { + override func instanceDescriptor() -> InstanceDescriptor { + let descriptor = super.instanceDescriptor() + descriptor.serverURL = String(format: "%@://%@/?bottom-sheet", descriptor.urlScheme!, descriptor.urlHostname!) + return descriptor + } +} + +class MyPanelLayout: FloatingPanelLayout { + let position: FloatingPanelPosition = .bottom + let initialState: FloatingPanelState = .hidden + var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [ + .half: FloatingPanelLayoutAnchor(fractionalInset: HALF_FRACTIONAL_INSET, edge: .bottom, referenceGuide: .superview), + .hidden: FloatingPanelLayoutAnchor(fractionalInset: 0, edge: .bottom, referenceGuide: .superview) + ] + let fullAnchor = FloatingPanelLayoutAnchor(absoluteInset: FULL_INSET, edge: .top, referenceGuide: .safeArea) + + func backdropAlpha(for state: FloatingPanelState) -> CGFloat { + switch state { + case .full: return 0.45 + case .half: return 0.35 + default: return 0.0 + } + } +} + +extension UIColor { + convenience init(hexString: String) { + let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) + var int = UInt64() + Scanner(string: hex).scanHexInt64(&int) + let a, r, g, b: UInt64 + switch hex.count { + case 3: // RGB (12-bit) + (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) + case 6: // RGB (24-bit) + (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) + case 8: // ARGB (32-bit) + (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) + default: + (a, r, g, b) = (255, 0, 0, 0) + } + self.init(red: CGFloat(r) / 255, green: CGFloat(g) / 255, blue: CGFloat(b) / 255, alpha: CGFloat(a) / 255) + } +} diff --git a/mobile/plugins/native-bottom-sheet/ios/Plugin/Info.plist b/mobile/plugins/native-bottom-sheet/ios/Plugin/Info.plist new file mode 100644 index 00000000..1007fd9d --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/ios/Plugin/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/mobile/plugins/native-bottom-sheet/ios/PluginTests/BottomSheetTests.swift b/mobile/plugins/native-bottom-sheet/ios/PluginTests/BottomSheetTests.swift new file mode 100644 index 00000000..1ddcc3ee --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/ios/PluginTests/BottomSheetTests.swift @@ -0,0 +1,16 @@ +import XCTest +@testable import Plugin + +class BottomSheetTests: XCTestCase { + + func testEcho() { + // This is an example of a functional test case for a plugin. + // Use XCTAssert and related functions to verify your tests produce the correct results. + + let implementation = BottomSheet() + let value = "Hello, World!" + let result = implementation.echo(value) + + XCTAssertEqual(value, result) + } +} diff --git a/mobile/plugins/native-bottom-sheet/ios/PluginTests/Info.plist b/mobile/plugins/native-bottom-sheet/ios/PluginTests/Info.plist new file mode 100644 index 00000000..6c40a6cd --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/ios/PluginTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/mobile/plugins/native-bottom-sheet/ios/Podfile b/mobile/plugins/native-bottom-sheet/ios/Podfile new file mode 100644 index 00000000..799af1f4 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/ios/Podfile @@ -0,0 +1,18 @@ +platform :ios, '13.0' + +def capacitor_pods + # Comment the next line if you're not using Swift and don't want to use dynamic frameworks + use_frameworks! + pod 'Capacitor', :path => '../node_modules/@capacitor/ios' + pod 'CapacitorCordova', :path => '../node_modules/@capacitor/ios' +end + +target 'Plugin' do + capacitor_pods + + pod 'FloatingPanel' +end + +target 'PluginTests' do + capacitor_pods +end diff --git a/mobile/plugins/native-bottom-sheet/package-lock.json b/mobile/plugins/native-bottom-sheet/package-lock.json new file mode 100644 index 00000000..e56c6dfc --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/package-lock.json @@ -0,0 +1,3452 @@ +{ + "name": "native-bottom-sheet", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "native-bottom-sheet", + "version": "0.0.1", + "license": "MIT", + "devDependencies": { + "@capacitor/android": "^5.0.0", + "@capacitor/core": "^5.0.0", + "@capacitor/docgen": "^0.0.18", + "@capacitor/ios": "^5.0.0", + "@ionic/eslint-config": "^0.3.0", + "@ionic/prettier-config": "^1.0.1", + "@ionic/swiftlint-config": "^1.1.2", + "eslint": "^7.11.0", + "prettier": "~2.3.0", + "prettier-plugin-java": "~1.0.2", + "rimraf": "^3.0.2", + "rollup": "^2.32.0", + "swiftlint": "^1.0.1", + "typescript": "~4.1.5" + }, + "peerDependencies": { + "@capacitor/core": "^5.0.0" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.12.11", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@capacitor/android": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-5.5.0.tgz", + "integrity": "sha512-ipJijb3M0FA6DvotS9zrbJ8p/mTEVg9EVtBmvUexogm8g5se1mc7i1gvOr3MQ/iTZ3PnNrRC/P7kHxa2R55iqg==", + "dev": true, + "peerDependencies": { + "@capacitor/core": "^5.5.0" + } + }, + "node_modules/@capacitor/core": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-5.5.0.tgz", + "integrity": "sha512-w59io0ctwnb7JRng7yO2H0YLHG8uz7XARUugRfp5aYTNiG55FqdSmSMOOqGCMPRg4sEnKjJTvAa4ImCYh3Kk1w==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@capacitor/docgen": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@capacitor/docgen/-/docgen-0.0.18.tgz", + "integrity": "sha512-BVqzrbSi9u5IaKRLlG0H/ZW8M23FDJpH2018RTGVHRn2Yk3na9jOcItBc3r+rYiwgRgAHylNw9Lt7+lWmJBD3Q==", + "dev": true, + "dependencies": { + "@types/node": "^14.17.3", + "colorette": "^1.2.2", + "github-slugger": "^1.3.0", + "minimist": "^1.2.5", + "typescript": "~4.2.4" + }, + "bin": { + "docgen": "bin/docgen" + }, + "engines": { + "node": ">=14.5.0" + } + }, + "node_modules/@capacitor/docgen/node_modules/typescript": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", + "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/@capacitor/ios": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-5.5.0.tgz", + "integrity": "sha512-kApjblUOlLY91+1OrWIx+vaVfEN1bl1kh1jSgK1/IdGfS9kFs1hxUE/okRoLJGT6tYeSOa6GA/19MLOs64wb6A==", + "dev": true, + "peerDependencies": { + "@capacitor/core": "^5.5.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "0.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.5.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@ionic/eslint-config": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@ionic/eslint-config/-/eslint-config-0.3.0.tgz", + "integrity": "sha512-Uf1hS2YIoHlcvXPF5LnsPM6auMewEdChQhR117Rt3sVEAutbyKMpFP4slNC2a6up3a5Q34zepqlf61Qgkf9XeQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "^4.1.0", + "@typescript-eslint/parser": "^4.1.0", + "eslint-config-prettier": "^6.11.0", + "eslint-plugin-import": "^2.22.0" + }, + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/@ionic/prettier-config": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@ionic/prettier-config/-/prettier-config-1.0.1.tgz", + "integrity": "sha512-/v8UOW7rxkw/hvrRe/QfjlQsdjkm3sfAHoE3uqffO5BoNGijQMARrT32JT9Ei0g6KySXPyxxW+7LzPHrQmfzCw==", + "dev": true, + "peerDependencies": { + "prettier": "^2.0.0" + } + }, + "node_modules/@ionic/swiftlint-config": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ionic/swiftlint-config/-/swiftlint-config-1.1.2.tgz", + "integrity": "sha512-UbE1AIlTowt9uR7fMzRtbQX4URcyuok7mcpdJfFDHAIGM6nDjohYMke+6xOr6ZYlLnEyVmBGNEg0+grEYRgcVg==", + "dev": true + }, + "node_modules/@ionic/utils-array": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.5.tgz", + "integrity": "sha512-HD72a71IQVBmQckDwmA8RxNVMTbxnaLbgFOl+dO5tbvW9CkkSFCv41h6fUuNsSEVgngfkn0i98HDuZC8mk+lTA==", + "dev": true, + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=10.3.0" + } + }, + "node_modules/@ionic/utils-fs": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.6.tgz", + "integrity": "sha512-eikrNkK89CfGPmexjTfSWl4EYqsPSBh0Ka7by4F0PLc1hJZYtJxUZV3X4r5ecA8ikjicUmcbU7zJmAjmqutG/w==", + "dev": true, + "dependencies": { + "@types/fs-extra": "^8.0.0", + "debug": "^4.0.0", + "fs-extra": "^9.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=10.3.0" + } + }, + "node_modules/@ionic/utils-object": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.5.tgz", + "integrity": "sha512-XnYNSwfewUqxq+yjER1hxTKggftpNjFLJH0s37jcrNDwbzmbpFTQTVAp4ikNK4rd9DOebX/jbeZb8jfD86IYxw==", + "dev": true, + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=10.3.0" + } + }, + "node_modules/@ionic/utils-process": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.10.tgz", + "integrity": "sha512-mZ7JEowcuGQK+SKsJXi0liYTcXd2bNMR3nE0CyTROpMECUpJeAvvaBaPGZf5ERQUPeWBVuwqAqjUmIdxhz5bxw==", + "dev": true, + "dependencies": { + "@ionic/utils-object": "2.1.5", + "@ionic/utils-terminal": "2.3.3", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=10.3.0" + } + }, + "node_modules/@ionic/utils-stream": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.5.tgz", + "integrity": "sha512-hkm46uHvEC05X/8PHgdJi4l4zv9VQDELZTM+Kz69odtO9zZYfnt8DkfXHJqJ+PxmtiE5mk/ehJWLnn/XAczTUw==", + "dev": true, + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=10.3.0" + } + }, + "node_modules/@ionic/utils-subprocess": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-2.1.11.tgz", + "integrity": "sha512-6zCDixNmZCbMCy5np8klSxOZF85kuDyzZSTTQKQP90ZtYNCcPYmuFSzaqDwApJT4r5L3MY3JrqK1gLkc6xiUPw==", + "dev": true, + "dependencies": { + "@ionic/utils-array": "2.1.5", + "@ionic/utils-fs": "3.1.6", + "@ionic/utils-process": "2.1.10", + "@ionic/utils-stream": "3.1.5", + "@ionic/utils-terminal": "2.3.3", + "cross-spawn": "^7.0.3", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=10.3.0" + } + }, + "node_modules/@ionic/utils-terminal": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.3.tgz", + "integrity": "sha512-RnuSfNZ5fLEyX3R5mtcMY97cGD1A0NVBbarsSQ6yMMfRJ5YHU7hHVyUfvZeClbqkBC/pAqI/rYJuXKCT9YeMCQ==", + "dev": true, + "dependencies": { + "@types/slice-ansi": "^4.0.0", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "slice-ansi": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "tslib": "^2.0.1", + "untildify": "^4.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.3.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/fs-extra": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.4.tgz", + "integrity": "sha512-OMcQKnlrkrOI0TaZ/MgVDA8LYFl7CykzFsjMj9l5x3un2nFxCY20ZFlnqrM0lcqlbs0Yro2HbnZlmopyRaoJ5w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", + "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "dev": true + }, + "node_modules/@types/parse-json": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.1.tgz", + "integrity": "sha512-3YmXzzPAdOTVljVMkTMBdBEvlOLg2cDQaDhnnhT3nT9uDbnJzjWhKlzb+desT12Y7tGqaN6d+AbozcKzyL36Ng==", + "dev": true + }, + "node_modules/@types/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz", + "integrity": "sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==", + "dev": true, + "dependencies": { + "@typescript-eslint/experimental-utils": "4.33.0", + "@typescript-eslint/scope-manager": "4.33.0", + "debug": "^4.3.1", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.1.8", + "regexpp": "^3.1.0", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^4.0.0", + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/experimental-utils": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz", + "integrity": "sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.7", + "@typescript-eslint/scope-manager": "4.33.0", + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/typescript-estree": "4.33.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz", + "integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "4.33.0", + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/typescript-estree": "4.33.0", + "debug": "^4.3.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz", + "integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/visitor-keys": "4.33.0" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz", + "integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==", + "dev": true, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz", + "integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/visitor-keys": "4.33.0", + "debug": "^4.3.1", + "globby": "^11.0.3", + "is-glob": "^4.0.1", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz", + "integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "4.33.0", + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "7.4.1", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", + "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chevrotain": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-6.5.0.tgz", + "integrity": "sha512-BwqQ/AgmKJ8jcMEjaSnfMybnKMgGTrtDKowfTP3pX4jwVy0kNjRsT/AP6h+wC3+3NC+X8X15VWBnTCQlX+wQFg==", + "dev": true, + "dependencies": { + "regexp-to-ast": "0.4.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", + "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.5", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.2", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.12", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "7.32.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz", + "integrity": "sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw==", + "dev": true, + "dependencies": { + "get-stdin": "^6.0.0" + }, + "bin": { + "eslint-config-prettier-check": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=3.14.1" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.28.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz", + "integrity": "sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.findlastindex": "^1.2.2", + "array.prototype.flat": "^1.3.1", + "array.prototype.flatmap": "^1.3.1", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.7", + "eslint-module-utils": "^2.8.0", + "has": "^1.0.3", + "is-core-module": "^2.13.0", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.6", + "object.groupby": "^1.0.0", + "object.values": "^1.1.6", + "semver": "^6.3.1", + "tsconfig-paths": "^3.14.2" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint/node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/espree": { + "version": "7.3.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat-cache": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", + "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/github-slugger": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", + "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==", + "dev": true + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "13.23.0", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", + "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "4.0.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.11" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/java-parser": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/java-parser/-/java-parser-1.0.2.tgz", + "integrity": "sha512-lBXc+F62ds2W83eH5MwGnzuWdb6kgGBV0x0R7w0B4JKGDrJzolMUEhRMzzzlIX68HvRU7XtfPon22YaB+dVg+A==", + "dev": true, + "dependencies": { + "chevrotain": "6.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1" + } + }, + "node_modules/object.values": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz", + "integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/prettier-plugin-java": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/prettier-plugin-java/-/prettier-plugin-java-1.0.2.tgz", + "integrity": "sha512-YgcN1WGZlrH0E+bHdqtIYtfDp6k2PHBnIaGjzdff/7t/NyDWAA6ypAmnD7YQVG2OuoIaXYkC37HN7cz68lLWLg==", + "dev": true, + "dependencies": { + "java-parser": "1.0.2", + "lodash": "4.17.21", + "prettier": "2.2.1" + } + }, + "node_modules/prettier-plugin-java/node_modules/prettier": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz", + "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/regexp-to-ast": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.4.0.tgz", + "integrity": "sha512-4qf/7IsIKfSNHQXSwial1IFmfM1Cc/whNBQqRwe0V2stPe7KmN1U0tWQiIx6JiirgSrisjE0eECdNf7Tav1Ntw==", + "dev": true + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "2.79.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", + "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swiftlint": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/swiftlint/-/swiftlint-1.0.2.tgz", + "integrity": "sha512-YhcS0N3vkwBatnZf/iJPg89LGQk7MbFnz67Cg3EZ6Ppqm2H8y6x7A1t6KMQ0jYVQpea9wQiFiFRFhkoChaQ29Q==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@ionic/utils-fs": "3.1.6", + "@ionic/utils-subprocess": "2.1.11", + "cosmiconfig": "^6.0.0" + }, + "bin": { + "node-swiftlint": "bin.js" + } + }, + "node_modules/table": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", + "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "4.1.6", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", + "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==", + "dev": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.4", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + } + } +} diff --git a/mobile/plugins/native-bottom-sheet/package.json b/mobile/plugins/native-bottom-sheet/package.json new file mode 100644 index 00000000..b03625c7 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/package.json @@ -0,0 +1,78 @@ +{ + "name": "native-bottom-sheet", + "version": "0.0.1", + "description": "Allows to open a native BottomSheet/FloatingPanel on iOS", + "main": "dist/plugin.cjs.js", + "module": "dist/esm/index.js", + "types": "dist/esm/index.d.ts", + "unpkg": "dist/plugin.js", + "files": [ + "android/src/main/", + "android/build.gradle", + "dist/", + "ios/Plugin/", + "NativeBottomSheet.podspec" + ], + "author": "MyTonWallet Team", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/mytonwalletorg/native-bottom-sheet.git" + }, + "bugs": { + "url": "https://github.com/mytonwalletorg/native-bottom-sheet/issues" + }, + "keywords": [ + "capacitor", + "plugin", + "native" + ], + "scripts": { + "verify": "npm run verify:ios && npm run verify:android && npm run verify:web", + "verify:ios": "cd ios && pod install && xcodebuild -workspace Plugin.xcworkspace -scheme Plugin -destination generic/platform=iOS && cd ..", + "verify:android": "cd android && ./gradlew clean build test && cd ..", + "verify:web": "npm run build", + "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint", + "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format", + "eslint": "eslint . --ext ts", + "prettier": "prettier \"**/*.{css,html,ts,js,java}\"", + "swiftlint": "node-swiftlint", + "docgen": "docgen --api BottomSheetPlugin --output-readme README.md --output-json dist/docs.json", + "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.js", + "clean": "rimraf ./dist", + "watch": "tsc --watch", + "prepublishOnly": "npm run build" + }, + "devDependencies": { + "@capacitor/android": "^5.0.0", + "@capacitor/core": "^5.0.0", + "@capacitor/docgen": "^0.0.18", + "@capacitor/ios": "^5.0.0", + "@ionic/eslint-config": "^0.3.0", + "@ionic/prettier-config": "^1.0.1", + "@ionic/swiftlint-config": "^1.1.2", + "eslint": "^7.11.0", + "prettier": "~2.3.0", + "prettier-plugin-java": "~1.0.2", + "rimraf": "^3.0.2", + "rollup": "^2.32.0", + "swiftlint": "^1.0.1", + "typescript": "~4.1.5" + }, + "peerDependencies": { + "@capacitor/core": "^5.0.0" + }, + "prettier": "@ionic/prettier-config", + "swiftlint": "@ionic/swiftlint-config", + "eslintConfig": { + "extends": "@ionic/eslint-config/recommended" + }, + "capacitor": { + "ios": { + "src": "ios" + }, + "android": { + "src": "android" + } + } +} diff --git a/mobile/plugins/native-bottom-sheet/rollup.config.js b/mobile/plugins/native-bottom-sheet/rollup.config.js new file mode 100644 index 00000000..b187fc9f --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/rollup.config.js @@ -0,0 +1,22 @@ +export default { + input: 'dist/esm/index.js', + output: [ + { + file: 'dist/plugin.js', + format: 'iife', + name: 'capacitorBottomSheet', + globals: { + '@capacitor/core': 'capacitorExports', + }, + sourcemap: true, + inlineDynamicImports: true, + }, + { + file: 'dist/plugin.cjs.js', + format: 'cjs', + sourcemap: true, + inlineDynamicImports: true, + }, + ], + external: ['@capacitor/core'], +}; diff --git a/mobile/plugins/native-bottom-sheet/src/definitions.ts b/mobile/plugins/native-bottom-sheet/src/definitions.ts new file mode 100644 index 00000000..94543f6c --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/src/definitions.ts @@ -0,0 +1,66 @@ +import { PluginListenerHandle } from '@capacitor/core'; + +export type BottomSheetKeys = + 'initial' + | 'receive' + | 'invoice' + | 'transfer' + | 'swap' + | 'stake' + | 'unstake' + | 'staking-info' + | 'transaction-info' + | 'swap-activity' + | 'backup' + | 'add-account' + | 'settings' + | 'qr-scanner' + | 'dapp-connect' + | 'dapp-transaction' + | 'disclaimer' + | 'backup-warning'; + +export interface BottomSheetPlugin { + prepare(): Promise; + + delegate(options: { key: BottomSheetKeys, globalJson: string }): Promise; + + release(options: { key: BottomSheetKeys | '*' }): Promise; + + openSelf(options: { key: BottomSheetKeys, height: string, backgroundColor: string }): Promise; + + closeSelf(options: { key: BottomSheetKeys }): Promise; + + setSelfSize(options: { size: 'half' | 'full' }): Promise; + + callActionInMain(options: { name: string, optionsJson: string }): Promise; + + callActionInNative(options: { name: string, optionsJson: string }): Promise; + + openInMain(options: { key: BottomSheetKeys }): Promise; + + addListener( + eventName: 'delegate', + handler: (options: { key: BottomSheetKeys, globalJson: string }) => void, + ): Promise & PluginListenerHandle; + + addListener( + eventName: 'move', + handler: () => void, + ): Promise & PluginListenerHandle; + + addListener( + eventName: 'callActionInMain', + handler: (options: { name: string, optionsJson: string }) => void, + ): Promise & PluginListenerHandle; + + addListener( + eventName: 'callActionInNative', + handler: (options: { name: string, optionsJson: string }) => void, + ): Promise & PluginListenerHandle; + + addListener( + eventName: 'openInMain', + handler: (options: { key: BottomSheetKeys }) => void, + ): Promise & PluginListenerHandle; +} diff --git a/mobile/plugins/native-bottom-sheet/src/index.ts b/mobile/plugins/native-bottom-sheet/src/index.ts new file mode 100644 index 00000000..bd2f5fa1 --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/src/index.ts @@ -0,0 +1,8 @@ +import { registerPlugin } from '@capacitor/core'; + +import type { BottomSheetPlugin } from './definitions'; + +const BottomSheet = registerPlugin('BottomSheet'); + +export * from './definitions'; +export { BottomSheet }; diff --git a/mobile/plugins/native-bottom-sheet/tsconfig.json b/mobile/plugins/native-bottom-sheet/tsconfig.json new file mode 100644 index 00000000..f2e88e6a --- /dev/null +++ b/mobile/plugins/native-bottom-sheet/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "allowUnreachableCode": false, + "declaration": true, + "esModuleInterop": true, + "inlineSources": true, + "lib": ["dom", "es2017"], + "module": "esnext", + "moduleResolution": "node", + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "outDir": "dist/esm", + "pretty": true, + "sourceMap": true, + "strict": true, + "target": "es2017" + }, + "files": ["src/index.ts"] +} diff --git a/package-lock.json b/package-lock.json index 620d785a..d0414122 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,31 @@ { "name": "mytonwallet", - "version": "1.16.4", + "version": "1.17.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mytonwallet", - "version": "1.16.4", + "version": "1.17.0", "license": "GPL-3.0-or-later", "dependencies": { + "@capacitor-mlkit/barcode-scanning": "^5.3.0", + "@capacitor/android": "^5.3.0", + "@capacitor/app": "^5.0.6", + "@capacitor/core": "^5.5.1", + "@capacitor/dialog": "^5.0.6", + "@capacitor/haptics": "^5.0.6", + "@capacitor/ios": "^5.5.1", + "@capacitor/status-bar": "^5.0.6", + "@capgo/capacitor-native-biometric": "^5.1.0", "@ledgerhq/hw-transport-webhid": "^6.27.12", + "@mauricewegner/capacitor-navigation-bar": "^2.0.3", "@ton-community/ton-ledger": "^6.0.0", "buffer": "^6.0.3", + "capacitor-plugin-safe-area": "^2.0.5", + "capacitor-splash-screen": "file:mobile/plugins/capacitor-splash-screen", "idb-keyval": "^6.2.0", + "native-bottom-sheet": "file:mobile/plugins/native-bottom-sheet", "pako": "^2.1.0", "qr-code-styling": "github:troman29/qr-code-styling#c00d0", "qrcode-generator": "^1.4.4", @@ -35,6 +48,7 @@ "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.21.0", "@babel/register": "^7.21.0", + "@capacitor/cli": "^5.3.0", "@peculiar/webcrypto": "^1.3.2", "@playwright/test": "^1.31.2", "@statoscope/cli": "^5.27.0", @@ -71,6 +85,8 @@ "electron": "^22.1.0", "electron-builder": "^24.4.0", "electron-context-menu": "^3.6.1", + "electron-rebuild": "^3.2.9", + "electron-store": "^8.1.0", "electron-updater": "^5.3.0", "electron-window-state": "^5.0.3", "electronmon": "^2.0.2", @@ -97,7 +113,7 @@ "jest": "^29.5.0", "jest-raw-loader": "^1.0.1", "js-yaml": "^4.1.0", - "lint-staged": "^13.2.0", + "lint-staged": "^15.0.2", "mini-css-extract-plugin": "^2.7.5", "postcss": "^8.4.14", "postcss-loader": "^7.1.0", @@ -116,7 +132,7 @@ "stylelint-group-selectors": "^1.0.9", "stylelint-high-performance-animation": "^1.8.0", "stylelint-order": "^5.0.0", - "typescript": "^5.0.2", + "typescript": "^5.2.2", "webpack": "^5.76.2", "webpack-dev-server": "^4.13.1" }, @@ -125,6 +141,799 @@ "npm": "^9" } }, + "mobile/native-bottom-sheet": { + "version": "0.0.1", + "extraneous": true, + "license": "MIT", + "devDependencies": { + "@capacitor/android": "^5.0.0", + "@capacitor/core": "^5.0.0", + "@capacitor/docgen": "^0.0.18", + "@capacitor/ios": "^5.0.0", + "@ionic/eslint-config": "^0.3.0", + "@ionic/prettier-config": "^1.0.1", + "@ionic/swiftlint-config": "^1.1.2", + "eslint": "^7.11.0", + "prettier": "~2.3.0", + "prettier-plugin-java": "~1.0.2", + "rimraf": "^3.0.2", + "rollup": "^2.32.0", + "swiftlint": "^1.0.1", + "typescript": "~4.1.5" + }, + "peerDependencies": { + "@capacitor/core": "^5.0.0" + } + }, + "mobile/plugins/capacitor-splash-screen": { + "version": "5.0.6.1", + "license": "MIT", + "devDependencies": { + "@capacitor/android": "^5.0.0", + "@capacitor/cli": "^5.0.0", + "@capacitor/core": "^5.0.0", + "@capacitor/docgen": "0.2.0", + "@capacitor/ios": "^5.0.0", + "@ionic/eslint-config": "^0.3.0", + "@ionic/prettier-config": "~1.0.1", + "@ionic/swiftlint-config": "^1.1.2", + "eslint": "^7.11.0", + "prettier": "~2.3.0", + "prettier-plugin-java": "~1.0.2", + "rimraf": "^3.0.2", + "rollup": "^2.32.0", + "swiftlint": "^1.0.1", + "typescript": "~4.1.5" + }, + "peerDependencies": { + "@capacitor/core": "^5.0.0" + } + }, + "mobile/plugins/capacitor-splash-screen/node_modules/@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "mobile/plugins/capacitor-splash-screen/node_modules/@capacitor/docgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@capacitor/docgen/-/docgen-0.2.0.tgz", + "integrity": "sha512-JoKuEx4Ep9LVnTY1I4zaoQPlxy+AWVMwwOSou/U9f/nXsL391aBRH7hC12lKNZGetiComnt369vQ+/r7n9iXfQ==", + "dev": true, + "dependencies": { + "@types/node": "^14.18.0", + "colorette": "^2.0.16", + "github-slugger": "^1.4.0", + "minimist": "^1.2.5", + "typescript": "~4.2.4" + }, + "bin": { + "docgen": "bin/docgen" + }, + "engines": { + "node": ">=14.5.0" + } + }, + "mobile/plugins/capacitor-splash-screen/node_modules/@capacitor/docgen/node_modules/typescript": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", + "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "mobile/plugins/capacitor-splash-screen/node_modules/@eslint/eslintrc": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "mobile/plugins/capacitor-splash-screen/node_modules/@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "mobile/plugins/capacitor-splash-screen/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "dev": true + }, + "mobile/plugins/capacitor-splash-screen/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "mobile/plugins/capacitor-splash-screen/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "mobile/plugins/capacitor-splash-screen/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "mobile/plugins/capacitor-splash-screen/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "mobile/plugins/capacitor-splash-screen/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "mobile/plugins/capacitor-splash-screen/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "mobile/plugins/capacitor-splash-screen/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "mobile/plugins/capacitor-splash-screen/node_modules/eslint": { + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "mobile/plugins/capacitor-splash-screen/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "mobile/plugins/capacitor-splash-screen/node_modules/espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "dependencies": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "mobile/plugins/capacitor-splash-screen/node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "mobile/plugins/capacitor-splash-screen/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "mobile/plugins/capacitor-splash-screen/node_modules/globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "mobile/plugins/capacitor-splash-screen/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "mobile/plugins/capacitor-splash-screen/node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "mobile/plugins/capacitor-splash-screen/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "mobile/plugins/capacitor-splash-screen/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, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "mobile/plugins/capacitor-splash-screen/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, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "mobile/plugins/capacitor-splash-screen/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "mobile/plugins/capacitor-splash-screen/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "mobile/plugins/capacitor-splash-screen/node_modules/typescript": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.6.tgz", + "integrity": "sha512-pxnwLxeb/Z5SP80JDRzVjh58KsM6jZHRAOtTpS7sXLS4ogXNKC9ANxHHZqLLeVHZN35jCtI4JdmLLbLiC1kBow==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "mobile/plugins/capacitor-splash-screen/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 + }, + "mobile/plugins/native-bottom-sheet": { + "version": "0.0.1", + "license": "MIT", + "devDependencies": { + "@capacitor/android": "^5.0.0", + "@capacitor/core": "^5.0.0", + "@capacitor/docgen": "^0.0.18", + "@capacitor/ios": "^5.0.0", + "@ionic/eslint-config": "^0.3.0", + "@ionic/prettier-config": "^1.0.1", + "@ionic/swiftlint-config": "^1.1.2", + "eslint": "^7.11.0", + "prettier": "~2.3.0", + "prettier-plugin-java": "~1.0.2", + "rimraf": "^3.0.2", + "rollup": "^2.32.0", + "swiftlint": "^1.0.1", + "typescript": "~4.1.5" + }, + "peerDependencies": { + "@capacitor/core": "^5.0.0" + } + }, + "mobile/plugins/native-bottom-sheet/node_modules/@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "mobile/plugins/native-bottom-sheet/node_modules/@eslint/eslintrc": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "mobile/plugins/native-bottom-sheet/node_modules/@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "mobile/plugins/native-bottom-sheet/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "mobile/plugins/native-bottom-sheet/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "mobile/plugins/native-bottom-sheet/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "mobile/plugins/native-bottom-sheet/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "mobile/plugins/native-bottom-sheet/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "mobile/plugins/native-bottom-sheet/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "mobile/plugins/native-bottom-sheet/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "mobile/plugins/native-bottom-sheet/node_modules/eslint": { + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "mobile/plugins/native-bottom-sheet/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "mobile/plugins/native-bottom-sheet/node_modules/espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "dependencies": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "mobile/plugins/native-bottom-sheet/node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "mobile/plugins/native-bottom-sheet/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "mobile/plugins/native-bottom-sheet/node_modules/globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "mobile/plugins/native-bottom-sheet/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "mobile/plugins/native-bottom-sheet/node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "mobile/plugins/native-bottom-sheet/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "mobile/plugins/native-bottom-sheet/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, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "mobile/plugins/native-bottom-sheet/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, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "mobile/plugins/native-bottom-sheet/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "mobile/plugins/native-bottom-sheet/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "mobile/plugins/native-bottom-sheet/node_modules/typescript": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.6.tgz", + "integrity": "sha512-pxnwLxeb/Z5SP80JDRzVjh58KsM6jZHRAOtTpS7sXLS4ogXNKC9ANxHHZqLLeVHZN35jCtI4JdmLLbLiC1kBow==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "mobile/plugins/native-bottom-sheet/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 + }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -135,9 +944,10 @@ } }, "node_modules/@adobe/css-tools": { - "version": "4.2.0", - "dev": true, - "license": "MIT" + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.1.tgz", + "integrity": "sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg==", + "dev": true }, "node_modules/@ampproject/remapping": { "version": "2.2.1", @@ -152,12 +962,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "dependencies": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" @@ -229,12 +1040,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", - "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -343,22 +1154,22 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" @@ -521,9 +1332,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true, "engines": { "node": ">=6.9.0" @@ -568,13 +1379,13 @@ } }, "node_modules/@babel/highlight": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", - "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -582,9 +1393,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.6.tgz", - "integrity": "sha512-EIQu22vNkceq3LbjAq7knDf/UmtI2qbcNI8GRBlijez6TpQLvSodJPYfydQmNA5buwkxxxa/PVI44jjYZ+/cLw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -2202,9 +3013,9 @@ } }, "node_modules/@babel/register/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -2236,58 +3047,336 @@ } }, "node_modules/@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.6.tgz", - "integrity": "sha512-53CijMvKlLIDlOTrdWiHileRddlIiwUIyCKqYa7lYnnPldXCG5dUSN38uT0cA6i7rHWNKJLH0VU/Kxdr1GzB3w==", + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@capacitor-mlkit/barcode-scanning": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@capacitor-mlkit/barcode-scanning/-/barcode-scanning-5.3.0.tgz", + "integrity": "sha512-ML1nEzcvegeOTu3AxufWYwLmvxkkeKbToyTuMHmpFlIYn69qVdAzwGrN39Kh3+jUwZ4cLy7KTCf27/p6ShT8ag==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/capawesome-team/" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/capawesome" + } + ], + "peerDependencies": { + "@capacitor/core": "^5.0.0" + } + }, + "node_modules/@capacitor/android": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-5.5.1.tgz", + "integrity": "sha512-WTnPnpaEvTtaEtTNRbh06Y1afF7A4plY/4uajAL0WW8tdR1FxieadF357yKGiAT6CudI/B+eOu6rxn6qWuphKg==", + "peerDependencies": { + "@capacitor/core": "^5.5.0" + } + }, + "node_modules/@capacitor/app": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@capacitor/app/-/app-5.0.6.tgz", + "integrity": "sha512-6ZXVdnNmaYILasC/RjQw+yfTmq2ZO7Q3v5lFcDVfq3PFGnybyYQh+RstBrYri+376OmXOXxBD7E6UxBhrMzXGA==", + "peerDependencies": { + "@capacitor/core": "^5.0.0" + } + }, + "node_modules/@capacitor/cli": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-5.3.0.tgz", + "integrity": "sha512-ku23HPqUHUnSgo/SyEWxVviEAxb4ieWvAVMI3KfrrBoinAhTOvNSZwT346rIpxZ9Xj3Qp41UjdIz0ME+DYwhfA==", + "dev": true, + "dependencies": { + "@ionic/cli-framework-output": "^2.2.5", + "@ionic/utils-fs": "^3.1.6", + "@ionic/utils-subprocess": "^2.1.11", + "@ionic/utils-terminal": "^2.3.3", + "commander": "^9.3.0", + "debug": "^4.3.4", + "env-paths": "^2.2.0", + "kleur": "^4.1.4", + "native-run": "^1.7.2", + "open": "^8.4.0", + "plist": "^3.0.5", + "prompts": "^2.4.2", + "rimraf": "^4.4.1", + "semver": "^7.3.7", + "tar": "^6.1.11", + "tslib": "^2.4.0", + "xml2js": "^0.5.0" + }, + "bin": { + "cap": "bin/capacitor", + "capacitor": "bin/capacitor" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@capacitor/cli/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@capacitor/cli/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/@capacitor/cli/node_modules/glob": { + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", + "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "minimatch": "^8.0.2", + "minipass": "^4.2.4", + "path-scurry": "^1.6.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@capacitor/cli/node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@capacitor/cli/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, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@capacitor/cli/node_modules/minimatch": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", + "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@capacitor/cli/node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@capacitor/cli/node_modules/rimraf": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.4.1.tgz", + "integrity": "sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og==", + "dev": true, + "dependencies": { + "glob": "^9.2.0" + }, + "bin": { + "rimraf": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@capacitor/cli/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, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@capacitor/cli/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 + }, + "node_modules/@capacitor/core": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-5.5.1.tgz", + "integrity": "sha512-VG6Iv8Q7ZAbvjodxpvjcSe0jfxUwZXnvjbi93ehuJ6eYP8U926qLSXyrT/DToZq+F6v/HyGyVgn3mrE/9jW2Tg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@capacitor/dialog": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@capacitor/dialog/-/dialog-5.0.6.tgz", + "integrity": "sha512-/F9aSADswh+5pBE5810vD/N+Ox3KmahLXn1rMqisao8gNVI/Lk4YanWSPqDJCauHwOfZyeZscmDsETizAlSLFA==", + "peerDependencies": { + "@capacitor/core": "^5.0.0" + } + }, + "node_modules/@capacitor/docgen": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@capacitor/docgen/-/docgen-0.0.18.tgz", + "integrity": "sha512-BVqzrbSi9u5IaKRLlG0H/ZW8M23FDJpH2018RTGVHRn2Yk3na9jOcItBc3r+rYiwgRgAHylNw9Lt7+lWmJBD3Q==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.6", - "@babel/types": "^7.22.5", - "debug": "^4.1.0", - "globals": "^11.1.0" + "@types/node": "^14.17.3", + "colorette": "^1.2.2", + "github-slugger": "^1.3.0", + "minimist": "^1.2.5", + "typescript": "~4.2.4" + }, + "bin": { + "docgen": "bin/docgen" }, "engines": { - "node": ">=6.9.0" + "node": ">=14.5.0" } }, - "node_modules/@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "node_modules/@capacitor/docgen/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "dev": true + }, + "node_modules/@capacitor/docgen/node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true + }, + "node_modules/@capacitor/docgen/node_modules/typescript": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", + "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, "engines": { - "node": ">=6.9.0" + "node": ">=4.2.0" } }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "dev": true, - "license": "MIT" + "node_modules/@capacitor/haptics": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-5.0.6.tgz", + "integrity": "sha512-UrMcR7p2X10ql4VLlowUuH/VckTeu0lj+RQpekxox14uxDmu5AGIFDK/iDTi8W6QZkxTJRZK6sbCjgwYgNJ7Pw==", + "peerDependencies": { + "@capacitor/core": "^5.0.0" + } + }, + "node_modules/@capacitor/ios": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-5.5.1.tgz", + "integrity": "sha512-h00qt8u32t8eEbIkuG4IjR0r34YZC0sIXglDH8fRDdA84xDkTybmz3WtdpRWDzh6ukE2RIY7rmD7p410WSJ2yA==", + "peerDependencies": { + "@capacitor/core": "^5.5.0" + } + }, + "node_modules/@capacitor/status-bar": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-5.0.6.tgz", + "integrity": "sha512-7od8CxsBnot1XMK3IeOkproFL4hgoKoWAc3pwUvmDOkQsXoxwQm4SR9mLwQavv1XfxtHbFV9Ukd7FwMxOPSViw==", + "peerDependencies": { + "@capacitor/core": "^5.0.0" + } + }, + "node_modules/@capgo/capacitor-native-biometric": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@capgo/capacitor-native-biometric/-/capacitor-native-biometric-5.1.0.tgz", + "integrity": "sha512-j7PxjX0mZy7sVPSbulAj3YTP2qyXPvHUUt6C9kgzsBh1VNWaIFlzz9TRd/r9IwdOY0L/ASsU5VOhK144SJI5Dg==", + "peerDependencies": { + "@capacitor/core": "^5.0.0" + } }, "node_modules/@csstools/selector-specificity": { "version": "2.2.0", @@ -2339,12 +3428,11 @@ } }, "node_modules/@electron/asar": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.4.tgz", - "integrity": "sha512-lykfY3TJRRWFeTxccEKdf1I6BLl2Plw81H0bbp4Fc5iEc67foDCa5pjJQULVgo0wF+Dli75f3xVcdb/67FFZ/g==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.7.tgz", + "integrity": "sha512-8FaSCAIiZGYFWyjeevPQt+0e9xCK9YmJ2Rjg5SXgdsXon6cRnU0Yxnbe6CvJbQn26baifur2Y2G5EBayRIsjyg==", "dev": true, "dependencies": { - "chromium-pickle-js": "^0.2.0", "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" @@ -2387,13 +3475,14 @@ } }, "node_modules/@electron/notarize": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-1.2.4.tgz", - "integrity": "sha512-W5GQhJEosFNafewnS28d3bpQ37/s91CDWqxVchHfmv2dQSTWpOzNlUVQwYzC1ay5bChRV/A9BTL68yj0Pa+TSg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.1.0.tgz", + "integrity": "sha512-Q02xem1D0sg4v437xHgmBLxI2iz/fc0D4K7fiVWHa/AnW8o7D751xyKNXgziA6HrTOme9ul1JfWN5ark8WH1xA==", "dev": true, "dependencies": { "debug": "^4.1.1", - "fs-extra": "^9.0.1" + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" }, "engines": { "node": ">= 10.0.0" @@ -2426,328 +3515,548 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/@electron/notarize/node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.0.5.tgz", + "integrity": "sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww==", + "dev": true, + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/universal": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.4.1.tgz", + "integrity": "sha512-lE/U3UNw1YHuowNbTmKNs9UlS3En3cPgwM5MI+agIgr/B1hSze9NdOP0qn7boZaI9Lph8IDv3/24g9IxnJP7aQ==", + "dev": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "@malept/cross-spawn-promise": "^1.1.0", + "debug": "^4.3.1", + "dir-compare": "^3.0.0", + "fs-extra": "^9.0.1", + "minimatch": "^3.0.4", + "plist": "^3.0.4" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.5.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.5.2", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.20.0", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.41.0", "dev": true, + "license": "MIT", "engines": { - "node": ">= 10.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@electron/osx-sign": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.0.4.tgz", - "integrity": "sha512-xfhdEcIOfAZg7scZ9RQPya1G1lWo8/zMCwUXAulq0SfY7ONIW+b9qGyKdMyuMctNYwllrIS+vmxfijSfjeh97g==", + "node_modules/@gar/promisify": { + "version": "1.1.3", "dev": true, - "dependencies": { - "compare-version": "^0.1.2", - "debug": "^4.3.4", - "fs-extra": "^10.0.0", - "isbinaryfile": "^4.0.8", - "minimist": "^1.2.6", - "plist": "^3.0.5" - }, - "bin": { - "electron-osx-flat": "bin/electron-osx-flat.js", - "electron-osx-sign": "bin/electron-osx-sign.js" - }, - "engines": { - "node": ">=12.0.0" - } + "license": "MIT" }, - "node_modules/@electron/osx-sign/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.8", "dev": true, + "license": "Apache-2.0", "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" }, "engines": { - "node": ">=12" + "node": ">=10.10.0" } }, - "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">= 8.0.0" + "node": ">=12.22" }, "funding": { - "url": "https://github.com/sponsors/gjtorikian/" + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@electron/osx-sign/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } + "license": "BSD-3-Clause" }, - "node_modules/@electron/osx-sign/node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "node_modules/@ionic/cli-framework-output": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.6.tgz", + "integrity": "sha512-YLPRwnk5Lw0XQ9pKWG+p2KoR5HjMBigZ6yv+/XtL3TGOnCS1+oAz56ABbAORCjTWhSJQisr8APNFiELAecY6QA==", "dev": true, + "dependencies": { + "@ionic/utils-terminal": "2.3.4", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, "engines": { - "node": ">= 10.0.0" + "node": ">=16.0.0" } }, - "node_modules/@electron/rebuild": { - "version": "3.2.13", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.2.13.tgz", - "integrity": "sha512-DH9Ol4JCnHDYVOD0fKWq+Qqbn/0WU1O6QR0mIpMXEVU4YFM4PlaqNC9K36mGShNBxxGFotZCMDrB1wl/iHM12g==", + "node_modules/@ionic/eslint-config": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@ionic/eslint-config/-/eslint-config-0.3.0.tgz", + "integrity": "sha512-Uf1hS2YIoHlcvXPF5LnsPM6auMewEdChQhR117Rt3sVEAutbyKMpFP4slNC2a6up3a5Q34zepqlf61Qgkf9XeQ==", "dev": true, "dependencies": { - "@malept/cross-spawn-promise": "^2.0.0", - "chalk": "^4.0.0", - "debug": "^4.1.1", - "detect-libc": "^2.0.1", - "fs-extra": "^10.0.0", - "got": "^11.7.0", - "node-abi": "^3.0.0", - "node-api-version": "^0.1.4", - "node-gyp": "^9.0.0", - "ora": "^5.1.0", - "semver": "^7.3.5", - "tar": "^6.0.5", - "yargs": "^17.0.1" - }, - "bin": { - "electron-rebuild": "lib/cli.js" + "@typescript-eslint/eslint-plugin": "^4.1.0", + "@typescript-eslint/parser": "^4.1.0", + "eslint-config-prettier": "^6.11.0", + "eslint-plugin-import": "^2.22.0" }, - "engines": { - "node": ">=12.13.0" + "peerDependencies": { + "eslint": ">=7" } }, - "node_modules/@electron/rebuild/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@ionic/eslint-config/node_modules/@typescript-eslint/eslint-plugin": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz", + "integrity": "sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==", "dev": true, "dependencies": { - "color-convert": "^2.0.1" + "@typescript-eslint/experimental-utils": "4.33.0", + "@typescript-eslint/scope-manager": "4.33.0", + "debug": "^4.3.1", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.1.8", + "regexpp": "^3.1.0", + "semver": "^7.3.5", + "tsutils": "^3.21.0" }, "engines": { - "node": ">=8" + "node": "^10.12.0 || >=12.0.0" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^4.0.0", + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@electron/rebuild/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/@ionic/eslint-config/node_modules/@typescript-eslint/experimental-utils": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz", + "integrity": "sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==", "dev": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "@types/json-schema": "^7.0.7", + "@typescript-eslint/scope-manager": "4.33.0", + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/typescript-estree": "4.33.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" }, "engines": { - "node": ">=10" + "node": "^10.12.0 || >=12.0.0" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" } }, - "node_modules/@electron/rebuild/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/@ionic/eslint-config/node_modules/@typescript-eslint/parser": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz", + "integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==", "dev": true, "dependencies": { - "color-name": "~1.1.4" + "@typescript-eslint/scope-manager": "4.33.0", + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/typescript-estree": "4.33.0", + "debug": "^4.3.1" }, "engines": { - "node": ">=7.0.0" + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@electron/rebuild/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/@electron/rebuild/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "node_modules/@ionic/eslint-config/node_modules/@typescript-eslint/scope-manager": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz", + "integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==", "dev": true, "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/visitor-keys": "4.33.0" }, "engines": { - "node": ">=12" + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@electron/rebuild/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/@ionic/eslint-config/node_modules/@typescript-eslint/types": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz", + "integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==", "dev": true, "engines": { - "node": ">=8" - } - }, - "node_modules/@electron/rebuild/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "dependencies": { - "universalify": "^2.0.0" + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@electron/rebuild/node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "node_modules/@ionic/eslint-config/node_modules/@typescript-eslint/typescript-estree": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz", + "integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==", "dev": true, "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/visitor-keys": "4.33.0", + "debug": "^4.3.1", + "globby": "^11.0.3", + "is-glob": "^4.0.1", + "semver": "^7.3.5", + "tsutils": "^3.21.0" }, "engines": { - "node": ">=10" + "node": "^10.12.0 || >=12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@electron/rebuild/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==", + "node_modules/@ionic/eslint-config/node_modules/@typescript-eslint/visitor-keys": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz", + "integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==", "dev": true, "dependencies": { - "yallist": "^4.0.0" + "@typescript-eslint/types": "4.33.0", + "eslint-visitor-keys": "^2.0.0" }, "engines": { - "node": ">=10" + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@electron/rebuild/node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "node_modules/@ionic/eslint-config/node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", "dev": true, "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" + "eslint-visitor-keys": "^2.0.0" }, "engines": { - "node": ">=10" + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" } }, - "node_modules/@electron/rebuild/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "node_modules/@ionic/eslint-config/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, "engines": { "node": ">=10" } }, - "node_modules/@electron/rebuild/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/@ionic/eslint-config/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, "dependencies": { - "has-flag": "^4.0.0" + "yallist": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" } }, - "node_modules/@electron/rebuild/node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "node_modules/@ionic/eslint-config/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, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, "engines": { - "node": ">= 10.0.0" + "node": ">=10" } }, - "node_modules/@electron/rebuild/node_modules/yallist": { + "node_modules/@ionic/eslint-config/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 }, - "node_modules/@electron/universal": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.3.4.tgz", - "integrity": "sha512-BdhBgm2ZBnYyYRLRgOjM5VHkyFItsbggJ0MHycOjKWdFGYwK97ZFXH54dTvUWEfha81vfvwr5On6XBjt99uDcg==", + "node_modules/@ionic/prettier-config": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@ionic/prettier-config/-/prettier-config-1.0.1.tgz", + "integrity": "sha512-/v8UOW7rxkw/hvrRe/QfjlQsdjkm3sfAHoE3uqffO5BoNGijQMARrT32JT9Ei0g6KySXPyxxW+7LzPHrQmfzCw==", + "dev": true, + "peerDependencies": { + "prettier": "^2.0.0" + } + }, + "node_modules/@ionic/swiftlint-config": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ionic/swiftlint-config/-/swiftlint-config-1.1.2.tgz", + "integrity": "sha512-UbE1AIlTowt9uR7fMzRtbQX4URcyuok7mcpdJfFDHAIGM6nDjohYMke+6xOr6ZYlLnEyVmBGNEg0+grEYRgcVg==", + "dev": true + }, + "node_modules/@ionic/utils-array": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz", + "integrity": "sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==", "dev": true, "dependencies": { - "@electron/asar": "^3.2.1", - "@malept/cross-spawn-promise": "^1.1.0", - "debug": "^4.3.1", - "dir-compare": "^3.0.0", - "fs-extra": "^9.0.1", - "minimatch": "^3.0.4", - "plist": "^3.0.4" + "debug": "^4.0.0", + "tslib": "^2.0.1" }, "engines": { - "node": ">=8.6" + "node": ">=16.0.0" } }, - "node_modules/@electron/universal/node_modules/@malept/cross-spawn-promise": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", - "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "node_modules/@ionic/utils-fs": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.7.tgz", + "integrity": "sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" - } - ], "dependencies": { - "cross-spawn": "^7.0.1" + "@types/fs-extra": "^8.0.0", + "debug": "^4.0.0", + "fs-extra": "^9.0.0", + "tslib": "^2.0.1" }, "engines": { - "node": ">= 10" + "node": ">=16.0.0" } }, - "node_modules/@electron/universal/node_modules/fs-extra": { + "node_modules/@ionic/utils-fs/node_modules/@types/fs-extra": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.2.tgz", + "integrity": "sha512-SvSrYXfWSc7R4eqnOzbQF4TZmfpNSM9FrSWLU3EUnWBuyZqNBOrv1B1JA3byUDPUl9z4Ab3jeZG2eDdySlgNMg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@ionic/utils-fs/node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", @@ -2762,7 +4071,7 @@ "node": ">=10" } }, - "node_modules/@electron/universal/node_modules/jsonfile": { + "node_modules/@ionic/utils-fs/node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", @@ -2774,7 +4083,7 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/@electron/universal/node_modules/universalify": { + "node_modules/@ionic/utils-fs/node_modules/universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", @@ -2783,117 +4092,166 @@ "node": ">= 10.0.0" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", + "node_modules/@ionic/utils-object": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.6.tgz", + "integrity": "sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==", "dev": true, - "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "debug": "^4.0.0", + "tslib": "^2.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "node": ">=16.0.0" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.5.1", + "node_modules/@ionic/utils-process": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.11.tgz", + "integrity": "sha512-Uavxn+x8j3rDlZEk1X7YnaN6wCgbCwYQOeIjv/m94i1dzslqWhqIHEqxEyeE8HsT5Negboagg7GtQiABy+BLbA==", "dev": true, - "license": "MIT", + "dependencies": { + "@ionic/utils-object": "2.1.6", + "@ionic/utils-terminal": "2.3.4", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2", + "tslib": "^2.0.1" + }, "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=16.0.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.0.3", + "node_modules/@ionic/utils-stream": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.6.tgz", + "integrity": "sha512-4+Kitey1lTA1yGtnigeYNhV/0tggI3lWBMjC7tBs1K9GXa/q7q4CtOISppdh8QgtOhrhAXS2Igp8rbko/Cj+lA==", "dev": true, - "license": "MIT", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "debug": "^4.0.0", + "tslib": "^2.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=16.0.0" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.20.0", + "node_modules/@ionic/utils-subprocess": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-2.1.12.tgz", + "integrity": "sha512-N05Y+dIXBHofKWJTheCMzVqmgY9wFmZcRv/LdNnfXaaA/mxLTyGxQYeig8fvQXTtDafb/siZXcrTkmQ+y6n3Yg==", "dev": true, - "license": "MIT", "dependencies": { - "type-fest": "^0.20.2" + "@ionic/utils-array": "2.1.6", + "@ionic/utils-fs": "3.1.7", + "@ionic/utils-process": "2.1.11", + "@ionic/utils-stream": "3.1.6", + "@ionic/utils-terminal": "2.3.4", + "cross-spawn": "^7.0.3", + "debug": "^4.0.0", + "tslib": "^2.0.1" }, "engines": { - "node": ">=8" + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-terminal": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.4.tgz", + "integrity": "sha512-cEiMFl3jklE0sW60r8JHH3ijFTwh/jkdEKWbylSyExQwZ8pPuwoXz7gpkWoJRLuoRHHSvg+wzNYyPJazIHfoJA==", + "dev": true, + "dependencies": { + "@types/slice-ansi": "^4.0.0", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "slice-ansi": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "tslib": "^2.0.1", + "untildify": "^4.0.0", + "wrap-ansi": "^7.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", + "node_modules/@ionic/utils-terminal/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "license": "(MIT OR CC0-1.0)", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@eslint/js": { - "version": "8.41.0", + "node_modules/@ionic/utils-terminal/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=7.0.0" } }, - "node_modules/@gar/promisify": { - "version": "1.1.3", - "dev": true, - "license": "MIT" + "node_modules/@ionic/utils-terminal/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.8", + "node_modules/@ionic/utils-terminal/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/@ionic/utils-terminal/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - }, "engines": { - "node": ">=10.10.0" + "node": ">=8" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", + "node_modules/@ionic/utils-terminal/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, - "license": "Apache-2.0", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, "engines": { - "node": ">=12.22" + "node": ">=10" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", + "node_modules/@ionic/utils-terminal/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, - "license": "BSD-3-Clause" + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", @@ -3805,9 +5163,9 @@ "license": "MIT" }, "node_modules/@malept/cross-spawn-promise": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", - "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", "dev": true, "funding": [ { @@ -3823,7 +5181,7 @@ "cross-spawn": "^7.0.1" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 10" } }, "node_modules/@malept/flatpak-bundler": { @@ -3877,6 +5235,14 @@ "node": ">= 10.0.0" } }, + "node_modules/@mauricewegner/capacitor-navigation-bar": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@mauricewegner/capacitor-navigation-bar/-/capacitor-navigation-bar-2.0.3.tgz", + "integrity": "sha512-E8HTcVkZEqm4tLJ7MpkTlqc93mboUSbJL41fFbvEaVwBhpgenEezkK268RxwQDij5hkudZmo4/eRXG3aJQwrzQ==", + "peerDependencies": { + "@capacitor/core": "^4.0.1 || ^5.0.0" + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "dev": true, @@ -3980,6 +5346,20 @@ "fsevents": "2.3.2" } }, + "node_modules/@playwright/test/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/@react-dnd/asap": { "version": "4.0.1", "license": "MIT" @@ -4676,9 +6056,9 @@ "license": "MIT" }, "node_modules/@types/debug": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", - "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.10.tgz", + "integrity": "sha512-tOSCru6s732pofZ+sMv9o4o3Zc+Sa8l3bxd/tweTQudFn06vAzb13ZX46Zi6m6EJ+RUbRTHvgQJ1gBtSgkaUYA==", "dev": true, "dependencies": { "@types/ms": "*" @@ -4849,9 +6229,9 @@ "license": "MIT" }, "node_modules/@types/ms": { - "version": "0.7.31", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", - "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", + "version": "0.7.33", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.33.tgz", + "integrity": "sha512-AuHIyzR5Hea7ij0P9q7vx7xu4z0C28ucwjAZC0ja7JhINyCnOw8/DnvAPQQ9TfOlCtZAmCERKQX9+o1mgQhuOQ==", "dev": true }, "node_modules/@types/node": { @@ -4877,9 +6257,9 @@ "license": "MIT" }, "node_modules/@types/plist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.2.tgz", - "integrity": "sha512-ULqvZNGMv0zRFvqn8/4LSPtnmN4MfhlPNtJCTpKuIIxGVGZ2rYWzFXrvEBoh9CVyqSE7D6YFRJ1hydLHI6kbWw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.4.tgz", + "integrity": "sha512-pTa9xUFQFM9WJGSWHajYNljD+DbVylE1q9IweK1LBhUYJdJ28YNU8j3KZ4Q1Qw+cSl4+QLLLOVmqNjhhvVO8fA==", "dev": true, "optional": true, "dependencies": { @@ -4975,6 +6355,12 @@ "@types/node": "*" } }, + "node_modules/@types/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==", + "dev": true + }, "node_modules/@types/sockjs": { "version": "0.3.33", "dev": true, @@ -4997,9 +6383,9 @@ } }, "node_modules/@types/verror": { - "version": "1.10.6", - "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.6.tgz", - "integrity": "sha512-NNm+gdePAX1VGvPcGZCDKQZKYSiAWigKhKaz5KF94hG6f2s8de9Ow5+7AbXoeKxL8gavZfk4UquSAygOF2duEQ==", + "version": "1.10.8", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.8.tgz", + "integrity": "sha512-YhUhnxRYs/NiVUbIs3F/EzviDP/NZCEAE2Mx5DUqLdldUmphOhFCVh7Kc+7zlYEExM0P8dzfbJi0yRlNb2Bw5g==", "dev": true, "optional": true }, @@ -5774,6 +7160,15 @@ "node": ">=8" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "dev": true, @@ -5837,16 +7232,15 @@ "dev": true }, "node_modules/app-builder-lib": { - "version": "24.5.2", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.5.2.tgz", - "integrity": "sha512-fZbUrFl3FW7yw92KiDpXV3Nd84EW+D7/WU7MEjX2eHDWM45Qx4hYOZpL9PaT9ZzZbaNfNLmt2EOnoqHQXHLdKw==", + "version": "24.6.4", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.6.4.tgz", + "integrity": "sha512-m9931WXb83teb32N0rKg+ulbn6+Hl8NV5SUpVDOVz9MWOXfhV6AQtTdftf51zJJvCQnQugGtSqoLvgw6mdF/Rg==", "dev": true, "dependencies": { "@develar/schema-utils": "~2.6.5", - "@electron/notarize": "^1.2.3", - "@electron/osx-sign": "^1.0.4", - "@electron/rebuild": "3.2.13", - "@electron/universal": "1.3.4", + "@electron/notarize": "2.1.0", + "@electron/osx-sign": "1.0.5", + "@electron/universal": "1.4.1", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", "7zip-bin": "~5.1.1", @@ -5949,9 +7343,9 @@ } }, "node_modules/app-builder-lib/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "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, "dependencies": { "lru-cache": "^6.0.0" @@ -6207,6 +7601,15 @@ "node": ">= 4.0.0" } }, + "node_modules/atomically": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz", + "integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==", + "dev": true, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/autoprefixer": { "version": "10.4.14", "dev": true, @@ -6550,6 +7953,15 @@ "dev": true, "license": "MIT" }, + "node_modules/big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "dev": true, @@ -6568,8 +7980,9 @@ }, "node_modules/bl": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, - "license": "MIT", "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -6578,6 +7991,8 @@ }, "node_modules/bl/node_modules/buffer": { "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "dev": true, "funding": [ { @@ -6593,7 +8008,6 @@ "url": "https://feross.org/support" } ], - "license": "MIT", "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -6747,6 +8161,18 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/bplist-parser": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", + "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==", + "dev": true, + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "dev": true, @@ -7176,6 +8602,18 @@ } ] }, + "node_modules/capacitor-plugin-safe-area": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/capacitor-plugin-safe-area/-/capacitor-plugin-safe-area-2.0.5.tgz", + "integrity": "sha512-Lg3P+/aW2/+C3gz2/xZFdfjgkOJ/ZSFTmZSxLaSY6lIudOTjwVpjoxhAyKUz1tc13Carjqdlq01yRrr++XOzAg==", + "peerDependencies": { + "@capacitor/core": "^5.0.0" + } + }, + "node_modules/capacitor-splash-screen": { + "resolved": "mobile/plugins/capacitor-splash-screen", + "link": true + }, "node_modules/capital-case": { "version": "1.0.4", "dev": true, @@ -7312,6 +8750,15 @@ "node": "*" } }, + "node_modules/chevrotain": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-6.5.0.tgz", + "integrity": "sha512-BwqQ/AgmKJ8jcMEjaSnfMybnKMgGTrtDKowfTP3pX4jwVy0kNjRsT/AP6h+wC3+3NC+X8X15VWBnTCQlX+wQFg==", + "dev": true, + "dependencies": { + "regexp-to-ast": "0.4.0" + } + }, "node_modules/chokidar": { "version": "3.5.3", "dev": true, @@ -7436,20 +8883,25 @@ } }, "node_modules/cli-cursor": { - "version": "3.1.0", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", "dev": true, - "license": "MIT", "dependencies": { - "restore-cursor": "^3.1.0" + "restore-cursor": "^4.0.0" }, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/cli-spinners": { - "version": "2.9.0", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.1.tgz", + "integrity": "sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" }, @@ -7459,8 +8911,9 @@ }, "node_modules/cli-truncate": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", + "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", "dev": true, - "license": "MIT", "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^5.0.0" @@ -7524,7 +8977,16 @@ "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=8" + "node": ">=8" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" } }, "node_modules/clone-deep": { @@ -7814,6 +9276,85 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/conf": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/conf/-/conf-10.2.0.tgz", + "integrity": "sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==", + "dev": true, + "dependencies": { + "ajv": "^8.6.3", + "ajv-formats": "^2.1.1", + "atomically": "^1.7.0", + "debounce-fn": "^4.0.0", + "dot-prop": "^6.0.1", + "env-paths": "^2.2.1", + "json-schema-typed": "^7.0.3", + "onetime": "^5.1.2", + "pkg-up": "^3.1.0", + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conf/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/conf/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/conf/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, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conf/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, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/conf/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 + }, "node_modules/config-file-ts": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.4.tgz", @@ -8351,6 +9892,30 @@ "url": "https://opencollective.com/date-fns" } }, + "node_modules/debounce-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz", + "integrity": "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/debounce-fn/node_modules/mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/debug": { "version": "4.3.4", "dev": true, @@ -8481,8 +10046,9 @@ }, "node_modules/defaults": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", "dev": true, - "license": "MIT", "dependencies": { "clone": "^1.0.2" }, @@ -8490,14 +10056,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/defaults/node_modules/clone": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -8562,9 +10120,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", "dev": true, "engines": { "node": ">=8" @@ -8613,12 +10171,12 @@ } }, "node_modules/dmg-builder": { - "version": "24.5.2", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.5.2.tgz", - "integrity": "sha512-4qWGO3OM+1ipqvrKvskZRLDEvAPZdZwil6e40Tb8dKogpEhabrzcjpwoRycBy8FAx8R2EBQaFCtIp5rBO/DM8A==", + "version": "24.6.4", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.6.4.tgz", + "integrity": "sha512-BNcHRc9CWEuI9qt0E655bUBU/j/3wUCYBVKGu1kVpbN5lcUdEJJJeiO0NHK3dgKmra6LUUZlo+mWqc+OCbi0zw==", "dev": true, "dependencies": { - "app-builder-lib": "24.5.2", + "app-builder-lib": "24.6.4", "builder-util": "24.5.0", "builder-util-runtime": "9.2.1", "fs-extra": "^10.1.0", @@ -8824,6 +10382,21 @@ "tslib": "^2.0.3" } }, + "node_modules/dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "dev": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dotenv": { "version": "16.0.3", "dev": true, @@ -8870,9 +10443,9 @@ } }, "node_modules/electron": { - "version": "22.3.15", - "resolved": "https://registry.npmjs.org/electron/-/electron-22.3.15.tgz", - "integrity": "sha512-KhxJkx2tfB8Q1moUI3sI/x48lehTk3wUEwwaKKkfzSKT3m7nK/g1YSYiYe4c8WuqODAcJKhB1MOvRv3WmhBYBw==", + "version": "22.3.27", + "resolved": "https://registry.npmjs.org/electron/-/electron-22.3.27.tgz", + "integrity": "sha512-7Rht21vHqj4ZFRnKuZdFqZFsvMBCmDqmjetiMqPtF+TmTBiGne1mnstVXOA/SRGhN2Qy5gY5bznJKpiqogjM8A==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -8888,21 +10461,21 @@ } }, "node_modules/electron-builder": { - "version": "24.5.2", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.5.2.tgz", - "integrity": "sha512-rxlUSSqziRMdTSSzti7It4R7wmuttouMhgTiF0HmoTXvaBKlmHPgkQjaI8ZFIZ0Rg+2TFPlPdMu2BwX3+6HJCg==", + "version": "24.6.4", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.6.4.tgz", + "integrity": "sha512-uNWQoU7pE7qOaIQ6CJHpBi44RJFVG8OHRBIadUxrsDJVwLLo8Nma3K/EEtx5/UyWAQYdcK4nVPYKoRqBb20hbA==", "dev": true, "dependencies": { - "app-builder-lib": "24.5.2", + "app-builder-lib": "24.6.4", "builder-util": "24.5.0", "builder-util-runtime": "9.2.1", "chalk": "^4.1.2", - "dmg-builder": "24.5.2", + "dmg-builder": "24.6.4", "fs-extra": "^10.1.0", "is-ci": "^3.0.0", "lazy-val": "^1.0.5", "read-config-file": "6.3.2", - "simple-update-notifier": "^1.1.0", + "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { @@ -9160,22 +10733,191 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/electron-publish": { - "version": "24.5.0", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.5.0.tgz", - "integrity": "sha512-zwo70suH15L15B4ZWNDoEg27HIYoPsGJUF7xevLJLSI7JUPC8l2yLBdLGwqueJ5XkDL7ucYyRZzxJVR8ElV9BA==", + "node_modules/electron-publish": { + "version": "24.5.0", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.5.0.tgz", + "integrity": "sha512-zwo70suH15L15B4ZWNDoEg27HIYoPsGJUF7xevLJLSI7JUPC8l2yLBdLGwqueJ5XkDL7ucYyRZzxJVR8ElV9BA==", + "dev": true, + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "24.5.0", + "builder-util-runtime": "9.2.1", + "chalk": "^4.1.2", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/electron-publish/node_modules/builder-util-runtime": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.1.tgz", + "integrity": "sha512-2rLv/uQD2x+dJ0J3xtsmI12AlRyk7p45TEbE/6o/fbb633e/S3pPgm+ct+JHsoY7r39dKHnGEFk/AASRFdnXmA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/electron-publish/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/electron-publish/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/electron-publish/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-rebuild": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/electron-rebuild/-/electron-rebuild-3.2.9.tgz", + "integrity": "sha512-FkEZNFViUem3P0RLYbZkUjC8LUFIK+wKq09GHoOITSJjfDAVQv964hwaNseTTWt58sITQX3/5fHNYcTefqaCWw==", + "deprecated": "Please use @electron/rebuild moving forward. There is no API change, just a package name change", + "dev": true, + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "chalk": "^4.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "fs-extra": "^10.0.0", + "got": "^11.7.0", + "lzma-native": "^8.0.5", + "node-abi": "^3.0.0", + "node-api-version": "^0.1.4", + "node-gyp": "^9.0.0", + "ora": "^5.1.0", + "semver": "^7.3.5", + "tar": "^6.0.5", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/src/cli.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/electron-rebuild/node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], "dependencies": { - "@types/fs-extra": "^9.0.11", - "builder-util": "24.5.0", - "builder-util-runtime": "9.2.1", - "chalk": "^4.1.2", - "fs-extra": "^10.1.0", - "lazy-val": "^1.0.5", - "mime": "^2.5.2" + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" } }, - "node_modules/electron-publish/node_modules/ansi-styles": { + "node_modules/electron-rebuild/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", @@ -9190,20 +10932,7 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/electron-publish/node_modules/builder-util-runtime": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.1.tgz", - "integrity": "sha512-2rLv/uQD2x+dJ0J3xtsmI12AlRyk7p45TEbE/6o/fbb633e/S3pPgm+ct+JHsoY7r39dKHnGEFk/AASRFdnXmA==", - "dev": true, - "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/electron-publish/node_modules/chalk": { + "node_modules/electron-rebuild/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", @@ -9219,7 +10948,7 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/electron-publish/node_modules/color-convert": { + "node_modules/electron-rebuild/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", @@ -9231,13 +10960,13 @@ "node": ">=7.0.0" } }, - "node_modules/electron-publish/node_modules/color-name": { + "node_modules/electron-rebuild/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/electron-publish/node_modules/fs-extra": { + "node_modules/electron-rebuild/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", @@ -9251,7 +10980,7 @@ "node": ">=12" } }, - "node_modules/electron-publish/node_modules/has-flag": { + "node_modules/electron-rebuild/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", @@ -9260,7 +10989,7 @@ "node": ">=8" } }, - "node_modules/electron-publish/node_modules/jsonfile": { + "node_modules/electron-rebuild/node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", @@ -9272,7 +11001,34 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/electron-publish/node_modules/supports-color": { + "node_modules/electron-rebuild/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, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-rebuild/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, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-rebuild/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", @@ -9284,7 +11040,7 @@ "node": ">=8" } }, - "node_modules/electron-publish/node_modules/universalify": { + "node_modules/electron-rebuild/node_modules/universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", @@ -9293,6 +11049,37 @@ "node": ">= 10.0.0" } }, + "node_modules/electron-rebuild/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 + }, + "node_modules/electron-store": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/electron-store/-/electron-store-8.1.0.tgz", + "integrity": "sha512-2clHg/juMjOH0GT9cQ6qtmIvK183B39ZXR0bUoPwKwYHJsEF3quqyDzMFUAu+0OP8ijmN2CbPRAelhNbWUbzwA==", + "dev": true, + "dependencies": { + "conf": "^10.2.0", + "type-fest": "^2.17.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/electron-store/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.449", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.449.tgz", @@ -9488,6 +11275,24 @@ "node": ">=8" } }, + "node_modules/elementtree": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz", + "integrity": "sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==", + "dev": true, + "dependencies": { + "sax": "1.1.4" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/elementtree/node_modules/sax": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz", + "integrity": "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==", + "dev": true + }, "node_modules/emittery": { "version": "0.13.1", "dev": true, @@ -9551,6 +11356,19 @@ "node": ">=0.6" } }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/entities": { "version": "2.2.0", "dev": true, @@ -9888,6 +11706,21 @@ "eslint-plugin-import": "^2.25.3" } }, + "node_modules/eslint-config-prettier": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz", + "integrity": "sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw==", + "dev": true, + "dependencies": { + "get-stdin": "^6.0.0" + }, + "bin": { + "eslint-config-prettier-check": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=3.14.1" + } + }, "node_modules/eslint-config-react-app": { "version": "7.0.1", "dev": true, @@ -9967,9 +11800,10 @@ } }, "node_modules/eslint-import-resolver-webpack/node_modules/semver": { - "version": "5.7.1", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver" } @@ -10290,6 +12124,30 @@ "node": ">=4.0" } }, + "node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/eslint-visitor-keys": { "version": "3.4.1", "dev": true, @@ -11135,6 +12993,20 @@ "dev": true, "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.1", "dev": true, @@ -11157,6 +13029,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true + }, "node_modules/functions-have-names": { "version": "1.2.3", "dev": true, @@ -11260,6 +13138,15 @@ "node": ">=8.0.0" } }, + "node_modules/get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "dev": true, @@ -11297,6 +13184,12 @@ "webpack": "^5.0.0" } }, + "node_modules/github-slugger": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", + "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==", + "dev": true + }, "node_modules/glob": { "version": "7.2.3", "dev": true, @@ -12531,8 +14424,9 @@ }, "node_modules/is-fullwidth-code-point": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -12569,8 +14463,9 @@ }, "node_modules/is-interactive": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } @@ -12621,6 +14516,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-path-inside": { "version": "3.0.3", "dev": true, @@ -12754,8 +14658,9 @@ }, "node_modules/is-unicode-supported": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -13003,6 +14908,16 @@ "node": ">=8" } }, + "node_modules/java-parser": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/java-parser/-/java-parser-1.0.2.tgz", + "integrity": "sha512-lBXc+F62ds2W83eH5MwGnzuWdb6kgGBV0x0R7w0B4JKGDrJzolMUEhRMzzzlIX68HvRU7XtfPon22YaB+dVg+A==", + "dev": true, + "dependencies": { + "chevrotain": "6.5.0", + "lodash": "4.17.21" + } + }, "node_modules/jest": { "version": "29.5.0", "dev": true, @@ -14598,6 +16513,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz", + "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==", + "dev": true + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "dev": true, @@ -14748,38 +16669,37 @@ "license": "MIT" }, "node_modules/lint-staged": { - "version": "13.2.2", + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.0.2.tgz", + "integrity": "sha512-vnEy7pFTHyVuDmCAIFKR5QDO8XLVlPFQQyujQ/STOxe40ICWqJ6knS2wSJ/ffX/Lw0rz83luRDh+ET7toN+rOw==", "dev": true, - "license": "MIT", "dependencies": { - "chalk": "5.2.0", - "cli-truncate": "^3.1.0", - "commander": "^10.0.0", - "debug": "^4.3.4", - "execa": "^7.0.0", + "chalk": "5.3.0", + "commander": "11.1.0", + "debug": "4.3.4", + "execa": "8.0.1", "lilconfig": "2.1.0", - "listr2": "^5.0.7", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-inspect": "^1.12.3", - "pidtree": "^0.6.0", - "string-argv": "^0.3.1", - "yaml": "^2.2.2" + "listr2": "7.0.2", + "micromatch": "4.0.5", + "pidtree": "0.6.0", + "string-argv": "0.3.2", + "yaml": "2.3.3" }, "bin": { "lint-staged": "bin/lint-staged.js" }, "engines": { - "node": "^14.13.1 || >=16.0.0" + "node": ">=18.12.0" }, "funding": { "url": "https://opencollective.com/lint-staged" } }, "node_modules/lint-staged/node_modules/chalk": { - "version": "5.2.0", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true, - "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -14788,47 +16708,63 @@ } }, "node_modules/lint-staged/node_modules/commander": { - "version": "10.0.1", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", "dev": true, - "license": "MIT", "engines": { - "node": ">=14" + "node": ">=16" } }, "node_modules/lint-staged/node_modules/execa": { - "version": "7.1.1", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", "dev": true, - "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", - "get-stream": "^6.0.1", - "human-signals": "^4.3.0", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", - "signal-exit": "^3.0.7", + "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" }, "engines": { - "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + "node": ">=16.17" }, "funding": { "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/lint-staged/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lint-staged/node_modules/human-signals": { - "version": "4.3.1", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", "dev": true, - "license": "Apache-2.0", "engines": { - "node": ">=14.18.0" + "node": ">=16.17.0" } }, "node_modules/lint-staged/node_modules/is-stream": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", "dev": true, - "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -14838,8 +16774,9 @@ }, "node_modules/lint-staged/node_modules/mimic-fn": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -14849,8 +16786,9 @@ }, "node_modules/lint-staged/node_modules/npm-run-path": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", "dev": true, - "license": "MIT", "dependencies": { "path-key": "^4.0.0" }, @@ -14863,8 +16801,9 @@ }, "node_modules/lint-staged/node_modules/onetime": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", "dev": true, - "license": "MIT", "dependencies": { "mimic-fn": "^4.0.0" }, @@ -14877,19 +16816,9 @@ }, "node_modules/lint-staged/node_modules/path-key": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/strip-final-newline": { - "version": "3.0.0", - "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -14897,130 +16826,116 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lint-staged/node_modules/yaml": { - "version": "2.3.1", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 14" - } - }, - "node_modules/listr2": { - "version": "5.0.8", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^2.1.0", - "colorette": "^2.0.19", - "log-update": "^4.0.0", - "p-map": "^4.0.0", - "rfdc": "^1.3.0", - "rxjs": "^7.8.0", - "through": "^2.3.8", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": "^14.13.1 || >=16.0.0" - }, - "peerDependencies": { - "enquirer": ">= 2.3.0 < 3" - }, - "peerDependenciesMeta": { - "enquirer": { - "optional": true - } - } - }, - "node_modules/listr2/node_modules/ansi-styles": { - "version": "4.3.0", + "node_modules/lint-staged/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" + "node": ">=14" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/listr2/node_modules/cli-truncate": { - "version": "2.1.0", + "node_modules/lint-staged/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/listr2/node_modules/color-convert": { - "version": "2.0.1", + "node_modules/lint-staged/node_modules/yaml": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.3.tgz", + "integrity": "sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ==", "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, "engines": { - "node": ">=7.0.0" + "node": ">= 14" } }, - "node_modules/listr2/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/listr2/node_modules/emoji-regex": { - "version": "8.0.0", + "node_modules/listr2": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-7.0.2.tgz", + "integrity": "sha512-rJysbR9GKIalhTbVL2tYbF2hVyDnrf7pFUZBwjPaMIdadYHmeT+EVi/Bu3qd7ETQPahTotg2WRCatXwRBW554g==", "dev": true, - "license": "MIT" + "dependencies": { + "cli-truncate": "^3.1.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^5.0.1", + "rfdc": "^1.3.0", + "wrap-ansi": "^8.1.0" + }, + "engines": { + "node": ">=16.0.0" + } }, - "node_modules/listr2/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", "dev": true, - "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/listr2/node_modules/rxjs": { - "version": "7.8.1", + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/listr2/node_modules/slice-ansi": { - "version": "3.0.0", + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, - "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/listr2/node_modules/string-width": { - "version": "4.2.3", + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, - "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/loader-runner": { @@ -15106,15 +17021,14 @@ "dev": true, "license": "MIT" }, - "node_modules/log-update": { - "version": "4.0.0", + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, - "license": "MIT", "dependencies": { - "ansi-escapes": "^4.3.0", - "cli-cursor": "^3.1.0", - "slice-ansi": "^4.0.0", - "wrap-ansi": "^6.2.0" + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" }, "engines": { "node": ">=10" @@ -15123,10 +17037,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update/node_modules/ansi-styles": { + "node_modules/log-symbols/node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -15137,10 +17052,27 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/log-update/node_modules/color-convert": { + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -15148,64 +17080,133 @@ "node": ">=7.0.0" } }, - "node_modules/log-update/node_modules/color-name": { + "node_modules/log-symbols/node_modules/color-name": { "version": "1.1.4", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, - "node_modules/log-update/node_modules/emoji-regex": { - "version": "8.0.0", + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "license": "MIT" + "engines": { + "node": ">=8" + } }, - "node_modules/log-update/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { "node": ">=8" } }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "4.0.0", + "node_modules/log-update": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-5.0.1.tgz", + "integrity": "sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==", "dev": true, - "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" + "ansi-escapes": "^5.0.0", + "cli-cursor": "^4.0.0", + "slice-ansi": "^5.0.0", + "strip-ansi": "^7.0.1", + "wrap-ansi": "^8.0.1" }, "engines": { - "node": ">=10" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update/node_modules/string-width": { - "version": "4.2.3", + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", + "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", "dev": true, - "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "type-fest": "^1.0.2" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/log-update/node_modules/wrap-ansi": { - "version": "6.2.0", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, - "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/loose-envify": { @@ -15251,6 +17252,30 @@ "es5-ext": "~0.10.2" } }, + "node_modules/lzma-native": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/lzma-native/-/lzma-native-8.0.6.tgz", + "integrity": "sha512-09xfg67mkL2Lz20PrrDeNYZxzeW7ADtpYFbwSQh9U8+76RIzx5QsJBMy8qikv3hbUPfpy6hqwxt6FcGK81g9AA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "node-addon-api": "^3.1.0", + "node-gyp-build": "^4.2.1", + "readable-stream": "^3.6.0" + }, + "bin": { + "lzmajs": "bin/lzmajs" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/lzma-native/node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true + }, "node_modules/make-dir": { "version": "3.1.0", "dev": true, @@ -15917,6 +17942,44 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/native-bottom-sheet": { + "resolved": "mobile/plugins/native-bottom-sheet", + "link": true + }, + "node_modules/native-run": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/native-run/-/native-run-1.7.2.tgz", + "integrity": "sha512-2aahC8iXIO8BcvEukVMrYwL5sXurkuIGyQgfSGBto832W6ejV+cB5Ww+2/CRxmyozhbxARJ2OMpEGPV8sTqsrQ==", + "dev": true, + "dependencies": { + "@ionic/utils-fs": "^3.1.6", + "@ionic/utils-terminal": "^2.3.3", + "bplist-parser": "^0.3.2", + "debug": "^4.3.4", + "elementtree": "^0.1.7", + "ini": "^3.0.1", + "plist": "^3.0.6", + "split2": "^4.1.0", + "through2": "^4.0.2", + "tslib": "^2.4.0", + "yauzl": "^2.10.0" + }, + "bin": { + "native-run": "bin/native-run" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/native-run/node_modules/ini": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-3.0.1.tgz", + "integrity": "sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==", + "dev": true, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "dev": true, @@ -15963,9 +18026,9 @@ } }, "node_modules/node-abi": { - "version": "3.43.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.43.0.tgz", - "integrity": "sha512-QB0MMv+tn9Ur2DtJrc8y09n0n6sw88CyDniWSX2cHW10goQXYPK9ZpFJOktDS4ron501edPX6h9i7Pg+RnH5nQ==", + "version": "3.51.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.51.0.tgz", + "integrity": "sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==", "dev": true, "dependencies": { "semver": "^7.3.5" @@ -15986,10 +18049,10 @@ "node": ">=10" } }, - "node_modules/node-abi/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "node_modules/node-abi/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, "dependencies": { "lru-cache": "^6.0.0" @@ -16036,9 +18099,9 @@ } }, "node_modules/node-api-version/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "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, "dependencies": { "lru-cache": "^6.0.0" @@ -16106,6 +18169,17 @@ "node": "^12.13 || ^14.13 || >=16" } }, + "node_modules/node-gyp-build": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.1.tgz", + "integrity": "sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==", + "dev": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-gyp/node_modules/lru-cache": { "version": "6.0.0", "dev": true, @@ -16532,6 +18606,124 @@ "node": ">= 0.8.0" } }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ora/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/ora/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-cancelable": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", @@ -16712,6 +18904,40 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", + "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz", + "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/path-to-regexp": { "version": "2.2.1", "dev": true, @@ -16833,6 +19059,79 @@ "node": ">=8" } }, + "node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dev": true, + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-up/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-up/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/playwright-core": { "version": "1.34.3", "dev": true, @@ -16858,7 +19157,9 @@ } }, "node_modules/postcss": { - "version": "8.4.24", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "funding": [ { @@ -16874,7 +19175,6 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", @@ -17484,6 +19784,41 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz", + "integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/prettier-plugin-java": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/prettier-plugin-java/-/prettier-plugin-java-1.0.2.tgz", + "integrity": "sha512-YgcN1WGZlrH0E+bHdqtIYtfDp6k2PHBnIaGjzdff/7t/NyDWAA6ypAmnD7YQVG2OuoIaXYkC37HN7cz68lLWLg==", + "dev": true, + "dependencies": { + "java-parser": "1.0.2", + "lodash": "4.17.21", + "prettier": "2.2.1" + } + }, + "node_modules/prettier-plugin-java/node_modules/prettier": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz", + "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/pretty-error": { "version": "4.0.0", "dev": true, @@ -17963,9 +20298,10 @@ } }, "node_modules/read-pkg/node_modules/semver": { - "version": "5.7.1", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver" } @@ -18062,6 +20398,12 @@ "@babel/runtime": "^7.8.4" } }, + "node_modules/regexp-to-ast": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.4.0.tgz", + "integrity": "sha512-4qf/7IsIKfSNHQXSwial1IFmfM1Cc/whNBQqRwe0V2stPe7KmN1U0tWQiIx6JiirgSrisjE0eECdNf7Tav1Ntw==", + "dev": true + }, "node_modules/regexp.prototype.flags": { "version": "1.5.0", "dev": true, @@ -18078,6 +20420,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, "node_modules/regexpu-core": { "version": "5.3.2", "dev": true, @@ -18329,15 +20683,19 @@ } }, "node_modules/restore-cursor": { - "version": "3.1.0", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", "dev": true, - "license": "MIT", "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" }, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/retry": { @@ -18359,8 +20717,9 @@ }, "node_modules/rfdc": { "version": "1.3.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true }, "node_modules/rimraf": { "version": "3.0.2", @@ -18376,6 +20735,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rollup": { + "version": "2.79.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", + "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "dev": true, @@ -18607,9 +20981,10 @@ } }, "node_modules/semver": { - "version": "6.3.0", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -18947,26 +21322,50 @@ "license": "ISC" }, "node_modules/simple-update-notifier": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", - "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", "dev": true, "dependencies": { - "semver": "~7.0.0" + "semver": "^7.5.3" }, "engines": { - "node": ">=8.10.0" + "node": ">=10" + } + }, + "node_modules/simple-update-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, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" } }, "node_modules/simple-update-notifier/node_modules/semver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", - "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "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, + "dependencies": { + "lru-cache": "^6.0.0" + }, "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, + "node_modules/simple-update-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 + }, "node_modules/sisteransi": { "version": "1.0.5", "dev": true, @@ -18982,8 +21381,9 @@ }, "node_modules/slice-ansi": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" @@ -18997,8 +21397,9 @@ }, "node_modules/slice-ansi/node_modules/ansi-styles": { "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -19184,6 +21585,15 @@ "wbuf": "^1.7.3" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "dev": true, @@ -19809,6 +22219,271 @@ "url": "https://github.com/fontello/svg2ttf?sponsor=1" } }, + "node_modules/swiftlint": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/swiftlint/-/swiftlint-1.0.2.tgz", + "integrity": "sha512-YhcS0N3vkwBatnZf/iJPg89LGQk7MbFnz67Cg3EZ6Ppqm2H8y6x7A1t6KMQ0jYVQpea9wQiFiFRFhkoChaQ29Q==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@ionic/utils-fs": "3.1.6", + "@ionic/utils-subprocess": "2.1.11", + "cosmiconfig": "^6.0.0" + }, + "bin": { + "node-swiftlint": "bin.js" + } + }, + "node_modules/swiftlint/node_modules/@ionic/utils-array": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.5.tgz", + "integrity": "sha512-HD72a71IQVBmQckDwmA8RxNVMTbxnaLbgFOl+dO5tbvW9CkkSFCv41h6fUuNsSEVgngfkn0i98HDuZC8mk+lTA==", + "dev": true, + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=10.3.0" + } + }, + "node_modules/swiftlint/node_modules/@ionic/utils-fs": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.6.tgz", + "integrity": "sha512-eikrNkK89CfGPmexjTfSWl4EYqsPSBh0Ka7by4F0PLc1hJZYtJxUZV3X4r5ecA8ikjicUmcbU7zJmAjmqutG/w==", + "dev": true, + "dependencies": { + "@types/fs-extra": "^8.0.0", + "debug": "^4.0.0", + "fs-extra": "^9.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=10.3.0" + } + }, + "node_modules/swiftlint/node_modules/@ionic/utils-object": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.5.tgz", + "integrity": "sha512-XnYNSwfewUqxq+yjER1hxTKggftpNjFLJH0s37jcrNDwbzmbpFTQTVAp4ikNK4rd9DOebX/jbeZb8jfD86IYxw==", + "dev": true, + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=10.3.0" + } + }, + "node_modules/swiftlint/node_modules/@ionic/utils-process": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.10.tgz", + "integrity": "sha512-mZ7JEowcuGQK+SKsJXi0liYTcXd2bNMR3nE0CyTROpMECUpJeAvvaBaPGZf5ERQUPeWBVuwqAqjUmIdxhz5bxw==", + "dev": true, + "dependencies": { + "@ionic/utils-object": "2.1.5", + "@ionic/utils-terminal": "2.3.3", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=10.3.0" + } + }, + "node_modules/swiftlint/node_modules/@ionic/utils-stream": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.5.tgz", + "integrity": "sha512-hkm46uHvEC05X/8PHgdJi4l4zv9VQDELZTM+Kz69odtO9zZYfnt8DkfXHJqJ+PxmtiE5mk/ehJWLnn/XAczTUw==", + "dev": true, + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=10.3.0" + } + }, + "node_modules/swiftlint/node_modules/@ionic/utils-subprocess": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-2.1.11.tgz", + "integrity": "sha512-6zCDixNmZCbMCy5np8klSxOZF85kuDyzZSTTQKQP90ZtYNCcPYmuFSzaqDwApJT4r5L3MY3JrqK1gLkc6xiUPw==", + "dev": true, + "dependencies": { + "@ionic/utils-array": "2.1.5", + "@ionic/utils-fs": "3.1.6", + "@ionic/utils-process": "2.1.10", + "@ionic/utils-stream": "3.1.5", + "@ionic/utils-terminal": "2.3.3", + "cross-spawn": "^7.0.3", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=10.3.0" + } + }, + "node_modules/swiftlint/node_modules/@ionic/utils-terminal": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.3.tgz", + "integrity": "sha512-RnuSfNZ5fLEyX3R5mtcMY97cGD1A0NVBbarsSQ6yMMfRJ5YHU7hHVyUfvZeClbqkBC/pAqI/rYJuXKCT9YeMCQ==", + "dev": true, + "dependencies": { + "@types/slice-ansi": "^4.0.0", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "slice-ansi": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "tslib": "^2.0.1", + "untildify": "^4.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.3.0" + } + }, + "node_modules/swiftlint/node_modules/@types/fs-extra": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.3.tgz", + "integrity": "sha512-7IdV01N0u/CaVO0fuY1YmEg14HQN3+EW8mpNgg6NEfxEl/lzCa5OxlBu3iFsCAdamnYOcTQ7oEi43Xc/67Rgzw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/swiftlint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/swiftlint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/swiftlint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/swiftlint/node_modules/cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/swiftlint/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/swiftlint/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swiftlint/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/swiftlint/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/swiftlint/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/swiftlint/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/swiftlint/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/symbol.inspect": { "version": "1.0.1", "license": "ISC" @@ -20116,10 +22791,14 @@ "dev": true, "license": "MIT" }, - "node_modules/through": { - "version": "2.3.8", + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", "dev": true, - "license": "MIT" + "dependencies": { + "readable-stream": "3" + } }, "node_modules/thunky": { "version": "1.1.0", @@ -20399,7 +23078,6 @@ }, "node_modules/tslib": { "version": "2.5.2", - "dev": true, "license": "0BSD" }, "node_modules/tsutils": { @@ -20555,15 +23233,16 @@ } }, "node_modules/typescript": { - "version": "5.0.4", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=12.20" + "node": ">=14.17" } }, "node_modules/uglify-js": { @@ -20668,6 +23347,15 @@ "node": ">= 0.8" } }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/unused-filename": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/unused-filename/-/unused-filename-2.1.0.tgz", @@ -20920,8 +23608,9 @@ }, "node_modules/wcwidth": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", "dev": true, - "license": "MIT", "dependencies": { "defaults": "^1.0.3" } @@ -21482,6 +24171,28 @@ } } }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", @@ -21593,8 +24304,9 @@ } }, "node_modules/zod": { - "version": "3.21.4", - "license": "MIT", + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 04b9ba2f..a8a43a5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mytonwallet", - "version": "1.16.4", + "version": "1.17.0", "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": { @@ -18,12 +18,18 @@ "extension-opera:package": "cross-env IS_OPERA_EXTENSION=1 IS_EXTENSION=1 webpack && bash ./deploy/package_extension.sh opera", "extension-opera:package:staging": "cross-env APP_ENV=staging npm run extension-opera:package", "extension-opera:package:production": "npm run extension-opera:package", - "electron:dev": "npm run electron:webpack && IS_ELECTRON=1 concurrently -n main,renderer,electron \"npm run electron:webpack -- --watch\" \"npm run dev\" \"electronmon dist/electron\"", + "electron:dev": "npm run electron:webpack && IS_ELECTRON_BUILD=1 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=1 npm run build:$ENV && electron-builder install-app-deps && electron-rebuild && ENV=$ENV npm run electron:webpack", + "electron:build": "IS_ELECTRON_BUILD=1 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", + "mobile:build": "IS_CAPACITOR=1 npm run build && cap sync", + "mobile:build:dev": "cross-env APP_ENV=development npm run mobile:build", + "mobile:build:staging": "cross-env APP_ENV=staging npm run mobile:build", + "mobile:build:production": "npm run mobile:build", + "mobile:run:android": "npm run mobile:build:dev && cap run android", + "mobile:run:ios": "npm run mobile:build:dev && cap run ios", "build:icons": "fantasticon", "check": "tsc && stylelint \"**/*.{css,scss}\" && eslint . --ext .ts,.tsx", "check:fix": "npm run check -- --fix", @@ -31,7 +37,9 @@ "test:playwright": "playwright test", "test:record": "playwright codegen localhost:1235", "prepare": "husky install", - "statoscope:validate-diff": "statoscope validate --input input.json --reference reference.json" + "statoscope:validate-diff": "statoscope validate --input input.json --reference reference.json", + "update_version": "node ./deploy/update_version.js", + "postversion": "rm -rf .patch-version && npm run update_version" }, "engines": { "node": "^18", @@ -72,6 +80,7 @@ "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.21.0", "@babel/register": "^7.21.0", + "@capacitor/cli": "^5.3.0", "@peculiar/webcrypto": "^1.3.2", "@playwright/test": "^1.31.2", "@statoscope/cli": "^5.27.0", @@ -108,6 +117,8 @@ "electron": "^22.1.0", "electron-builder": "^24.4.0", "electron-context-menu": "^3.6.1", + "electron-rebuild": "^3.2.9", + "electron-store": "^8.1.0", "electron-updater": "^5.3.0", "electron-window-state": "^5.0.3", "electronmon": "^2.0.2", @@ -134,7 +145,7 @@ "jest": "^29.5.0", "jest-raw-loader": "^1.0.1", "js-yaml": "^4.1.0", - "lint-staged": "^13.2.0", + "lint-staged": "^15.0.2", "mini-css-extract-plugin": "^2.7.5", "postcss": "^8.4.14", "postcss-loader": "^7.1.0", @@ -153,15 +164,28 @@ "stylelint-group-selectors": "^1.0.9", "stylelint-high-performance-animation": "^1.8.0", "stylelint-order": "^5.0.0", - "typescript": "^5.0.2", + "typescript": "^5.2.2", "webpack": "^5.76.2", "webpack-dev-server": "^4.13.1" }, "dependencies": { + "@capacitor-mlkit/barcode-scanning": "^5.3.0", + "@capacitor/android": "^5.3.0", + "@capacitor/app": "^5.0.6", + "@capacitor/core": "^5.5.1", + "@capacitor/dialog": "^5.0.6", + "@capacitor/haptics": "^5.0.6", + "@capacitor/ios": "^5.5.1", + "@capacitor/status-bar": "^5.0.6", + "@capgo/capacitor-native-biometric": "^5.1.0", "@ledgerhq/hw-transport-webhid": "^6.27.12", + "@mauricewegner/capacitor-navigation-bar": "^2.0.3", "@ton-community/ton-ledger": "^6.0.0", "buffer": "^6.0.3", + "capacitor-plugin-safe-area": "^2.0.5", + "capacitor-splash-screen": "file:mobile/plugins/capacitor-splash-screen", "idb-keyval": "^6.2.0", + "native-bottom-sheet": "file:mobile/plugins/native-bottom-sheet", "pako": "^2.1.0", "qr-code-styling": "github:troman29/qr-code-styling#c00d0", "qrcode-generator": "^1.4.4", diff --git a/public/electronVersion.txt b/public/electronVersion.txt new file mode 100644 index 00000000..092afa15 --- /dev/null +++ b/public/electronVersion.txt @@ -0,0 +1 @@ +1.17.0 diff --git a/public/get/.well-known/apple-app-site-association b/public/get/.well-known/apple-app-site-association new file mode 100644 index 00000000..5347fbc6 --- /dev/null +++ b/public/get/.well-known/apple-app-site-association @@ -0,0 +1,11 @@ +{ + "applinks": { + "apps": [], + "details": [ + { + "appID": "Y54Z4K69Z9.org.mytonwallet.app", + "paths": ["*"] + } + ] + } +} diff --git a/public/get/.well-known/assetlinks.json b/public/get/.well-known/assetlinks.json new file mode 100644 index 00000000..2b133408 --- /dev/null +++ b/public/get/.well-known/assetlinks.json @@ -0,0 +1,15 @@ +[ + { + "relation": [ + "delegate_permission/common.handle_all_urls" + ], + "target": { + "namespace": "android_app", + "package_name": "org.mytonwallet.app", + "sha256_cert_fingerprints": [ + "93:31:8E:37:31:AE:D8:29:A9:9F:18:FB:9A:1F:0C:7A:D4:B6:02:2C:14:E6:89:5C:DA:D0:BE:37:A9:DE:58:97", + "C2:F6:EC:F2:AA:46:B9:76:7E:B4:11:7E:7E:D0:16:AB:04:44:03:5C:5D:8C:7A:AC:5A:9F:04:AC:07:A0:B6:40" + ] + } + } +] \ No newline at end of file diff --git a/public/logo.svg b/public/logo.svg index 40c5b004..5e4d3a57 100644 --- a/public/logo.svg +++ b/public/logo.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/version.txt b/public/version.txt new file mode 100644 index 00000000..73d74673 --- /dev/null +++ b/public/version.txt @@ -0,0 +1 @@ +1.17.0 \ No newline at end of file diff --git a/src/api/blockchains/ton/address.ts b/src/api/blockchains/ton/address.ts index 6d684ce1..b3a8c081 100644 --- a/src/api/blockchains/ton/address.ts +++ b/src/api/blockchains/ton/address.ts @@ -40,3 +40,7 @@ export async function resolveAddress(network: ApiNetwork, address: string): Prom return undefined; } } + +export function normalizeAddress(address: string) { + return toBase64Address(address, true); +} diff --git a/src/api/blockchains/ton/constants.ts b/src/api/blockchains/ton/constants.ts index 81acd65c..f41d9449 100644 --- a/src/api/blockchains/ton/constants.ts +++ b/src/api/blockchains/ton/constants.ts @@ -9,10 +9,11 @@ export const UNSTAKE_COMMENT = 'w'; export const ATTEMPTS = 5; export const DEFAULT_DECIMALS = 9; +export const DEFAULT_IS_BOUNCEABLE = false; export const LEDGER_SUPPORTED_PAYLOADS: ApiParsedPayload['type'][] = [ - 'transfer-nft', - 'transfer-tokens', + 'nft:transfer', + 'tokens:transfer', 'comment', ]; @@ -33,3 +34,17 @@ export enum JettonOpCode { export enum NftOpCode { TransferOwnership = 0x5fcc3d14, } + +export enum LiquidStakingOpCode { + // Pool + RequestLoan = 0xe642c965, + LoanRepayment = 0xdfdca27b, + Deposit = 0x47d54391, + Withdraw = 0x319B0CDC, + Withdrawal = 0x0a77535c, + DeployController = 0xb27edcad, + Touch = 0x4bc7c2df, + Donate = 0x73affe21, + // NFT + DistributedAsset = 0xdb3b8abd, +} diff --git a/src/api/blockchains/ton/index.ts b/src/api/blockchains/ton/index.ts index 9cc742ca..2fbcad7d 100644 --- a/src/api/blockchains/ton/index.ts +++ b/src/api/blockchains/ton/index.ts @@ -7,9 +7,11 @@ export { seedToKeyPair, validateMnemonic, verifyPassword, + fetchPrivateKey, } from './auth'; export { getAccountNfts, getNftUpdates } from './nfts'; export { oneCellFromBoc } from './util/tonweb'; +export { buildTokenSlug } from './util'; export { checkTransactionDraft, getAccountNewestTxId, @@ -23,6 +25,7 @@ export { sendSignedMessage, sendSignedMessages, decryptComment, + waitUntilTransactionAppears, } from './transactions'; export { getAccountBalance, @@ -33,7 +36,9 @@ export { getWalletStateInit, getWalletBalance, getWalletSeqno, - isWalletInitialized, + isAddressInitialized, + isActiveSmartContract, + getWalletInfo, } from './wallet'; export { checkStakeDraft, @@ -41,14 +46,14 @@ export { submitStake, submitUnstake, getStakingState, - getBackendStakingState, } from './staking'; export { packPayloadToBoc, + checkApiAvailability, } from './other'; export { getAccountTokenBalances, - importToken, + fetchToken, resolveTokenBySlug, } from './tokens'; export { @@ -58,3 +63,6 @@ export { export { parsePayloadBase64, } from './util/metadata'; +export { + normalizeAddress, +} from './address'; diff --git a/src/api/blockchains/ton/nfts.ts b/src/api/blockchains/ton/nfts.ts index b3b0fc39..83f62537 100644 --- a/src/api/blockchains/ton/nfts.ts +++ b/src/api/blockchains/ton/nfts.ts @@ -13,7 +13,7 @@ export async function getAccountNfts(accountId: string, offset?: number, limit?: const { network } = parseAccountId(accountId); const address = await fetchStoredAddress(accountId); - const rawNfts = await fetchAccountNfts(network, address, offset, limit); + const rawNfts = await fetchAccountNfts(network, address, { offset, limit }); return compact(rawNfts.map(buildNft)); } @@ -30,20 +30,25 @@ export function buildNft(rawNft: NftItem): ApiNft | undefined { metadata: { name, image, + attributes, + description, }, previews, sale, } = rawNft; + const isHidden = attributes?.render_type === 'hidden' || description === 'SCAM'; + return { index, name, - address: toBase64Address(address), + address: toBase64Address(address, true), image, thumbnail: previews!.find((x) => x.resolution === '500x500')!.url, isOnSale: Boolean(sale), + isHidden, ...(collection && { - collectionAddress: toBase64Address(collection.address), + collectionAddress: toBase64Address(collection.address, true), collectionName: collection.name, }), }; @@ -72,12 +77,12 @@ export async function getNftUpdates(accountId: string, fromSec: number) { const { sender, recipient, nft: rawNftAddress } = action.nftItemTransfer; if (!sender || !recipient) continue; to = toBase64Address(recipient.address); - nftAddress = toBase64Address(rawNftAddress); + nftAddress = toBase64Address(rawNftAddress, true); } else if (action.nftPurchase) { const { buyer } = action.nftPurchase; to = toBase64Address(buyer.address); rawNft = action.nftPurchase.nft; - nftAddress = toBase64Address(rawNft.address); + nftAddress = toBase64Address(rawNft.address, true); } else { continue; } @@ -86,12 +91,16 @@ export async function getNftUpdates(accountId: string, fromSec: number) { if (!rawNft) { [rawNft] = await fetchNftItems(network, [nftAddress]); } - updates.push({ - type: 'nftReceived', - accountId, - nftAddress, - nft: buildNft(rawNft)!, - }); + const nft = buildNft(rawNft); + + if (nft) { + updates.push({ + type: 'nftReceived', + accountId, + nftAddress, + nft, + }); + } } else if (!isPurchase && await isActiveSmartContract(network, to)) { updates.push({ type: 'nftPutUpForSale', diff --git a/src/api/blockchains/ton/other.ts b/src/api/blockchains/ton/other.ts index f3dce1cf..68d69d0e 100644 --- a/src/api/blockchains/ton/other.ts +++ b/src/api/blockchains/ton/other.ts @@ -1,6 +1,9 @@ import TonWeb from 'tonweb'; import type { Cell as CellType } from 'tonweb/dist/types/boc/cell'; +import type { ApiNetwork } from '../../types'; + +import { getTonWeb } from './util/tonweb'; import { bytesToBase64 } from '../../common/utils'; const { Cell } = TonWeb.boc; @@ -21,3 +24,12 @@ export async function packPayloadToBoc(payload: string | Uint8Array | CellType) } return bytesToBase64(await payloadCell.toBoc()); } + +export async function checkApiAvailability(network: ApiNetwork) { + try { + await getTonWeb(network).provider.getMasterchainInfo(); + return true; + } catch (err: any) { + return false; + } +} diff --git a/src/api/blockchains/ton/staking.ts b/src/api/blockchains/ton/staking.ts index f0eabf7f..7707ec3b 100644 --- a/src/api/blockchains/ton/staking.ts +++ b/src/api/blockchains/ton/staking.ts @@ -1,158 +1,290 @@ -import TonWeb from 'tonweb'; import BN from 'bn.js'; import type { ApiBackendStakingState, ApiNetwork, + ApiStakingCommonData, ApiStakingState, + ApiStakingType, } from '../../types'; -import { - ApiTransactionDraftError, -} from '../../types'; +import type { CheckTransactionDraftResult, SubmitTransferResult } from './transactions'; +import type { TonTransferParams } from './types'; +import { ApiCommonError, ApiLiquidUnstakeMode, ApiTransactionDraftError } from '../../types'; -import { TON_TOKEN_SLUG } from '../../../config'; +import { + LIQUID_JETTON, LIQUID_POOL, STAKING_MIN_AMOUNT, TON_TOKEN_SLUG, +} from '../../../config'; +import { Big } from '../../../lib/big.js'; import { parseAccountId } from '../../../util/account'; -import memoized from '../../../util/memoized'; -import { getTonWeb, toBase64Address } from './util/tonweb'; +import { + buildLiquidStakingDepositBody, + buildLiquidStakingWithdrawBody, + fromNano, + getTokenBalance, + getTonWeb, + resolveTokenWalletAddress, + toBase64Address, + toNano, +} from './util/tonweb'; import { NominatorPool } from './contracts/NominatorPool'; import { fetchStoredAddress } from '../../common/accounts'; -import { callBackendGet } from '../../common/backend'; -import { isKnownStakingPool } from '../../common/utils'; -import { STAKE_COMMENT, UNSTAKE_COMMENT } from './constants'; +import { apiDb } from '../../db'; +import { DEFAULT_DECIMALS, STAKE_COMMENT, UNSTAKE_COMMENT } from './constants'; import { checkTransactionDraft, submitTransfer } from './transactions'; +import { isAddressInitialized } from './wallet'; -const { toNano } = TonWeb.utils; +const LIQUID_STAKE_AMOUNT = '1'; +const LIQUID_UNSTAKE_AMOUNT = '1'; +const UNSTAKE_AMOUNT = '1'; +const MIN_NOMINATORS_AMOUNT = '10001'; -const ONE_TON = '1000000000'; -const UNSTAKE_AMOUNT = ONE_TON; -const MIN_STAKE_AMOUNT = 10001; -const CACHE_TTL = 60; // 1 m. -const DISABLE_CACHE_PERIOD = 30; // 30 s. +export async function checkStakeDraft( + accountId: string, + amount: string, + commonData: ApiStakingCommonData, + backendState: ApiBackendStakingState, +) { + const staked = await getStakingState(accountId, commonData, backendState); -export const fetchStakingStateMemo = memoized(fetchBackendStakingState, CACHE_TTL); + let type: ApiStakingType; + let result: CheckTransactionDraftResult; + const bigAmount = Big(fromNano(amount)); -export async function checkStakeDraft(accountId: string, amount: string) { - const address = await fetchStoredAddress(accountId); - const { poolAddress } = await fetchStakingStateMemo(address); - - const result: { - error?: ApiTransactionDraftError; - fee?: string; - } = {}; - - const staked = await getStakingState(accountId); - if (!staked) { - if (new BN(amount).lt(toNano(MIN_STAKE_AMOUNT.toString()))) { - result.error = ApiTransactionDraftError.InvalidAmount; - return result; - } + if ( + (staked?.type === 'nominators' && bigAmount.gte(STAKING_MIN_AMOUNT)) + || (staked.type === 'empty' && bigAmount.gte(MIN_NOMINATORS_AMOUNT)) + ) { + type = 'nominators'; + + const poolAddress = backendState.nominatorsPool.address; + amount = new BN(amount).add(toNano(LIQUID_STAKE_AMOUNT)).toString(); + result = await checkTransactionDraft(accountId, TON_TOKEN_SLUG, poolAddress, amount, STAKE_COMMENT); + } else if (bigAmount.lt(STAKING_MIN_AMOUNT)) { + return { error: ApiTransactionDraftError.InvalidAmount }; + } else { + type = 'liquid'; + + const body = buildLiquidStakingDepositBody(); + result = await checkTransactionDraft(accountId, TON_TOKEN_SLUG, LIQUID_POOL, amount, body); } - return checkTransactionDraft( - accountId, - TON_TOKEN_SLUG, - poolAddress, - amount, - STAKE_COMMENT, - ); + return { + ...result, + type, + }; } -export async function checkUnstakeDraft(accountId: string) { +export async function checkUnstakeDraft( + accountId: string, + amount: string, + commonData: ApiStakingCommonData, + backendState: ApiBackendStakingState, +) { + const { network } = parseAccountId(accountId); const address = await fetchStoredAddress(accountId); - const { poolAddress } = await fetchStakingStateMemo(address); - return checkTransactionDraft( - accountId, - TON_TOKEN_SLUG, - poolAddress, - UNSTAKE_AMOUNT, - UNSTAKE_COMMENT, - ); + const staked = await getStakingState(accountId, commonData, backendState); + + let type: ApiStakingType; + let result: CheckTransactionDraftResult; + let tokenAmount: string | undefined; + + if (staked.type === 'nominators') { + type = 'nominators'; + + const poolAddress = backendState.nominatorsPool.address; + result = await checkTransactionDraft( + accountId, TON_TOKEN_SLUG, poolAddress, toNano(UNSTAKE_AMOUNT).toString(), UNSTAKE_COMMENT, + ); + } else if (staked.type === 'liquid') { + type = 'liquid'; + + const bigAmount = Big(fromNano(amount).toString()); + if (bigAmount.gt(staked.amount)) { + return { error: ApiTransactionDraftError.InsufficientBalance }; + } else if (bigAmount.eq(staked.amount)) { + tokenAmount = staked.tokenAmount; + } else { + const { currentRate } = commonData.liquid; + tokenAmount = bigAmount.div(currentRate).toFixed(DEFAULT_DECIMALS); + } + + tokenAmount = toNano(tokenAmount).toString(); + + const params = await buildLiquidStakingWithdraw(network, address, tokenAmount); + result = await checkTransactionDraft( + accountId, TON_TOKEN_SLUG, params.toAddress, params.amount, params.payload, + ); + } else { + return { error: ApiCommonError.Unexpected }; + } + + return { + ...result, + type, + tokenAmount, + }; } -export async function submitStake(accountId: string, password: string, amount: string) { - const address = await fetchStoredAddress(accountId); - const { poolAddress } = await fetchStakingStateMemo(address); - const result = await submitTransfer( - accountId, - password, - TON_TOKEN_SLUG, - toBase64Address(poolAddress), - amount, - STAKE_COMMENT, - ); - onStakingChangeExpected(); +export async function submitStake( + accountId: string, + password: string, + amount: string, + type: ApiStakingType, + backendState: ApiBackendStakingState, +) { + let result: SubmitTransferResult; + + if (type === 'liquid') { + amount = new BN(amount).add(toNano(LIQUID_STAKE_AMOUNT)).toString(); + result = await submitTransfer( + accountId, + password, + TON_TOKEN_SLUG, + LIQUID_POOL, + amount, + buildLiquidStakingDepositBody(), + ); + } else { + const poolAddress = backendState.nominatorsPool.address; + result = await submitTransfer( + accountId, + password, + TON_TOKEN_SLUG, + toBase64Address(poolAddress, true), + amount, + STAKE_COMMENT, + ); + } + return result; } -export async function submitUnstake(accountId: string, password: string) { +export async function submitUnstake( + accountId: string, + password: string, + type: ApiStakingType, + amount: string, + backendState: ApiBackendStakingState, +) { + const { network } = parseAccountId(accountId); const address = await fetchStoredAddress(accountId); - const { poolAddress } = await fetchStakingStateMemo(address); - const result = await submitTransfer( - accountId, - password, - TON_TOKEN_SLUG, - toBase64Address(poolAddress), - UNSTAKE_AMOUNT, - UNSTAKE_COMMENT, - ); - onStakingChangeExpected(); + + let result: SubmitTransferResult; + + if (type === 'liquid') { + const params = await buildLiquidStakingWithdraw(network, address, amount); + result = await submitTransfer( + accountId, + password, + TON_TOKEN_SLUG, + params.toAddress, + params.amount, + params.payload, + ); + } else { + const poolAddress = backendState.nominatorsPool.address; + result = await submitTransfer( + accountId, + password, + TON_TOKEN_SLUG, + toBase64Address(poolAddress, true), + toNano(UNSTAKE_AMOUNT).toString(), + UNSTAKE_COMMENT, + ); + } + return result; } -function onStakingChangeExpected() { - fetchStakingStateMemo.disableCache(DISABLE_CACHE_PERIOD); +export async function buildLiquidStakingWithdraw( + network: ApiNetwork, + address: string, + amount: string, + mode: ApiLiquidUnstakeMode = ApiLiquidUnstakeMode.Default, +): Promise { + const tokenWalletAddress = await resolveTokenWalletAddress(network, address, LIQUID_JETTON); + + const payload = buildLiquidStakingWithdrawBody({ + amount, + responseAddress: address, + fillOrKill: mode === ApiLiquidUnstakeMode.Instant, + waitTillRoundEnd: mode === ApiLiquidUnstakeMode.BestRate, + }); + + return { + amount: toNano(LIQUID_UNSTAKE_AMOUNT).toString(), + toAddress: tokenWalletAddress, + payload, + }; } -export async function getStakingState(accountId: string): Promise { +export async function getStakingState( + accountId: string, + commonData: ApiStakingCommonData, + backendState: ApiBackendStakingState, +): Promise { const { network } = parseAccountId(accountId); - const address = await fetchStoredAddress(accountId); - const contract = await getPoolContract(network, address); - if (network !== 'mainnet' || !contract) { + const address = toBase64Address(await fetchStoredAddress(accountId), true); + + const { currentRate, collection } = commonData.liquid; + const tokenBalance = Big(await getLiquidStakingTokenBalance(accountId)); + let unstakeAmount = Big(0); + + if (collection) { + const nfts = await apiDb.nfts.where({ collectionAddress: collection }).toArray(); + + for (const nft of nfts) { + const billAmount = nft.name?.match(/Bill for (?[\d.]+) Pool Jetton/)?.groups?.amount; + unstakeAmount = unstakeAmount.plus(billAmount ?? 0); + } + } + + if (tokenBalance.gt(0) || unstakeAmount.gt(0)) { + const fullTokenAmount = tokenBalance.plus(unstakeAmount); + const amount = Big(currentRate).times(fullTokenAmount).toFixed(DEFAULT_DECIMALS); + return { - amount: 0, - pendingDepositAmount: 0, - isUnstakeRequested: false, + type: 'liquid', + tokenAmount: tokenBalance.toFixed(DEFAULT_DECIMALS), + amount: parseFloat(amount), + unstakeRequestAmount: unstakeAmount.toNumber(), }; } - const nominators = await contract.getListNominators(); + const poolAddress = backendState.nominatorsPool.address; + const nominatorPool = getPoolContract(network, poolAddress); + const nominators = await nominatorPool.getListNominators(); const currentNominator = nominators.find((n) => n.address === address); - if (!currentNominator) { + + if (currentNominator) { return { - amount: 0, - pendingDepositAmount: 0, - isUnstakeRequested: false, + type: 'nominators', + amount: parseFloat(currentNominator.amount), + pendingDepositAmount: parseFloat(currentNominator.pendingDepositAmount), + isUnstakeRequested: currentNominator.withdrawRequested, }; } - return { - amount: parseFloat(currentNominator.amount), - pendingDepositAmount: parseFloat(currentNominator.pendingDepositAmount), - isUnstakeRequested: currentNominator.withdrawRequested, - }; + return { type: 'empty' }; } -async function getPoolContract(network: ApiNetwork, address: string) { - const { poolAddress } = await fetchStakingStateMemo(address); +function getPoolContract(network: ApiNetwork, poolAddress: string) { return new NominatorPool(getTonWeb(network).provider, { address: poolAddress }); } -export async function getBackendStakingState(accountId: string): Promise { +async function getLiquidStakingTokenBalance(accountId: string) { const { network } = parseAccountId(accountId); if (network !== 'mainnet') { - return undefined; + return '0'; } const address = await fetchStoredAddress(accountId); - return fetchStakingStateMemo(address); -} - -export async function fetchBackendStakingState(address: string) { - const stakingState = await callBackendGet(`/staking-state?account=${address}`) as ApiBackendStakingState; + const walletAddress = await resolveTokenWalletAddress(network, address, LIQUID_JETTON); + const isInitialized = await isAddressInitialized(network, walletAddress); - if (!isKnownStakingPool(stakingState.poolAddress)) { - throw Error('Unexpected pool address, likely a malicious activity'); + if (!isInitialized) { + return '0'; } - return stakingState; + return getTokenBalance(network, walletAddress); } diff --git a/src/api/blockchains/ton/tokens.ts b/src/api/blockchains/ton/tokens.ts index 71cf306f..022d31af 100644 --- a/src/api/blockchains/ton/tokens.ts +++ b/src/api/blockchains/ton/tokens.ts @@ -6,13 +6,14 @@ import type { } from '../../types'; import type { AnyPayload, ApiTransactionExtra, JettonMetadata } from './types'; +import { DEFAULT_DECIMAL_PLACES, TON_SYMBOL, TON_TOKEN_SLUG } from '../../../config'; import { parseAccountId } from '../../../util/account'; import { logDebugError } from '../../../util/logs'; +import { fixIpfsUrl } from '../../../util/metadata'; import { buildTokenSlug } from './util'; import { + fetchJettonMetadata, fixBase64ImageData, - fixIpfsUrl, - getJettonMetadata, parseJettonWalletMsgBody, } from './util/metadata'; import { fetchJettonBalances } from './util/tonapiio'; @@ -41,10 +42,11 @@ export type TokenBalanceParsed = { const KNOWN_TOKENS: ApiBaseToken[] = [ { - slug: 'toncoin', + slug: TON_TOKEN_SLUG, name: 'Toncoin', - symbol: 'TON', - decimals: 9, + cmcSlug: TON_TOKEN_SLUG, + symbol: TON_SYMBOL, + decimals: DEFAULT_DECIMAL_PLACES, }, ]; @@ -66,7 +68,7 @@ function parseTokenBalance(balanceRaw: JettonBalance): TokenBalanceParsed { try { const { balance, jetton } = balanceRaw; - const minterAddress = toBase64Address(jetton.address); + const minterAddress = toBase64Address(jetton.address, true); const token = buildTokenByMetadata(minterAddress, jetton); return { slug: token.slug, balance, token }; @@ -174,8 +176,8 @@ export function addKnownTokens(tokens: ApiBaseToken[]) { } } -export async function importToken(network: ApiNetwork, address: string) { - const metadata = await getJettonMetadata(network, address); +export async function fetchToken(network: ApiNetwork, address: string) { + const metadata = await fetchJettonMetadata(network, address); return buildTokenByMetadata(address, metadata); } diff --git a/src/api/blockchains/ton/transactions.ts b/src/api/blockchains/ton/transactions.ts index 8db1b853..1df000e2 100644 --- a/src/api/blockchains/ton/transactions.ts +++ b/src/api/blockchains/ton/transactions.ts @@ -5,11 +5,17 @@ import type { Cell } from 'tonweb/dist/types/boc/cell'; import type { WalletContract } from 'tonweb/dist/types/contract/wallet/wallet-contract'; import type { - ApiNetwork, ApiSignedTransfer, ApiTransaction, ApiTransactionActivity, ApiTxIdBySlug, + ApiAnyDisplayError, + ApiNetwork, + ApiSignedTransfer, + ApiTransaction, + ApiTransactionActivity, + ApiTransactionType, + ApiTxIdBySlug, } from '../../types'; import type { JettonWalletType } from './tokens'; import type { AnyPayload, ApiTransactionExtra, TonTransferParams } from './types'; -import { ApiTransactionDraftError, ApiTransactionError } from '../../types'; +import { ApiCommonError, ApiTransactionDraftError, ApiTransactionError } from '../../types'; import { TON_TOKEN_SLUG } from '../../../config'; import { parseAccountId } from '../../../util/account'; @@ -33,10 +39,11 @@ import { resolveTokenWalletAddress, toBase64Address, } from './util/tonweb'; -import { fetchStoredAccount, fetchStoredAddress, fetchStoredPublicKey } from '../../common/accounts'; +import { fetchStoredAccount, fetchStoredAddress } from '../../common/accounts'; import { getAddressInfo } from '../../common/addresses'; import { updateTransactionMetadata } from '../../common/helpers'; import { bytesToBase64, isKnownStakingPool } from '../../common/utils'; +import { ApiServerError, handleServerError } from '../../errors'; import { resolveAddress } from './address'; import { fetchKeyPair, fetchPrivateKey } from './auth'; import { ATTEMPTS, STAKE_COMMENT, UNSTAKE_COMMENT } from './constants'; @@ -44,10 +51,19 @@ import { buildTokenTransfer, getTokenWalletBalance, parseTokenTransaction, resolveTokenBySlug, } from './tokens'; import { - getWalletBalance, getWalletInfo, isWalletInitialized, pickAccountWallet, + getWalletBalance, getWalletInfo, isAddressInitialized, pickAccountWallet, } from './wallet'; -type SubmitTransferResult = { +export type CheckTransactionDraftResult = { + fee?: string; + addressName?: string; + isScam?: boolean; + resolvedAddress?: string; + isToAddressNew?: boolean; + error?: ApiAnyDisplayError; +}; + +export type SubmitTransferResult = { normalizedAddress: string; amount: string; seqno: number; @@ -57,9 +73,7 @@ type SubmitTransferResult = { }; type SubmitMultiTransferResult = { - messages: (TonTransferParams & { - resolvedAddress: string; - })[]; + messages: TonTransferParams[]; amount: string; seqno: number; boc: string; @@ -75,6 +89,7 @@ const GET_TRANSACTIONS_LIMIT = 50; const GET_TRANSACTIONS_MAX_LIMIT = 100; const WAIT_SEQNO_TIMEOUT = 40000; // 40 sec. const WAIT_SEQNO_PAUSE = 5000; // 5 sec. +const WAIT_TRANSACTION_PAUSE = 500; // 0.5 sec. const lastTransfers: Record { const { network } = parseAccountId(accountId); - const result: { - fee?: string; - addressName?: string; - isScam?: boolean; - resolvedAddress?: string; - normalizedAddress?: string; - isToAddressNew?: boolean; - } = {}; + const result: CheckTransactionDraftResult = {}; - const resolved = await resolveAddress(network, toAddress); - if (resolved) { - result.addressName = resolved.domain; - toAddress = resolved.address; - } else { - return { ...result, error: ApiTransactionDraftError.DomainNotResolved }; - } + try { + const resolved = await resolveAddress(network, toAddress); + if (resolved) { + result.addressName = resolved.domain; + toAddress = resolved.address; + } else { + return { + ...result, + error: ApiTransactionDraftError.DomainNotResolved, + }; + } - if (!Address.isValid(toAddress)) { - return { ...result, error: ApiTransactionDraftError.InvalidToAddress }; - } + if (!Address.isValid(toAddress)) { + return { + ...result, + error: ApiTransactionDraftError.InvalidToAddress, + }; + } - const { - isUserFriendly, isTestOnly, isBounceable, - } = new Address(toAddress); + const { + isUserFriendly, + isTestOnly, + isBounceable, + } = new Address(toAddress); - const regex = /[+=/]/; // Temp check for `isUrlSafe`. Remove after TonWeb fixes the issue - const isUrlSafe = !regex.test(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 (!isUserFriendly || !isUrlSafe || (network === 'mainnet' && isTestOnly)) { + return { + ...result, + error: ApiTransactionDraftError.InvalidAddressFormat, + }; + } - if (isBounceable) { - const isInitialized = await isWalletInitialized(network, toAddress); - if (!isInitialized) { - result.isToAddressNew = !(await checkHasTransaction(network, toAddress)); - if (tokenSlug === TON_TOKEN_SLUG) { - toAddress = toBase64Address(toAddress, false); + const isInitialized = await isAddressInitialized(network, toAddress); + + if (isBounceable) { + if (!isInitialized) { + result.isToAddressNew = !(await checkHasTransaction(network, toAddress)); + if (tokenSlug === TON_TOKEN_SLUG) { + // Force non-bounceable for non-initialized recipients + toAddress = toBase64Address(toAddress, false); + } } + } else if (isInitialized) { + toAddress = toBase64Address(toAddress, true); } - } - result.resolvedAddress = toAddress; - result.normalizedAddress = toBase64Address(toAddress); + result.resolvedAddress = toAddress; - const addressInfo = await getAddressInfo(toAddress); - if (addressInfo?.name) result.addressName = addressInfo.name; - if (addressInfo?.isScam) result.isScam = addressInfo.isScam; + const addressInfo = await getAddressInfo(toAddress); + if (addressInfo?.name) result.addressName = addressInfo.name; + if (addressInfo?.isScam) result.isScam = addressInfo.isScam; - if (BigInt(amount) < BigInt(0)) { - return { ...result, error: ApiTransactionDraftError.InvalidAmount }; - } + if (BigInt(amount) < BigInt(0)) { + return { + ...result, + error: ApiTransactionDraftError.InvalidAmount, + }; + } - const wallet = await pickAccountWallet(accountId); - if (!wallet) { - return { ...result, error: ApiTransactionDraftError.Unexpected }; - } + const wallet = await pickAccountWallet(accountId); + if (!wallet) { + return { + ...result, + error: ApiCommonError.Unexpected, + }; + } - if (typeof data === 'string' && isBase64Data) { - data = parseBase64(data); - } + if (typeof data === 'string' && isBase64Data) { + data = parseBase64(data); + } - if (data && typeof data === 'string' && shouldEncrypt) { - const toPublicKey = await getWalletPublicKey(network, toAddress); - if (!toPublicKey) { - return { ...result, error: ApiTransactionDraftError.WalletNotInitialized }; + if (data && typeof data === 'string' && shouldEncrypt) { + const toPublicKey = await getWalletPublicKey(network, toAddress); + if (!toPublicKey) { + return { + ...result, + error: ApiTransactionDraftError.WalletNotInitialized, + }; + } } - } - const account = await fetchStoredAccount(accountId); - const isLedger = !!account?.ledger; + const account = await fetchStoredAccount(accountId); + const isLedger = !!account.ledger; - if (data && typeof data === 'string' && !isBase64Data && !isLedger) { - data = commentToBytes(data); - } - - if (tokenSlug === TON_TOKEN_SLUG) { - if (data && isLedger && (typeof data !== 'string' || shouldEncrypt || !isValidLedgerComment(data))) { - return { ...result, error: ApiTransactionDraftError.UnsupportedHardwarePayload }; + if (data && typeof data === 'string' && !isBase64Data && !isLedger) { + data = commentToBytes(data); } - if (data instanceof Uint8Array) { - data = packBytesAsSnake(data); - } - } else { - const address = await fetchStoredAddress(accountId); - const tokenAmount: string = amount; - let tokenWallet: JettonWalletType; - ({ - tokenWallet, - amount, - toAddress, - payload: data, - } = await buildTokenTransfer(network, tokenSlug, address, toAddress, amount, data)); - - const tokenBalance = await getTokenWalletBalance(tokenWallet!); - if (BigInt(tokenBalance) < BigInt(tokenAmount!)) { - return { ...result, error: ApiTransactionDraftError.InsufficientBalance }; + if (tokenSlug === TON_TOKEN_SLUG) { + if (data && isLedger && (typeof data !== 'string' || shouldEncrypt || !isValidLedgerComment(data))) { + return { + ...result, + error: ApiTransactionDraftError.UnsupportedHardwarePayload, + }; + } + + if (data instanceof Uint8Array) { + data = packBytesAsSnake(data); + } + } else { + const address = await fetchStoredAddress(accountId); + const tokenAmount: string = amount; + let tokenWallet: JettonWalletType; + ({ + tokenWallet, + amount, + toAddress, + payload: data, + } = await buildTokenTransfer(network, tokenSlug, address, toAddress, amount, data)); + + const tokenBalance = await getTokenWalletBalance(tokenWallet!); + if (BigInt(tokenBalance) < BigInt(tokenAmount!)) { + return { + ...result, + error: ApiTransactionDraftError.InsufficientBalance, + }; + } } - } - const isOurWalletInitialized = await isWalletInitialized(network, wallet); - result.fee = await calculateFee(isOurWalletInitialized, async () => (await signTransaction( - network, wallet, toAddress, amount, data, stateInit, - )).query); + const isOurWalletInitialized = await isAddressInitialized(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)) { - return { ...result, error: ApiTransactionDraftError.InsufficientBalance }; - } + const balance = await getWalletBalance(network, wallet); + if (BigInt(balance) < BigInt(amount) + BigInt(result.fee)) { + return { + ...result, + error: ApiTransactionDraftError.InsufficientBalance, + }; + } - return result as { - fee: string; - resolvedAddress: string; - normalizedAddress: string; - addressName?: string; - isScam?: boolean; - isToAddressNew?: boolean; - }; + return result as { + fee: string; + resolvedAddress: string; + addressName?: string; + isScam?: boolean; + isToAddressNew?: boolean; + }; + } catch (err: any) { + return { + ...handleServerError(err), + ...result, + }; + } } export async function submitTransfer( @@ -244,7 +290,7 @@ export async function submitTransfer( const { publicKey, secretKey } = keyPair!; let encryptedComment: string | undefined; - // Force default bounceable address for `waitTxComplete` to work properly + // Fix address format for `waitTxComplete` to work properly const normalizedAddress = toBase64Address(toAddress); if (data && typeof data === 'string') { @@ -296,10 +342,13 @@ export async function submitTransfer( } } -function resolveTransactionError(error: any): ApiTransactionError { - const message = typeof error === 'string' ? error : error?.message; - if (message?.includes('exitcode=35,')) { - return ApiTransactionError.IncorrectDeviceTime; +export function resolveTransactionError(error: any): ApiAnyDisplayError { + if (error instanceof ApiServerError) { + if (error.message.includes('exitcode=35,')) { + return ApiTransactionError.IncorrectDeviceTime; + } else if (error.displayError) { + return error.displayError; + } } return ApiTransactionError.UnsuccesfulTransfer; } @@ -337,15 +386,15 @@ export async function getAccountNewestTxId(accountId: string) { export async function getAccountTransactionSlice( accountId: string, - fromTxId?: string, toTxId?: string, + fromTxId?: string, limit?: number, ) { const { network } = parseAccountId(accountId); const address = await fetchStoredAddress(accountId); let transactions = await fetchTransactions( - network, address, limit ?? GET_TRANSACTIONS_LIMIT, fromTxId, toTxId, + network, address, limit ?? GET_TRANSACTIONS_LIMIT, toTxId, fromTxId, ); transactions = await Promise.all( @@ -388,12 +437,12 @@ export async function getMergedTransactionSlice(accountId: string, lastTxIds: Ap export async function getTokenTransactionSlice( accountId: string, tokenSlug: string, - fromTxId?: string, toTxId?: string, + fromTxId?: string, limit?: number, ): Promise { if (tokenSlug === TON_TOKEN_SLUG) { - return getAccountTransactionSlice(accountId, fromTxId, toTxId, limit); + return getAccountTransactionSlice(accountId, toTxId, fromTxId, limit); } const { network } = parseAccountId(accountId); @@ -403,7 +452,7 @@ export async function getTokenTransactionSlice( const tokenWalletAddress = await resolveTokenWalletAddress(network, address, minterAddress); const transactions = await fetchTransactions( - network, tokenWalletAddress, limit ?? GET_TRANSACTIONS_LIMIT, fromTxId, toTxId, + network, tokenWalletAddress, limit ?? GET_TRANSACTIONS_LIMIT, toTxId, fromTxId, ); return transactions @@ -420,24 +469,33 @@ function omitExtraData(tx: ApiTransactionExtra): ApiTransaction { function updateTransactionType(transaction: ApiTransactionExtra) { const { - fromAddress, toAddress, comment, amount, + fromAddress, toAddress, comment, amount, extraData, } = transaction; - if (fromAddress && toAddress) { - const amountNumber = Math.abs(Number(fromNano(amount))); + const amountNumber = Math.abs(Number(fromNano(amount))); + let type: ApiTransactionType | undefined; - if (isKnownStakingPool(fromAddress) && amountNumber > 1) { - transaction.type = 'unstake'; - } else if (isKnownStakingPool(toAddress)) { - if (comment === STAKE_COMMENT) { - transaction.type = 'stake'; - } else if (comment === UNSTAKE_COMMENT) { - transaction.type = 'unstakeRequest'; - } + if (isKnownStakingPool(fromAddress) && amountNumber > 1) { + type = 'unstake'; + } else if (isKnownStakingPool(toBase64Address(toAddress, true))) { + if (comment === STAKE_COMMENT) { + type = 'stake'; + } else if (comment === UNSTAKE_COMMENT) { + type = 'unstakeRequest'; + } + } else if (extraData?.parsedPayload) { + const payload = extraData.parsedPayload; + + if (payload.type === 'tokens:burn' && payload.isLiquidUnstakeRequest) { + type = 'unstakeRequest'; + } else if (payload.type === 'liquid-staking:deposit') { + type = 'stake'; + } else if (payload.type === 'liquid-staking:withdrawal' || payload.type === 'liquid-staking:withdrawal-nft') { + type = 'unstake'; } } - return transaction; + return { ...transaction, type }; } function transactionToActivity(transaction: ApiTransaction): ApiTransactionActivity { @@ -458,34 +516,38 @@ export async function checkMultiTransactionDraft(accountId: string, messages: To let totalAmount: bigint = 0n; - for (const { toAddress, amount } of messages) { - if (BigInt(amount) < BigInt(0)) { - return { ...result, error: ApiTransactionDraftError.InvalidAmount }; - } - if (!Address.isValid(toAddress)) { - return { ...result, error: ApiTransactionDraftError.InvalidToAddress }; + try { + for (const { toAddress, amount } of messages) { + if (BigInt(amount) < BigInt(0)) { + return { ...result, error: ApiTransactionDraftError.InvalidAmount }; + } + if (!Address.isValid(toAddress)) { + return { ...result, error: ApiTransactionDraftError.InvalidToAddress }; + } + totalAmount += BigInt(amount); } - totalAmount += BigInt(amount); - } - const wallet = await pickAccountWallet(accountId); + const wallet = await pickAccountWallet(accountId); - if (!wallet) { - return { ...result, error: ApiTransactionDraftError.Unexpected }; - } + if (!wallet) { + return { ...result, error: ApiCommonError.Unexpected }; + } - const { isInitialized, balance } = await getWalletInfo(network, wallet); + const { isInitialized, balance } = await getWalletInfo(network, wallet); - result.fee = await calculateFee(isInitialized, async () => (await signMultiTransaction( - network, wallet, messages, - )).query); - result.totalAmount = totalAmount.toString(); + result.fee = await calculateFee(isInitialized, async () => (await signMultiTransaction( + network, wallet, messages, + )).query); + result.totalAmount = totalAmount.toString(); - if (BigInt(balance) < totalAmount + BigInt(result.fee)) { - return { ...result, error: ApiTransactionDraftError.InsufficientBalance }; - } + if (BigInt(balance) < totalAmount + BigInt(result.fee)) { + return { ...result, error: ApiTransactionDraftError.InsufficientBalance }; + } - return result as { fee: string; totalAmount: string }; + return result as { fee: string; totalAmount: string }; + } catch (err: any) { + return handleServerError(err); + } } export async function submitMultiTransfer( @@ -504,29 +566,15 @@ export async function submitMultiTransfer( ]); let totalAmount = 0n; - const preparedMessages = await Promise.all(messages.map(async (message) => { - let { toAddress } = message; - - // Force default bounceable address for `waitTxComplete` to work properly - const resolvedAddress = toBase64Address(toAddress); - toAddress = await isWalletInitialized(network, toAddress) - ? resolvedAddress - : toBase64Address(toAddress, false); - + messages.forEach((message) => { totalAmount += BigInt(message.amount); - - return { - ...message, - toAddress, - resolvedAddress, - }; - })); + }); await waitLastTransfer(network, fromAddress); const { isInitialized, balance } = await getWalletInfo(network, wallet!); - const { seqno, query } = await signMultiTransaction(network, wallet!, preparedMessages, privateKey, expireAt); + const { seqno, query } = await signMultiTransaction(network, wallet!, messages, privateKey, expireAt); const boc = bytesToBase64(await (await query.getQuery()).toBoc()); @@ -542,7 +590,7 @@ export async function submitMultiTransfer( return { seqno, amount: totalAmount.toString(), - messages: preparedMessages, + messages, boc, }; } catch (err) { @@ -638,12 +686,8 @@ async function calculateFee(isInitialized: boolean, query: Method | (() => Promi export async function sendSignedMessage(accountId: string, message: ApiSignedTransfer) { const { network } = parseAccountId(accountId); - const [fromAddress, publicKey, account] = await Promise.all([ - fetchStoredAddress(accountId), - fetchStoredPublicKey(accountId), - fetchStoredAccount(accountId), - ]); - const wallet = getTonWalletContract(publicKey!, account!.version!); + const { address: fromAddress, publicKey, version } = await fetchStoredAccount(accountId); + const wallet = getTonWalletContract(publicKey, version!); const client = getTonClient(network); const contract = client.open(wallet); @@ -655,12 +699,8 @@ export async function sendSignedMessage(accountId: string, message: ApiSignedTra export async function sendSignedMessages(accountId: string, messages: ApiSignedTransfer[]) { const { network } = parseAccountId(accountId); - const [fromAddress, publicKey, account] = await Promise.all([ - fetchStoredAddress(accountId), - fetchStoredPublicKey(accountId), - fetchStoredAccount(accountId), - ]); - const wallet = getTonWalletContract(publicKey!, account!.version!); + const { address: fromAddress, publicKey, version } = await fetchStoredAccount(accountId); + const wallet = getTonWalletContract(publicKey, version!); const client = getTonClient(network); const contract = client.open(wallet); @@ -701,3 +741,20 @@ export async function decryptComment( return decryptMessageComment(buffer, publicKey, secretKey, fromAddress); } + +export async function waitUntilTransactionAppears(network: ApiNetwork, address: string, txId: string) { + const { lt } = parseTxId(txId); + + if (lt === 0) { + return; + } + + // eslint-disable-next-line no-constant-condition + while (true) { + const transaction = (await fetchTransactions(network, address, 1))[0]; + if (parseTxId(transaction.txId).lt >= lt) { + return; + } + await pause(WAIT_TRANSACTION_PAUSE); + } +} diff --git a/src/api/blockchains/ton/types.ts b/src/api/blockchains/ton/types.ts index ef9a370d..e9858134 100644 --- a/src/api/blockchains/ton/types.ts +++ b/src/api/blockchains/ton/types.ts @@ -4,7 +4,7 @@ import type { Cell } from 'tonweb/dist/types/boc/cell'; import type { HttpProvider } from 'tonweb/dist/types/providers/http-provider'; import type { Address as AddressType } from 'tonweb/dist/types/utils/address'; -import type { ApiTransaction } from '../../types'; +import type { ApiParsedPayload, ApiTransaction } from '../../types'; declare class Dns { readonly provider: HttpProvider; @@ -20,7 +20,11 @@ export declare class MyTonWeb extends TonWeb { export type AnyPayload = string | Uint8Array | Cell; export interface ApiTransactionExtra extends ApiTransaction { - extraData?: { body?: any }; + extraData: { + normalizedAddress: string; + body?: string; + parsedPayload?: ApiParsedPayload; + }; } export interface TokenTransferBodyParams { diff --git a/src/api/blockchains/ton/util/CustomHttpProvider.ts b/src/api/blockchains/ton/util/CustomHttpProvider.ts index fd48def7..8a3d77e4 100644 --- a/src/api/blockchains/ton/util/CustomHttpProvider.ts +++ b/src/api/blockchains/ton/util/CustomHttpProvider.ts @@ -1,8 +1,8 @@ import TonWeb from 'tonweb'; import type { HttpProviderOptions } from 'tonweb/dist/types/providers/http-provider'; -import { logDebugError } from '../../../../util/logs'; import { pause } from '../../../../util/schedulers'; +import { ApiServerError } from '../../../errors'; type Options = HttpProviderOptions & { headers?: AnyLiteral; @@ -27,8 +27,6 @@ class CustomHttpProvider extends TonWeb.HttpProvider { async sendRequest(apiUrl: string, request: any) { const method: string = request.method; - let lastError: any; - let lastStatusCode: number | undefined; const headers: AnyLiteral = { ...this.options.headers, @@ -39,35 +37,40 @@ class CustomHttpProvider extends TonWeb.HttpProvider { } const body = JSON.stringify(request); + let message = 'Unknown error'; + let statusCode: number | undefined; + for (let i = 1; i <= ATTEMPTS; i++) { try { const response = await fetch(apiUrl, { method: 'POST', headers, body, }); - lastStatusCode = response.status; - const { error, result } = await response.json(); - if (error) { + statusCode = response.status; + + if (statusCode >= 400) { + if (response.headers.get('content-type') !== 'application/json') { + throw new Error(`HTTP Error ${statusCode}`); + } + const { error } = await response.json(); throw new Error(error); } + + const { result } = await response.json(); return result; } catch (err: any) { - lastError = err; + message = typeof err === 'string' ? err : err.message ?? message; - const message: string = typeof err === 'string' ? err : err.message; - if (isNotTemporaryError(method, message, lastStatusCode)) { - throw err; + if (isNotTemporaryError(method, message, statusCode)) { + throw new ApiServerError(message); } if (i < ATTEMPTS) { - if (!isFrequentError(message)) { - logDebugError('HttpProvider:sendRequest', 'retry', err); - } await pause(ERROR_PAUSE * i); } } } - throw lastError; + throw new ApiServerError(message); } } @@ -75,8 +78,4 @@ function isNotTemporaryError(method: string, message: string, statusCode?: numbe return statusCode === 422 || (method === 'sendBoc' && message?.includes('exitcode=')); } -function isFrequentError(message: string) { - return message.includes('LITE_SERVER_NOTREADY') || message.includes('LITE_SERVER_UNKNOWN'); -} - export default CustomHttpProvider; diff --git a/src/api/blockchains/ton/util/index.ts b/src/api/blockchains/ton/util/index.ts index aeb71570..e787520c 100644 --- a/src/api/blockchains/ton/util/index.ts +++ b/src/api/blockchains/ton/util/index.ts @@ -2,7 +2,7 @@ export function cloneDeep(value: T): T { return JSON.parse(JSON.stringify(value)); } -export function stringifyTxId({ lt, hash }: { lt: number; hash: string }) { +export function stringifyTxId({ lt, hash }: { lt: number | string; hash: string }) { return `${lt}:${hash}`; } @@ -15,7 +15,3 @@ export function buildTokenSlug(minterAddress: string) { const addressPart = minterAddress.replace(/[^a-z\d]/gi, '').slice(0, 10); return `ton-${addressPart}`.toLowerCase(); } - -export function buildSwapId(backendId: string) { - return `swap:${backendId}`; -} diff --git a/src/api/blockchains/ton/util/metadata.ts b/src/api/blockchains/ton/util/metadata.ts index e9c74746..fcdffe00 100644 --- a/src/api/blockchains/ton/util/metadata.ts +++ b/src/api/blockchains/ton/util/metadata.ts @@ -6,16 +6,18 @@ import { import type { ApiNetwork, ApiParsedPayload } from '../../../types'; import type { ApiTransactionExtra, JettonMetadata } from '../types'; +import { LIQUID_JETTON } from '../../../../config'; import { pick, range } from '../../../../util/iteratees'; import { logDebugError } from '../../../../util/logs'; -import { base64ToString, handleFetchErrors, sha256 } from '../../../common/utils'; -import { JettonOpCode, NftOpCode, OpCode } from '../constants'; +import { fetchJsonMetadata } from '../../../../util/metadata'; +import { base64ToString, sha256 } from '../../../common/utils'; +import { + DEFAULT_IS_BOUNCEABLE, JettonOpCode, LiquidStakingOpCode, NftOpCode, OpCode, +} from '../constants'; import { buildTokenSlug } from './index'; import { fetchNftItems } from './tonapiio'; import { getJettonMinterData, resolveTokenMinterAddress } from './tonweb'; -const IPFS_GATEWAY_BASE_URL: string = 'https://ipfs.io/ipfs/'; - const ONCHAIN_CONTENT_PREFIX = 0x00; const SNAKE_PREFIX = 0x00; @@ -64,7 +66,7 @@ export function parseJettonWalletMsgBody(body?: string) { queryId, jettonAmount, responseAddress, - address: address ? toBounceableAddress(address) : undefined, + address: address ? toBase64Address(address) : undefined, forwardAmount, comment, encryptedComment, @@ -76,14 +78,6 @@ export function parseJettonWalletMsgBody(body?: string) { return undefined; } -export function toBounceableAddress(address: Address) { - return address.toString({ urlSafe: true, bounceable: true }); -} - -export function fixIpfsUrl(url: string) { - return url.replace('ipfs://', IPFS_GATEWAY_BASE_URL); -} - export function fixBase64ImageData(data: string) { const decodedData = base64ToString(data); if (decodedData.includes(' { +export async function fetchJettonOffchainMetadata(uri: string): Promise { const metadata = await fetchJsonMetadata(uri); return pick(metadata, ['name', 'description', 'symbol', 'decimals', 'image', 'image_data']); } -async function fetchJsonMetadata(uri: string) { - uri = fixIpfsUrl(uri); - - const response = await fetch(uri); - handleFetchErrors(response); - return response.json(); -} - export async function parseWalletTransactionBody( network: ApiNetwork, transaction: ApiTransactionExtra, ): Promise { const body = transaction.extraData?.body; - if (!body) return transaction; + if (!body || transaction.comment || transaction.encryptedComment) { + return transaction; + } try { const slice = dataToSlice(body); if (slice.remainingBits > 32) { const parsedPayload = await parsePayloadSlice(network, transaction.toAddress, slice); + transaction.extraData!.parsedPayload = parsedPayload; - if (parsedPayload?.type === 'encrypted-comment') { + if (parsedPayload?.type === 'comment') { + transaction = { + ...transaction, + comment: parsedPayload.comment, + }; + } else if (parsedPayload?.type === 'encrypted-comment') { transaction = { ...transaction, encryptedComment: parsedPayload.encryptedComment, @@ -229,10 +223,12 @@ export async function parsePayloadSlice( const opCode = slice.loadUint(32); if (opCode === OpCode.Comment) { - const comment = slice.loadStringTail(); + const buffer = readSnakeBytes(slice); + const comment = buffer.toString('utf-8'); return { type: 'comment', comment }; } else if (opCode === OpCode.Encrypted) { - const encryptedComment = slice.loadBuffer(slice.remainingBits / 8).toString('base64'); + const buffer = readSnakeBytes(slice); + const encryptedComment = buffer.toString('base64'); return { type: 'encrypted-comment', encryptedComment }; } @@ -249,9 +245,9 @@ export async function parsePayloadSlice( if (!responseDestination) { return { - type: 'transfer-tokens:non-standard', + type: 'tokens:transfer-non-standard', queryId, - destination: toBounceableAddress(destination), + destination: toBase64Address(destination), amount: amount.toString(), slug, }; @@ -269,11 +265,11 @@ export async function parsePayloadSlice( } return { - type: 'transfer-tokens', + type: 'tokens:transfer', queryId, amount: amount.toString(), - destination: toBounceableAddress(destination), - responseDestination: toBounceableAddress(responseDestination), + destination: toBase64Address(destination), + responseDestination: toBase64Address(responseDestination), customPayload: customPayload?.toBoc().toString('base64'), forwardAmount: forwardAmount.toString(), forwardPayload: forwardPayload?.toBoc().toString('base64'), @@ -298,10 +294,10 @@ export async function parsePayloadSlice( const nftAddress = toAddress; const [nft] = await fetchNftItems(network, [nftAddress]); return { - type: 'transfer-nft', + type: 'nft:transfer', queryId, - newOwner: toBounceableAddress(newOwner), - responseDestination: toBounceableAddress(responseDestination), + newOwner: toBase64Address(newOwner), + responseDestination: toBase64Address(responseDestination), customPayload: customPayload?.toBoc().toString('base64'), forwardAmount: forwardAmount.toString(), forwardPayload: forwardPayload?.toBoc().toString('base64'), @@ -309,6 +305,44 @@ export async function parsePayloadSlice( nftName: nft?.metadata?.name, }; } + case JettonOpCode.Burn: { + const minterAddress = await resolveTokenMinterAddress(network, toAddress); + const slug = buildTokenSlug(minterAddress); + + const amount = slice.loadCoins(); + const address = slice.loadAddress(); + const customPayload = slice.loadMaybeRef(); + const isLiquidUnstakeRequest = minterAddress === LIQUID_JETTON; + + return { + type: 'tokens:burn', + queryId, + amount: amount.toString(), + address: toBase64Address(address), + customPayload: customPayload?.toBoc().toString('base64'), + slug, + isLiquidUnstakeRequest, + }; + } + case LiquidStakingOpCode.DistributedAsset: { + return { + type: 'liquid-staking:withdrawal-nft', + queryId, + }; + } + case LiquidStakingOpCode.Withdrawal: { + return { + type: 'liquid-staking:withdrawal', + queryId, + }; + } + case LiquidStakingOpCode.Deposit: { + // const amount = slice.loadCoins(); + return { + type: 'liquid-staking:deposit', + queryId, + }; + } } } catch (err) { logDebugError('parsePayload', err); @@ -317,6 +351,10 @@ export async function parsePayloadSlice( return undefined; } +function toBase64Address(address: Address, isBounceable = DEFAULT_IS_BOUNCEABLE) { + return address.toString({ urlSafe: true, bounceable: isBounceable }); +} + function dataToSlice(data: string | Buffer | Uint8Array): Slice { let buffer: Buffer; if (typeof data === 'string') { diff --git a/src/api/blockchains/ton/util/tonapiio.ts b/src/api/blockchains/ton/util/tonapiio.ts index a032a8b0..0bf219d7 100644 --- a/src/api/blockchains/ton/util/tonapiio.ts +++ b/src/api/blockchains/ton/util/tonapiio.ts @@ -8,11 +8,10 @@ import { import type { ApiNetwork } from '../../../types'; +import { TONAPIIO_MAINNET_URL, TONAPIIO_TESTNET_URL } from '../../../../config'; import { logDebugError } from '../../../../util/logs'; import { API_HEADERS } from '../../../environment'; -const TONAPIIO_MAINNET_URL = process.env.TONAPIIO_MAINNET_URL || 'https://tonapi.io'; -const TONAPIIO_TESTNET_URL = process.env.TONAPIIO_TESTNET_URL || 'https://testnet.tonapi.io'; const MAX_LIMIT = 1000; const configurationMainnet = new Configuration({ @@ -53,13 +52,20 @@ export function fetchNftItems(network: ApiNetwork, addresses: string[]) { })).nftItems, []); } -export function fetchAccountNfts(network: ApiNetwork, address: string, offset?: number, limit?: number) { +export function fetchAccountNfts(network: ApiNetwork, address: string, options?: { + collection?: string; + offset?: number; + limit?: number; +}) { + const { collection, offset, limit } = options ?? {}; const api = tonapiioByNetwork[network].accountsApi; + return tonapiioErrorHandler(async () => (await api.getNftItemsByOwner({ accountId: address, offset: offset ?? 0, limit: limit ?? MAX_LIMIT, indirectOwnership: true, + collection, })).nftItems, []); } diff --git a/src/api/blockchains/ton/util/tonweb.ts b/src/api/blockchains/ton/util/tonweb.ts index cb126755..0f014a9c 100644 --- a/src/api/blockchains/ton/util/tonweb.ts +++ b/src/api/blockchains/ton/util/tonweb.ts @@ -13,12 +13,19 @@ import { TONHTTPAPI_MAINNET_URL, TONHTTPAPI_TESTNET_API_KEY, TONHTTPAPI_TESTNET_URL, + TONINDEXER_MAINNET_URL, + TONINDEXER_TESTNET_URL, } from '../../../../config'; import { logDebugError } from '../../../../util/logs'; import withCacheAsync from '../../../../util/withCacheAsync'; -import { base64ToBytes, hexToBytes } from '../../../common/utils'; +import { base64ToBytes, fetchJson, hexToBytes } from '../../../common/utils'; import { API_HEADERS } from '../../../environment'; -import { JettonOpCode, OpCode } from '../constants'; +import { + DEFAULT_IS_BOUNCEABLE, + JettonOpCode, + LiquidStakingOpCode, + OpCode, +} from '../constants'; import { parseTxId, stringifyTxId } from './index'; import CustomHttpProvider from './CustomHttpProvider'; @@ -26,6 +33,7 @@ import CustomHttpProvider from './CustomHttpProvider'; const { Cell } = TonWeb.boc; const { Address } = TonWeb.utils; const { JettonMinter, JettonWallet } = TonWeb.token.jetton; +export const { toNano, fromNano } = TonWeb.utils; const TON_MAX_COMMENT_BYTES = 127; @@ -43,13 +51,13 @@ const tonwebByNetwork = { export const resolveTokenWalletAddress = withCacheAsync( async (network: ApiNetwork, address: string, minterAddress: string) => { const minter = new JettonMinter(getTonWeb(network).provider, { address: minterAddress } as any); - return toBase64Address(await minter.getJettonWalletAddress(new Address(address))); + return toBase64Address(await minter.getJettonWalletAddress(new Address(address)), true); }, ); export const resolveTokenMinterAddress = withCacheAsync(async (network: ApiNetwork, tokenWalletAddress: string) => { const tokenWallet = new JettonWallet(getTonWeb(network).provider, { address: tokenWalletAddress } as any); - return toBase64Address((await tokenWallet.getData()).jettonMinterAddress); + return toBase64Address((await tokenWallet.getData()).jettonMinterAddress, true); }); export const getWalletPublicKey = withCacheAsync(async (network: ApiNetwork, address: string) => { @@ -74,48 +82,58 @@ export async function getJettonMinterData(network: ApiNetwork, address: string) return { ...data, totalSupply: data.totalSupply.toString(), - adminAddress: data.adminAddress ? toBase64Address(data.adminAddress) : undefined, + adminAddress: data.adminAddress ? toBase64Address(data.adminAddress, false) : undefined, }; } export async function fetchNewestTxId(network: ApiNetwork, address: string) { - const tonWeb = getTonWeb(network); - - const result: any[] = await tonWeb.provider.getTransactions( - address, - 1, - undefined, - undefined, - undefined, - true, - ); - if (!result?.length) { + const transactions = await fetchTransactions(network, address, 1); + + if (!transactions.length) { return undefined; } - return stringifyTxId(result[0].transaction_id); + return transactions[0].txId; } export async function fetchTransactions( - network: ApiNetwork, address: string, limit: number, fromTxId?: string, toTxId?: string, + network: ApiNetwork, + address: string, + limit: number, + toTxId?: string, + fromTxId?: string, ): Promise { - const tonWeb = getTonWeb(network); - - const fromLt = fromTxId ? parseTxId(fromTxId).lt : undefined; - const fromHash = fromTxId ? parseTxId(fromTxId).hash : undefined; - const toLt = toTxId ? parseTxId(toTxId).lt : undefined; - - let rawTransactions = await tonWeb.provider.getTransactions( - address, limit, fromLt, fromHash, toLt, true, - ) as any[]; - - if ( - fromTxId - && rawTransactions.length - && Number(rawTransactions[0].transaction_id.lt) === fromLt - && rawTransactions[0].transaction_id.hash === fromHash - ) { - rawTransactions = rawTransactions.slice(1); + const indexerUrl = network === 'testnet' ? TONINDEXER_TESTNET_URL : TONINDEXER_MAINNET_URL; + const apiKey = network === 'testnet' ? TONHTTPAPI_TESTNET_API_KEY : TONHTTPAPI_MAINNET_API_KEY; + + const fromLt = fromTxId ? parseTxId(fromTxId).lt.toString() : undefined; + const toLt = toTxId ? parseTxId(toTxId).lt.toString() : undefined; + + let rawTransactions: any[] = await fetchJson(`${indexerUrl}/transactions`, { + account: address, + limit, + start_lt: fromLt, + end_lt: toLt, + sort: 'desc', + }, { + headers: { + ...(apiKey && { 'X-Api-Key': apiKey }), + ...API_HEADERS, + }, + }); + + if (!rawTransactions.length) { + return []; + } + + if (limit > 1) { + if (fromLt && rawTransactions[rawTransactions.length - 1].lt === fromLt) { + rawTransactions.pop(); + } + + if (toLt && rawTransactions[0]?.lt === toLt) { + rawTransactions = rawTransactions.slice(1); + } } return rawTransactions.map(parseRawTransaction).flat(); @@ -123,15 +141,14 @@ export async function fetchTransactions( function parseRawTransaction(rawTx: any): ApiTransactionExtra[] { const { - utime, - transaction_id: { - lt, - hash, - }, + now, + lt, + hash, fee, } = rawTx; + const txId = stringifyTxId({ lt, hash }); - const timestamp = utime as number * 1000; + const timestamp = now as number * 1000; const isIncoming = !!rawTx.in_msg.source; const msgs: any[] = isIncoming ? [rawTx.in_msg] : rawTx.out_msgs; @@ -139,41 +156,27 @@ function parseRawTransaction(rawTx: any): ApiTransactionExtra[] { return msgs.map((msg, i) => { const { source, destination, value } = msg; + const normalizedAddress = toBase64Address(isIncoming ? source : destination, true); return { txId: msgs.length > 1 ? `${txId}:${i + 1}` : txId, timestamp, isIncoming, - fromAddress: source, - toAddress: destination, + fromAddress: toBase64Address(source), + toAddress: toBase64Address(destination), amount: isIncoming ? value : `-${value}`, - comment: getComment(msg), - encryptedComment: getEncryptedComment(msg), slug: TON_TOKEN_SLUG, fee, extraData: { + normalizedAddress, body: getRawBody(msg), }, }; }); } -function getComment(msg: any): string | undefined { - if (!msg.msg_data) return undefined; - if (msg.msg_data['@type'] !== 'msg.dataText') return undefined; - const base64 = msg.msg_data.text; - return new TextDecoder().decode(TonWeb.utils.base64ToBytes(base64)); -} - -function getEncryptedComment(msg: any): string | undefined { - if (!msg.msg_data) return undefined; - if (msg.msg_data['@type'] !== 'msg.dataEncryptedText') return undefined; - return msg.msg_data.text; -} - function getRawBody(msg: any) { - if (!msg.msg_data) return undefined; - if (msg.msg_data['@type'] !== 'msg.dataRaw') return undefined; - return msg.msg_data.body; + if (!msg.message_content) return undefined; + return msg.message_content.body; } export function getTonWeb(network: ApiNetwork = 'mainnet') { @@ -184,7 +187,7 @@ export function oneCellFromBoc(boc: Uint8Array) { return TonWeb.boc.Cell.oneFromBoc(boc); } -export function toBase64Address(address: AddressType, isBounceable = true) { +export function toBase64Address(address: AddressType, isBounceable = DEFAULT_IS_BOUNCEABLE) { return new TonWeb.utils.Address(address).toString(true, true, isBounceable); } @@ -275,3 +278,44 @@ export function packBytesAsSnake(bytes: Uint8Array, maxBytes = TON_MAX_COMMENT_B return cell; } + +export function buildLiquidStakingDepositBody(queryId?: number) { + const cell = new Cell(); + cell.bits.writeUint(LiquidStakingOpCode.Deposit, 32); + cell.bits.writeUint(queryId || 0, 64); + return cell; +} + +export function buildLiquidStakingWithdrawBody(options: { + queryId?: number; + amount: string | BN; + responseAddress: AddressType; + waitTillRoundEnd?: boolean; // opposite of request_immediate_withdrawal + fillOrKill?: boolean; +}) { + const { + queryId, amount, responseAddress, waitTillRoundEnd, fillOrKill, + } = options; + + const customPayload = new Cell(); + customPayload.bits.writeUint(Number(waitTillRoundEnd), 1); + customPayload.bits.writeUint(Number(fillOrKill), 1); + + const cell = new Cell(); + cell.bits.writeUint(JettonOpCode.Burn, 32); + cell.bits.writeUint(queryId ?? 0, 64); + cell.bits.writeCoins(new BN(amount)); + cell.bits.writeAddress(new Address(responseAddress)); + cell.bits.writeBit(1); + cell.refs.push(customPayload); + + return cell; +} + +export async function getTokenBalance(network: ApiNetwork, walletAddress: string) { + const jettonWallet = new JettonWallet(getTonWeb(network).provider, { + address: walletAddress, + }); + const wallletData = await jettonWallet.getData(); + return fromNano(wallletData.balance); +} diff --git a/src/api/blockchains/ton/wallet.ts b/src/api/blockchains/ton/wallet.ts index d99e61a2..a9ca64b3 100644 --- a/src/api/blockchains/ton/wallet.ts +++ b/src/api/blockchains/ton/wallet.ts @@ -5,13 +5,14 @@ import type { ApiNetwork, ApiWalletVersion } from '../../types'; import { parseAccountId } from '../../../util/account'; import { compact } from '../../../util/iteratees'; import withCacheAsync from '../../../util/withCacheAsync'; +import { stringifyTxId } from './util'; import { getTonWeb, toBase64Address } from './util/tonweb'; -import { fetchStoredAccount, fetchStoredAddress, fetchStoredPublicKey } from '../../common/accounts'; +import { fetchStoredAccount, fetchStoredAddress } from '../../common/accounts'; import { bytesToBase64, hexToBytes } from '../../common/utils'; const DEFAULT_WALLET_VERSION: ApiWalletVersion = 'v4R2'; -export const isWalletInitialized = withCacheAsync( +export const isAddressInitialized = withCacheAsync( async (network: ApiNetwork, walletOrAddress: WalletContract | string) => { return (await getWalletInfo(network, walletOrAddress)).isInitialized; }, @@ -19,8 +20,8 @@ export const isWalletInitialized = withCacheAsync( export const isActiveSmartContract = withCacheAsync(async (network: ApiNetwork, address: string) => { const { isInitialized, isWallet } = await getWalletInfo(network, address); - return isInitialized && !isWallet; -}); + return isInitialized ? !isWallet : undefined; +}, (value) => value !== undefined); export async function publicKeyToAddress( network: ApiNetwork, @@ -30,7 +31,7 @@ export async function publicKeyToAddress( const wallet = buildWallet(network, publicKey, walletVersion); const address = await wallet.getAddress(); - return address.toString(true, true, true); + return toBase64Address(address, false); } export function buildWallet(network: ApiNetwork, publicKey: Uint8Array, walletVersion: ApiWalletVersion) { @@ -44,24 +45,31 @@ export async function getWalletInfo(network: ApiNetwork, walletOrAddress: Wallet isWallet: boolean; seqno: number; balance: string; + lastTxId?: string; }> { const address = typeof walletOrAddress === 'string' ? walletOrAddress - : (await walletOrAddress.getAddress()).toString(true, true, true); + : toBase64Address(await walletOrAddress.getAddress()); const { account_state: accountState, wallet: isWallet, seqno, - balance = '0', + balance, + last_transaction_id: { + lt, + hash, + }, } = await getTonWeb(network).provider.getWalletInfo(address); - const isInitialized = accountState === 'active'; return { - isInitialized, + isInitialized: accountState === 'active', isWallet, seqno, - balance, + balance: balance || '0', + lastTxId: lt === '0' + ? undefined + : stringifyTxId({ lt, hash }), }; } @@ -115,13 +123,13 @@ export async function getWalletStateInit(accountId: string) { } export async function pickWalletByAddress(network: ApiNetwork, publicKey: Uint8Array, address: string) { - address = toBase64Address(address); + address = toBase64Address(address, false); const tonWeb = getTonWeb(network); const walletClasses = tonWeb.wallet.list; const allWallets = await Promise.all(walletClasses.map(async (WalletClass) => { const wallet = new WalletClass(tonWeb.provider, { publicKey, wc: 0 }); - const walletAddress = (await wallet.getAddress()).toString(true, true, true); + const walletAddress = toBase64Address(await wallet.getAddress(), false); return { wallet, @@ -136,19 +144,15 @@ export async function pickWalletByAddress(network: ApiNetwork, publicKey: Uint8A export async function pickAccountWallet(accountId: string) { const { network } = parseAccountId(accountId); - const [publicKeyHex, address, account] = await Promise.all([ - fetchStoredPublicKey(accountId), - fetchStoredAddress(accountId), - fetchStoredAccount(accountId), - ]); + const { address, publicKey, version } = await fetchStoredAccount(accountId); - const publicKey = hexToBytes(publicKeyHex); + const publicKeyBytes = hexToBytes(publicKey); - if (account?.version) { - return buildWallet(network, publicKey, account.version); + if (version) { + return buildWallet(network, publicKeyBytes, version); } - return pickWalletByAddress(network, publicKey, address); + return pickWalletByAddress(network, publicKeyBytes, address); } export function resolveWalletVersion(wallet: WalletContract) { diff --git a/src/api/common/accounts.ts b/src/api/common/accounts.ts index e0c2e17e..6237a840 100644 --- a/src/api/common/accounts.ts +++ b/src/api/common/accounts.ts @@ -1,5 +1,5 @@ import type { StorageKey } from '../storages/types'; -import type { ApiAccountInfo, ApiNetwork } from '../types'; +import type { ApiAccount, ApiNetwork } from '../types'; import { buildAccountId, parseAccountId } from '../../util/account'; import { buildCollectionByKey } from '../../util/iteratees'; @@ -14,7 +14,7 @@ const loginPromise = new Promise((resolve) => { }); export async function getAccountIds(): Promise { - return Object.keys(await storage.getItem('addresses') || {}); + return Object.keys(await storage.getItem('accounts') || {}); } export async function getMainAccountId() { @@ -62,16 +62,24 @@ export async function getNewAccountId(network: ApiNetwork) { }); } -export function fetchStoredAccount(accountId: string): Promise { - return getAccountValue(accountId, 'accounts'); +export async function fetchStoredPublicKey(accountId: string): Promise { + return (await fetchStoredAccount(accountId)).publicKey; } -export function fetchStoredPublicKey(accountId: string): Promise { - return getAccountValue(accountId, 'publicKeys'); +export async function fetchStoredAddress(accountId: string): Promise { + return (await fetchStoredAccount(accountId)).address; } -export function fetchStoredAddress(accountId: string): Promise { - return getAccountValue(accountId, 'addresses'); +export function fetchStoredAccount(accountId: string): Promise { + return getAccountValue(accountId, 'accounts'); +} + +export async function updateStoredAccount(accountId: string, partial: Partial): Promise { + const account = await fetchStoredAccount(accountId); + return setAccountValue(accountId, 'accounts', { + ...account, + ...partial, + }); } export async function getAccountValue(accountId: string, key: StorageKey) { diff --git a/src/api/common/backend.ts b/src/api/common/backend.ts index ea194d9f..7a16eb26 100644 --- a/src/api/common/backend.ts +++ b/src/api/common/backend.ts @@ -3,20 +3,29 @@ import { handleFetchErrors } from './utils'; const BAD_REQUEST_CODE = 400; -export async function callBackendPost(path: string, data: AnyLiteral, isAllowBadRequest?: boolean) { +export async function callBackendPost(path: string, data: AnyLiteral, options?: { + authToken?: string; + isAllowBadRequest?: boolean; +}): Promise { + const { authToken, isAllowBadRequest } = options ?? {}; + const response = await fetch(`${BRILLIANT_API_BASE_URL}${path}`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + ...(authToken && { 'X-Auth-Token': authToken }), + }, body: JSON.stringify(data), }); handleFetchErrors(response, isAllowBadRequest ? [BAD_REQUEST_CODE] : undefined); return response.json(); } -export async function callBackendGet(path: string, data?: AnyLiteral, headers?: AnyLiteral) { +export async function callBackendGet(path: string, data?: AnyLiteral, headers?: AnyLiteral): Promise { const url = new URL(`${BRILLIANT_API_BASE_URL}${path}`); if (data) { Object.entries(data).forEach(([key, value]) => { + if (value === undefined) return; url.searchParams.set(key, value.toString()); }); } diff --git a/src/api/common/helpers.ts b/src/api/common/helpers.ts index 1106455b..0aab5b8a 100644 --- a/src/api/common/helpers.ts +++ b/src/api/common/helpers.ts @@ -2,6 +2,7 @@ import type { ApiTransactionExtra } from '../blockchains/ton/types'; import type { StorageKey } from '../storages/types'; import type { AccountIdParsed, + ApiAccount, ApiLocalTransactionParams, ApiTransaction, ApiTransactionActivity, @@ -10,15 +11,15 @@ import type { import { IS_EXTENSION, MAIN_ACCOUNT_ID } from '../../config'; import { buildAccountId, parseAccountId } from '../../util/account'; +import { toBase64Address } from '../blockchains/ton/util/tonweb'; import { storage } from '../storages'; import idbStorage from '../storages/idb'; import { getKnownAddresses, getScamMarkers } from './addresses'; -import { whenTxComplete } from './txCallbacks'; let localCounter = 0; const getNextLocalId = () => `${Date.now()}|${localCounter++}`; -const actualStateVersion = 7; +const actualStateVersion = 8; let migrationEnsurePromise: Promise; export function resolveBlockchainKey(accountId: string) { @@ -34,41 +35,10 @@ export function buildInternalAccountId(account: Omit return `${id}-${blockchain}`; } -export function createLocalTransaction( - onUpdate: OnApiUpdate, - accountId: string, +export function buildLocalTransaction( params: ApiLocalTransactionParams, - onTxComplete?: (transaction: ApiTransactionActivity) => void, -) { - const { amount, toAddress } = params; - - const localTransaction = buildLocalTransaction(params); - - onUpdate({ - type: 'newLocalTransaction', - transaction: localTransaction, - accountId, - }); - - whenTxComplete(toAddress, amount) - .then(({ transaction }) => { - if (onTxComplete) { - onTxComplete(transaction); - } - onUpdate({ - type: 'updateTxComplete', - accountId, - toAddress, - amount, - txId: transaction.txId, - localTxId: localTransaction.txId, - }); - }); - - return localTransaction; -} - -function buildLocalTransaction(params: ApiLocalTransactionParams): ApiTransactionActivity { + normalizedAddress: string, +): ApiTransactionActivity { const { amount, ...restParams } = params; const transaction: ApiTransaction = updateTransactionMetadata({ @@ -77,6 +47,9 @@ function buildLocalTransaction(params: ApiLocalTransactionParams): ApiTransactio isIncoming: false, amount: `-${amount}`, ...restParams, + extraData: { + normalizedAddress, + }, }); return { @@ -87,17 +60,14 @@ function buildLocalTransaction(params: ApiLocalTransactionParams): ApiTransactio } export function updateTransactionMetadata(transaction: ApiTransactionExtra): ApiTransactionExtra { - const { - isIncoming, fromAddress, toAddress, comment, - } = transaction; + const { extraData, comment } = transaction; let { metadata = {} } = transaction; const knownAddresses = getKnownAddresses(); const scamMarkers = getScamMarkers(); - const address = isIncoming ? fromAddress : toAddress; - if (address in knownAddresses) { - metadata = { ...metadata, ...knownAddresses[address] }; + if (extraData.normalizedAddress in knownAddresses) { + metadata = { ...metadata, ...knownAddresses[extraData.normalizedAddress] }; } if (comment && scamMarkers.map((sm) => sm.test(comment)).find(Boolean)) { @@ -121,8 +91,8 @@ export function isUpdaterAlive(onUpdate: OnApiUpdate) { return currentOnUpdate === onUpdate; } -export function startStorageMigration() { - migrationEnsurePromise = migrateStorage(); +export function startStorageMigration(onUpdate: OnApiUpdate) { + migrationEnsurePromise = migrateStorage(onUpdate); return migrationEnsurePromise; } @@ -130,14 +100,14 @@ export function waitStorageMigration() { return migrationEnsurePromise; } -export async function migrateStorage() { +export async function migrateStorage(onUpdate: OnApiUpdate) { let version = Number(await storage.getItem('stateVersion')); if (version === actualStateVersion) { return; } - if (!version && !(await storage.getItem('addresses'))) { + if (!version && !(await storage.getItem('addresses' as StorageKey))) { version = await idbStorage.getItem('stateVersion'); if (IS_EXTENSION && version) { @@ -182,7 +152,7 @@ export async function migrateStorage() { } if (version === 1) { - const addresses = await storage.getItem('addresses') as string | undefined; + const addresses = await storage.getItem('addresses' as StorageKey) as string | undefined; if (addresses && addresses.includes('-undefined')) { for (const field of ['mnemonicsEncrypted', 'addresses', 'publicKeys'] as StorageKey[]) { const newValue = (await storage.getItem(field) as string).replace('-undefined', '-ton'); @@ -243,4 +213,39 @@ export async function migrateStorage() { version = 7; await storage.setItem('stateVersion', version); } + + if (version === 7) { + const addresses = (await storage.getItem('addresses' as StorageKey)) as Record | undefined; + + if (addresses) { + const publicKeys = (await storage.getItem('publicKeys' as StorageKey)) as Record; + const accounts = (await storage.getItem('accounts') ?? {}) as Record; + + for (const [accountId, oldAddress] of Object.entries(addresses)) { + const newAddress = toBase64Address(oldAddress, false); + + accounts[accountId] = { + ...accounts[accountId], + address: newAddress, + publicKey: publicKeys[accountId], + }; + + onUpdate({ + type: 'updateAccount', + accountId, + partial: { + address: newAddress, + }, + }); + } + + await storage.setItem('accounts', accounts); + + await storage.removeItem('addresses' as StorageKey); + await storage.removeItem('publicKeys' as StorageKey); + } + + version = 8; + await storage.setItem('stateVersion', version); + } } diff --git a/src/api/common/utils.ts b/src/api/common/utils.ts index 56f4d301..2b6359a2 100644 --- a/src/api/common/utils.ts +++ b/src/api/common/utils.ts @@ -1,6 +1,7 @@ import TonWeb from 'tonweb'; import { STAKING_POOLS } from '../../config'; +import { ApiServerError } from '../errors'; export function bytesToHex(bytes: Uint8Array) { return TonWeb.utils.bytesToHex(bytes); @@ -44,3 +45,19 @@ export function sumBigString(a: string, b: string) { export function isKnownStakingPool(address: string) { return STAKING_POOLS.some((poolPart) => address.endsWith(poolPart)); } + +export async function fetchJson(url: string, data?: AnyLiteral, init?: RequestInit) { + const urlObject = new URL(url); + if (data) { + Object.entries(data).forEach(([key, value]) => { + if (value === undefined) return; + urlObject.searchParams.set(key, value.toString()); + }); + } + + const response = await fetch(urlObject, init); + if (!response.ok) { + throw new ApiServerError(`Http error ${response.status}`); + } + return response.json(); +} diff --git a/src/api/db.ts b/src/api/db.ts new file mode 100644 index 00000000..1a82634d --- /dev/null +++ b/src/api/db.ts @@ -0,0 +1,24 @@ +import type { ApiNft } from './types'; + +import Dexie from '../lib/dexie/dexie'; +import Table = Dexie.Table; + +export type ApiDbNft = ApiNft & { + accountId: string; + collectionAddress: string; +}; + +const DB_NANE = 'tables'; + +export class ApiDb extends Dexie { + nfts!: Table; + + constructor() { + super(DB_NANE); + this.version(1).stores({ + nfts: '[accountId+address], accountId, address, collectionAddress', + }); + } +} + +export const apiDb = new ApiDb(); diff --git a/src/api/environment.ts b/src/api/environment.ts index 2f39b71a..29427764 100644 --- a/src/api/environment.ts +++ b/src/api/environment.ts @@ -2,13 +2,13 @@ * This module is to be used instead of /src/util/environment.ts * when `window` is not available (e.g. in a web worker). */ -import { APP_ENV, IS_ELECTRON, IS_EXTENSION } from '../config'; +import { APP_ENV, IS_ELECTRON_BUILD, IS_EXTENSION } from '../config'; // eslint-disable-next-line no-restricted-globals export const IS_CHROME_EXTENSION = Boolean(self?.chrome?.system); // eslint-disable-next-line no-restricted-globals export const X_APP_ORIGIN = self.origin; -export const API_HEADERS = IS_EXTENSION || (IS_ELECTRON && APP_ENV !== 'development') +export const API_HEADERS = IS_EXTENSION || (IS_ELECTRON_BUILD && APP_ENV !== 'development') ? { 'X-App-Origin': X_APP_ORIGIN } : undefined; diff --git a/src/api/errors.ts b/src/api/errors.ts index bac8008a..e79b198c 100644 --- a/src/api/errors.ts +++ b/src/api/errors.ts @@ -1,10 +1,10 @@ // eslint-disable-next-line max-classes-per-file import type { ApiAnyDisplayError } from './types'; +import { ApiCommonError } from './types'; export class ApiBaseError extends Error { constructor(message?: string, public displayError?: ApiAnyDisplayError) { super(message); - Error.captureStackTrace(this); this.name = this.constructor.name; } } @@ -14,3 +14,26 @@ export class ApiUserRejectsError extends ApiBaseError { super(message); } } + +export class ApiServerError extends ApiBaseError { + constructor(message: string) { + super(message, ApiCommonError.ServerError); + } +} + +export function maybeApiErrors(fn: AnyAsyncFunction) { + return async (...args: any) => { + try { + return await fn(...args); + } catch (err) { + return handleServerError(err); + } + }; +} + +export function handleServerError(err: any) { + if (err instanceof ApiServerError) { + return { error: err.displayError! }; + } + throw err; +} diff --git a/src/api/extensionMethods/init.ts b/src/api/extensionMethods/init.ts index fb8528ef..2051142b 100644 --- a/src/api/extensionMethods/init.ts +++ b/src/api/extensionMethods/init.ts @@ -9,7 +9,7 @@ import * as extensionMethods from '.'; addHooks({ onWindowNeeded: openPopupWindow, onFullLogout: extensionMethods.onFullLogout, - onDappDisconnected: () => { + onDappDisconnected: (_, origin) => { siteMethods.updateSites({ type: 'disconnectSite', origin, diff --git a/src/api/extensionMethods/legacy.ts b/src/api/extensionMethods/legacy.ts index 4718a49d..ba38074d 100644 --- a/src/api/extensionMethods/legacy.ts +++ b/src/api/extensionMethods/legacy.ts @@ -6,12 +6,15 @@ import { parseAccountId } from '../../util/account'; import { logDebugError } from '../../util/logs'; import blockchains from '../blockchains'; import { - fetchStoredAccount, fetchStoredAddress, fetchStoredPublicKey, getCurrentAccountId, getCurrentAccountIdOrFail, + fetchStoredAccount, + fetchStoredAddress, + getCurrentAccountId, + getCurrentAccountIdOrFail, waitLogin, } from '../common/accounts'; import { createDappPromise } from '../common/dappPromises'; -import { createLocalTransaction } from '../common/helpers'; import { base64ToBytes, hexToBytes } from '../common/utils'; +import { createLocalTransaction } from '../methods'; import { openPopupWindow } from './window'; const ton = blockchains.ton; @@ -52,9 +55,8 @@ export async function requestWallets() { return []; } - const [address, publicKey, wallet] = await Promise.all([ - fetchStoredAddress(accountId), - fetchStoredPublicKey(accountId), + const [{ address, publicKey }, wallet] = await Promise.all([ + fetchStoredAccount(accountId), ton.pickAccountWallet(accountId), ]); @@ -116,7 +118,7 @@ export async function sendTransaction(params: { const { promiseId, promise } = createDappPromise(); const account = await fetchStoredAccount(accountId); - if (account?.ledger) { + if (account.ledger) { return sendLedgerTransaction(accountId, promiseId, promise, checkResult.fee!, params); } @@ -142,7 +144,7 @@ export async function sendTransaction(params: { } const fromAddress = await fetchStoredAddress(accountId); - createLocalTransaction(onPopupUpdate, accountId, { + createLocalTransaction(accountId, { amount, fromAddress, toAddress, @@ -221,7 +223,7 @@ async function sendLedgerTransaction( return false; } - createLocalTransaction(onPopupUpdate, accountId, { + createLocalTransaction(accountId, { amount, fromAddress, toAddress, diff --git a/src/api/hooks.ts b/src/api/hooks.ts index d5161a8f..701fe3f8 100644 --- a/src/api/hooks.ts +++ b/src/api/hooks.ts @@ -6,6 +6,7 @@ interface Hooks { onWindowNeeded: AnyFunction; onDappDisconnected: (accountId: string, origin: string) => any; onDappsChanged: AnyFunction; + onSwapCreated: (accountId: string, fromTimestamp: number) => any; } const hooks: Partial<{ diff --git a/src/api/methods/accounts.ts b/src/api/methods/accounts.ts index 43a2fce0..5c6b9888 100644 --- a/src/api/methods/accounts.ts +++ b/src/api/methods/accounts.ts @@ -1,4 +1,4 @@ -import type { ApiAccountInfo, ApiTxIdBySlug } from '../types'; +import type { ApiAccount, ApiTxIdBySlug } from '../types'; import { IS_EXTENSION } from '../../config'; import { parseAccountId } from '../../util/account'; @@ -9,8 +9,9 @@ import { storage } from '../storages'; import { deactivateAccountDapp, deactivateAllDapps, onActiveDappAccountUpdated } from './dapps'; import { sendUpdateTokens, - setupBackendStakingStatePolling, setupBalanceBasedPolling, + setupStakingPolling, + setupSwapPolling, } from './polling'; let activeAccountId: string | undefined; @@ -40,7 +41,8 @@ export async function activateAccount(accountId: string, newestTxIds?: ApiTxIdBy } void setupBalanceBasedPolling(accountId, newestTxIds); - void setupBackendStakingStatePolling(accountId); + void setupStakingPolling(accountId); + void setupSwapPolling(accountId); } export function deactivateAllAccounts() { @@ -63,7 +65,7 @@ export function isAccountActive(accountId: string) { return activeAccountId === accountId; } -export function fetchAccount(accountId: string): Promise { +export function fetchAccount(accountId: string): Promise { return fetchStoredAccount(accountId); } diff --git a/src/api/methods/auth.ts b/src/api/methods/auth.ts index 1f151a16..ae85d34e 100644 --- a/src/api/methods/auth.ts +++ b/src/api/methods/auth.ts @@ -1,12 +1,19 @@ import type { LedgerWalletInfo } from '../../util/ledger/types'; -import type { ApiAccountInfo, ApiNetwork, ApiTxIdBySlug } from '../types'; +import type { ApiAccount, ApiNetwork, ApiTxIdBySlug } from '../types'; import { IS_DAPP_SUPPORTED } from '../../config'; import blockchains from '../blockchains'; +import { toBase64Address } from '../blockchains/ton/util/tonweb'; import { - getNewAccountId, removeAccountValue, removeNetworkAccountsValue, setAccountValue, + getAccountIds, + getNewAccountId, + removeAccountValue, + removeNetworkAccountsValue, + setAccountValue, } from '../common/accounts'; import { bytesToHex } from '../common/utils'; +import { apiDb } from '../db'; +import { handleServerError } from '../errors'; import { storage } from '../storages'; import { activateAccount, deactivateAllAccounts, deactivateCurrentAccount } from './accounts'; import { removeAccountDapps, removeAllDapps, removeNetworkDapps } from './dapps'; @@ -31,7 +38,10 @@ export async function createWallet(network: ApiNetwork, mnemonic: string[], pass const address = await publicKeyToAddress(network, publicKey); const accountId = await getNewAccountId(network); - await storeAccount(accountId, mnemonic, password, publicKey, address); + await storeAccount(accountId, mnemonic, password, { + address, + publicKey: bytesToHex(publicKey), + }); void activateAccount(accountId); return { @@ -57,11 +67,21 @@ export async function importMnemonic(network: ApiNetwork, mnemonic: string[], pa const seedBase64 = await mnemonicToSeed(mnemonic); const { publicKey } = seedToKeyPair(seedBase64); - const wallet = await pickBestWallet(network, publicKey); - const address = (await wallet.getAddress()).toString(true, true, true); + let wallet: Awaited>; + + try { + wallet = await pickBestWallet(network, publicKey); + } catch (err: any) { + return handleServerError(err); + } + + const address = toBase64Address(await wallet.getAddress(), false); const accountId: string = await getNewAccountId(network); - await storeAccount(accountId, mnemonic, password, publicKey, address); + await storeAccount(accountId, mnemonic, password, { + publicKey: bytesToHex(publicKey), + address, + }); void activateAccount(accountId); return { @@ -76,7 +96,9 @@ export async function importLedgerWallet(network: ApiNetwork, walletInfo: Ledger publicKey, address, index, driver, deviceId, deviceName, version, } = walletInfo; - await storeHardwareAccount(accountId, publicKey, address, { + await storeHardwareAccount(accountId, { + address, + publicKey, version, ledger: { index, @@ -90,37 +112,16 @@ export async function importLedgerWallet(network: ApiNetwork, walletInfo: Ledger return { accountId, address, walletInfo }; } -async function storeHardwareAccount( - accountId: string, - publicKey: Uint8Array | string, - address: string, - accountInfo: ApiAccountInfo = {}, -) { - const publicKeyHex = typeof publicKey === 'string' ? publicKey : bytesToHex(publicKey); - - await Promise.all([ - setAccountValue(accountId, 'publicKeys', publicKeyHex), - setAccountValue(accountId, 'addresses', address), - setAccountValue(accountId, 'accounts', accountInfo), - ]); +function storeHardwareAccount(accountId: string, account?: ApiAccount) { + return setAccountValue(accountId, 'accounts', account); } -async function storeAccount( - accountId: string, - mnemonic: string[], - password: string, - publicKey: Uint8Array | string, - address: string, - accountInfo: ApiAccountInfo = {}, -) { +async function storeAccount(accountId: string, mnemonic: string[], password: string, account: ApiAccount) { const mnemonicEncrypted = await blockchains.ton.encryptMnemonic(mnemonic, password); - const publicKeyHex = typeof publicKey === 'string' ? publicKey : bytesToHex(publicKey); await Promise.all([ setAccountValue(accountId, 'mnemonicsEncrypted', mnemonicEncrypted), - setAccountValue(accountId, 'publicKeys', publicKeyHex), - setAccountValue(accountId, 'addresses', address), - setAccountValue(accountId, 'accounts', accountInfo), + setAccountValue(accountId, 'accounts', account), ]); } @@ -128,8 +129,6 @@ 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), @@ -140,24 +139,35 @@ export async function resetAccounts() { deactivateAllAccounts(); await Promise.all([ - storage.removeItem('addresses'), - storage.removeItem('publicKeys'), storage.removeItem('mnemonicsEncrypted'), storage.removeItem('accounts'), storage.removeItem('currentAccountId'), IS_DAPP_SUPPORTED && removeAllDapps(), + apiDb.nfts.clear(), ]); } export async function removeAccount(accountId: string, nextAccountId: string, newestTxIds?: ApiTxIdBySlug) { await Promise.all([ - removeAccountValue(accountId, 'addresses'), - removeAccountValue(accountId, 'publicKeys'), removeAccountValue(accountId, 'mnemonicsEncrypted'), removeAccountValue(accountId, 'accounts'), IS_DAPP_SUPPORTED && removeAccountDapps(accountId), + apiDb.nfts.where({ accountId }).delete(), ]); deactivateCurrentAccount(); await activateAccount(nextAccountId, newestTxIds); } + +export async function changePassword(oldPassword: string, password: string) { + for (const accountId of await getAccountIds()) { + const mnemonic = await blockchains.ton.fetchMnemonic(accountId, oldPassword); + + if (!mnemonic) { + throw new Error('Incorrect password'); + } + + const encryptedMnemonic = await blockchains.ton.encryptMnemonic(mnemonic, password); + await setAccountValue(accountId, 'mnemonicsEncrypted', encryptedMnemonic); + } +} diff --git a/src/api/methods/index.ts b/src/api/methods/index.ts index 64d5cadf..bb9de3d1 100644 --- a/src/api/methods/index.ts +++ b/src/api/methods/index.ts @@ -19,3 +19,6 @@ export { export { startSseConnection, } from '../tonConnect/sse'; +export * from './swap'; +export * from './other'; +export * from './prices'; diff --git a/src/api/methods/init.ts b/src/api/methods/init.ts index f21e52fa..99153177 100644 --- a/src/api/methods/init.ts +++ b/src/api/methods/init.ts @@ -10,6 +10,7 @@ import * as methods from '.'; addHooks({ onDappDisconnected: sendSseDisconnect, onDappsChanged: resetupSseConnection, + onSwapCreated: methods.setupSwapPolling, }); // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -18,14 +19,16 @@ export default async function init(onUpdate: OnApiUpdate, args: ApiInitArgs) { methods.initPolling(onUpdate, methods.isAccountActive); methods.initTransactions(onUpdate); - methods.initStaking(onUpdate); + methods.initStaking(); + methods.initSwap(onUpdate); + methods.initNfts(onUpdate); if (IS_DAPP_SUPPORTED) { methods.initDapps(onUpdate); tonConnect.initTonConnect(onUpdate); } - await startStorageMigration(); + await startStorageMigration(onUpdate); if (IS_SSE_SUPPORTED) { void resetupSseConnection(); diff --git a/src/api/methods/nfts.ts b/src/api/methods/nfts.ts index 87482255..a3c8a2b9 100644 --- a/src/api/methods/nfts.ts +++ b/src/api/methods/nfts.ts @@ -1,8 +1,57 @@ +import type { ApiDbNft } from '../db'; +import type { ApiNft, ApiUpdate, OnApiUpdate } from '../types'; + import blockchains from '../blockchains'; import { resolveBlockchainKey } from '../common/helpers'; +import { apiDb } from '../db'; + +let onUpdate: OnApiUpdate; + +export function initNfts(_onUpdate: OnApiUpdate) { + onUpdate = _onUpdate; +} export function fetchNfts(accountId: string) { const blockchain = blockchains[resolveBlockchainKey(accountId)!]; return blockchain.getAccountNfts(accountId); } + +export async function processNftUpdates(accountId: string, updates: ApiUpdate[]) { + updates.filter((update) => !(update.type === 'nftReceived' && update.nft.isHidden)).forEach(onUpdate); + + for (const update of updates) { + if (update.type === 'nftSent') { + const key = [accountId, update.nftAddress]; + await apiDb.nfts.delete(key); + } else if (update.type === 'nftReceived') { + const dbNft = convertToDbEntity(accountId, update.nft); + await apiDb.nfts.put(dbNft); + } else if (update.type === 'nftPutUpForSale') { + const key = [accountId, update.nftAddress]; + await apiDb.nfts.update(key, { isOnSale: true }); + } + } +} + +export async function updateNfts(accountId: string, nfts: ApiNft[]) { + const visibleNfts = nfts.filter((nft) => !nft.isHidden); + onUpdate({ + type: 'updateNfts', + accountId, + nfts: visibleNfts, + }); + + const dbNfts = nfts.map((nft) => convertToDbEntity(accountId, nft)); + + await apiDb.nfts.where({ accountId }).delete(); + await apiDb.nfts.bulkPut(dbNfts); +} + +function convertToDbEntity(accountId: string, nft: ApiNft): ApiDbNft { + return { + ...nft, + collectionAddress: nft.collectionAddress ?? '', + accountId, + }; +} diff --git a/src/api/methods/other.ts b/src/api/methods/other.ts new file mode 100644 index 00000000..313bd780 --- /dev/null +++ b/src/api/methods/other.ts @@ -0,0 +1,47 @@ +import nacl from 'tweetnacl'; + +import type { ApiBlockchainKey, ApiNetwork } from '../types'; + +import { parseAccountId } from '../../util/account'; +import blockchains from '../blockchains'; +import { fetchStoredAccount, updateStoredAccount } from '../common/accounts'; + +const SIGN_MESSAGE = Buffer.from('MyTonWallet_AuthToken_n6i0k4w8pb'); + +export function checkApiAvailability(options: { + accountId: string; +} | { + network: ApiNetwork; + blockchainKey: ApiBlockchainKey; +}) { + let network: ApiNetwork; + let blockchainKey: ApiBlockchainKey; + if ('network' in options) { + ({ network, blockchainKey } = options); + } else { + ({ network, blockchain: blockchainKey } = parseAccountId(options.accountId)); + } + + const blockchain = blockchains[blockchainKey]; + + return blockchain.checkApiAvailability(network); +} + +export async function getBackendAuthToken(accountId: string, password: string) { + const account = await fetchStoredAccount(accountId); + let authToken = account.authToken; + + if (!authToken) { + const privateKey = await blockchains.ton.fetchPrivateKey(accountId, password); + const signature = nacl.sign.detached(SIGN_MESSAGE, privateKey!); + authToken = Buffer.from(signature).toString('base64'); + + await updateStoredAccount(accountId, { authToken }); + } + + if (!account.isInitialized) { + authToken += `:${account.publicKey}`; + } + + return authToken; +} diff --git a/src/api/methods/polling.ts b/src/api/methods/polling.ts index 4e5d0747..0a7f94e3 100644 --- a/src/api/methods/polling.ts +++ b/src/api/methods/polling.ts @@ -2,8 +2,15 @@ import { randomBytes } from 'tweetnacl'; import type { TokenBalanceParsed } from '../blockchains/ton/tokens'; import type { + ApiActivity, + ApiBackendStakingState, + ApiBaseCurrency, ApiBaseToken, ApiNftUpdate, + ApiStakingCommonData, + ApiStakingState, + ApiSwapAsset, + ApiSwapHistoryItem, ApiToken, ApiTokenPrice, ApiTransactionActivity, @@ -11,38 +18,58 @@ import type { OnApiUpdate, } from '../types'; -import { APP_ENV, APP_VERSION, TON_TOKEN_SLUG } from '../../config'; +import { + APP_ENV, APP_VERSION, DEFAULT_PRICE_CURRENCY, TON_TOKEN_SLUG, +} from '../../config'; +import { parseAccountId } from '../../util/account'; +import { areDeepEqual } from '../../util/areDeepEqual'; import { compareActivities } from '../../util/compareActivities'; import { logDebugError } from '../../util/logs'; import { pause } from '../../util/schedulers'; import blockchains from '../blockchains'; import { addKnownTokens, getKnownTokens } from '../blockchains/ton/tokens'; +import { fetchStoredAccount, updateStoredAccount } from '../common/accounts'; import { tryUpdateKnownAddresses } from '../common/addresses'; import { callBackendGet } from '../common/backend'; import { isUpdaterAlive, resolveBlockchainKey } from '../common/helpers'; import { txCallbacks } from '../common/txCallbacks'; import { X_APP_ORIGIN } from '../environment'; import { storage } from '../storages'; -import { getBackendStakingState } from './staking'; +import { processNftUpdates, updateNfts } from './nfts'; +import { getBaseCurrency } from './prices'; +import { getBackendStakingState, getStakingCommonData, tryUpdateStakingCommonData } from './staking'; +import { + swapGetAssets, swapGetHistory, swapItemToActivity, swapReplaceTransactions, +} from './swap'; type IsAccountActiveFn = (accountId: string) => boolean; const POLLING_INTERVAL = 1100; // 1.1 sec const BACKEND_POLLING_INTERVAL = 30000; // 30 sec const LONG_BACKEND_POLLING_INTERVAL = 60000; // 1 min -const PAUSE_AFTER_BALANCE_CHANGE = 1000; // 1 sec -const FIRST_TRANSACTIONS_LIMIT = 20; + +const FIRST_TRANSACTIONS_LIMIT = 50; + const NFT_FULL_POLLING_INTERVAL = 30000; // 30 sec const NFT_FULL_UPDATE_FREQUNCY = Math.round(NFT_FULL_POLLING_INTERVAL / POLLING_INTERVAL); const DOUBLE_CHECK_TOKENS_PAUSE = 30000; // 30 sec +const SWAP_POLLING_INTERVAL = 3000; // 3 sec +const SWAP_FINISHED_STATUSES = new Set(['failed', 'completed', 'expired']); + let onUpdate: OnApiUpdate; let isAccountActive: IsAccountActiveFn; let clientId: string | undefined; let preloadEnsurePromise: Promise; -let pricesBySlug: Record = {}; - +const prices: { + baseCurrency: ApiBaseCurrency; + bySlug: Record; +} = { + baseCurrency: DEFAULT_PRICE_CURRENCY, + bySlug: {}, +}; +let swapPollingAccountId: string | undefined; const lastBalanceCache: Record; @@ -55,6 +82,8 @@ export function initPolling(_onUpdate: OnApiUpdate, _isAccountActive: IsAccountA preloadEnsurePromise = Promise.all([ tryUpdateKnownAddresses(), tryUpdateTokens(_onUpdate), + tryLoadSwapTokens(_onUpdate), + tryUpdateStakingCommonData(), ]); void setupBackendPolling(); @@ -71,7 +100,7 @@ function registerNewTokens(tokenBalances: TokenBalanceParsed[]) { areNewTokensFound = true; tokens[token.slug] = { ...token, - quote: pricesBySlug[token.slug] || { + quote: prices.bySlug[token.slug] || { price: 0.0, percentChange1h: 0.0, percentChange24h: 0.0, @@ -91,6 +120,11 @@ export async function setupBalanceBasedPolling(accountId: string, newestTxIds: A delete lastBalanceCache[accountId]; + const { network } = parseAccountId(accountId); + const account = await fetchStoredAccount(accountId); + const { address } = account; + let { isInitialized } = account; + let nftFromSec = Math.round(Date.now() / 1000); let nftUpdates: ApiNftUpdate[]; let i = 0; @@ -100,31 +134,22 @@ export async function setupBalanceBasedPolling(accountId: string, newestTxIds: A while (isUpdaterAlive(localOnUpdate) && isAccountActive(accountId)) { try { - const [balance, stakingState] = await Promise.all([ - blockchain.getAccountBalance(accountId).catch(logAndRescue), - blockchain.getStakingState(accountId).catch(logAndRescue), - ]); + const walletInfo = await blockchain.getWalletInfo(network, address); if (!isUpdaterAlive(localOnUpdate) || !isAccountActive(accountId)) return; - if (stakingState) { - onUpdate({ - type: 'updateStakingState', - accountId, - stakingState, - }); - } + const { balance, lastTxId } = walletInfo ?? {}; // Full update NFTs every ~30 seconds if (i % NFT_FULL_UPDATE_FREQUNCY === 0) { - nftFromSec = Math.round(Date.now() / 1000); - const nfts = await blockchain.getAccountNfts(accountId); + const nfts = await blockchain.getAccountNfts(accountId).catch(logAndRescue); if (!isUpdaterAlive(localOnUpdate) || !isAccountActive(accountId)) return; - onUpdate({ - type: 'updateNfts', - accountId, - nfts, - }); + if (nfts) { + nftFromSec = Math.round(Date.now() / 1000); + if (!isUpdaterAlive(localOnUpdate) || !isAccountActive(accountId)) return; + + void updateNfts(accountId, nfts); + } } // Process TON balance @@ -132,21 +157,16 @@ export async function setupBalanceBasedPolling(accountId: string, newestTxIds: A const changedTokenSlugs: string[] = []; const isTonBalanceChanged = balance && balance !== cache?.balance; + const balancesToUpdate: Record = {}; + if (isTonBalanceChanged) { changedTokenSlugs.push(TON_TOKEN_SLUG); - onUpdate({ - type: 'updateBalance', - accountId, - slug: TON_TOKEN_SLUG, - balance, - }); + balancesToUpdate[TON_TOKEN_SLUG] = balance; lastBalanceCache[accountId] = { ...lastBalanceCache[accountId], balance, }; - - await pause(PAUSE_AFTER_BALANCE_CHANGE); } // Fetch and process token balances @@ -154,24 +174,19 @@ export async function setupBalanceBasedPolling(accountId: string, newestTxIds: A doubleCheckTokensTime = isTonBalanceChanged ? Date.now() + DOUBLE_CHECK_TOKENS_PAUSE : undefined; const tokenBalances = await blockchain.getAccountTokenBalances(accountId).catch(logAndRescue); + if (!isUpdaterAlive(localOnUpdate) || !isAccountActive(accountId)) return; if (tokenBalances) { registerNewTokens(tokenBalances); - for (const { slug, balance: tokenBalance } of tokenBalances) { + tokenBalances.forEach(({ slug, balance: tokenBalance }) => { const cachedBalance = cache?.tokenBalances && cache.tokenBalances[slug]; - if (cachedBalance === tokenBalance) continue; + if (cachedBalance === tokenBalance) return; changedTokenSlugs.push(slug); - - onUpdate({ - type: 'updateBalance', - accountId, - slug, - balance: tokenBalance, - }); - } + balancesToUpdate[slug] = tokenBalance; + }); lastBalanceCache[accountId] = { ...lastBalanceCache[accountId], @@ -180,19 +195,40 @@ export async function setupBalanceBasedPolling(accountId: string, newestTxIds: A )), }; } + + if (Object.keys(balancesToUpdate).length > 0) { + onUpdate({ + type: 'updateBalances', + accountId, + balancesToUpdate, + }); + } } // Fetch transactions for tokens with a changed balance if (changedTokenSlugs.length) { + if (lastTxId) { + await blockchain.waitUntilTransactionAppears(network, address, lastTxId); + } + const newTxIds = await processNewTokenActivities(accountId, newestTxIds, changedTokenSlugs); newestTxIds = { ...newestTxIds, ...newTxIds }; } // Fetch NFT updates if (isTonBalanceChanged) { - [nftFromSec, nftUpdates] = await blockchain.getNftUpdates(accountId, nftFromSec); + const nftResult = await blockchain.getNftUpdates(accountId, nftFromSec).catch(logAndRescue); if (!isUpdaterAlive(localOnUpdate) || !isAccountActive(accountId)) return; - nftUpdates.forEach(onUpdate); + + if (nftResult) { + [nftFromSec, nftUpdates] = nftResult; + void processNftUpdates(accountId, nftUpdates); + } + } + + if (isTonBalanceChanged && !isInitialized && await blockchain.isAddressInitialized(network, address)) { + isInitialized = true; + await updateStoredAccount(accountId, { isInitialized }); } i++; @@ -204,6 +240,53 @@ export async function setupBalanceBasedPolling(accountId: string, newestTxIds: A } } +export async function setupStakingPolling(accountId: string) { + const { blockchain: blockchainKey, network } = parseAccountId(accountId); + const blockchain = blockchains[blockchainKey]; + + if (network !== 'mainnet') { + return; + } + + const localOnUpdate = onUpdate; + let lastState: { + stakingCommonData: ApiStakingCommonData; + backendStakingState: ApiBackendStakingState; + stakingState: ApiStakingState; + } | undefined; + + while (isUpdaterAlive(localOnUpdate) && isAccountActive(accountId)) { + try { + const stakingCommonData = getStakingCommonData(); + const backendStakingState = await getBackendStakingState(accountId); + const stakingState = await blockchain.getStakingState( + accountId, stakingCommonData, backendStakingState, + ); + + if (!isUpdaterAlive(localOnUpdate) || !isAccountActive(accountId)) return; + + const state = { + stakingCommonData, + backendStakingState, + stakingState, + }; + + if (!areDeepEqual(state, lastState)) { + lastState = state; + onUpdate({ + type: 'updateStaking', + accountId, + ...state, + }); + } + } catch (err) { + logDebugError('setupBalancePolling', err); + } + + await pause(POLLING_INTERVAL); + } +} + async function processNewTokenActivities( accountId: string, newestTxIds: ApiTxIdBySlug, @@ -216,32 +299,34 @@ async function processNewTokenActivities( } let allTransactions: ApiTransactionActivity[] = []; + let allActivities: ApiActivity[] = []; const entries = await Promise.all(tokenSlugs.map(async (slug) => { let newestTxId = newestTxIds[slug]; const transactions = await blockchain.getTokenTransactionSlice( - accountId, slug, undefined, newestTxId, + accountId, slug, undefined, newestTxId, FIRST_TRANSACTIONS_LIMIT, ); + const activities = await swapReplaceTransactions(accountId, transactions, slug); if (transactions.length) { newestTxId = transactions[0]!.txId; - allTransactions = allTransactions.concat(transactions.slice(0, FIRST_TRANSACTIONS_LIMIT)); + + allActivities = allActivities.concat(activities); + allTransactions = allTransactions.concat(transactions); } return [slug, newestTxId]; })); - allTransactions.sort((a, b) => compareActivities(a, b, true)); + allTransactions = allTransactions.sort((a, b) => compareActivities(a, b, true)); allTransactions.forEach((transaction) => { txCallbacks.runCallbacks(transaction); }); - const activities = allTransactions; - onUpdate({ type: 'newActivities', - activities, + activities: allActivities, accountId, }); @@ -267,36 +352,46 @@ export async function setupLongBackendPolling() { while (isUpdaterAlive(onUpdate)) { await pause(LONG_BACKEND_POLLING_INTERVAL); - try { - await tryUpdateKnownAddresses(); - } catch (err) { - logDebugError('setupLongBackendPolling', err); - } + await Promise.all([ + tryUpdateKnownAddresses(), + tryUpdateStakingCommonData(), + ]); } } -export async function tryUpdateTokens(localOnUpdate: OnApiUpdate) { +export async function tryUpdateTokens(localOnUpdate?: OnApiUpdate) { + if (!localOnUpdate) { + localOnUpdate = onUpdate; + } + try { + const baseCurrency = await getBaseCurrency(); + const pricesHeaders: AnyLiteral = { + 'X-App-Origin': X_APP_ORIGIN, + 'X-App-Version': APP_VERSION, + 'X-App-ClientID': clientId ?? await getClientId(), + 'X-App-Env': APP_ENV, + }; + const [pricesData, tokens] = await Promise.all([ - callBackendGet('/prices', undefined, { - 'X-App-Origin': X_APP_ORIGIN, - 'X-App-Version': APP_VERSION, - 'X-App-ClientID': clientId ?? await getClientId(), - 'X-App-Env': APP_ENV, - }) as Promise>, - callBackendGet('/known-tokens') as Promise, + callBackendGet>('/prices', { base: baseCurrency }, pricesHeaders), + callBackendGet('/known-tokens'), ]); if (!isUpdaterAlive(localOnUpdate)) return; addKnownTokens(tokens); - pricesBySlug = Object.values(pricesData).reduce((acc, { slugs, quote }) => { + prices.bySlug = Object.values(pricesData).reduce((acc, { slugs, quote }) => { for (const slug of slugs) { acc[slug] = quote; } return acc; }, {} as Record); + prices.baseCurrency = baseCurrency; sendUpdateTokens(); } catch (err) { @@ -304,6 +399,29 @@ export async function tryUpdateTokens(localOnUpdate: OnApiUpdate) { } } +export async function tryLoadSwapTokens(localOnUpdate: OnApiUpdate) { + try { + const assets = await swapGetAssets(); + + if (!isUpdaterAlive(localOnUpdate)) return; + + const tokens = assets.reduce((acc: Record, asset) => { + acc[asset.slug] = { + ...asset, + contract: asset.contract ?? asset.slug, + }; + return acc; + }, {}); + + onUpdate({ + type: 'updateSwapTokens', + tokens, + }); + } catch (err) { + logDebugError('tryLoadSwapTokens', err); + } +} + async function getClientId() { clientId = await storage.getItem('clientId'); if (!clientId) { @@ -316,37 +434,108 @@ async function getClientId() { export function sendUpdateTokens() { const tokens = getKnownTokens(); Object.values(tokens).forEach((token) => { - if (token.slug in pricesBySlug) { - token.quote = pricesBySlug[token.slug]; + if (token.slug in prices.bySlug) { + token.quote = prices.bySlug[token.slug]; } }); onUpdate({ type: 'updateTokens', tokens, + baseCurrency: prices.baseCurrency, }); } -export async function setupBackendStakingStatePolling(accountId: string) { - while (isUpdaterAlive(onUpdate) && isAccountActive(accountId)) { +export async function setupSwapPolling(accountId: string) { + if (swapPollingAccountId === accountId) return; // Double launch is not allowed + swapPollingAccountId = accountId; + + const { address, lastFinishedSwapTimestamp } = await fetchStoredAccount(accountId); + + let fromTimestamp = lastFinishedSwapTimestamp ?? await getActualLastFinishedSwapTimestamp(accountId, address); + + const localOnUpdate = onUpdate; + const swapById: Record = {}; + + while (isUpdaterAlive(localOnUpdate) && isAccountActive(accountId)) { try { - const backendStakingState = await getBackendStakingState(accountId); - if (!isUpdaterAlive(onUpdate) || !isAccountActive(accountId)) return; + const swaps = await swapGetHistory(address, { + fromTimestamp, + }); + if (!isUpdaterAlive(onUpdate) || !isAccountActive(accountId)) break; + if (!swaps.length) break; - if (backendStakingState) { - onUpdate({ - type: 'updateBackendStakingState', - backendStakingState, + swaps.reverse(); + + let isLastFinishedSwapUpdated = false; + let isPrevFinished = true; + + for (const swap of swaps) { + if (swap.cex) { + if (swap.cex.status === swapById[swap.id]?.cex!.status) { + continue; + } + } else if (swap.status === swapById[swap.id]?.status) { + continue; + } + + swapById[swap.id] = swap; + + const isFinished = SWAP_FINISHED_STATUSES.has(swap.status); + if (isFinished && isPrevFinished) { + fromTimestamp = swap.timestamp; + isLastFinishedSwapUpdated = true; + } + isPrevFinished = isFinished; + + if (!swap.cex && swap.status !== 'completed') { + // Completed onchain swaps are processed in swapReplaceTransactions + onUpdate({ + type: 'newActivities', + accountId, + activities: [swapItemToActivity(swap)], + }); + } + } + + if (isLastFinishedSwapUpdated) { + await updateStoredAccount(accountId, { + lastFinishedSwapTimestamp: fromTimestamp, }); } } catch (err) { - logDebugError('setupBackendStakingStatePolling', err); + logDebugError('setupSwapCexPolling', err); } - await pause(BACKEND_POLLING_INTERVAL); + await pause(SWAP_POLLING_INTERVAL); + } + + if (accountId === swapPollingAccountId) { + swapPollingAccountId = undefined; } } +async function getActualLastFinishedSwapTimestamp(accountId: string, address: string) { + const swaps = await swapGetHistory(address, {}); + + swaps.reverse(); + + let timestamp = Date.now(); + for (const swap of swaps) { + if (SWAP_FINISHED_STATUSES.has(swap.status)) { + timestamp = swap.timestamp; + } else { + break; + } + } + + await updateStoredAccount(accountId, { + lastFinishedSwapTimestamp: timestamp, + }); + + return timestamp; +} + function logAndRescue(err: Error) { logDebugError('Polling error', err); diff --git a/src/api/methods/prices.ts b/src/api/methods/prices.ts new file mode 100644 index 00000000..832dc4c0 --- /dev/null +++ b/src/api/methods/prices.ts @@ -0,0 +1,12 @@ +import type { ApiBaseCurrency } from '../types'; + +import { DEFAULT_PRICE_CURRENCY } from '../../config'; +import { storage } from '../storages'; + +export async function getBaseCurrency() { + return (await storage.getItem('baseCurrency')) ?? DEFAULT_PRICE_CURRENCY; +} + +export function setBaseCurrency(currency: ApiBaseCurrency) { + return storage.setItem('baseCurrency', currency); +} diff --git a/src/api/methods/staking.ts b/src/api/methods/staking.ts index 8a88fcca..0b5f0bc0 100644 --- a/src/api/methods/staking.ts +++ b/src/api/methods/staking.ts @@ -1,39 +1,61 @@ -import type { ApiBackendStakingState, OnApiUpdate } from '../types'; +import type { + ApiBackendStakingState, + ApiStakingCommonData, + ApiStakingHistory, + ApiStakingType, +} from '../types'; import { TON_TOKEN_SLUG } from '../../config'; +import { logDebugError } from '../../util/logs'; import blockchains from '../blockchains'; import { STAKE_COMMENT, UNSTAKE_COMMENT } from '../blockchains/ton/constants'; import { fetchStoredAddress } from '../common/accounts'; -import { createLocalTransaction, resolveBlockchainKey } from '../common/helpers'; +import { callBackendGet } from '../common/backend'; +import { resolveBlockchainKey } from '../common/helpers'; +import { isKnownStakingPool } from '../common/utils'; +import { createLocalTransaction } from './transactions'; -let onUpdate: OnApiUpdate; +const CACHE_TTL = 60000; // 1 m. +let backendStakingStateByAddress: Record = {}; +let stakingCommonData: ApiStakingCommonData; -export function initStaking(_onUpdate: OnApiUpdate) { - onUpdate = _onUpdate; +// let onUpdate: OnApiUpdate; + +export function initStaking() { + // onUpdate = _onUpdate; } -export function checkStakeDraft(accountId: string, amount: string) { +export async function checkStakeDraft(accountId: string, amount: string) { const blockchain = blockchains[resolveBlockchainKey(accountId)!]; - return blockchain.checkStakeDraft(accountId, amount); + const backendState = await getBackendStakingState(accountId); + return blockchain.checkStakeDraft(accountId, amount, stakingCommonData!, backendState!); } -export function checkUnstakeDraft(accountId: string) { +export async function checkUnstakeDraft(accountId: string, amount: string) { const blockchain = blockchains[resolveBlockchainKey(accountId)!]; - return blockchain.checkUnstakeDraft(accountId); + const backendState = await getBackendStakingState(accountId); + return blockchain.checkUnstakeDraft(accountId, amount, stakingCommonData!, backendState!); } -export async function submitStake(accountId: string, password: string, amount: string, fee?: string) { +export async function submitStake( + accountId: string, password: string, amount: string, type: ApiStakingType, fee?: string, +) { const blockchain = blockchains[resolveBlockchainKey(accountId)!]; const fromAddress = await fetchStoredAddress(accountId); - const result = await blockchain.submitStake(accountId, password, amount); + const backendState = await getBackendStakingState(accountId); + const result = await blockchain.submitStake( + accountId, password, amount, type, backendState!, + ); if ('error' in result) { return false; } - const localTransaction = createLocalTransaction(onUpdate, accountId, { + onStakingChangeExpected(); + + const localTransaction = createLocalTransaction(accountId, { amount: result.amount, fromAddress, toAddress: result.normalizedAddress, @@ -49,16 +71,25 @@ export async function submitStake(accountId: string, password: string, amount: s }; } -export async function submitUnstake(accountId: string, password: string, fee?: string) { +export async function submitUnstake( + accountId: string, + password: string, + type: ApiStakingType, + amount: string, + fee?: string, +) { const blockchain = blockchains[resolveBlockchainKey(accountId)!]; const fromAddress = await fetchStoredAddress(accountId); - const result = await blockchain.submitUnstake(accountId, password); + const backendState = await getBackendStakingState(accountId); + const result = await blockchain.submitUnstake(accountId, password, type, amount, backendState!); if ('error' in result) { return false; } - const localTransaction = createLocalTransaction(onUpdate, accountId, { + onStakingChangeExpected(); + + const localTransaction = createLocalTransaction(accountId, { amount: result.amount, fromAddress, toAddress: result.normalizedAddress, @@ -74,25 +105,62 @@ export async function submitUnstake(accountId: string, password: string, fee?: s }; } -export function getStakingState(accountId: string) { - const blockchain = blockchains[resolveBlockchainKey(accountId)!]; +export async function getBackendStakingState(accountId: string): Promise { + const address = await fetchStoredAddress(accountId); + const state = await fetchBackendStakingState(address); + return { + ...state, + nominatorsPool: { + ...state.nominatorsPool, + start: state.nominatorsPool.start * 1000, + end: state.nominatorsPool.end * 1000, + }, + }; +} + +export async function fetchBackendStakingState(address: string): Promise { + const cacheItem = backendStakingStateByAddress[address]; + if (cacheItem && cacheItem[0] > Date.now()) { + return cacheItem[1]; + } + + const stakingState = await callBackendGet(`/staking/state/${address}`); - return blockchain.getStakingState(accountId); + if (!isKnownStakingPool(stakingState.nominatorsPool.address)) { + throw Error('Unexpected pool address, likely a malicious activity'); + } + + backendStakingStateByAddress[address] = [Date.now() + CACHE_TTL, stakingState]; + + return stakingState; +} + +export async function getStakingHistory( + accountId: string, limit?: number, offset?: number, +): Promise { + const address = await fetchStoredAddress(accountId); + return callBackendGet(`/staking/profits/${address}`, { limit, offset }); +} + +export function onStakingChangeExpected() { + backendStakingStateByAddress = {}; } -export async function getBackendStakingState(accountId: string): Promise { - const state = await blockchains.ton.getBackendStakingState(accountId); - if (!state) { - return state; +export async function tryUpdateStakingCommonData() { + try { + const data: ApiStakingCommonData = await callBackendGet('/staking/common'); + data.round.start *= 1000; + data.round.end *= 1000; + data.round.unlock *= 1000; + data.prevRound.start *= 1000; + data.prevRound.end *= 1000; + data.prevRound.unlock *= 1000; + stakingCommonData = data; + } catch (err) { + logDebugError('tryUpdateLiquidStakingState', err); } +} - const poolState = state.poolState; - return { - ...state, - poolState: { - ...poolState, - startOfCycle: poolState!.startOfCycle * 1000, - endOfCycle: poolState!.endOfCycle * 1000, - }, - }; +export function getStakingCommonData() { + return stakingCommonData!; } diff --git a/src/api/methods/swap.ts b/src/api/methods/swap.ts new file mode 100644 index 00000000..9ac980db --- /dev/null +++ b/src/api/methods/swap.ts @@ -0,0 +1,280 @@ +import type { + ApiActivity, + ApiSwapActivity, + ApiSwapAsset, + ApiSwapBuildRequest, + ApiSwapBuildResponse, + ApiSwapCexCreateTransactionRequest, + ApiSwapCexCreateTransactionResponse, + ApiSwapCexEstimateRequest, + ApiSwapCexEstimateResponse, + ApiSwapEstimateRequest, + ApiSwapEstimateResponse, + ApiSwapHistoryItem, + ApiSwapPairAsset, + ApiSwapTonAsset, + ApiSwapTransfer, + ApiTransactionActivity, + OnApiUpdate, +} from '../types'; + +import { TON_SYMBOL, TON_TOKEN_SLUG } from '../../config'; +import { logDebugError } from '../../util/logs'; +import { pause } from '../../util/schedulers'; +import { buildSwapId } from '../../util/swap/buildSwapId'; +import blockchains from '../blockchains'; +import { buildTokenSlug, parseTxId } from '../blockchains/ton/util'; +import { fetchStoredAddress } from '../common/accounts'; +import { callBackendGet, callBackendPost } from '../common/backend'; +import { whenTxComplete } from '../common/txCallbacks'; +import { callHook } from '../hooks'; +import { getBackendAuthToken } from './other'; + +type LtRange = [number, number]; + +const SWAP_MAX_LT = 50; +const SWAP_WAITING_TIME = 5000; // 5 sec +const SWAP_WAITING_PAUSE = 500; // 0.5 sec + +const pendingLtRanges: LtRange[] = []; +const ton = blockchains.ton; + +let onUpdate: OnApiUpdate; + +export function initSwap(_onUpdate: OnApiUpdate) { + onUpdate = _onUpdate; +} + +export async function swapBuildTransfer(accountId: string, password: string, params: ApiSwapBuildRequest) { + const authToken = await getBackendAuthToken(accountId, password); + + const { id, transfers } = await swapBuild(authToken, params); + + const transferList = transfers.map((transfer) => ({ + ...transfer, + isBase64Payload: true, + })); + const result = await ton.checkMultiTransactionDraft(accountId, transferList); + + if ('error' in result) { + return result; + } + + return { ...result, id, transfers }; +} + +export async function swapSubmit( + accountId: string, + password: string, + fee: string, + transfers: ApiSwapTransfer[], + historyItem: ApiSwapHistoryItem, +) { + const transferList = transfers.map((transfer) => ({ + ...transfer, + isBase64Payload: true, + })); + const result = await ton.submitMultiTransfer(accountId, password, transferList); + + if ('error' in result) { + return result; + } + + const { amount, toAddress } = transfers[0]; + + const from = getSwapItemSlug(historyItem, historyItem.from); + const to = getSwapItemSlug(historyItem, historyItem.to); + + const swap: ApiSwapActivity = { + ...historyItem, + id: buildSwapId(historyItem.id), + from, + to, + kind: 'swap', + }; + + function onTxComplete(transaction: ApiTransactionActivity) { + const lt = parseTxId(transaction.txId).lt; + pendingLtRanges.push([lt, lt + SWAP_MAX_LT]); + } + + whenTxComplete(toAddress, amount) + .then(({ transaction }) => onTxComplete(transaction)); + + onUpdate({ + type: 'newActivities', + accountId, + activities: [swap], + }); + + void callHook('onSwapCreated', accountId, swap.timestamp - 1); + + return result; +} + +function getSwapItemSlug(item: ApiSwapHistoryItem, asset: string) { + if (asset === TON_SYMBOL) return TON_TOKEN_SLUG; + if (item.cex) return asset; + return buildTokenSlug(asset); +} + +export async function swapReplaceTransactions( + accountId: string, + transactions: ApiTransactionActivity[], + slug?: string, +): Promise { + if (!transactions.length) { + return transactions; + } + + try { + const address = await fetchStoredAddress(accountId); + + const firstLt = parseTxId(transactions[0].txId).lt; + const lastLt = parseTxId(transactions[transactions.length - 1].txId).lt; + + const firstTimestamp = transactions[0].timestamp; + const lastTimestamp = transactions[transactions.length - 1].timestamp; + + const [fromLt, fromTimestamp] = firstLt > lastLt ? [lastLt, lastTimestamp] : [firstLt, firstTimestamp]; + const [toLt, toTimestamp] = firstLt > lastLt ? [firstLt, firstTimestamp] : [lastLt, lastTimestamp]; + + const waitUntil = Date.now() + SWAP_WAITING_TIME; + while (Date.now() < waitUntil) { + const pendingSwaps = await swapGetHistory(address, { + status: 'pending', + isCex: false, + }); + if (!pendingSwaps.length) { + break; + } + await pause(SWAP_WAITING_PAUSE); + } + + const swaps = await swapGetHistory(address, { + fromLt, toLt, fromTimestamp, toTimestamp, + }); + + if (!swaps.length) { + return transactions; + } + + const skipLtRanges: LtRange[] = [...pendingLtRanges]; + const result: ApiActivity[] = []; + + for (const swap of swaps) { + if (swap.lt) { + skipLtRanges.push([swap.lt, swap.lt + SWAP_MAX_LT]); + } + + const swapActivity = swapItemToActivity(swap); + + if (slug && swapActivity.from !== slug && swapActivity.to !== slug) { + continue; + } + result.push(swapActivity); + } + + for (const transaction of transactions) { + const lt = parseTxId(transaction.txId).lt; + const swapIndex = skipLtRanges.findIndex(([startLt, endLt]) => lt >= startLt && lt <= endLt); + + if (swapIndex < 0) { + result.push(transaction); + } else { + result.push({ + ...transaction, + shouldHide: true, + }); + } + } + + return result; + } catch (err) { + logDebugError('swapReplaceTransactions', err); + return transactions; + } +} + +export function swapItemToActivity(swap: ApiSwapHistoryItem): ApiSwapActivity { + return { + ...swap, + id: buildSwapId(swap.id), + kind: 'swap', + from: getSwapItemSlug(swap, swap.from), + to: getSwapItemSlug(swap, swap.to), + }; +} + +export function swapEstimate(params: ApiSwapEstimateRequest): Promise { + return callBackendPost('/swap/ton/estimate', params, { + isAllowBadRequest: true, + }); +} + +export function swapBuild(authToken: string, params: ApiSwapBuildRequest): Promise { + return callBackendPost('/swap/ton/build', params, { + authToken, + }); +} + +export function swapGetAssets(): Promise { + return callBackendGet('/swap/assets'); +} + +export function swapGetTonCurrencies(): Promise { + return callBackendGet('/swap/ton/tokens'); +} + +export function swapGetPairs(symbolOrMinter: string): Promise { + return callBackendGet('/swap/pairs', { asset: symbolOrMinter }); +} + +export function swapGetHistory(address: string, params: { + fromLt?: number; + toLt?: number; + fromTimestamp?: number; + toTimestamp?: number; + status?: ApiSwapHistoryItem['status']; + isCex?: boolean; +}): Promise { + return callBackendGet(`/swap/history/${address}`, params); +} + +export function swapGetHistoryItem(address: string, id: number): Promise { + return callBackendGet(`/swap/history/${address}/${id}`); +} + +export function swapCexEstimate(params: ApiSwapCexEstimateRequest): Promise { + return callBackendPost('/swap/cex/estimate', params); +} + +export function swapCexValidateAddress(params: { slug: string; address: string }): Promise<{ + result: boolean; + message?: string; +}> { + return callBackendGet('/swap/cex/validate-address', params); +} + +export async function swapCexCreateTransaction( + accountId: string, + password: string, + params: ApiSwapCexCreateTransactionRequest, +): Promise<{ swap: ApiSwapHistoryItem; activity: ApiSwapActivity }> { + const authToken = await getBackendAuthToken(accountId, password); + + const { swap } = await callBackendPost('/swap/cex/createTransaction', params, { + authToken, + }); + const activity = swapItemToActivity(swap); + + onUpdate({ + type: 'newActivities', + accountId, + activities: [activity], + }); + + void callHook('onSwapCreated', accountId, swap.timestamp - 1); + + return { swap, activity }; +} diff --git a/src/api/methods/tokens.ts b/src/api/methods/tokens.ts index 4f1836a3..e939138c 100644 --- a/src/api/methods/tokens.ts +++ b/src/api/methods/tokens.ts @@ -3,12 +3,11 @@ import type { ApiNetwork } from '../types'; import { parseAccountId } from '../../util/account'; import blockchains from '../blockchains'; -export function importToken(accountId: string, address: string) { +export function fetchToken(accountId: string, address: string) { const { network, blockchain: blockchainKey } = parseAccountId(accountId); - const blockchain = blockchains[blockchainKey]; - return blockchain.importToken(network, address); + return blockchain.fetchToken(network, address); } export function resolveTokenBySlug(slug: string) { @@ -28,3 +27,9 @@ export function resolveTokenMinterAddress(network: ApiNetwork, tokenWalletAddres return blockchain.resolveTokenMinterAddress(network, tokenWalletAddress); } + +export function buildTokenSlug(address: string) { + const blockchain = blockchains.ton; + + return blockchain.buildTokenSlug(address); +} diff --git a/src/api/methods/transactions.ts b/src/api/methods/transactions.ts index 16f09424..16e10d87 100644 --- a/src/api/methods/transactions.ts +++ b/src/api/methods/transactions.ts @@ -1,11 +1,20 @@ import type { - ApiSignedTransfer, ApiSubmitTransferOptions, ApiTxIdBySlug, OnApiUpdate, + ApiLocalTransactionParams, + ApiSignedTransfer, + ApiSubmitTransferOptions, + ApiTxIdBySlug, + OnApiUpdate, } from '../types'; import { parseAccountId } from '../../util/account'; +import { logDebugError } from '../../util/logs'; import blockchains from '../blockchains'; import { fetchStoredAddress } from '../common/accounts'; -import { createLocalTransaction, resolveBlockchainKey } from '../common/helpers'; +import { + buildLocalTransaction, resolveBlockchainKey, +} from '../common/helpers'; +import { handleServerError } from '../errors'; +import { swapReplaceTransactions } from './swap'; let onUpdate: OnApiUpdate; @@ -15,18 +24,24 @@ export function initTransactions(_onUpdate: OnApiUpdate) { export async function fetchTokenActivitySlice(accountId: string, slug: string, fromTxId?: string, limit?: number) { const blockchain = blockchains[resolveBlockchainKey(accountId)!]; - - const transactions = await blockchain.getTokenTransactionSlice(accountId, slug, fromTxId, undefined, limit); - - return transactions; + try { + const transactions = await blockchain.getTokenTransactionSlice(accountId, slug, fromTxId, undefined, limit); + return await swapReplaceTransactions(accountId, transactions, slug); + } catch (err) { + logDebugError('fetchTokenActivitySlice', err); + return handleServerError(err); + } } export async function fetchAllActivitySlice(accountId: string, lastTxIds: ApiTxIdBySlug, limit: number) { const blockchain = blockchains[resolveBlockchainKey(accountId)!]; - - const transactions = await blockchain.getMergedTransactionSlice(accountId, lastTxIds, limit); - - return transactions; + try { + const transactions = await blockchain.getMergedTransactionSlice(accountId, lastTxIds, limit); + return await swapReplaceTransactions(accountId, transactions); + } catch (err) { + logDebugError('fetchAllActivitySlice', err); + return handleServerError(err); + } } export function checkTransactionDraft( @@ -39,7 +54,7 @@ export function checkTransactionDraft( ); } -export async function submitTransfer(options: ApiSubmitTransferOptions) { +export async function submitTransfer(options: ApiSubmitTransferOptions, shouldCreateLocalTransaction = true) { const { accountId, password, slug, toAddress, amount, comment, fee, shouldEncrypt, } = options; @@ -55,7 +70,12 @@ export async function submitTransfer(options: ApiSubmitTransferOptions) { } const { encryptedComment } = result; - const localTransaction = createLocalTransaction(onUpdate, accountId, { + + if (!shouldCreateLocalTransaction) { + return result; + } + + const localTransaction = createLocalTransaction(accountId, { amount, fromAddress, toAddress: result.normalizedAddress, @@ -85,7 +105,7 @@ export async function sendSignedTransferMessage(accountId: string, message: ApiS await blockchain.sendSignedMessage(accountId, message); - const localTransaction = createLocalTransaction(onUpdate, accountId, message.params); + const localTransaction = createLocalTransaction(accountId, message.params); return localTransaction.txId; } @@ -96,7 +116,7 @@ export async function sendSignedTransferMessages(accountId: string, messages: Ap const result = await blockchain.sendSignedMessages(accountId, messages); for (let i = 0; i < result.successNumber; i++) { - createLocalTransaction(onUpdate, accountId, messages[i].params); + createLocalTransaction(accountId, messages[i].params); } return result; @@ -107,3 +127,22 @@ export function decryptComment(accountId: string, encryptedComment: string, from return blockchain.decryptComment(accountId, encryptedComment, fromAddress, password); } + +export function createLocalTransaction(accountId: string, params: ApiLocalTransactionParams) { + const blockchainKey = parseAccountId(accountId).blockchain; + const blockchain = blockchains[blockchainKey]; + + const { toAddress } = params; + + const normalizedAddress = blockchain.normalizeAddress(toAddress); + + const localTransaction = buildLocalTransaction(params, normalizedAddress); + + onUpdate({ + type: 'newLocalTransaction', + transaction: localTransaction, + accountId, + }); + + return localTransaction; +} diff --git a/src/api/methods/wallet.ts b/src/api/methods/wallet.ts index fb31bfec..c37f3d23 100644 --- a/src/api/methods/wallet.ts +++ b/src/api/methods/wallet.ts @@ -6,7 +6,6 @@ import { parseAccountId } from '../../util/account'; import blockchains from '../blockchains'; import { fetchStoredAddress, - fetchStoredPublicKey, getMainAccountId, } from '../common/accounts'; import * as dappPromises from '../common/dappPromises'; @@ -61,12 +60,14 @@ export function fetchAddress(accountId: string) { return fetchStoredAddress(accountId); } -export function fetchPublicKey(accountId: string) { - return fetchStoredPublicKey(accountId); +export function isWalletInitialized(network: ApiNetwork, address: string) { + const blockchain = blockchains.ton; + + return blockchain.isAddressInitialized(network, address); } -export function isWalletInitialized(network: ApiNetwork, address: string) { +export function getWalletBalance(network: ApiNetwork, address: string) { const blockchain = blockchains.ton; - return blockchain.isWalletInitialized(network, address); + return blockchain.getWalletBalance(network, address); } diff --git a/src/api/providers/extension/connectorForPopup.ts b/src/api/providers/extension/connectorForPopup.ts index 0078de66..80bfb7b7 100644 --- a/src/api/providers/extension/connectorForPopup.ts +++ b/src/api/providers/extension/connectorForPopup.ts @@ -29,7 +29,6 @@ export async function callApi(methodName: T, ...args: M args, }) as MethodResponse); } catch (err) { - logDebugError('callApi', err); return undefined; } } diff --git a/src/api/providers/worker/connector.ts b/src/api/providers/worker/connector.ts index 11d3d284..c6b2cecd 100644 --- a/src/api/providers/worker/connector.ts +++ b/src/api/providers/worker/connector.ts @@ -30,7 +30,6 @@ export async function callApi(fnName: T, ...args: Al args, }) as AllMethodResponse); } catch (err) { - logDebugError('callApi', err); return undefined; } } diff --git a/src/api/storages/idb.ts b/src/api/storages/idb.ts index 39286a3c..d8f3daae 100644 --- a/src/api/storages/idb.ts +++ b/src/api/storages/idb.ts @@ -1,24 +1,27 @@ import * as idb from 'idb-keyval'; -import type { Storage } from './types'; +import type { Storage, StorageKey } from './types'; +import { INDEXED_DB_NAME, INDEXED_DB_STORE_NAME } from '../../config'; import { fromKeyValueArrays } from '../../util/iteratees'; +const store = idb.createStore(INDEXED_DB_NAME, INDEXED_DB_STORE_NAME); + export default { - getItem: idb.get, - setItem: idb.set, - removeItem: idb.del, - clear: idb.clear, + getItem: (name: StorageKey) => idb.get(name, store), + setItem: (name: StorageKey, value: any) => idb.set(name, value, store), + removeItem: (name: StorageKey) => idb.del(name, store), + clear: () => idb.clear(store), getAll: async () => { - const keys = await idb.keys() as string[]; - const values = await idb.getMany(keys); + const keys = await idb.keys(store) as string[]; + const values = await idb.getMany(keys, store); return fromKeyValueArrays(keys, values); }, getMany: async (keys) => { - const values = await idb.getMany(keys); + const values = await idb.getMany(keys, store); return fromKeyValueArrays(keys, values); }, setMany: (items) => { - return idb.setMany(Object.entries(items)); + return idb.setMany(Object.entries(items), store); }, } as Storage; diff --git a/src/api/storages/types.ts b/src/api/storages/types.ts index cb3c1955..b2082c29 100644 --- a/src/api/storages/types.ts +++ b/src/api/storages/types.ts @@ -20,13 +20,12 @@ export interface Storage { getMany(keys: string[]): Promise; } -export type StorageKey = 'addresses' -| 'mnemonicsEncrypted' -| 'publicKeys' +export type StorageKey = 'mnemonicsEncrypted' | 'accounts' | 'stateVersion' | 'currentAccountId' | 'clientId' +| 'baseCurrency' // For extension | 'dapps' | 'dappMethods:lastAccountId' diff --git a/src/api/tonConnect/index.ts b/src/api/tonConnect/index.ts index e4a0711d..25e6ad91 100644 --- a/src/api/tonConnect/index.ts +++ b/src/api/tonConnect/index.ts @@ -21,19 +21,19 @@ import type { ApiDappRequest, ApiNetwork, ApiSignedTransfer, - ApiTransactionDraftError, OnApiUpdate, } from '../types'; import type { ApiTonConnectProof, LocalConnectEvent, TransactionPayload, TransactionPayloadMessage, } from './types'; -import { ApiTransactionError } from '../types'; +import { ApiCommonError, ApiTransactionError } from '../types'; import { CONNECT_EVENT_ERROR_CODES, SEND_TRANSACTION_ERROR_CODES, SIGN_DATA_ERROR_CODES } from './types'; import { IS_EXTENSION, TON_TOKEN_SLUG } from '../../config'; import { parseAccountId } from '../../util/account'; import { isValidLedgerComment } from '../../util/ledger/utils'; import { logDebugError } from '../../util/logs'; +import { fetchJsonMetadata } from '../../util/metadata'; import blockchains from '../blockchains'; import { parsePayloadBase64 } from '../blockchains/ton'; import { fetchKeyPair } from '../blockchains/ton/auth'; @@ -43,11 +43,12 @@ import { fetchStoredAccount, fetchStoredAddress, fetchStoredPublicKey, getCurrentAccountId, getCurrentAccountIdOrFail, } from '../common/accounts'; import { createDappPromise } from '../common/dappPromises'; -import { createLocalTransaction, isUpdaterAlive } from '../common/helpers'; +import { isUpdaterAlive } from '../common/helpers'; import { - base64ToBytes, bytesToBase64, handleFetchErrors, sha256, + base64ToBytes, bytesToBase64, sha256, } from '../common/utils'; import * as apiErrors from '../errors'; +import { ApiServerError } from '../errors'; import { callHook } from '../hooks'; import { activateDapp, @@ -57,8 +58,10 @@ import { deleteDapp, findLastConnectedAccount, getDappsByOrigin, - isDappConnected, updateDapp, + isDappConnected, + updateDapp, } from '../methods/dapps'; +import { createLocalTransaction } from '../methods/transactions'; import * as errors from './errors'; import { BadRequestError } from './errors'; import { isValidString, isValidUrl } from './utils'; @@ -85,6 +88,11 @@ export async function connect( id: number, ): Promise { try { + onPopupUpdate({ + type: 'dappLoading', + connectionType: 'connect', + }); + const { origin } = await validateRequest(request, true); const dapp = { ...await fetchDappMetadata(message.manifestUrl, origin), @@ -208,6 +216,11 @@ export async function sendTransaction( message: SendTransactionRpcRequest, ): Promise { try { + onPopupUpdate({ + type: 'dappLoading', + connectionType: 'sendTransaction', + }); + const { origin, accountId } = await validateRequest(request); const txPayload = JSON.parse(message.params[0]) as TransactionPayload; @@ -220,7 +233,7 @@ export async function sendTransaction( const { network } = parseAccountId(accountId); const account = await fetchStoredAccount(accountId); - const isLedger = !!account?.ledger; + const isLedger = !!account.ledger; await openExtensionPopup(true); @@ -285,12 +298,12 @@ export async function sendTransaction( const fromAddress = await fetchStoredAddress(accountId); const successTransactions = transactionsForRequest.slice(0, successNumber!); - successTransactions.forEach(({ amount, resolvedAddress, payload }) => { + successTransactions.forEach(({ amount, normalizedAddress, payload }) => { const comment = payload?.type === 'comment' ? payload.comment : undefined; - createLocalTransaction(onPopupUpdate, accountId, { + createLocalTransaction(accountId, { amount, fromAddress, - toAddress: resolvedAddress, + toAddress: normalizedAddress, comment, fee: checkResult.fee!, slug: TON_TOKEN_SLUG, @@ -315,8 +328,10 @@ export async function sendTransaction( code = err.code; errorMessage = err.message; displayError = err.displayError; + } else if (err instanceof ApiServerError) { + displayError = err.displayError; } else { - displayError = ApiTransactionError.Unexpected; + displayError = ApiCommonError.Unexpected; } if (onPopupUpdate && isUpdaterAlive(onPopupUpdate) && displayError) { @@ -365,7 +380,11 @@ async function checkTransactionMessages(accountId: string, messages: Transaction const checkResult = await ton.checkMultiTransactionDraft(accountId, preparedMessages); if ('error' in checkResult) { - handleDraftError(checkResult.error); + onPopupUpdate({ + type: 'showError', + error: checkResult.error, + }); + throw new errors.BadRequestError(checkResult.error); } return { @@ -382,30 +401,30 @@ function prepareTransactionForRequest(network: ApiNetwork, messages: Transaction payload: rawPayload, stateInit, }) => { - const isInitialized = await ton.isWalletInitialized(network, address); - const resolvedAddress = toBase64Address(address); - + const isActiveContract = await ton.isAddressInitialized(network, address); // Force non-bounceable for non-initialized recipients - const toAddress = isInitialized ? resolvedAddress : toBase64Address(address, false); + const toAddress = toBase64Address(address, isActiveContract); + // Fix address format for `waitTxComplete` to work properly + const normalizedAddress = toBase64Address(address); const payload = rawPayload ? await parsePayloadBase64(network, toAddress, rawPayload) : undefined; if (isLedger && payload) { if ( !LEDGER_SUPPORTED_PAYLOADS.includes(payload.type) || (payload.type === 'comment' && !isValidLedgerComment(payload.comment)) - || (payload.type === 'transfer-nft' && !!payload.forwardPayload) + || (payload.type === 'nft:transfer' && !!payload.forwardPayload) ) { throw new BadRequestError('Unsupported payload', ApiTransactionError.UnsupportedHardwarePayload); } } return { - resolvedAddress, toAddress, amount, rawPayload, payload, stateInit, + normalizedAddress, }; }, )); @@ -527,10 +546,9 @@ function buildTonProofReplyItem(proof: ApiTonConnectProof, signature: string): T export async function fetchDappMetadata(manifestUrl: string, origin?: string): Promise { try { - const response = await fetch(manifestUrl); - handleFetchErrors(response); + const data = await fetchJsonMetadata(manifestUrl); - const { url, name, iconUrl } = await response.json(); + const { url, name, iconUrl } = await data; if (!isValidUrl(url) || !isValidString(name) || !isValidUrl(iconUrl)) { throw new Error('Invalid data'); } @@ -569,14 +587,6 @@ async function validateRequest(request: ApiDappRequest, skipConnection = false) return { origin, accountId }; } -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 false; diff --git a/src/api/tonConnect/sse.ts b/src/api/tonConnect/sse.ts index 1f28dae2..5e5164db 100644 --- a/src/api/tonConnect/sse.ts +++ b/src/api/tonConnect/sse.ts @@ -3,7 +3,7 @@ import type { ConnectEvent, ConnectRequest, DeviceInfo, - DisconnectRpcResponse, + DisconnectEvent, RpcRequests, } from '@tonconnect/protocol'; import nacl, { randomBytes } from 'tweetnacl'; @@ -27,6 +27,8 @@ type SseDapp = { origin: string; } & ApiSseOptions; +type ReturnStrategy = 'back' | 'none' | string; + const BRIDGE_URL = 'https://tonconnectbridge.mytonwallet.org/bridge'; const TTL_SEC = 300; const NONCE_SIZE = 24; @@ -34,16 +36,18 @@ const NONCE_SIZE = 24; let sseEventSource: EventSource | undefined; let sseDapps: SseDapp[] = []; -export async function startSseConnection(url: string, deviceInfo: DeviceInfo) { - await waitLogin(); - +export async function startSseConnection(url: string, deviceInfo: DeviceInfo): Promise { const params = new URL(url).searchParams; + const ret = params.get('ret') as ReturnStrategy | null; + + if (!ret || !params.get('r')) { + return ret ?? undefined; + } + const version = Number(params.get('v') as string); const appClientId = params.get('id') as string; const connectRequest = JSON.parse(params.get('r') as string) as ConnectRequest; - const ret = params.get('ret') as 'back' | 'none' | string | null; - const { origin } = await tonConnect.fetchDappMetadata(connectRequest.manifestUrl); logDebug('SSE Start connection:', { @@ -65,6 +69,8 @@ export async function startSseConnection(url: string, deviceInfo: DeviceInfo) { }, }; + await waitLogin(); + const result = await tonConnect.connect(request, connectRequest, lastOutputId) as ConnectEvent; if (result.event === 'connect') { result.payload.device = deviceInfo; @@ -72,11 +78,11 @@ export async function startSseConnection(url: string, deviceInfo: DeviceInfo) { await sendMessage(result, secretKey, clientId, appClientId); - if (result.event === 'connect_error') { - return; + if (result.event !== 'connect_error') { + await resetupSseConnection(); } - void resetupSseConnection(); + return ret; } export async function resetupSseConnection() { @@ -136,9 +142,10 @@ export async function sendSseDisconnect(accountId: string, origin: string) { const { secretKey, clientId, appClientId } = sseDapp; const lastOutputId = sseDapp.lastOutputId + 1; - const response: DisconnectRpcResponse = { - id: lastOutputId.toString(), - result: {}, + const response: DisconnectEvent = { + event: 'disconnect', + id: lastOutputId, + payload: {}, }; await sendMessage(response, secretKey, clientId, appClientId); diff --git a/src/api/types/activity.ts b/src/api/types/activity.ts index b996d606..5b708026 100644 --- a/src/api/types/activity.ts +++ b/src/api/types/activity.ts @@ -1,8 +1,15 @@ +import type { ApiSwapHistoryItem } from './backend'; import type { ApiTransaction } from './misc'; export type ApiTransactionActivity = ApiTransaction & { id: string; kind: 'transaction'; + shouldHide?: boolean; }; -export type ApiActivity = ApiTransactionActivity; +export type ApiSwapActivity = ApiSwapHistoryItem & { + kind: 'swap'; + shouldHide?: boolean; +}; + +export type ApiActivity = ApiTransactionActivity | ApiSwapActivity; diff --git a/src/api/types/backend.ts b/src/api/types/backend.ts new file mode 100644 index 00000000..23422136 --- /dev/null +++ b/src/api/types/backend.ts @@ -0,0 +1,137 @@ +// Decentralized swap of TON and tokens +export type ApiSwapEstimateRequest = { + from: string; + to: string; + slippage: number; + fromAmount?: string; + toAmount?: string; +}; + +export type ApiSwapEstimateResponse = ApiSwapEstimateRequest & { + toAmount: string; + fromAmount: string; + toMinAmount: string; + networkFee: number; + realNetworkFee: number; + swapFee: string; + swapFeePercent: number; + impact: number; + dexLabel: string; +}; + +export type ApiSwapBuildRequest = Omit & { + fromAddress: string; +}; + +export type ApiSwapTransfer = { + toAddress: string; + amount: string; + payload: string; +}; + +export type ApiSwapBuildResponse = { + id: string; + request: ApiSwapBuildRequest; + transfers: ApiSwapTransfer[]; +}; + +// Swap assets and history +export type ApiSwapAsset = { + name: string; + symbol: string; + blockchain: string; + slug: string; + decimals: number; + image?: string; + contract?: string; + keywords?: string[]; +}; + +export type ApiSwapTonAsset = ApiSwapAsset & { + blockchain: 'ton'; +}; + +export type ApiSwapPairAsset = { + symbol: string; + slug: string; + contract?: string; + isReverseProhibited?: boolean; +}; + +export type ApiSwapHistoryItem = { + id: string; + timestamp: number; + lt?: number; + from: string; + fromAmount: string; + to: string; + toAmount: string; + networkFee: number; + swapFee: string; + status: 'pending' | 'completed' | 'failed' | 'expired'; + txId?: string; + cex?: { + payinAddress: string; + payinExtraId?: string; + status: ApiSwapCexTransactionStatus; + transactionId: string; + }; +}; + +// Cross-chain centralized swap +type ApiSwapCexTransactionStatus = 'new' | 'waiting' | 'confirming' | 'exchanging' | 'sending' | 'finished' +| 'failed' | 'refunded' | 'hold' | 'overdue' | 'expired'; + +export type ApiSwapCexEstimateRequest = { + from: string; + fromAmount: string; + to: string; +}; + +export type ApiSwapCexEstimateResponse = { + from: string; + fromAmount: string; + to: string; + toAmount: string; + swapFee: string; + // additional + fromMin: string; + fromMax: string; +}; + +export type ApiSwapCexCreateTransactionRequest = { + from: string; + fromAmount: string; + fromAddress: string; // Always TON address + to: string; + toAddress: string; // TON or other crypto address + payoutExtraId?: string; + swapFee: string; // from estimate request + networkFee?: number; // only for sent TON +}; + +export type ApiSwapCexCreateTransactionResponse = { + request: ApiSwapCexCreateTransactionRequest; + swap: ApiSwapHistoryItem; +}; + +// Staking +export type ApiStakingCommonData = { + liquid: { + currentRate: number; + nextRoundRate: number; + collection?: string; + apy: number; + available: string; + }; + round: { + start: number; + end: number; + unlock: number; + }; + prevRound: { + start: number; + end: number; + unlock: number; + }; +}; diff --git a/src/api/types/errors.ts b/src/api/types/errors.ts index 2affec70..ebe550dc 100644 --- a/src/api/types/errors.ts +++ b/src/api/types/errors.ts @@ -1,8 +1,12 @@ +export enum ApiCommonError { + Unexpected = 'Unexpected', + ServerError = 'ServerError', +} + export enum ApiTransactionDraftError { InvalidAmount = 'InvalidAmount', InvalidToAddress = 'InvalidToAddress', InsufficientBalance = 'InsufficientBalance', - Unexpected = 'Unexpected', DomainNotResolved = 'DomainNotResolved', WalletNotInitialized = 'WalletNotInitialized', UnsupportedHardwarePayload = 'UnsupportedHardwarePayload', @@ -15,7 +19,6 @@ export enum ApiTransactionError { InsufficientBalance = 'InsufficientBalance', UnsuccesfulTransfer = 'UnsuccesfulTransfer', UnsupportedHardwarePayload = 'UnsupportedHardwarePayload', - Unexpected = 'Unexpected', } -export type ApiAnyDisplayError = ApiTransactionDraftError | ApiTransactionError; +export type ApiAnyDisplayError = ApiCommonError | ApiTransactionDraftError | ApiTransactionError; diff --git a/src/api/types/index.ts b/src/api/types/index.ts index e7bf33dc..e2d5a267 100644 --- a/src/api/types/index.ts +++ b/src/api/types/index.ts @@ -2,5 +2,6 @@ export * from './updates'; export * from './misc'; export * from './payload'; export * from './errors'; +export * from './backend'; export * from './storage'; export * from './activity'; diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index ec135739..f362aece 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -27,6 +27,7 @@ export interface ApiBaseToken { image?: string; isPopular?: boolean; keywords?: string[]; + cmcSlug?: string; } export interface ApiToken extends ApiBaseToken { @@ -52,7 +53,7 @@ export interface ApiAddressInfo { } export type ApiTxIdBySlug = Record; -export type ApiTransactionType = 'stake' | 'unstake' | 'unstakeRequest' | undefined; +export type ApiTransactionType = 'stake' | 'unstake' | 'unstakeRequest' | 'swap' | undefined; export interface ApiTransaction { txId: string; @@ -81,35 +82,46 @@ export interface ApiNft { collectionName?: string; collectionAddress?: string; isOnSale: boolean; + isHidden?: boolean; } export type ApiHistoryList = Array<[number, number]>; export type ApiTokenSimple = Omit; -export interface ApiPoolState { - startOfCycle: number; - endOfCycle: number; - lastApy: number; - minStake: number; -} +export type ApiStakingType = 'nominators' | 'liquid'; -export interface ApiStakingState { +export type ApiStakingState = { + type: 'nominators'; amount: number; pendingDepositAmount: number; isUnstakeRequested: boolean; +} | { + type: 'liquid'; + tokenAmount: string; + amount: number; + unstakeRequestAmount: number; +} | { + type: 'empty'; +}; + +export interface ApiNominatorsPool { + address: string; + apy: number; + start: number; + end: number; } export interface ApiBackendStakingState { - poolAddress: string; balance: number; totalProfit: number; - poolState: ApiPoolState; - profitHistory: { - timestamp: number; - profit: number; - }[]; + nominatorsPool: ApiNominatorsPool; } +export type ApiStakingHistory = { + timestamp: number; + profit: number; +}[]; + export interface ApiDappPermissions { isAddressRequired?: boolean; isPasswordRequired?: boolean; @@ -158,3 +170,11 @@ export interface ApiSignedTransfer { } export type ApiLocalTransactionParams = Omit; + +export type ApiBaseCurrency = 'USD' | 'EUR' | 'RUB' | 'CNY' | 'BTC' | 'TON'; + +export enum ApiLiquidUnstakeMode { + Default, + Instant, + BestRate, +} diff --git a/src/api/types/payload.ts b/src/api/types/payload.ts index 8d87f188..cf2c0780 100644 --- a/src/api/types/payload.ts +++ b/src/api/types/payload.ts @@ -8,8 +8,8 @@ export type ApiEncryptedCommentPayload = { encryptedComment: string; }; -export type ApiTransferNftPayload = { - type: 'transfer-nft'; +export type ApiNftTransferPayload = { + type: 'nft:transfer'; queryId: string; newOwner: string; responseDestination: string; @@ -21,8 +21,8 @@ export type ApiTransferNftPayload = { nftName?: string; }; -export type ApiTransferTokensPayload = { - type: 'transfer-tokens'; +export type ApiTokensTransferPayload = { + type: 'tokens:transfer'; queryId: string; amount: string; destination: string; @@ -34,8 +34,8 @@ export type ApiTransferTokensPayload = { slug: string; }; -export type ApiTransferTokensNonStandardPayload = { - type: 'transfer-tokens:non-standard'; +export type ApiTokensTransferNonStandardPayload = { + type: 'tokens:transfer-non-standard'; queryId: string; amount: string; destination: string; @@ -48,9 +48,39 @@ export type ApiUnknownPayload = { base64: string; }; +export type ApiTokensBurnPayload = { + type: 'tokens:burn'; + queryId: string; + amount: string; + address: string; + customPayload?: string; + // Specific to UI + slug: string; + isLiquidUnstakeRequest: boolean; +}; + +export type ApiLiquidStakingDepositPayload = { + type: 'liquid-staking:deposit'; + queryId: string; +}; + +export type ApiLiquidStakingWithdrawalNftPayload = { + type: 'liquid-staking:withdrawal-nft'; + queryId: string; +}; + +export type ApiLiquidStakingWithdrawalPayload = { + type: 'liquid-staking:withdrawal'; + queryId: string; +}; + export type ApiParsedPayload = ApiCommentPayload | ApiEncryptedCommentPayload -| ApiTransferNftPayload -| ApiTransferTokensPayload -| ApiTransferTokensNonStandardPayload -| ApiUnknownPayload; +| ApiNftTransferPayload +| ApiTokensTransferPayload +| ApiTokensTransferNonStandardPayload +| ApiUnknownPayload +| ApiTokensBurnPayload +| ApiLiquidStakingDepositPayload +| ApiLiquidStakingWithdrawalPayload +| ApiLiquidStakingWithdrawalNftPayload; diff --git a/src/api/types/storage.ts b/src/api/types/storage.ts index bfa1e379..33202bba 100644 --- a/src/api/types/storage.ts +++ b/src/api/types/storage.ts @@ -1,6 +1,8 @@ import type { ApiLedgerDriver, ApiWalletVersion } from './misc'; -export interface ApiAccountInfo { +export interface ApiAccount { + address: string; + publicKey: string; version?: ApiWalletVersion; ledger?: { index: number; @@ -8,6 +10,9 @@ export interface ApiAccountInfo { deviceId?: string; deviceName?: string; }; + lastFinishedSwapTimestamp?: number; + authToken?: string; + isInitialized?: boolean; } export interface ApiDappMetadata { diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 80fa8d93..7a5b064a 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -1,15 +1,17 @@ import type { ApiTonConnectProof } from '../tonConnect/types'; import type { ApiActivity, ApiTransactionActivity } from './activity'; +import type { ApiStakingCommonData, ApiSwapAsset } from './backend'; import type { ApiAnyDisplayError } from './errors'; import type { ApiBackendStakingState, + ApiBaseCurrency, ApiDappTransaction, ApiNft, ApiStakingState, ApiToken, } from './misc'; import type { ApiParsedPayload } from './payload'; -import type { ApiDapp } from './storage'; +import type { ApiAccount, ApiDapp } from './storage'; export type ApiUpdateBalance = { type: 'updateBalance'; @@ -18,6 +20,12 @@ export type ApiUpdateBalance = { balance: string; }; +export type ApiUpdateBalances = { + type: 'updateBalances'; + accountId: string; + balancesToUpdate: Record; +}; + export type ApiUpdateNewActivities = { type: 'newActivities'; accountId: string; @@ -33,6 +41,12 @@ export type ApiUpdateNewLocalTransaction = { export type ApiUpdateTokens = { type: 'updateTokens'; tokens: Record; + baseCurrency: ApiBaseCurrency; +}; + +export type ApiUpdateSwapTokens = { + type: 'updateSwapTokens'; + tokens: Record; }; export type ApiUpdateCreateTransaction = { @@ -53,28 +67,16 @@ export type ApiUpdateCreateSignature = { dataHex: string; }; -export type ApiUpdateTxComplete = { - type: 'updateTxComplete'; - accountId: string; - toAddress: string; - amount: string; - txId: string; - localTxId: string; -}; - export type ApiUpdateShowError = { type: 'showError'; error?: ApiAnyDisplayError; }; -export type ApiUpdateStakingState = { - type: 'updateStakingState'; +export type ApiUpdateStaking = { + type: 'updateStaking'; accountId: string; + stakingCommonData: ApiStakingCommonData; stakingState: ApiStakingState; -}; - -export type ApiUpdateBackendStakingState = { - type: 'updateBackendStakingState'; backendStakingState: ApiBackendStakingState; }; @@ -111,6 +113,11 @@ export type ApiUpdateDappDisconnect = { origin: string; }; +export type ApiUpdateDappLoading = { + type: 'dappLoading'; + connectionType: 'connect' | 'sendTransaction'; +}; + export type ApiUpdatePrepareTransaction = { type: 'prepareTransaction'; toAddress: string; @@ -145,23 +152,31 @@ export type ApiUpdateNftPutUpForSale = { export type ApiNftUpdate = ApiUpdateNftReceived | ApiUpdateNftSent | ApiUpdateNftPutUpForSale; +export type ApiUpdateAccount = { + type: 'updateAccount'; + accountId: string; + partial: Partial; +}; + export type ApiUpdate = ApiUpdateBalance + | ApiUpdateBalances | ApiUpdateNewActivities | ApiUpdateNewLocalTransaction | ApiUpdateTokens + | ApiUpdateSwapTokens | ApiUpdateCreateTransaction | ApiUpdateCreateSignature - | ApiUpdateTxComplete - | ApiUpdateStakingState + | ApiUpdateStaking | ApiUpdateActiveDapp | ApiUpdateDappSendTransactions | ApiUpdateDappConnect | ApiUpdateDappDisconnect - | ApiUpdateBackendStakingState + | ApiUpdateDappLoading | ApiUpdatePrepareTransaction | ApiUpdateShowError | ApiUpdateNfts - | ApiNftUpdate; + | ApiNftUpdate + | ApiUpdateAccount; export type OnApiUpdate = (update: ApiUpdate) => void; diff --git a/src/assets/blockchain/chain_avalanche.png b/src/assets/blockchain/chain_avalanche.png new file mode 100644 index 00000000..2faa5a9f Binary files /dev/null and b/src/assets/blockchain/chain_avalanche.png differ diff --git a/src/assets/blockchain/chain_bitcoin.png b/src/assets/blockchain/chain_bitcoin.png new file mode 100644 index 00000000..c6dd1be8 Binary files /dev/null and b/src/assets/blockchain/chain_bitcoin.png differ diff --git a/src/assets/blockchain/chain_bitcoincash.png b/src/assets/blockchain/chain_bitcoincash.png new file mode 100644 index 00000000..e49b38af Binary files /dev/null and b/src/assets/blockchain/chain_bitcoincash.png differ diff --git a/src/assets/blockchain/chain_bnb.png b/src/assets/blockchain/chain_bnb.png new file mode 100644 index 00000000..ebacc0ac Binary files /dev/null and b/src/assets/blockchain/chain_bnb.png differ diff --git a/src/assets/blockchain/chain_cardano.png b/src/assets/blockchain/chain_cardano.png new file mode 100644 index 00000000..495730b8 Binary files /dev/null and b/src/assets/blockchain/chain_cardano.png differ diff --git a/src/assets/blockchain/chain_cosmos.png b/src/assets/blockchain/chain_cosmos.png new file mode 100644 index 00000000..11a863bf Binary files /dev/null and b/src/assets/blockchain/chain_cosmos.png differ diff --git a/src/assets/blockchain/chain_dash.png b/src/assets/blockchain/chain_dash.png new file mode 100644 index 00000000..31cc2e50 Binary files /dev/null and b/src/assets/blockchain/chain_dash.png differ diff --git a/src/assets/blockchain/chain_doge.png b/src/assets/blockchain/chain_doge.png new file mode 100644 index 00000000..7bc0a7f3 Binary files /dev/null and b/src/assets/blockchain/chain_doge.png differ diff --git a/src/assets/blockchain/chain_eos.png b/src/assets/blockchain/chain_eos.png new file mode 100644 index 00000000..fd568993 Binary files /dev/null and b/src/assets/blockchain/chain_eos.png differ diff --git a/src/assets/blockchain/chain_ethereum.png b/src/assets/blockchain/chain_ethereum.png new file mode 100644 index 00000000..7b0550b5 Binary files /dev/null and b/src/assets/blockchain/chain_ethereum.png differ diff --git a/src/assets/blockchain/chain_ethereumclassic.png b/src/assets/blockchain/chain_ethereumclassic.png new file mode 100644 index 00000000..9d9e077d Binary files /dev/null and b/src/assets/blockchain/chain_ethereumclassic.png differ diff --git a/src/assets/blockchain/chain_internetcomputer.png b/src/assets/blockchain/chain_internetcomputer.png new file mode 100644 index 00000000..8cc186a8 Binary files /dev/null and b/src/assets/blockchain/chain_internetcomputer.png differ diff --git a/src/assets/blockchain/chain_iota.png b/src/assets/blockchain/chain_iota.png new file mode 100644 index 00000000..ebc8f14f Binary files /dev/null and b/src/assets/blockchain/chain_iota.png differ diff --git a/src/assets/blockchain/chain_litecoin.png b/src/assets/blockchain/chain_litecoin.png new file mode 100644 index 00000000..91f73a06 Binary files /dev/null and b/src/assets/blockchain/chain_litecoin.png differ diff --git a/src/assets/blockchain/chain_monero.png b/src/assets/blockchain/chain_monero.png new file mode 100644 index 00000000..c25b1dfb Binary files /dev/null and b/src/assets/blockchain/chain_monero.png differ diff --git a/src/assets/blockchain/chain_polkadot.png b/src/assets/blockchain/chain_polkadot.png new file mode 100644 index 00000000..82457749 Binary files /dev/null and b/src/assets/blockchain/chain_polkadot.png differ diff --git a/src/assets/blockchain/chain_ripple.png b/src/assets/blockchain/chain_ripple.png new file mode 100644 index 00000000..f46adba2 Binary files /dev/null and b/src/assets/blockchain/chain_ripple.png differ diff --git a/src/assets/blockchain/chain_solana.png b/src/assets/blockchain/chain_solana.png new file mode 100644 index 00000000..d6c21f89 Binary files /dev/null and b/src/assets/blockchain/chain_solana.png differ diff --git a/src/assets/blockchain/chain_stellar.png b/src/assets/blockchain/chain_stellar.png new file mode 100644 index 00000000..c08886bf Binary files /dev/null and b/src/assets/blockchain/chain_stellar.png differ diff --git a/src/assets/blockchain/chain_ton.png b/src/assets/blockchain/chain_ton.png new file mode 100644 index 00000000..a78be991 Binary files /dev/null and b/src/assets/blockchain/chain_ton.png differ diff --git a/src/assets/blockchain/chain_tron.png b/src/assets/blockchain/chain_tron.png new file mode 100644 index 00000000..9f43659d Binary files /dev/null and b/src/assets/blockchain/chain_tron.png differ diff --git a/src/assets/blockchain/chain_zcash.png b/src/assets/blockchain/chain_zcash.png new file mode 100644 index 00000000..a6bf58a9 Binary files /dev/null and b/src/assets/blockchain/chain_zcash.png differ diff --git a/src/assets/coins/btc.png b/src/assets/coins/btc.png deleted file mode 100644 index efb40ec1..00000000 Binary files a/src/assets/coins/btc.png and /dev/null differ diff --git a/src/assets/coins/coin_btc.png b/src/assets/coins/coin_btc.png new file mode 100644 index 00000000..8f6a33f9 Binary files /dev/null and b/src/assets/coins/coin_btc.png differ diff --git a/src/assets/coins/coin_ton.png b/src/assets/coins/coin_ton.png new file mode 100644 index 00000000..ecec399b Binary files /dev/null and b/src/assets/coins/coin_ton.png differ diff --git a/src/assets/coins/ton.png b/src/assets/coins/ton.png deleted file mode 100644 index ab89ee4c..00000000 Binary files a/src/assets/coins/ton.png and /dev/null differ diff --git a/src/assets/font-icons/accept.svg b/src/assets/font-icons/accept.svg index 1d551a69..bbd5175a 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/arrow-down.svg b/src/assets/font-icons/arrow-down.svg index 0862a707..cf5ba322 100644 --- a/src/assets/font-icons/arrow-down.svg +++ b/src/assets/font-icons/arrow-down.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/arrow-right-swap.svg b/src/assets/font-icons/arrow-right-swap.svg new file mode 100644 index 00000000..1e7904f6 --- /dev/null +++ b/src/assets/font-icons/arrow-right-swap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/arrow-up-swap.svg b/src/assets/font-icons/arrow-up-swap.svg new file mode 100644 index 00000000..de7da1af --- /dev/null +++ b/src/assets/font-icons/arrow-up-swap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/backspace.svg b/src/assets/font-icons/backspace.svg new file mode 100644 index 00000000..9d7c3313 --- /dev/null +++ b/src/assets/font-icons/backspace.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/font-icons/caret-down.svg b/src/assets/font-icons/caret-down.svg index 0d66f0c2..d0aa3b96 100644 --- a/src/assets/font-icons/caret-down.svg +++ b/src/assets/font-icons/caret-down.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/changelly.svg b/src/assets/font-icons/changelly.svg new file mode 100644 index 00000000..cfcdb2a9 --- /dev/null +++ b/src/assets/font-icons/changelly.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/chevron-down.svg b/src/assets/font-icons/chevron-down.svg index b5359e68..671daf51 100644 --- a/src/assets/font-icons/chevron-down.svg +++ b/src/assets/font-icons/chevron-down.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/chevron-left.svg b/src/assets/font-icons/chevron-left.svg index 419a52dd..e44da171 100644 --- a/src/assets/font-icons/chevron-left.svg +++ b/src/assets/font-icons/chevron-left.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/clock.svg b/src/assets/font-icons/clock.svg index a56d1796..a0dc9ad4 100644 --- a/src/assets/font-icons/clock.svg +++ b/src/assets/font-icons/clock.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/cog.svg b/src/assets/font-icons/cog.svg index e5fffea9..5268b3e3 100644 --- a/src/assets/font-icons/cog.svg +++ b/src/assets/font-icons/cog.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/coinmarket.svg b/src/assets/font-icons/coinmarket.svg index 2046bc2d..bcd18a1f 100644 --- a/src/assets/font-icons/coinmarket.svg +++ b/src/assets/font-icons/coinmarket.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/copy.svg b/src/assets/font-icons/copy.svg index 7802118f..7a847806 100644 --- a/src/assets/font-icons/copy.svg +++ b/src/assets/font-icons/copy.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 974982d4..44ab8611 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/earn.svg b/src/assets/font-icons/earn.svg index ebc62083..6501700c 100644 --- a/src/assets/font-icons/earn.svg +++ b/src/assets/font-icons/earn.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/eye-closed.svg b/src/assets/font-icons/eye-closed.svg index 4ee30b79..a1899bfb 100644 --- a/src/assets/font-icons/eye-closed.svg +++ b/src/assets/font-icons/eye-closed.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/eye.svg b/src/assets/font-icons/eye.svg index 712f0e10..d48f9ca2 100644 --- a/src/assets/font-icons/eye.svg +++ b/src/assets/font-icons/eye.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/face-id.svg b/src/assets/font-icons/face-id.svg new file mode 100644 index 00000000..934f8c7c --- /dev/null +++ b/src/assets/font-icons/face-id.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/flashlight.svg b/src/assets/font-icons/flashlight.svg new file mode 100644 index 00000000..302a4446 --- /dev/null +++ b/src/assets/font-icons/flashlight.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/github.svg b/src/assets/font-icons/github.svg index 0914b11b..d3c13e21 100644 --- a/src/assets/font-icons/github.svg +++ b/src/assets/font-icons/github.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/laptop.svg b/src/assets/font-icons/laptop.svg index 574375f1..d816a10b 100644 --- a/src/assets/font-icons/laptop.svg +++ b/src/assets/font-icons/laptop.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/ledger.svg b/src/assets/font-icons/ledger.svg index f24dbe12..49cf6e33 100644 --- a/src/assets/font-icons/ledger.svg +++ b/src/assets/font-icons/ledger.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 index 3bf98138..e96b9c8e 100644 --- a/src/assets/font-icons/params.svg +++ b/src/assets/font-icons/params.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/paste.svg b/src/assets/font-icons/paste.svg index f130eb9e..8e51f0c5 100644 --- a/src/assets/font-icons/paste.svg +++ b/src/assets/font-icons/paste.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/pen.svg b/src/assets/font-icons/pen.svg index c3ffe107..a38165c9 100644 --- a/src/assets/font-icons/pen.svg +++ b/src/assets/font-icons/pen.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/percent.svg b/src/assets/font-icons/percent.svg index 906bd23b..0ea6f023 100644 --- a/src/assets/font-icons/percent.svg +++ b/src/assets/font-icons/percent.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/plus.svg b/src/assets/font-icons/plus.svg index ac7c6ec6..717ee6bc 100644 --- a/src/assets/font-icons/plus.svg +++ b/src/assets/font-icons/plus.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/qr-scanner-alt.svg b/src/assets/font-icons/qr-scanner-alt.svg new file mode 100644 index 00000000..475037ab --- /dev/null +++ b/src/assets/font-icons/qr-scanner-alt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/qr-scanner.svg b/src/assets/font-icons/qr-scanner.svg new file mode 100644 index 00000000..a4883ee9 --- /dev/null +++ b/src/assets/font-icons/qr-scanner.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/qrcode.svg b/src/assets/font-icons/qrcode.svg deleted file mode 100644 index de4e34b4..00000000 --- a/src/assets/font-icons/qrcode.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/font-icons/question.svg b/src/assets/font-icons/question.svg index 61e8f3a3..0868e523 100644 --- a/src/assets/font-icons/question.svg +++ b/src/assets/font-icons/question.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/receive.svg b/src/assets/font-icons/receive.svg index 018eb62a..6184e71d 100644 --- a/src/assets/font-icons/receive.svg +++ b/src/assets/font-icons/receive.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/search.svg b/src/assets/font-icons/search.svg index 66e33b2c..807325f2 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/share.svg b/src/assets/font-icons/share.svg index f85a8c0b..ef979655 100644 --- a/src/assets/font-icons/share.svg +++ b/src/assets/font-icons/share.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/star-filled.svg b/src/assets/font-icons/star-filled.svg index c6da8cd6..90091e00 100644 --- a/src/assets/font-icons/star-filled.svg +++ b/src/assets/font-icons/star-filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/star.svg b/src/assets/font-icons/star.svg index a209b618..d1fd7f8f 100644 --- a/src/assets/font-icons/star.svg +++ b/src/assets/font-icons/star.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/telegram.svg b/src/assets/font-icons/telegram.svg index 6f1be8db..7b9667a6 100644 --- a/src/assets/font-icons/telegram.svg +++ b/src/assets/font-icons/telegram.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/tonscan.svg b/src/assets/font-icons/tonscan.svg index 2e6b8f9a..fb2dd471 100644 --- a/src/assets/font-icons/tonscan.svg +++ b/src/assets/font-icons/tonscan.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/touch-id.svg b/src/assets/font-icons/touch-id.svg new file mode 100644 index 00000000..16e1de60 --- /dev/null +++ b/src/assets/font-icons/touch-id.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/trash.svg b/src/assets/font-icons/trash.svg index 9fcab435..281ef4cf 100644 --- a/src/assets/font-icons/trash.svg +++ b/src/assets/font-icons/trash.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/windows-maximize.svg b/src/assets/font-icons/windows-maximize.svg index 39614d39..27b0d7dc 100644 --- a/src/assets/font-icons/windows-maximize.svg +++ b/src/assets/font-icons/windows-maximize.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/font-icons/windows-minimize.svg b/src/assets/font-icons/windows-minimize.svg index 29f6debc..3d8b6014 100644 --- a/src/assets/font-icons/windows-minimize.svg +++ b/src/assets/font-icons/windows-minimize.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/lottie/duck_guard.tgs b/src/assets/lottie/duck_guard.tgs new file mode 100644 index 00000000..1e6c8448 Binary files /dev/null and b/src/assets/lottie/duck_guard.tgs differ diff --git a/src/assets/lottie/duck_yeee.tgs b/src/assets/lottie/duck_yeee.tgs new file mode 100644 index 00000000..06d69179 Binary files /dev/null and b/src/assets/lottie/duck_yeee.tgs differ diff --git a/src/assets/lottiePreview/duck_guard.png b/src/assets/lottiePreview/duck_guard.png new file mode 100644 index 00000000..d1d78c2c Binary files /dev/null and b/src/assets/lottiePreview/duck_guard.png differ diff --git a/src/assets/lottiePreview/duck_yeee.png b/src/assets/lottiePreview/duck_yeee.png new file mode 100644 index 00000000..1a9ad2d7 Binary files /dev/null and b/src/assets/lottiePreview/duck_yeee.png differ diff --git a/src/assets/sale.png b/src/assets/sale.png index e5d993a5..b42e92e8 100644 Binary files a/src/assets/sale.png and b/src/assets/sale.png differ diff --git a/src/assets/settings/settings_biometrics.svg b/src/assets/settings/settings_biometrics.svg new file mode 100644 index 00000000..f0cc76c1 --- /dev/null +++ b/src/assets/settings/settings_biometrics.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/settings/settings_face-id.svg b/src/assets/settings/settings_face-id.svg new file mode 100644 index 00000000..fd7721d2 --- /dev/null +++ b/src/assets/settings/settings_face-id.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/App.module.scss b/src/components/App.module.scss index cdf1e4f6..20b9caa8 100644 --- a/src/components/App.module.scss +++ b/src/components/App.module.scss @@ -26,6 +26,17 @@ } } +.appSlide.appSlideTransparent { + background: transparent !important; + + transition: background-color 150ms; + transition-delay: 350ms; + + :global(html.is-ios) & { + transition-delay: 650ms; + } +} + .appSlideContent { /* These styles need to be applied via regular CSS and not as conditional class, since Transition does not work well when `slideClassName` updates */ :global(html.is-extension) & { @@ -35,6 +46,10 @@ @include respond-below(xs) { overflow-y: scroll; } + + @supports (padding-top: env(safe-area-inset-top)) { + padding-top: env(safe-area-inset-top); + } } :global(html.is-electron) .transitionContainer { diff --git a/src/components/App.tsx b/src/components/App.tsx index 3b14feca..7a323fbc 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,16 +1,22 @@ -import React, { memo, useEffect } from '../lib/teact/teact'; +import { BarcodeScanner } from '@capacitor-mlkit/barcode-scanning'; +import React, { memo, useEffect, useState } from '../lib/teact/teact'; import { getActions, withGlobal } from '../global'; import { AppState } from '../global/types'; -import { INACTIVE_MARKER, IS_ELECTRON } from '../config'; +import { INACTIVE_MARKER, IS_CAPACITOR } from '../config'; import { setActiveTabChangeListener } from '../util/activeTabMonitor'; import buildClassName from '../util/buildClassName'; -import { IS_ANDROID, IS_LINUX } from '../util/windowEnvironment'; +import { + CAN_DELEGATE_BOTTOM_SHEET, IS_ANDROID, IS_DELEGATED_BOTTOM_SHEET, IS_ELECTRON, IS_IOS, IS_LINUX, +} from '../util/windowEnvironment'; import { updateSizes } from '../util/windowSize'; import { useDeviceScreen } from '../hooks/useDeviceScreen'; +import useEffectOnce from '../hooks/useEffectOnce'; import useFlag from '../hooks/useFlag'; +import useLang from '../hooks/useLang'; +import useLastCallback from '../hooks/useLastCallback'; import useSyncEffect from '../hooks/useSyncEffect'; import useTimeout from '../hooks/useTimeout'; @@ -22,13 +28,18 @@ import Dialogs from './Dialogs'; import ElectronHeader from './electron/ElectronHeader'; import LedgerModal from './ledger/LedgerModal'; import Main from './main/Main'; +import AddAccountModal from './main/modals/AddAccountModal'; import BackupModal from './main/modals/BackupModal'; +import QrScannerModal from './main/modals/QrScannerModal'; import SignatureModal from './main/modals/SignatureModal'; +import SwapActivityModal from './main/modals/SwapActivityModal'; import TransactionModal from './main/modals/TransactionModal'; import Notifications from './main/Notifications'; import Settings from './settings/Settings'; import SettingsModal from './settings/SettingsModal'; +import SwapModal from './swap/SwapModal'; import TransferModal from './transfer/TransferModal'; +import ConfettiContainer from './ui/ConfettiContainer'; import Transition from './ui/Transition'; // import Test from './components/test/TestNoRedundancy'; @@ -38,6 +49,7 @@ interface StateProps { appState: AppState; accountId?: string; isBackupWalletModalOpen?: boolean; + isQrScannerOpen?: boolean; isHardwareModalOpen?: boolean; areSettingsOpen?: boolean; } @@ -50,6 +62,7 @@ function App({ accountId, isBackupWalletModalOpen, isHardwareModalOpen, + isQrScannerOpen, areSettingsOpen, }: StateProps) { // return ; @@ -58,22 +71,41 @@ function App({ closeHardwareWalletModal, closeSettings, cancelCaching, + openQrScanner, + closeQrScanner, + showNotification, + openDeeplink, } = getActions(); + + const lang = useLang(); const { isPortrait } = useDeviceScreen(); - const areSettingsInModal = !isPortrait || IS_ELECTRON; + const areSettingsInModal = !isPortrait || IS_ELECTRON || CAN_DELEGATE_BOTTOM_SHEET || IS_DELEGATED_BOTTOM_SHEET; + const [isBarcodeSupported, setIsBarcodeSupported] = useState(false); const [isInactive, markInactive] = useFlag(false); const [canPrerenderMain, prerenderMain] = useFlag(); const renderingKey = isInactive ? AppState.Inactive - : ((areSettingsOpen && !areSettingsInModal) ? AppState.Settings : appState); + : ((areSettingsOpen && !areSettingsInModal) + ? AppState.Settings : appState + ); useTimeout( prerenderMain, renderingKey === AppState.Auth && !canPrerenderMain ? PRERENDER_MAIN_DELAY : undefined, ); + useEffectOnce(() => { + if (!IS_CAPACITOR) return; + + BarcodeScanner + .isSupported() + .then((result) => { + setIsBarcodeSupported(result.supported); + }); + }); + useEffect(() => { updateSizes(); setActiveTabChangeListener(() => { @@ -91,6 +123,23 @@ function App({ } }, [accountId]); + const handleOpenQrScanner = useLastCallback(async () => { + const granted = await requestCameraPermissions(); + + if (!granted) { + showNotification({ + message: lang('Permission denied. Please grant camera permission to use the QR code scanner.'), + }); + return; + } + + openQrScanner(); + }); + + const handleQrScan = useLastCallback((scanResult) => { + openDeeplink({ url: scanResult }); + }); + // eslint-disable-next-line consistent-return function renderContent(isActive: boolean, isFrom: boolean, currentKey: number) { switch (currentKey) { @@ -111,7 +160,11 @@ function App({ nextKey={renderingKey === AppState.Auth && canPrerenderMain ? mainKey + 1 : undefined} slideClassName={slideFullClassName} > -
+
); } @@ -129,9 +182,9 @@ function App({ {IS_ELECTRON && !IS_LINUX && } @@ -153,13 +206,24 @@ function App({ isOpen={isBackupWalletModalOpen} onClose={closeBackupWalletModal} /> - + + + - + + {!IS_DELEGATED_BOTTOM_SHEET && } + {IS_CAPACITOR && ( + + )} + {!IS_DELEGATED_BOTTOM_SHEET && } )} @@ -173,6 +237,7 @@ export default memo(withGlobal((global): StateProps => { isBackupWalletModalOpen: global.isBackupWalletModalOpen, isHardwareModalOpen: global.isHardwareModalOpen, areSettingsOpen: global.areSettingsOpen, + isQrScannerOpen: global.isQrScannerOpen, }; })(App)); @@ -180,3 +245,9 @@ async function handleCloseBrowserTab() { const tab = await chrome.tabs.getCurrent(); await chrome.tabs.remove(tab.id!); } + +async function requestCameraPermissions(): Promise { + const { camera } = await BarcodeScanner.requestPermissions(); + + return camera === 'granted' || camera === 'limited'; +} diff --git a/src/components/Dialogs.tsx b/src/components/Dialogs.tsx index c5c830b1..27ce496f 100644 --- a/src/components/Dialogs.tsx +++ b/src/components/Dialogs.tsx @@ -1,9 +1,11 @@ +import { Dialog } from '@capacitor/dialog'; import type { FC } from '../lib/teact/teact'; import React, { memo, useEffect } from '../lib/teact/teact'; import { getActions, withGlobal } from '../global'; import renderText from '../global/helpers/renderText'; import { pick } from '../util/iteratees'; +import { CAN_DELEGATE_BOTTOM_SHEET, IS_DELEGATED_BOTTOM_SHEET } from '../util/windowEnvironment'; import useFlag from '../hooks/useFlag'; import useLang from '../hooks/useLang'; @@ -24,27 +26,37 @@ const Dialogs: FC = ({ dialogs }) => { const lang = useLang(); const [isModalOpen, openModal, closeModal] = useFlag(); + const message = dialogs[dialogs.length - 1]; + const title = lang('Something went wrong'); + useEffect(() => { - if (dialogs.length > 0) { + if (CAN_DELEGATE_BOTTOM_SHEET || IS_DELEGATED_BOTTOM_SHEET) { + if (message) { + Dialog.alert({ + title, + message: lang(message), + }).then(() => { + dismissDialog(); + }); + } + } else if (message) { openModal(); } else { closeModal(); } - }, [dialogs, openModal]); + }, [dialogs, lang, message, openModal, title]); - if (!dialogs.length) { + if (!message || CAN_DELEGATE_BOTTOM_SHEET || IS_DELEGATED_BOTTOM_SHEET) { return undefined; } - const message = dialogs[dialogs.length - 1]; - return (
{renderText(lang(message))} diff --git a/src/components/auth/Auth.module.scss b/src/components/auth/Auth.module.scss index d006fddd..8ee66e58 100644 --- a/src/components/auth/Auth.module.scss +++ b/src/components/auth/Auth.module.scss @@ -17,6 +17,10 @@ .transitionSlide { background: var(--color-background-second); + + @supports (padding-top: env(safe-area-inset-top)) { + padding-top: env(safe-area-inset-top); + } } .container { @@ -49,7 +53,7 @@ } @supports (padding-bottom: env(safe-area-inset-bottom)) { - padding-bottom: calc(1rem + env(safe-area-inset-bottom)); + padding-bottom: max(1rem, env(safe-area-inset-bottom)); } @include respond-above(sm) { @@ -57,6 +61,12 @@ } } +.containerFullSize { + overflow: hidden; + + padding: 0 !important; +} + .logo { display: block !important; @@ -296,6 +306,10 @@ column-gap: 1rem; margin: 1.5rem 1rem 1rem; + + @supports (margin-bottom: max(env(safe-area-inset-bottom), 1rem)) { + margin-bottom: max(env(safe-area-inset-bottom), 1rem); + } } .modalSticker { @@ -303,19 +317,27 @@ } .form { + display: flex; + flex-direction: column; + flex-grow: 1; + + width: 100%; +} + +.formWidgets { width: 100%; margin-top: 1.75rem; } .checkMnemonicInput { - margin-bottom: 1rem; + margin-bottom: 1.25rem; } .errors { width: 100%; padding: 0 0.5rem; - font-size: 0.9375rem; + font-size: 0.8125rem; font-weight: 600; color: var(--color-gray-1); } @@ -324,7 +346,7 @@ width: 100%; padding: 0 0.5rem; - font-size: 0.9375rem; + font-size: 0.8125rem; color: var(--color-gray-1); } @@ -343,8 +365,6 @@ } .error { - margin-top: 0.5rem; - font-size: 0.9375rem; font-weight: 600; color: var(--color-red); @@ -500,6 +520,121 @@ margin: 0.75rem -1rem 0; } +.biometricsStep { + align-self: center; + + margin-bottom: 1rem; + padding: 0.75rem; + + font-size: 0.9375rem; + font-weight: 600; + color: var(--light-gray-1); + white-space: nowrap; + + background-color: var(--color-gray-button-background-light); + border-radius: var(--border-radius-buttons); +} + +.biometricsError { + align-self: center; + + margin-top: 0.25rem; + padding: 0.375rem 0.5rem; + + font-size: 1.0625rem; + font-weight: 700; + line-height: 1; + color: var(--color-transaction-red-text); + + background-color: var(--color-transaction-red-background); + border-radius: var(--border-radius-tiny); +} + +.stepTransition { + width: auto !important; + height: auto !important; + + white-space: nowrap; +} + +.passwordFormContainer { + overflow-y: scroll; + display: flex; + flex-direction: column; + align-items: center; + + max-width: 27rem; + height: 32.5rem; + max-height: 100%; + margin: 0 auto; + padding: 0 1rem 1rem; + + @include adapt-padding-to-scrollbar(1rem); + + @include respond-above(xs) { + max-width: 31.4375rem; + } + + @supports (padding-bottom: env(safe-area-inset-bottom)) { + padding-bottom: calc(1rem + env(safe-area-inset-bottom)); + } +} + +.pinPadHeader { + display: flex; + flex-direction: column; + align-items: center; + + margin-top: auto; +} + +.headerBack { + cursor: var(--custom-cursor, pointer); + + position: absolute; + top: 1.5rem; + left: 0.5rem; + + display: flex; + align-items: center; + + padding: 0; + + font-size: 1.0625rem; + color: var(--color-blue); + + @supports (top: max(calc(env(safe-area-inset-top) + 0.375rem), 1.5rem)) { + top: max(calc(env(safe-area-inset-top) + 0.375rem), 1.5rem); + } +} + +.iconChevron { + font-size: 1.5rem; +} + +.biometricsIcon { + width: 7rem; + height: 7rem; + margin: 5rem auto 2rem; +} + +.biometricsTitle { + margin-bottom: 2rem; + + font-size: 1.6875rem; + font-weight: 800; + text-align: center; +} + +.biometricsSubtitle { + margin: 0 2rem; + padding-bottom: 2rem; + + font-size: 1.0625rem; + color: var(--color-gray-1); + text-align: center; +} + @supports (padding-bottom: env(safe-area-inset-bottom)) { @include respond-below(xs) { .disclaimerBackupDialog { diff --git a/src/components/auth/Auth.tsx b/src/components/auth/Auth.tsx index 5c38c6e9..46546367 100644 --- a/src/components/auth/Auth.tsx +++ b/src/components/auth/Auth.tsx @@ -14,8 +14,13 @@ import useLastCallback from '../../hooks/useLastCallback'; import SettingsAbout from '../settings/SettingsAbout'; import Transition from '../ui/Transition'; +import AuthBackupWalletModal from './AuthBackupWalletModal'; +import AuthConfirmPin from './AuthConfirmPin'; import AuthCreateBackup from './AuthCreateBackup'; +import AuthCreateBiometrics from './AuthCreateBiometrics'; +import AuthCreateNativeBiometrics from './AuthCreateNativeBiometrics'; import AuthCreatePassword from './AuthCreatePassword'; +import AuthCreatePin from './AuthCreatePin'; import AuthCreatingWallet from './AuthCreatingWallet'; import AuthDisclaimer from './AuthDisclaimer'; import AuthImportMnemonic from './AuthImportMnemonic'; @@ -24,16 +29,20 @@ import AuthStart from './AuthStart'; import styles from './Auth.module.scss'; type StateProps = Pick; const RENDER_COUNT = Object.keys(AuthState).length / 2; const Auth = ({ state, + biometricsStep, + error, isLoading, mnemonic, mnemonicCheckIndexes, + isBackupModalOpen, method, }: StateProps) => { const { @@ -48,9 +57,11 @@ const Auth = ({ true, ) ?? -1; + const [prevKey, setPrevKey] = useState(undefined); const [nextKey, setNextKey] = useState(renderingAuthState + 1); - const updateNextKey = useLastCallback(() => { + const updateRenderingKeys = useLastCallback(() => { setNextKey(renderingAuthState + 1); + setPrevKey(renderingAuthState === AuthState.confirmPin ? AuthState.createPin : undefined); }); // eslint-disable-next-line consistent-return @@ -60,57 +71,95 @@ const Auth = ({ return ; case AuthState.creatingWallet: return ; + case AuthState.createPin: + return ; + case AuthState.confirmPin: + return ; + case AuthState.createBiometrics: + return ( + + ); + case AuthState.createNativeBiometrics: + return ( + + ); case AuthState.createPassword: return ; case AuthState.createBackup: - return ; + return ; case AuthState.disclaimerAndBackup: return ( - + ); case AuthState.importWallet: return ; + case AuthState.importWalletCreatePin: + return ; + case AuthState.importWalletConfirmPin: + return ; case AuthState.disclaimer: return ( ); + case AuthState.importWalletCreateNativeBiometrics: + return ( + + ); case AuthState.importWalletCreatePassword: return ; + case AuthState.importWalletCreateBiometrics: + return ( + + ); case AuthState.about: return ; } } return ( - - {renderAuthScreen} - + <> + + {renderAuthScreen} + + + ); }; export default memo(withGlobal((global): StateProps => { return pick(global.auth, [ - 'state', 'mnemonic', 'mnemonicCheckIndexes', 'isLoading', 'method', + 'state', 'biometricsStep', 'error', 'mnemonic', 'mnemonicCheckIndexes', 'isLoading', 'method', + 'isBackupModalOpen', ]); })(Auth)); diff --git a/src/components/auth/AuthBackupWalletModal.tsx b/src/components/auth/AuthBackupWalletModal.tsx new file mode 100644 index 00000000..dd599d22 --- /dev/null +++ b/src/components/auth/AuthBackupWalletModal.tsx @@ -0,0 +1,127 @@ +import React, { memo, useState } from '../../lib/teact/teact'; +import { getActions } from '../../global'; + +import resolveModalTransitionName from '../../util/resolveModalTransitionName'; + +import useLang from '../../hooks/useLang'; +import useLastCallback from '../../hooks/useLastCallback'; + +import Modal from '../ui/Modal'; +import Transition from '../ui/Transition'; +import MnemonicCheck from './MnemonicCheck'; +import MnemonicList from './MnemonicList'; +import SafetyRules from './SafetyRules'; + +import modalStyles from '../ui/Modal.module.scss'; +import styles from './Auth.module.scss'; + +interface OwnProps { + isOpen?: boolean; + mnemonic?: string[]; + checkIndexes?: number[]; +} + +const SLIDE_ANIMATION_DURATION_MS = 250; + +enum BackupState { + Accept, + View, + Confirm, +} +function AuthBackupWalletModal({ + isOpen, mnemonic, checkIndexes, +}: OwnProps) { + const { + restartCheckMnemonicIndexes, + closeAuthBackupWalletModal, + } = getActions(); + + const lang = useLang(); + const [renderingKey, setRenderingKey] = useState(BackupState.Accept); + const [nextKey, setNextKey] = useState(BackupState.View); + + const handleModalClose = useLastCallback(() => { + setRenderingKey(BackupState.Accept); + setNextKey(BackupState.View); + }); + + const handleMnemonicView = useLastCallback(() => { + setRenderingKey(BackupState.View); + setNextKey(BackupState.Confirm); + }); + const handleRestartCheckMnemonic = useLastCallback(() => { + handleMnemonicView(); + + setTimeout(() => { + restartCheckMnemonicIndexes(); + }, SLIDE_ANIMATION_DURATION_MS); + }); + + const handleShowMnemonicCheck = useLastCallback(() => { + setRenderingKey(BackupState.Confirm); + setNextKey(undefined); + }); + + const handleMnemonicCheckSubmit = useLastCallback(() => { + closeAuthBackupWalletModal({ isBackupCreated: true }); + }); + + // eslint-disable-next-line consistent-return + function renderModalContent(isScreenActive: boolean, isFrom: boolean, currentScreenKey: number) { + switch (currentScreenKey) { + case BackupState.Accept: + return ( + + ); + + case BackupState.View: + return ( + + ); + + case BackupState.Confirm: + return ( + + ); + } + } + return ( + + + {renderModalContent} + + + ); +} + +export default memo(AuthBackupWalletModal); diff --git a/src/components/auth/AuthBackupWarning.tsx b/src/components/auth/AuthBackupWarning.tsx new file mode 100644 index 00000000..46007d50 --- /dev/null +++ b/src/components/auth/AuthBackupWarning.tsx @@ -0,0 +1,50 @@ +import React, { memo } from '../../lib/teact/teact'; +import { getActions } from '../../global'; + +import renderText from '../../global/helpers/renderText'; +import buildClassName from '../../util/buildClassName'; + +import useLang from '../../hooks/useLang'; + +import Button from '../ui/Button'; +import Modal from '../ui/Modal'; + +import styles from './Auth.module.scss'; + +interface OwnProps { + isOpen: boolean; + onSkip: NoneToVoidFunction; + onClose: NoneToVoidFunction; +} + +function AuthBackupWarning({ isOpen, onSkip, onClose }: OwnProps) { + const { openAuthBackupWalletModal } = getActions(); + + const lang = useLang(); + + return ( + +

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

+
+ + +
+
+ ); +} + +export default memo(AuthBackupWarning); diff --git a/src/components/auth/AuthConfirmPin.tsx b/src/components/auth/AuthConfirmPin.tsx new file mode 100644 index 00000000..bcc0c9fb --- /dev/null +++ b/src/components/auth/AuthConfirmPin.tsx @@ -0,0 +1,111 @@ +import React, { memo, useEffect, useState } from '../../lib/teact/teact'; +import { getActions, withGlobal } from '../../global'; + +import type { AuthMethod } from '../../global/types'; + +import { PIN_LENGTH } from '../../config'; +import buildClassName from '../../util/buildClassName'; +import { pause } from '../../util/schedulers'; +import { ANIMATED_STICKERS_PATHS } from '../ui/helpers/animatedAssets'; + +import useHistoryBack from '../../hooks/useHistoryBack'; +import useLang from '../../hooks/useLang'; +import useLastCallback from '../../hooks/useLastCallback'; + +import AnimatedIconWithPreview from '../ui/AnimatedIconWithPreview'; +import Button from '../ui/Button'; +import PinPad from '../ui/PinPad'; + +import styles from './Auth.module.scss'; + +interface OwnProps { + isActive?: boolean; + method?: AuthMethod; +} + +interface StateProps { + pin: string; +} + +const SUBMIT_PAUSE_MS = 1500; + +const AuthConfirmPin = ({ + isActive, + method, + pin, +}: OwnProps & StateProps) => { + const { confirmPin, cancelConfirmPin } = getActions(); + + const lang = useLang(); + const [pinConfirm, setPinConfirm] = useState(''); + const [error, setError] = useState(''); + const [isConfirmed, setIsConfirmed] = useState(false); + const isImporting = method !== 'createAccount'; + + const handleBackClick = useLastCallback(() => { + cancelConfirmPin({ isImporting }); + }); + + useHistoryBack({ + isActive, + onBack: handleBackClick, + }); + + useEffect(() => { + if (isActive) { + setPinConfirm(''); + setError(''); + setIsConfirmed(false); + } + }, [isActive]); + + const handleChange = useLastCallback((value: string) => { + setPinConfirm(value); + setError(''); + }); + + const handleSubmit = useLastCallback(async (value: string) => { + if (value === pin) { + setIsConfirmed(true); + await pause(SUBMIT_PAUSE_MS); + confirmPin({ isImporting }); + } else { + setError(lang('Codes don\'t match')); + } + }); + + return ( +
+ + +
+ +
{lang(isImporting ? 'Wallet is imported!' : 'Wallet is ready!')}
+
+ + +
+ ); +}; + +export default memo(withGlobal((global) => { + return { + pin: global.auth.password, + }; +})(AuthConfirmPin)); diff --git a/src/components/auth/AuthCreateBackup.tsx b/src/components/auth/AuthCreateBackup.tsx index 53892093..a3b0d097 100644 --- a/src/components/auth/AuthCreateBackup.tsx +++ b/src/components/auth/AuthCreateBackup.tsx @@ -1,111 +1,31 @@ -import React, { memo, useState } from '../../lib/teact/teact'; +import React, { memo } from '../../lib/teact/teact'; import { getActions } from '../../global'; import renderText from '../../global/helpers/renderText'; import buildClassName from '../../util/buildClassName'; import { ANIMATED_STICKERS_PATHS } from '../ui/helpers/animatedAssets'; -import useFlag from '../../hooks/useFlag'; import useLang from '../../hooks/useLang'; -import useLastCallback from '../../hooks/useLastCallback'; import AnimatedIconWithPreview from '../ui/AnimatedIconWithPreview'; import Button from '../ui/Button'; -import Modal from '../ui/Modal'; -import Transition from '../ui/Transition'; -import MnemonicCheck from './MnemonicCheck'; -import MnemonicList from './MnemonicList'; -import SafetyRules from './SafetyRules'; -import modalStyles from '../ui/Modal.module.scss'; import styles from './Auth.module.scss'; interface OwnProps { isActive?: boolean; - mnemonic?: string[]; - checkIndexes?: number[]; } -enum BackupState { - Accept, - View, - Confirm, -} - -const SLIDE_ANIMATION_DURATION_MS = 250; - -const AuthCreateBackup = ({ isActive, mnemonic, checkIndexes }: OwnProps) => { - const { afterCheckMnemonic, skipCheckMnemonic, restartCheckMnemonicIndexes } = getActions(); +const AuthCreateBackup = ({ isActive }: OwnProps) => { + const { skipCheckMnemonic, openAuthBackupWalletModal } = getActions(); const lang = useLang(); - const [isModalOpen, openModal, closeModal] = useFlag(); - - const [renderingKey, setRenderingKey] = useState(BackupState.Accept); - const [nextKey, setNextKey] = useState(BackupState.View); - - const handleModalClose = useLastCallback(() => { - setRenderingKey(BackupState.Accept); - setNextKey(BackupState.View); - }); - - const handleMnemonicView = useLastCallback(() => { - setRenderingKey(BackupState.View); - setNextKey(BackupState.Confirm); - }); - - const handleRestartCheckMnemonic = useLastCallback(() => { - handleMnemonicView(); - - setTimeout(() => { - restartCheckMnemonicIndexes(); - }, SLIDE_ANIMATION_DURATION_MS); - }); - - const handleShowMnemonicCheck = useLastCallback(() => { - setRenderingKey(BackupState.Confirm); - setNextKey(undefined); - }); - - const handleMnemonicCheckSubmit = useLastCallback(() => { - closeModal(); - afterCheckMnemonic(); - }); - - // eslint-disable-next-line consistent-return - function renderModalContent(isScreenActive: boolean, isFrom: boolean, currentScreenKey: number) { - switch (currentScreenKey) { - case BackupState.Accept: - return ; - - case BackupState.View: - return ( - - ); - - case BackupState.Confirm: - return ( - - ); - } - } return (
{

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

-
- - - - {renderModalContent} - -
); }; diff --git a/src/components/auth/AuthCreateBiometrics.tsx b/src/components/auth/AuthCreateBiometrics.tsx new file mode 100644 index 00000000..22c16c7e --- /dev/null +++ b/src/components/auth/AuthCreateBiometrics.tsx @@ -0,0 +1,143 @@ +import React, { memo } from '../../lib/teact/teact'; +import { getActions } from '../../global'; + +import type { AuthMethod } from '../../global/types'; + +import { ANIMATED_STICKER_HUGE_SIZE_PX } from '../../config'; +import buildClassName from '../../util/buildClassName'; +import { ANIMATED_STICKERS_PATHS } from '../ui/helpers/animatedAssets'; +import { getFormId } from './helpers/getFormId'; + +import useCurrentOrPrev from '../../hooks/useCurrentOrPrev'; +import useFlag from '../../hooks/useFlag'; +import useHistoryBack from '../../hooks/useHistoryBack'; +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 CreatePasswordForm from '../ui/CreatePasswordForm'; +import Modal from '../ui/Modal'; + +import styles from './Auth.module.scss'; + +interface OwnProps { + isActive?: boolean; + method?: AuthMethod; + isLoading?: boolean; + error?: string; + biometricsStep?: 1 | 2; +} + +const AuthCreateBiometrics = ({ + isActive, + method, + biometricsStep, + error, + isLoading, +}: OwnProps) => { + const { afterCreatePassword, afterCreateBiometrics, restartAuth } = getActions(); + + const lang = useLang(); + const [isPasswordModalOpen, openPasswordModal, closePasswordModal] = useFlag(false); + const isImporting = method !== 'createAccount'; + const formId = getFormId(method!); + const { + shouldRender: shouldRenderSteps, + transitionClassNames: stepsClassNames, + } = useShowTransition(Boolean(biometricsStep)); + const { + shouldRender: shouldRenderError, + transitionClassNames: errorClassNames, + } = useShowTransition(Boolean(error && !shouldRenderSteps)); + const renderingError = useCurrentOrPrev(error, true); + + useHistoryBack({ + isActive, + onBack: restartAuth, + }); + + const handleSubmit = useLastCallback((password: string, isPasswordNumeric: boolean) => { + closePasswordModal(); + afterCreatePassword({ password, isPasswordNumeric }); + }); + + return ( + <> +
+ +
{lang('Congratulations!')}
+

+ {lang(isImporting ? 'The wallet is imported' : 'The wallet is ready')}. +

+

+ {lang('Create a password or use biometric authentication to protect it.')} +

+ + {shouldRenderSteps && ( +
+ {lang(biometricsStep === 1 ? 'Step 1 of 2. Registration' : 'Step 2 of 2. Verification')} +
+ )} + {shouldRenderError && !shouldRenderSteps && ( +
+ {lang(renderingError || 'Unknown error')} +
+ )} + +
+ + +
+
+ + + + + + + ); +}; + +export default memo(AuthCreateBiometrics); diff --git a/src/components/auth/AuthCreateNativeBiometrics.tsx b/src/components/auth/AuthCreateNativeBiometrics.tsx new file mode 100644 index 00000000..2297e3f6 --- /dev/null +++ b/src/components/auth/AuthCreateNativeBiometrics.tsx @@ -0,0 +1,77 @@ +import React, { memo } from '../../lib/teact/teact'; +import { getActions } from '../../global'; + +import buildClassName from '../../util/buildClassName'; +import { getIsFaceIdAvailable, getIsTouchIdAvailable } from '../../util/capacitor'; + +import useHistoryBack from '../../hooks/useHistoryBack'; +import useLang from '../../hooks/useLang'; + +import Button from '../ui/Button'; + +import styles from './Auth.module.scss'; + +import touchIdSvg from '../../assets/settings/settings_biometrics.svg'; +import faceIdSvg from '../../assets/settings/settings_face-id.svg'; + +interface OwnProps { + isActive?: boolean; + isLoading?: boolean; +} + +const AuthCreateNativeBiometrics = ({ isActive, isLoading }: OwnProps) => { + const { afterCreateNativeBiometrics, skipCreateNativeBiometrics, restartAuth } = getActions(); + + const lang = useLang(); + + const isFaceId = getIsFaceIdAvailable(); + const isTouchId = getIsTouchIdAvailable(); + + useHistoryBack({ + isActive, + onBack: restartAuth, + }); + + return ( +
+ + + + +
+ {isFaceId ? lang('Use Face ID') : (isTouchId ? lang('Use Touch ID') : lang('Use Biometrics'))} +
+
+ {lang('You can connect your biometric data for more convenience')} +
+ +
+ + +
+
+ ); +}; + +export default memo(AuthCreateNativeBiometrics); diff --git a/src/components/auth/AuthCreatePassword.tsx b/src/components/auth/AuthCreatePassword.tsx index 4ec925de..534a2bd7 100644 --- a/src/components/auth/AuthCreatePassword.tsx +++ b/src/components/auth/AuthCreatePassword.tsx @@ -1,6 +1,4 @@ -import React, { - memo, useEffect, useRef, useState, -} from '../../lib/teact/teact'; +import React, { memo } from '../../lib/teact/teact'; import { getActions } from '../../global'; import type { AuthMethod } from '../../global/types'; @@ -8,18 +6,13 @@ import type { AuthMethod } from '../../global/types'; import buildClassName from '../../util/buildClassName'; import { ANIMATED_STICKERS_PATHS } from '../ui/helpers/animatedAssets'; -import useFlag from '../../hooks/useFlag'; -import useFocusAfterAnimation from '../../hooks/useFocusAfterAnimation'; +import useHistoryBack from '../../hooks/useHistoryBack'; import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; -import { usePasswordValidation } from '../../hooks/usePasswordValidation'; import AnimatedIconWithPreview from '../ui/AnimatedIconWithPreview'; -import Button from '../ui/Button'; -import Input from '../ui/Input'; -import Modal from '../ui/Modal'; +import CreatePasswordForm from '../ui/CreatePasswordForm'; -import modalStyles from '../ui/Modal.module.scss'; import styles from './Auth.module.scss'; interface OwnProps { @@ -36,140 +29,20 @@ const AuthCreatePassword = ({ const { afterCreatePassword, restartAuth } = getActions(); const lang = useLang(); - - // eslint-disable-next-line no-null/no-null - const firstInputRef = useRef(null); - const [firstPassword, setFirstPassword] = useState(''); - const [secondPassword, setSecondPassword] = useState(''); - const [hasError, setHasError] = useState(false); - const [isJustSubmitted, setIsJustSubmitted] = useState(false); - const [isPasswordFocused, markPasswordFocused, unmarkPasswordFocused] = useFlag(false); - const [isSecondPasswordFocused, markSecondPasswordFocused, unmarkSecondPasswordFocused] = useFlag(false); - const [isPasswordsNotEqual, setIsPasswordsNotEqual] = useState(false); - const [isWeakPasswordModalOpen, openWeakPasswordModal, closeWeakPasswordModal] = useFlag(false); - const canSubmit = firstPassword.length > 0 && secondPassword.length > 0 && !hasError; const isImporting = method !== 'createAccount'; const formId = getFormId(method!); - const validation = usePasswordValidation({ - firstPassword, - secondPassword, - }); - - useFocusAfterAnimation(firstInputRef, !isActive); - - useEffect(() => { - setIsPasswordsNotEqual(false); - if (firstPassword === '' || !isActive || isPasswordFocused) { - setHasError(false); - return; - } - - const { noEqual } = validation; - - if ((!isSecondPasswordFocused || isJustSubmitted) && noEqual && secondPassword !== '') { - setHasError(true); - setIsPasswordsNotEqual(true); - } else if (!noEqual || secondPassword === '' || (isSecondPasswordFocused && !isJustSubmitted)) { - setHasError(false); - } - }, [ - isActive, firstPassword, secondPassword, validation, isSecondPasswordFocused, isPasswordFocused, isJustSubmitted, - ]); - - const handleFirstPasswordChange = useLastCallback((value: string) => { - setFirstPassword(value); - if (isJustSubmitted) { - setIsJustSubmitted(false); - } + useHistoryBack({ + isActive, + onBack: restartAuth, }); - const handleSecondPasswordChange = useLastCallback((value: string) => { - setSecondPassword(value); - if (isJustSubmitted) { - setIsJustSubmitted(false); - } - }); - - const handleCancel = useLastCallback(() => { - restartAuth(); - }); - - const handleSubmit = useLastCallback((e: React.FormEvent) => { - e.preventDefault(); - e.stopPropagation(); - - if (!canSubmit) { - return; - } - - if (firstPassword !== secondPassword) { - setIsJustSubmitted(true); - setHasError(true); - setIsPasswordsNotEqual(true); - return; - } - - const isWeakPassword = Object.values(validation).find((rule) => rule); - - if (isWeakPassword && !isWeakPasswordModalOpen) { - openWeakPasswordModal(); - return; - } - - if (isWeakPasswordModalOpen) { - closeWeakPasswordModal(); - } - afterCreatePassword({ password: firstPassword }); + const handleSubmit = useLastCallback((password: string, isPasswordNumeric: boolean) => { + afterCreatePassword({ password, isPasswordNumeric }); }); - const shouldRenderError = hasError && !isPasswordFocused; - - function renderErrors() { - if (isPasswordsNotEqual) { - return ( -
- {lang('Passwords must be equal.')} -
- ); - } - - const { - invalidLength, - noUpperCase, - noLowerCase, - noNumber, - noSpecialChar, - } = validation; - - return ( -
- {lang('To protect your wallet as much as possible, use a password with')} - - {' '}{lang('$auth_password_rule_8chars')}, - - - {' '}{lang('$auth_password_rule_one_small_char')}, - - - {' '}{lang('$auth_password_rule_one_capital_char')}, - - - {' '}{lang('$auth_password_rule_one_digit')}, - - - {' '}{lang('$auth_password_rule_one_special_char')} - . -
- ); - } - return ( -
+
-
- - -
- - {renderErrors()} - -
- - -
- - -

- {lang('Your have entered an insecure password, which can be easily guessed by scammers.')} -

-

- {lang('Continue or change password to something more secure?')} -

-
- - -
-
- + +
); }; -function getValidationRuleClass(shouldRenderError: boolean, ruleHasError: boolean) { - return buildClassName( - styles.passwordRule, - !ruleHasError ? styles.valid : shouldRenderError ? styles.invalid : undefined, - ); -} - // eslint-disable-next-line consistent-return function getFormId(method: AuthMethod) { switch (method) { diff --git a/src/components/auth/AuthCreatePin.tsx b/src/components/auth/AuthCreatePin.tsx new file mode 100644 index 00000000..2cebf4bf --- /dev/null +++ b/src/components/auth/AuthCreatePin.tsx @@ -0,0 +1,82 @@ +import React, { memo, useEffect, useState } from '../../lib/teact/teact'; +import { getActions } from '../../global'; + +import type { AuthMethod } from '../../global/types'; + +import { PIN_LENGTH } from '../../config'; +import buildClassName from '../../util/buildClassName'; +import { pause } from '../../util/schedulers'; +import { ANIMATED_STICKERS_PATHS } from '../ui/helpers/animatedAssets'; + +import useHistoryBack from '../../hooks/useHistoryBack'; +import useLang from '../../hooks/useLang'; +import useLastCallback from '../../hooks/useLastCallback'; + +import AnimatedIconWithPreview from '../ui/AnimatedIconWithPreview'; +import Button from '../ui/Button'; +import PinPad from '../ui/PinPad'; + +import styles from './Auth.module.scss'; + +interface OwnProps { + isActive?: boolean; + method?: AuthMethod; +} + +const SUBMIT_PAUSE_MS = 750; + +const AuthCreatePin = ({ + isActive, + method, +}: OwnProps) => { + const { createPin, restartAuth } = getActions(); + + const lang = useLang(); + const [pin, setPin] = useState(''); + const isImporting = method !== 'createAccount'; + + useEffect(() => { + if (isActive) { + setPin(''); + } + }, [isActive]); + + useHistoryBack({ + isActive, + onBack: restartAuth, + }); + + const handleSubmit = useLastCallback(async (value: string) => { + await pause(SUBMIT_PAUSE_MS); + createPin({ pin: value, isImporting }); + }); + + return ( +
+ + +
+ +
{lang(isImporting ? 'Wallet is imported!' : 'Wallet is ready!')}
+
+ +
+ ); +}; + +export default memo(AuthCreatePin); diff --git a/src/components/auth/AuthDisclaimer.tsx b/src/components/auth/AuthDisclaimer.tsx index e8cf8a1f..7630c498 100644 --- a/src/components/auth/AuthDisclaimer.tsx +++ b/src/components/auth/AuthDisclaimer.tsx @@ -1,4 +1,4 @@ -import React, { memo, useState } from '../../lib/teact/teact'; +import React, { memo } from '../../lib/teact/teact'; import { getActions } from '../../global'; import { ANIMATED_STICKER_MIDDLE_SIZE_PX } from '../../config'; @@ -6,7 +6,9 @@ import renderText from '../../global/helpers/renderText'; import buildClassName from '../../util/buildClassName'; import { ANIMATED_STICKERS_PATHS } from '../ui/helpers/animatedAssets'; +import { useOpenFromMainBottomSheet } from '../../hooks/useDelegatedBottomSheet'; import useFlag from '../../hooks/useFlag'; +import useHistoryBack from '../../hooks/useHistoryBack'; import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; import useShowTransition from '../../hooks/useShowTransition'; @@ -14,50 +16,44 @@ 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'; -import MnemonicList from './MnemonicList'; -import SafetyRules from './SafetyRules'; +import AuthBackupWarning from './AuthBackupWarning'; -import modalStyles from '../ui/Modal.module.scss'; import styles from './Auth.module.scss'; interface OwnProps { isActive?: boolean; isImport?: boolean; - mnemonic?: string[]; - checkIndexes?: number[]; } -enum BackupState { - Accept, - View, - Confirm, -} - -const SLIDE_ANIMATION_DURATION_MS = 250; - const AuthDisclaimer = ({ - isActive, isImport, mnemonic, checkIndexes, + isActive, isImport, }: OwnProps) => { const { - afterCheckMnemonic, skipCheckMnemonic, - restartCheckMnemonicIndexes, confirmDisclaimer, + cancelDisclaimer, } = getActions(); const lang = useLang(); - const [isModalOpen, openModal, closeModal] = useFlag(); - const [isInformationConfirmed, setIsInformationConfirmed] = useState(false); + const [isInformationConfirmed, markInformationConfirmed, unmarkInformationConfirmed] = useFlag(false); const { shouldRender: shouldRenderStartButton, transitionClassNames: startButtonTransitionClassNames, } = useShowTransition(isInformationConfirmed && isImport); - const [renderingKey, setRenderingKey] = useState(BackupState.Accept); - const [nextKey, setNextKey] = useState(BackupState.View); + useHistoryBack({ + isActive, + onBack: cancelDisclaimer, + }); + const setIsInformationConfirmed = useLastCallback((isConfirmed: boolean) => { + if (isConfirmed) { + markInformationConfirmed(); + } else { + unmarkInformationConfirmed(); + } + }); + + useOpenFromMainBottomSheet('backup-warning', markInformationConfirmed); const handleCloseBackupWarningModal = useLastCallback(() => { setIsInformationConfirmed(false); @@ -68,72 +64,12 @@ const AuthDisclaimer = ({ handleCloseBackupWarningModal(); }); - const handleModalClose = useLastCallback(() => { - setRenderingKey(BackupState.Accept); - setNextKey(BackupState.View); - }); - - const handleMnemonicView = useLastCallback(() => { - setRenderingKey(BackupState.View); - setNextKey(BackupState.Confirm); - }); - - const handleRestartCheckMnemonic = useLastCallback(() => { - handleMnemonicView(); - - setTimeout(() => { - restartCheckMnemonicIndexes(); - }, SLIDE_ANIMATION_DURATION_MS); - }); - - const handleShowMnemonicCheck = useLastCallback(() => { - setRenderingKey(BackupState.Confirm); - setNextKey(undefined); - }); - - const handleMnemonicCheckSubmit = useLastCallback(() => { - closeModal(); - // Don't flicker the backup notice modal after submitting a mnemonic - setIsInformationConfirmed(false); - afterCheckMnemonic(); - }); - - // eslint-disable-next-line consistent-return - function renderModalContent(isScreenActive: boolean, isFrom: boolean, currentScreenKey: number) { - switch (currentScreenKey) { - case BackupState.Accept: - return ; - - case BackupState.View: - return ( - - ); - - case BackupState.Confirm: - return ( - - ); - } - } - return (
- -

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

-
- - -
-
- - - - {renderModalContent} - - + {!isImport && ( + + )}
); }; diff --git a/src/components/auth/AuthImportMnemonic.tsx b/src/components/auth/AuthImportMnemonic.tsx index 3dd8c964..2d904f8e 100644 --- a/src/components/auth/AuthImportMnemonic.tsx +++ b/src/components/auth/AuthImportMnemonic.tsx @@ -11,6 +11,7 @@ import { ANIMATED_STICKERS_PATHS } from '../ui/helpers/animatedAssets'; import useClipboardPaste from '../../hooks/useClipboardPaste'; import { useDeviceScreen } from '../../hooks/useDeviceScreen'; +import useHistoryBack from '../../hooks/useHistoryBack'; import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; @@ -99,6 +100,11 @@ const AuthImportMnemonic = ({ isActive, isLoading, error }: OwnProps & StateProp } }); + useHistoryBack({ + isActive, + onBack: handleCancel, + }); + useEffect(() => { return isSubmitDisabled ? undefined diff --git a/src/components/auth/MnemonicCheck.tsx b/src/components/auth/MnemonicCheck.tsx index bd8f8b91..a0a8bf0a 100644 --- a/src/components/auth/MnemonicCheck.tsx +++ b/src/components/auth/MnemonicCheck.tsx @@ -8,7 +8,7 @@ import renderText from '../../global/helpers/renderText'; import buildClassName from '../../util/buildClassName'; import { areSortedArraysEqual } from '../../util/iteratees'; -import { useDeviceScreen } from '../../hooks/useDeviceScreen'; +import useHistoryBack from '../../hooks/useHistoryBack'; import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; @@ -36,7 +36,6 @@ function MnemonicCheck({ const lang = useLang(); const [words, setWords] = useState>({}); const [hasMnemonicError, setHasMnemonicError] = useState(false); - const { isPortrait } = useDeviceScreen(); useEffect(() => { if (isActive) { @@ -45,6 +44,11 @@ function MnemonicCheck({ } }, [isActive]); + useHistoryBack({ + isActive, + onBack: onCancel, + }); + const handleSetWord = useLastCallback((value: string, index: number) => { setWords({ ...words, @@ -83,7 +87,7 @@ function MnemonicCheck({ labelText={`${key + 1}`} value={words[key]} isInModal={isInModal} - suggestionsPosition={i > 1 || isPortrait ? 'top' : undefined} + suggestionsPosition={i > 1 ? 'top' : undefined} inputArg={key} className={styles.checkMnemonicInput} onInput={handleSetWord} @@ -98,8 +102,8 @@ function MnemonicCheck({ )}
- - + +
diff --git a/src/components/auth/MnemonicList.tsx b/src/components/auth/MnemonicList.tsx index d254daff..010fd3e0 100644 --- a/src/components/auth/MnemonicList.tsx +++ b/src/components/auth/MnemonicList.tsx @@ -4,6 +4,7 @@ import { MNEMONIC_COUNT } from '../../config'; import renderText from '../../global/helpers/renderText'; import buildClassName from '../../util/buildClassName'; +import useHistoryBack from '../../hooks/useHistoryBack'; import useLang from '../../hooks/useLang'; import Button from '../ui/Button'; @@ -13,16 +14,22 @@ import modalStyles from '../ui/Modal.module.scss'; import styles from './Auth.module.scss'; type OwnProps = { + isActive?: boolean; mnemonic?: string[]; onClose: NoneToVoidFunction; onNext: NoneToVoidFunction; }; function MnemonicList({ - mnemonic, onNext, onClose, + isActive, mnemonic, onNext, onClose, }: OwnProps) { const lang = useLang(); + useHistoryBack({ + isActive, + onBack: onClose, + }); + return (
diff --git a/src/components/auth/helpers/getFormId.ts b/src/components/auth/helpers/getFormId.ts new file mode 100644 index 00000000..10ac865a --- /dev/null +++ b/src/components/auth/helpers/getFormId.ts @@ -0,0 +1,13 @@ +import type { AuthMethod } from '../../../global/types'; + +// eslint-disable-next-line consistent-return +export function getFormId(method: AuthMethod) { + switch (method) { + case 'createAccount': + return 'auth_create_password'; + case 'importMnemonic': + return 'auth_import_mnemonic_password'; + case 'importHardwareWallet': + return 'auth_import_hardware_password'; + } +} diff --git a/src/components/common/Countdown.module.scss b/src/components/common/Countdown.module.scss new file mode 100644 index 00000000..478f7cf3 --- /dev/null +++ b/src/components/common/Countdown.module.scss @@ -0,0 +1,12 @@ +.time { + font-size: 0.8125rem; + font-weight: 700; + line-height: 0.8125rem; + color: var(--color-gray-2); + + transition: color 150ms; +} + +.timeWarning { + color: var(--color-red); +} diff --git a/src/components/common/Countdown.tsx b/src/components/common/Countdown.tsx new file mode 100644 index 00000000..1092c697 --- /dev/null +++ b/src/components/common/Countdown.tsx @@ -0,0 +1,69 @@ +import React, { memo, useEffect, useState } from '../../lib/teact/teact'; + +import type { LangFn } from '../../hooks/useLang'; + +import buildClassName from '../../util/buildClassName'; + +import useLang from '../../hooks/useLang'; + +import styles from './Countdown.module.scss'; + +interface OwnProps { + timestamp: number; + deadline: number; + onCompleted?: NoneToVoidFunction; +} + +const WARNING_TIME = 5 * 60; // 5 minutes in seconds; +const SECOND = 1000; + +function Countdown({ + timestamp, + deadline, + onCompleted, +}: OwnProps) { + const lang = useLang(); + const initialSeconds = Math.floor((timestamp + deadline - Date.now()) / 1000); + const [secondsLeft, setSecondsLeft] = useState(Math.max(initialSeconds, 0)); + const shouldShowWarning = secondsLeft <= WARNING_TIME; + + useEffect(() => { + const timerId = setTimeout(() => { + if (secondsLeft <= 0) return; + + setSecondsLeft(Math.floor((timestamp + deadline - Date.now()) / 1000)); + }, SECOND); + + if (secondsLeft <= 0) { + onCompleted?.(); + } + + return () => clearTimeout(timerId); + }, [secondsLeft, onCompleted, timestamp, deadline]); + + return ( + + {formatTime(lang, secondsLeft)} + + ); +} + +function formatTime(lang: LangFn, seconds: number): string { + const pad = (num: number) => num.toString().padStart(2, '0'); + + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const remainingSeconds = seconds % 60; + + let formattedTime = hours > 0 ? `${hours}:` : ''; + formattedTime += `${hours > 0 ? pad(minutes) : minutes}:`; + formattedTime += pad(remainingSeconds); + + return formattedTime; +} + +export default memo(Countdown); diff --git a/src/components/common/SwapResult.module.scss b/src/components/common/SwapResult.module.scss new file mode 100644 index 00000000..21167db6 --- /dev/null +++ b/src/components/common/SwapResult.module.scss @@ -0,0 +1,60 @@ +.sticker { + margin: 0 auto 1.25rem; +} + +.buttons { + display: flex; + justify-content: center; + + margin-top: 2rem; +} + +.button { + min-width: auto !important; + height: auto !important; + padding: 0.75rem 1.25rem !important; + + line-height: 1rem !important; + + border-radius: 0 !important; + + &:first-child { + border-top-left-radius: var(--border-radius-buttons) !important; + border-bottom-left-radius: var(--border-radius-buttons) !important; + } + + &:last-child { + border-top-right-radius: var(--border-radius-buttons) !important; + border-bottom-right-radius: var(--border-radius-buttons) !important; + } + + & + & { + /* stylelint-disable-next-line plugin/whole-pixel */ + box-shadow: inset 0.025rem 0 0 0 var(--color-separator) + } +} + + +.changellyInfoBlock { + display: flex; + flex-direction: column; + gap: 1rem; + align-items: center; + + margin-top: 2rem; +} + +.changellyDescription { + font-size: 0.8125rem; + font-weight: 500; + color: var(--color-gray-2); + text-align: center; +} + +.changellyDescriptionBold { + font-weight: 700; +} + +.changellyTextField { + width: 100%; +} diff --git a/src/components/common/SwapResult.tsx b/src/components/common/SwapResult.tsx new file mode 100644 index 00000000..258defb1 --- /dev/null +++ b/src/components/common/SwapResult.tsx @@ -0,0 +1,124 @@ +import React, { memo } from '../../lib/teact/teact'; + +import type { UserSwapToken } from '../../global/types'; +import { SwapType } from '../../global/types'; + +import getBlockchainNetworkName from '../../util/swap/getBlockchainNetworkName'; +import { ANIMATED_STICKERS_PATHS } from '../ui/helpers/animatedAssets'; + +import useLang from '../../hooks/useLang'; + +import AnimatedIconWithPreview from '../ui/AnimatedIconWithPreview'; +import Button from '../ui/Button'; +import InteractiveTextField from '../ui/InteractiveTextField'; +import SwapTokensInfo from './SwapTokensInfo'; + +import styles from './SwapResult.module.scss'; + +interface OwnProps { + tokenIn?: UserSwapToken; + tokenOut?: UserSwapToken; + amountIn?: number; + amountOut?: number; + playAnimation?: boolean; + firstButtonText?: string; + secondButtonText?: string; + swapType?: SwapType; + toAddress?: string; + onFirstButtonClick?: NoneToVoidFunction; + onSecondButtonClick?: NoneToVoidFunction; +} + +function SwapResult({ + tokenIn, + tokenOut, + amountIn, + amountOut, + playAnimation, + firstButtonText, + secondButtonText, + swapType, + toAddress = '', + onFirstButtonClick, + onSecondButtonClick, +}: OwnProps) { + const lang = useLang(); + + function renderButtons() { + if (!firstButtonText && !secondButtonText) { + return undefined; + } + + return ( +
+ {firstButtonText && ( + + )} + {secondButtonText && ( + + )} +
+ ); + } + + function renderSticker() { + if (swapType === SwapType.CrosschainFromTon) return undefined; + + return ( + + ); + } + + function renderChangellyInfo() { + if (swapType !== SwapType.CrosschainFromTon) return undefined; + + return ( +
+ + { + lang('$swap_changelly_from_ton_description', { + blockchain: ( + + {getBlockchainNetworkName(tokenOut?.blockchain)} + + ), + }) + } + + +
+ ); + } + + return ( + <> + {renderSticker()} + + + + {renderChangellyInfo()} + + {renderButtons()} + + ); +} + +export default memo(SwapResult); diff --git a/src/components/common/SwapTokensInfo.module.scss b/src/components/common/SwapTokensInfo.module.scss new file mode 100644 index 00000000..d9087358 --- /dev/null +++ b/src/components/common/SwapTokensInfo.module.scss @@ -0,0 +1,94 @@ + +.infoBlock { + position: relative; + + display: flex; + flex-direction: column; + gap: 1.25rem; + + width: 100%; +} + +.infoRow { + display: flex; + justify-content: space-between; + + padding: 0.875rem 1rem; + + background-color: var(--color-background-first); + border-radius: var(--border-radius-default); +} + +.infoRowIcon { + width: 2.25rem; + height: 2.25rem; + + border-radius: 50%; +} + +.infoRowToken { + display: flex; + gap: 0.625rem; +} + +.infoRowText { + display: flex; + flex-direction: column; +} + +.infoRowTextCenter { + justify-content: center; +} + +.infoRowTitle { + font-size: 0.9375rem; + font-weight: 600; + color: var(--color-black); +} + +.infoRowDescription { + font-size: 0.75rem; + font-weight: 600; + color: var(--color-gray-2); +} + +.infoRowAmount { + font-size: 0.9375rem; + font-weight: 600; + color: var(--color-gray-1); +} + +.infoRowAmountGreen { + color: var(--color-green); +} + +.infoRowAmountError { + color: var(--color-gray-3) +} + +.infoSeparator { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + display: flex; + align-items: center; + justify-content: center; + + width: 2.5rem; + height: 2.5rem; + + background-color: var(--color-background-second); + border-radius: 50%; +} + +.infoSeparatorIcon { + font-size: 1.5rem; + color: var(--color-gray-1); +} + +.infoSeparatorIconError { + font-size: 2.5rem; + color: var(--color-red); +} diff --git a/src/components/common/SwapTokensInfo.tsx b/src/components/common/SwapTokensInfo.tsx new file mode 100644 index 00000000..56ff90c4 --- /dev/null +++ b/src/components/common/SwapTokensInfo.tsx @@ -0,0 +1,71 @@ +import React, { memo } from '../../lib/teact/teact'; + +import type { ApiSwapAsset } from '../../api/types'; +import type { UserSwapToken } from '../../global/types'; + +import buildClassName from '../../util/buildClassName'; +import { formatCurrencyExtended } from '../../util/formatNumber'; +import getBlockchainNetworkName from '../../util/swap/getBlockchainNetworkName'; +import { ASSET_LOGO_PATHS } from '../ui/helpers/assetLogos'; + +import styles from './SwapTokensInfo.module.scss'; + +interface OwnProps { + tokenIn?: UserSwapToken | ApiSwapAsset; + amountIn?: number; + tokenOut?: UserSwapToken | ApiSwapAsset; + amountOut?: number; + isError?: boolean; +} + +function SwapTokensInfo({ + tokenIn, amountIn, tokenOut, amountOut, isError = false, +}: OwnProps) { + function renderTokenInfo(token?: UserSwapToken | ApiSwapAsset, amount = 0, isReceived = false) { + const image = token?.image ?? ASSET_LOGO_PATHS[token?.symbol.toLowerCase() as keyof typeof ASSET_LOGO_PATHS]; + const amountWithSign = isReceived ? amount : -amount; + return ( +
+
+ {token?.symbol} +
+ {token?.name} + {getBlockchainNetworkName(token?.blockchain)} +
+
+
+ {formatCurrencyExtended(amountWithSign, token?.symbol ?? '')} + +
+
+ ); + } + + return ( +
+ {renderTokenInfo(tokenIn, amountIn)} +
+ +
+ {renderTokenInfo(tokenOut, amountOut, true)} +
+ ); +} + +export default memo(SwapTokensInfo); diff --git a/src/components/common/TokenPriceChart.tsx b/src/components/common/TokenPriceChart.tsx index ce863ef3..ccc4c547 100644 --- a/src/components/common/TokenPriceChart.tsx +++ b/src/components/common/TokenPriceChart.tsx @@ -16,6 +16,7 @@ interface OwnProps { const IS_SMOOTH = true; const PATH_SMOOTHING = 0.0001; +const MIN_CHART_HEIGHT = 0.000000001; function TokenPriceChart({ width, @@ -35,7 +36,7 @@ function TokenPriceChart({ return { width: prices.length - 1, - height: max - min, + height: Math.max(MIN_CHART_HEIGHT, max - min), min, }; }, [prices]); diff --git a/src/components/common/TokenSelector.module.scss b/src/components/common/TokenSelector.module.scss new file mode 100644 index 00000000..a1d0264c --- /dev/null +++ b/src/components/common/TokenSelector.module.scss @@ -0,0 +1,288 @@ +@import "../../styles/mixins"; + +.tokenSelectInputWrapper { + position: relative; + + display: flex; + align-items: center; + + height: 2.25rem; + min-height: 2.25rem; + margin: 0 0.75rem 0.5rem; + + font-size: 1.25rem; + line-height: 1; + color: var(--color-gray-2); + + background-color: var(--color-close-button-background); + border-radius: var(--border-radius-buttons); +} + +.tokenSelectSearchIcon { + margin-left: 0.5rem; +} + +.tokenSelectSearchResetWrapper { + position: absolute; + right: 0.5rem; + + display: flex; + + width: 1rem; + height: 1rem; +} + +.tokenSelectSearchReset { + cursor: var(--custom-cursor, pointer); + + font-size: 1rem; + color: var(--color-close-button-background); + + background-color: var(--color-gray-2); + border-radius: 50%; +} + +.tokenSelectInput { + display: flex; + + width: 100%; + padding: 0 2.125rem 0 0.25rem; + + font-size: 1rem; + font-weight: 600; + color: var(--color-black); + + background: transparent; + border: none; + outline: none; + + appearance: none; + + &::placeholder { + font-weight: 600; + color: var(--color-gray-2); + } + + &:hover, + &:focus { + &::placeholder { + color: var(--color-interactive-input-text-hover-active); + } + } +} + +.tokenSelectContent { + overflow-y: scroll; + + height: 100%; + padding: 0 0.25rem 0 0; + + background-color: var(--color-background-first); + + @include adapt-padding-to-scrollbar(0.25rem); +} + +.tokenGroupContainer { + display: flex; + flex-direction: column; +} + +.tokenGroupHeader { + display: flex; + justify-content: space-between; + + padding: 0.5rem 1rem; + + background: var(--color-background-first-disabled); +} + +.tokenGroupTitle, +.tokenGroupAdditionalTitle { + font-size: 0.8125rem; + font-weight: 600; + color: var(--color-gray-2); +} + +.tokenGroupAdditionalTitle { + cursor: var(--custom-cursor, pointer); +} + +.tokenContainer { + cursor: var(--custom-cursor, pointer); + + position: relative; + + display: flex; + align-items: center; + justify-content: space-between; + + height: 4rem; + padding: 0 1rem; + + @media (hover: hover) { + &:hover { + background-color: var(--color-interactive-item-hover); + } + } + + @media (pointer: coarse) { + &:active { + background-color: var(--color-interactive-item-hover); + } + } +} + +.tokenContainerDisabled { + cursor: auto; +} + +.tokenLogoContainer { + display: flex; + gap: 0.625rem; + align-items: center; +} + +.logoContainer { + position: relative; + + width: 2.25rem; + height: 2.25rem; +} + +.tokenLogo, +.tokenLogoSkeleton, +.tokenLogoSymbol { + width: 2.25rem; + height: 2.25rem; + + font-size: 0; + + border-radius: 100%; +} + +.tokenLogoDisabled { + opacity: 0.5; +} + +.tokenLogoSymbol { + position: absolute; + top: 0; + + display: flex; + align-items: center; + justify-content: center; + + font-size: 1.0625rem; + font-weight: 800; + color: var(--color-gray-3); + + background-color: var(--color-background-first); + box-shadow: inset 0 0 0 0.0625rem var(--color-gray-4); +} + +.tokenLogoSkeleton { + background-color: var(--color-separator-input-stroke); +} + +.tokenNetworkLogo, +.tokenNetworkLogoSkeleton { + position: absolute; + z-index: 1; + top: 1.5rem; + right: -0.25rem; + + width: 1.125rem; + height: 1.125rem; + + border-radius: 100%; + + /* stylelint-disable-next-line plugin/whole-pixel */ + box-shadow: 0 0 0 0.0938rem var(--color-background-drop-down), + inset 0 0 0 0.125rem var(--color-background-drop-down); +} + +.tokenNetworkLogoSkeleton { + background-color: var(--color-separator-input-stroke); + + /* stylelint-disable-next-line plugin/whole-pixel */ + box-shadow: 0 0 0 0.0938rem var(--color-background-drop-down); +} + +.tokenNetworkLogoDisabled { + position: absolute; + z-index: 1; + top: 1.5rem; + right: -0.25rem; + + width: 1.125rem; + height: 1.125rem; + + opacity: 0.5; + background-color: var(--color-background-first); + border-radius: 100%; +} + +.nameContainer, +.tokenPriceContainer { + display: flex; + flex-direction: column; + gap: 0.3125rem; +} + +.tokenPriceContainer { + align-items: flex-end; +} + +.tokenName, +.tokenAmount { + font-size: 0.9375rem; + font-weight: 600; +} + +.tokenNetwork, +.tokenValue { + font-size: 0.75rem; + font-weight: 500; + color: var(--color-gray-2); +} + +.tokenNameSkeleton { + width: 5rem; + height: 0.875rem; + + background-color: var(--color-separator-input-stroke); + border-radius: var(--border-radius-tiny); +} + +.tokenValueSkeleton { + width: 3rem; + height: 0.6875rem; + + background-color: var(--color-separator-input-stroke); + border-radius: var(--border-radius-tiny); +} + +.tokenNotFound { + display: flex; + flex-direction: column; + gap: 1.25rem; + align-items: center; + justify-content: center; + + padding-top: 2rem; +} + +.tokenNotFoundTitle { + font-size: 1.0625rem; + font-weight: 700; +} + +.tokenNotFoundDesc { + font-size: 0.9375rem; + font-weight: 400; + color: var(--color-gray-2); +} + +.tokenTextDisabled { + color: var(--color-gray-2); +} diff --git a/src/components/common/TokenSelector.tsx b/src/components/common/TokenSelector.tsx new file mode 100644 index 00000000..87f1a8fa --- /dev/null +++ b/src/components/common/TokenSelector.tsx @@ -0,0 +1,519 @@ +import React, { + memo, useEffect, useLayoutEffect, useMemo, useRef, useState, +} from '../../lib/teact/teact'; +import { getActions, withGlobal } from '../../global'; + +import type { ApiBaseCurrency } from '../../api/types'; +import { + type AssetPairs, + SettingsState, + type UserSwapToken, + type UserToken, +} from '../../global/types'; + +import { ANIMATED_STICKER_MIDDLE_SIZE_PX, TON_BLOCKCHAIN } from '../../config'; +import { Big } from '../../lib/big.js/index.js'; +import { + selectCurrentAccountTokens, + selectPopularTokensWithoutAccountTokens, + selectSwapTokens, +} from '../../global/selectors'; +import buildClassName from '../../util/buildClassName'; +import { + formatCurrency, getShortCurrencySymbol, +} from '../../util/formatNumber'; +import { getIsAddressValid } from '../../util/getIsAddressValid'; +import getBlockchainNetworkIcon from '../../util/swap/getBlockchainNetworkIcon'; +import getBlockchainNetworkName from '../../util/swap/getBlockchainNetworkName'; +import { ANIMATED_STICKERS_PATHS } from '../ui/helpers/animatedAssets'; +import { ASSET_LOGO_PATHS } from '../ui/helpers/assetLogos'; + +import { useDeviceScreen } from '../../hooks/useDeviceScreen'; +import useFocusAfterAnimation from '../../hooks/useFocusAfterAnimation'; +import useHistoryBack from '../../hooks/useHistoryBack'; +import useLang from '../../hooks/useLang'; +import useLastCallback from '../../hooks/useLastCallback'; +import usePrevious from '../../hooks/usePrevious'; +import useScrolledState from '../../hooks/useScrolledState'; +import useSyncEffect from '../../hooks/useSyncEffect'; + +import AnimatedIconWithPreview from '../ui/AnimatedIconWithPreview'; +import ModalHeader from '../ui/ModalHeader'; +import Transition from '../ui/Transition'; + +import styles from './TokenSelector.module.scss'; + +type Token = UserToken | UserSwapToken; + +interface StateProps { + token?: Token; + userTokens?: Token[]; + popularTokens?: Token[]; + swapTokens?: UserSwapToken[]; + tokenInSlug?: string; + pairsBySlug?: Record; + baseCurrency?: ApiBaseCurrency; + isLoading?: boolean; +} + +interface OwnProps { + isActive?: boolean; + shouldFilter?: boolean; + onlyPopular?: boolean; + onClose: NoneToVoidFunction; + onBack: NoneToVoidFunction; +} + +enum SearchState { + Initial, + Search, + Loading, + Token, + Empty, +} + +const EMPTY_ARRAY: Token[] = []; + +function TokenSelector({ + token, + userTokens, + swapTokens, + popularTokens, + shouldFilter, + onlyPopular, + baseCurrency, + tokenInSlug, + pairsBySlug, + isActive, + isLoading, + onBack, + onClose, +}: OwnProps & StateProps) { + const { + importToken, + resetImportToken, + openSettingsWithState, + setSwapTokenIn, + setSwapTokenOut, + addToken, + } = getActions(); + const lang = useLang(); + + const shortBaseSymbol = getShortCurrencySymbol(baseCurrency); + + // eslint-disable-next-line no-null/no-null + const scrollContainerRef = useRef(null); + + // eslint-disable-next-line no-null/no-null + const searchInputRef = useRef(null); + + useHistoryBack({ + isActive, + onBack, + }); + + useFocusAfterAnimation(searchInputRef, !isActive); + + const { + handleScroll: handleContentScroll, + } = useScrolledState(); + const { isPortrait } = useDeviceScreen(); + + const [searchValue, setSearchValue] = useState(''); + const [isResetButtonVisible, setIsResetButtonVisible] = useState(false); + const [renderingKey, setRenderingKey] = useState(SearchState.Initial); + const [searchTokenList, setSearchTokenList] = useState([]); + + const popularTokensPrev = usePrevious(popularTokens); + + const filterTokens = useLastCallback((tokens: Token[]) => filterAndSortTokens(tokens, tokenInSlug, pairsBySlug)); + + const { userTokensWithFilter, popularTokensWithFilter, swapTokensWithFilter } = useMemo(() => { + const currentUserTokens = userTokens ?? EMPTY_ARRAY; + const currentSwapTokens = swapTokens ?? EMPTY_ARRAY; + const currentPopularTokens = popularTokensPrev ?? popularTokens ?? EMPTY_ARRAY; + + if (!shouldFilter) { + return { + userTokensWithFilter: currentUserTokens, + popularTokensWithFilter: currentPopularTokens, + swapTokensWithFilter: currentSwapTokens, + }; + } + + const filteredPopularTokens = filterTokens(currentPopularTokens); + let filteredUserTokens: Token[]; + let filteredSwapTokens: Token[]; + + if (onlyPopular) { + filteredUserTokens = EMPTY_ARRAY; + filteredSwapTokens = EMPTY_ARRAY; + } else { + filteredUserTokens = filterTokens(currentUserTokens); + filteredSwapTokens = filterTokens(currentSwapTokens); + } + + return { + userTokensWithFilter: filteredUserTokens, + popularTokensWithFilter: filteredPopularTokens, + swapTokensWithFilter: filteredSwapTokens, + }; + }, [filterTokens, onlyPopular, popularTokens, popularTokensPrev, shouldFilter, swapTokens, userTokens]); + + const filteredTokenList = useMemo(() => { + const tokensToFilter = onlyPopular ? popularTokensWithFilter : swapTokensWithFilter; + const lowerCaseSearchValue = searchValue.toLowerCase().trim(); + + return tokensToFilter.filter(({ + name, symbol, keywords, isDisabled, + }) => { + if (isDisabled) { + return false; + } + + const isName = name.toLowerCase().includes(lowerCaseSearchValue); + const isSymbol = symbol.toLowerCase().includes(lowerCaseSearchValue); + const isKeyword = keywords?.some((key) => key.toLowerCase().includes(lowerCaseSearchValue)); + + return isName || isSymbol || isKeyword; + }).sort((a, b) => b.amount - a.amount) ?? []; + }, [onlyPopular, popularTokensWithFilter, searchValue, swapTokensWithFilter]); + + const resetSearch = () => { + setSearchValue(''); + }; + + useSyncEffect(() => { + setIsResetButtonVisible(Boolean(searchValue.length)); + + const isValidAddress = getIsAddressValid(searchValue); + let newRenderingKey = SearchState.Initial; + + if (isLoading && isValidAddress) { + newRenderingKey = SearchState.Loading; + } else if (token && isValidAddress) { + newRenderingKey = SearchState.Token; + } else if (searchValue.length && filteredTokenList.length !== 0) { + newRenderingKey = SearchState.Search; + } else if (filteredTokenList.length === 0) { + newRenderingKey = SearchState.Empty; + } + + setRenderingKey(newRenderingKey); + + if (newRenderingKey !== SearchState.Initial) { + setSearchTokenList(filteredTokenList); + } + }, [searchTokenList.length, isLoading, searchValue, token, filteredTokenList]); + + useEffect(() => { + if (getIsAddressValid(searchValue)) { + importToken({ address: searchValue, isSwap: true }); + setRenderingKey(SearchState.Loading); + } else { + resetImportToken(); + } + }, [searchValue]); + + useLayoutEffect(() => { + if (!isActive || !scrollContainerRef.current) return; + + scrollContainerRef.current.scrollTop = 0; + }, [isActive]); + + const handleTokenClick = useLastCallback((selectedToken: Token) => { + if (isPortrait) { + onBack(); + } else { + onClose(); + } + + if (onlyPopular) { + addToken({ token: selectedToken as UserToken }); + } else { + const setToken = shouldFilter ? setSwapTokenOut : setSwapTokenIn; + setToken({ tokenSlug: selectedToken.slug }); + } + + resetSearch(); + }); + + const handleOpenSettings = useLastCallback(() => { + onClose(); + openSettingsWithState({ state: SettingsState.Assets }); + }); + + function renderSearch() { + return ( +
+ + setSearchValue(e.target.value)} + placeholder={lang('Name or Address...')} + value={searchValue} + /> + + {isResetButtonVisible && ( + + )} + +
+ ); + } + + function renderToken(currentToken: Token) { + const image = ASSET_LOGO_PATHS[ + currentToken?.symbol.toLowerCase() as keyof typeof ASSET_LOGO_PATHS + ] ?? currentToken?.image; + const blockchain = 'blockchain' in currentToken ? currentToken.blockchain : TON_BLOCKCHAIN; + const price = 'price' in currentToken ? currentToken.price : 1; + + const isAvailable = !shouldFilter || currentToken.canSwap; + const descriptionText = isAvailable + ? getBlockchainNetworkName(blockchain) + : lang('Unavailable'); + const currencyHoldings = Big(price).mul(currentToken.amount); + const handleClick = isAvailable ? () => handleTokenClick(currentToken) : undefined; + + return ( +
+
+
+ {currentToken.symbol} + {blockchain} + {!isAvailable && } +
+
+ + {currentToken.name} + + + {descriptionText} + +
+
+
+ + {formatCurrency(currentToken.amount, currentToken.symbol)} + + + {formatCurrency(currencyHoldings.toNumber(), shortBaseSymbol)} + +
+
+ ); + } + + function renderTokenGroup(tokens: Token[], title: string, shouldShowSettings?: boolean) { + return ( +
+
+ {title} + {shouldShowSettings && ( + + {lang('Settings')} + + )} +
+ {tokens.map(renderToken)} +
+ ); + } + + function renderAllTokens(tokens: Token[]) { + return ( +
+ {tokens.map(renderToken)} +
+ ); + } + + function renderTokenSkeleton() { + return ( +
+
+
+
+
+
+
+ + +
+
+
+ + +
+
+ ); + } + + function renderNotFound(shouldPlay: boolean) { + return ( +
+ + {lang('Not Found')} + {lang('Try another keyword or address.')} +
+ ); + } + + function renderSearchResults(tokenToImport?: Token) { + if (tokenToImport) { + return ( +
+ {renderToken(tokenToImport)} +
+ ); + } + + return ( + <> + {renderTokenSkeleton()} + {renderTokenSkeleton()} + {renderTokenSkeleton()} + {renderTokenSkeleton()} + {renderTokenSkeleton()} + + ); + } + + function renderTokenGroups() { + if (onlyPopular) { + return renderTokenGroup(popularTokensWithFilter, lang('POPULAR')); + } + + return ( + <> + {renderTokenGroup(userTokensWithFilter, lang('MY'), true)} + {renderTokenGroup(popularTokensWithFilter, lang('POPULAR'))} + {renderTokenGroup(swapTokensWithFilter, lang('A-Z'))} + + ); + } + + // eslint-disable-next-line consistent-return + function renderContent(isContentActive: boolean, isFrom: boolean, currentKey: number) { + switch (currentKey) { + case SearchState.Initial: + return renderTokenGroups(); + case SearchState.Loading: + return renderSearchResults(); + case SearchState.Search: + return renderAllTokens(searchTokenList); + case SearchState.Token: + return renderSearchResults(token); + case SearchState.Empty: + return renderNotFound(isContentActive); + } + } + + return ( + <> + + {renderSearch()} + +
+ + {renderContent} + +
+ + ); +} + +export default memo(withGlobal((global): StateProps => { + const { isLoading, token } = global.settings.importToken ?? {}; + const { pairs, tokenInSlug } = global.currentSwap ?? {}; + + const userTokens = selectCurrentAccountTokens(global); + const popularTokens = selectPopularTokensWithoutAccountTokens(global); + const swapTokens = selectSwapTokens(global); + const { baseCurrency } = global.settings; + + return { + isLoading, + token, + userTokens, + popularTokens, + swapTokens, + tokenInSlug, + baseCurrency, + pairsBySlug: pairs?.bySlug, + }; +})(TokenSelector)); + +function filterAndSortTokens(tokens: Token[], tokenInSlug?: string, pairsBySlug?: Record) { + if (!tokens.length || !tokenInSlug) return []; + + return tokens.map((token) => { + const canSwap = Boolean(pairsBySlug?.[tokenInSlug]?.[token.slug]); + return { ...token, canSwap }; + }).sort((a, b) => Number(b.canSwap) - Number(a.canSwap)); +} diff --git a/src/components/common/TransferResult.module.scss b/src/components/common/TransferResult.module.scss index ba4fc78a..9fe32dbd 100644 --- a/src/components/common/TransferResult.module.scss +++ b/src/components/common/TransferResult.module.scss @@ -55,6 +55,9 @@ } .buttons { + --color-gray-button-background: var(--color-gray-button-background-light); + --color-gray-button-background-hover: var(--color-gray-button-background-light-hover); + display: flex; justify-content: center; diff --git a/src/components/common/TransferResult.tsx b/src/components/common/TransferResult.tsx index 9c6ca9c0..a1706fe2 100644 --- a/src/components/common/TransferResult.tsx +++ b/src/components/common/TransferResult.tsx @@ -41,8 +41,11 @@ function TransferResult({ onFirstButtonClick, onSecondButtonClick, }: OwnProps) { - const withBalanceChange = balance && operationAmount; - const finalBalance = withBalanceChange ? balance + operationAmount - (fee ?? 0) : 0; + const withBalanceChange = Boolean(balance !== undefined && operationAmount); + let finalBalance = withBalanceChange ? balance! + operationAmount! : 0; + if (finalBalance && fee && tokenSymbol === TON_SYMBOL) { + finalBalance -= fee; + } const [wholePart, fractionPart] = formatCurrencyExtended(amount, '', noSign).split('.'); function renderButtons() { diff --git a/src/components/dapps/Dapp.module.scss b/src/components/dapps/Dapp.module.scss index 69a0b44a..54f9c020 100644 --- a/src/components/dapps/Dapp.module.scss +++ b/src/components/dapps/Dapp.module.scss @@ -100,7 +100,7 @@ display: block; - margin-bottom: 0.25rem; + margin-bottom: 0.5rem; padding: 0 0.5rem; font-size: 0.8125rem; @@ -182,6 +182,10 @@ opacity: 1; } + &_disabled { + cursor: auto; + } + .accounts_single & { grid-column-start: 2; } @@ -293,7 +297,7 @@ overflow: hidden; box-sizing: border-box; - margin-bottom: 1rem; + margin-bottom: 1.25rem; padding: 0.875rem 0.75rem; font-size: 1rem; @@ -508,3 +512,176 @@ background: var(--color-background-first); border-radius: 1rem; } + +@keyframes shimmer { + 0% { + background-position: 200%; + } + 100% { + background-position: -200%; + } +} + +@mixin skeleton($color: var(--color-background-first), $edgeColor: var(--color-separator-input-stroke)) { + background: linear-gradient( + 90deg, + $edgeColor, + $edgeColor, + $color, + $edgeColor, + $edgeColor + ); + background-repeat: no-repeat; + background-size: 400% 100%; + + animation: shimmer 2s infinite linear; +} + +.dappInfoSkeleton { + display: flex; + gap: 0.625rem; + align-items: center; + + height: 4rem; + padding: 0.875rem 1rem; + + background-color: var(--color-background-first); + border-radius: var(--border-radius-default); +} + +.accountWrapperSkeleton { + margin-top: 1.25rem; +} + +.dappInfoIconSkeleton { + @include skeleton(); + + width: 2.25rem; + height: 2.25rem; + + background-color: var(--color-separator-input-stroke); + border-radius: var(--border-radius-tiny); +} + +.transactionDappIconSkeleton { + @include skeleton( + var(--color-background-first), + #00000000 + ); + + background-color: var(--color-card-button); +} + +.dappInfoTextSkeleton { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.nameSkeleton { + @include skeleton(); + + width: 4rem; + height: 0.75rem; + + background-color: var(--color-separator-input-stroke); + border-radius: var(--border-radius-tiny); +} + +.descSkeleton { + @include skeleton(); + + width: 5rem; + height: 0.75rem; + + background-color: var(--color-separator-input-stroke); + border-radius: var(--border-radius-tiny); +} + +.nameDappSkeleton { + @include skeleton( + var(--color-background-first), + #00000000 + ); + + height: 0.875rem; + + background-color: var(--color-card-button); +} +.descDappSkeleton { + @include skeleton( + var(--color-background-first), + #00000000 + ); + + height: 0.6875rem; + + background-color: var(--color-card-button); +} + +.transactionDirectionLeftSkeleton { + display: flex; + flex-direction: column; + gap: 0.25rem; + + margin-left: 1rem; +} + +.transactionDirectionRightSkeleton { + display: flex; + gap: 0.5rem; + align-items: center; + justify-self: center; +} + +.dappInfoDataSkeleton { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.rowContainerSkeleton { + display: flex; + flex-direction: column; + gap: 0.5rem; + + margin-bottom: 1.25rem; +} + +.rowSkeleton { + @include skeleton( + var(--color-background-second) + ); + + width: 100%; + height: 3rem; + + background-color: var(--color-separator-input-stroke); + border-radius: var(--border-radius-default); +} + +.rowLargeSkeleton { + height: 4.25rem; +} + +.rowTextSkeleton { + @include skeleton( + var(--color-background-second) + ); + + width: 3.0625rem; + height: 0.8125rem; + margin-left: 0.5rem; + + background-color: var(--color-separator-input-stroke); + border-radius: var(--border-radius-default); +} + +.rowTextLargeSkeleton { + width: 6.8125rem; +} + +.skeletonTransitionWrapper { + display: flex; + flex-direction: column; +} diff --git a/src/components/dapps/DappConnectModal.tsx b/src/components/dapps/DappConnectModal.tsx index efc75abe..2b8927cf 100644 --- a/src/components/dapps/DappConnectModal.tsx +++ b/src/components/dapps/DappConnectModal.tsx @@ -15,6 +15,7 @@ import { bigStrToHuman } from '../../global/helpers'; import { selectCurrentAccountTokens, selectNetworkAccounts } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import { formatCurrency } from '../../util/formatNumber'; +import resolveModalTransitionName from '../../util/resolveModalTransitionName'; import { shortenAddress } from '../../util/shortenAddress'; import useFlag from '../../hooks/useFlag'; @@ -27,9 +28,9 @@ import LedgerConnect from '../ledger/LedgerConnect'; import Button from '../ui/Button'; import Modal from '../ui/Modal'; import ModalHeader from '../ui/ModalHeader'; -import PasswordForm from '../ui/PasswordForm'; import Transition from '../ui/Transition'; import DappInfo from './DappInfo'; +import DappPassword from './DappPassword'; import modalStyles from '../ui/Modal.module.scss'; import styles from './Dapp.module.scss'; @@ -72,7 +73,6 @@ function DappConnectModal({ const { submitDappConnectRequestConfirm, submitDappConnectRequestConfirmHardware, - clearDappConnectRequestError, cancelDappConnectRequestConfirm, setDappConnectRequestState, } = getActions(); @@ -84,6 +84,8 @@ function DappConnectModal({ const { renderingKey, nextKey } = useModalTransitionKeys(state ?? 0, isModalOpen); + const isLoading = dapp === undefined; + useEffect(() => { if (hasConnectRequest) { openModal(); @@ -145,10 +147,11 @@ function DappConnectModal({ function renderAccount(accountId: string, address: string, title?: string) { const balance = accountsData?.[accountId].balances?.bySlug[tonToken.slug] || '0'; const isActive = accountId === selectedAccount; - const onClick = isActive ? undefined : () => setSelectedAccount(accountId); + const onClick = isActive || isLoading ? undefined : () => setSelectedAccount(accountId); const fullClassName = buildClassName( styles.account, isActive && styles.account_current, + isLoading && styles.account_disabled, ); return ( @@ -188,24 +191,6 @@ function DappConnectModal({ ); } - function renderPasswordForm(isActive: boolean) { - return ( - <> - - - - ); - } - function renderDappInfo() { return ( <> @@ -228,16 +213,53 @@ function DappConnectModal({ ); } + function renderWaitForConnection() { + return ( + <> + +
+
+
+
+
+
+
+
+
+ {shouldRenderAccounts && renderAccounts()} +
+
+ + ); + } + + function renderDappInfoWithSkeleton() { + return ( + + {isLoading ? renderWaitForConnection() : renderDappInfo()} + + ); + } + // eslint-disable-next-line consistent-return function renderContent(isActive: boolean, isFrom: boolean, currentKey: number) { switch (currentKey) { case DappConnectState.Info: - return renderDappInfo(); + return renderDappInfoWithSkeleton(); case DappConnectState.Password: - return renderPasswordForm(isActive); + return ( + + ); case DappConnectState.ConnectHardware: return ( { const accounts = selectNetworkAccounts(global); - const hasConnectRequest = Boolean(global.dappConnectRequest?.dapp); + const hasConnectRequest = global.dappConnectRequest?.state !== undefined; const { state, dapp, error, accountId, permissions, proof, diff --git a/src/components/dapps/DappLedgerWarning.tsx b/src/components/dapps/DappLedgerWarning.tsx index c3fe529a..99fc731a 100644 --- a/src/components/dapps/DappLedgerWarning.tsx +++ b/src/components/dapps/DappLedgerWarning.tsx @@ -69,10 +69,11 @@ function DappLedgerWarning({
- + + @@ -201,14 +231,23 @@ function LedgerConnect({ } function renderContent() { - if (state === HardwareConnectState.WaitingForBrowser) { + if (isWaitingForBrowser) { return renderWaitingForBrowser(); } return renderConnect(); } - return renderContent(); + return ( + + {renderContent} + + ); } export default memo(LedgerConnect); diff --git a/src/components/ledger/LedgerModal.module.scss b/src/components/ledger/LedgerModal.module.scss index dfeb30ba..31d68e7d 100644 --- a/src/components/ledger/LedgerModal.module.scss +++ b/src/components/ledger/LedgerModal.module.scss @@ -155,10 +155,12 @@ padding: 0.5rem; - @include adapt-padding-to-scrollbar(0.5rem); - background-color: var(--color-background-first); border-radius: var(--border-radius-default); + + &_two { + grid-template-columns: repeat(6, 1fr); + } } .account { @@ -196,6 +198,22 @@ opacity: 1; } + .accounts_single & { + grid-column-start: 2; + } + + .accounts_two & { + grid-column: span 2; + + &:first-child { + grid-column-end: 4; + } + + &:nth-child(2) { + grid-column-end: 6; + } + } + @media (min-resolution: 1.5dppx) { background-image: url('../../assets/account_button_bg@2x.jpg'), linear-gradient(125deg, #71A9ED 0, #436CB6 100%); } diff --git a/src/components/ledger/LedgerModal.tsx b/src/components/ledger/LedgerModal.tsx index 0877478c..573f9812 100644 --- a/src/components/ledger/LedgerModal.tsx +++ b/src/components/ledger/LedgerModal.tsx @@ -8,6 +8,7 @@ import type { LedgerWalletInfo } from '../../util/ledger/types'; import { selectNetworkAccounts } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; +import resolveModalTransitionName from '../../util/resolveModalTransitionName'; import useLastCallback from '../../hooks/useLastCallback'; @@ -50,6 +51,7 @@ function LedgerModal({ isRemoteTab, }: OwnProps & StateProps) { const { + afterSelectHardwareWallets, resetHardwareWalletConnect, } = getActions(); @@ -60,7 +62,16 @@ function LedgerModal({ LedgerModalState.SelectWallets, ); - const handleConnected = useLastCallback(() => { + const handleAddLedgerWallet = useLastCallback(() => { + afterSelectHardwareWallets({ hardwareSelectedIndices: [hardwareWallets![0].index] }); + onClose(); + }); + + const handleConnected = useLastCallback((isSingleWallet: boolean) => { + if (isSingleWallet) { + handleAddLedgerWallet(); + return; + } setCurrentSlide(LedgerModalState.SelectWallets); }); @@ -75,6 +86,7 @@ function LedgerModal({ case LedgerModalState.Connect: return ( ; - onCancel?: NoneToVoidFunction ; - onClose: NoneToVoidFunction ; + onCancel?: NoneToVoidFunction; + onClose: NoneToVoidFunction; }; -const ACCOUNT_ADDRESS_SHIFT = 6; -const ACCOUNT_ADDRESS_SHIFT_END = 6; +const ACCOUNT_ADDRESS_SHIFT = 4; const ACCOUNT_BALANCE_DECIMALS = 3; function LedgerSelectWallets({ + isActive, hardwareWallets, accounts, onCancel, @@ -44,6 +46,11 @@ function LedgerSelectWallets({ const [selectedAccountIndices, setSelectedAccountIndices] = useState([]); const shouldCloseOnCancel = !onCancel; + useHistoryBack({ + isActive, + onBack: onCancel ?? onClose, + }); + const handleAccountToggle = useLastCallback((index: number) => { if (selectedAccountIndices.includes(index)) { setSelectedAccountIndices(selectedAccountIndices.filter((id) => id !== index)); @@ -63,12 +70,12 @@ function LedgerSelectWallets({ ); function renderAccount(address: string, balance: string, index: number, isConnected: boolean) { - const isActive = isConnected || selectedAccountIndices.includes(index); + const isActiveAccount = isConnected || selectedAccountIndices.includes(index); return (
handleAccountToggle(index)} > @@ -78,20 +85,25 @@ function LedgerSelectWallets({
- {shortenAddress(address, ACCOUNT_ADDRESS_SHIFT, ACCOUNT_ADDRESS_SHIFT_END)} + {shortenAddress(address, ACCOUNT_ADDRESS_SHIFT, ACCOUNT_ADDRESS_SHIFT)}
-
+
); } function renderAccounts() { const list = hardwareWallets ?? []; + const fullClassName = buildClassName( + styles.accounts, + list.length === 1 && styles.accounts_single, + list.length === 2 && styles.accounts_two, + ); return ( -
+
{list.map( ({ address, balance, index }) => renderAccount( address, diff --git a/src/components/main/Main.module.scss b/src/components/main/Main.module.scss index f1428c7f..15619ca3 100644 --- a/src/components/main/Main.module.scss +++ b/src/components/main/Main.module.scss @@ -28,6 +28,10 @@ $scrollOffset: 0.1875rem; } } + @supports (padding-top: env(safe-area-inset-top)) { + min-height: calc(var(--vh, 1vh) * 100 - env(safe-area-inset-top)); + } + .head { width: 100%; max-width: 27rem; diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index f7532611..cc0cc4bc 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -1,3 +1,4 @@ +import { BottomSheet } from 'native-bottom-sheet'; import React, { memo, useEffect, useRef, useState, } from '../../lib/teact/teact'; @@ -5,10 +6,18 @@ import { getActions, withGlobal } from '../../global'; import { ContentTab } from '../../global/types'; +import { IS_CAPACITOR } from '../../config'; import { selectCurrentAccount, selectCurrentAccountState } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; -import { REM } from '../../util/windowEnvironment'; +import { getStatusBarHeight } from '../../util/capacitor'; +import { captureEvents, SwipeDirection } from '../../util/captureEvents'; +import { setStatusBarStyle } from '../../util/switchTheme'; +import { + getSafeAreaTop, IS_DELEGATED_BOTTOM_SHEET, IS_TOUCH_ENV, REM, +} from '../../util/windowEnvironment'; +import { useOpenFromMainBottomSheet } from '../../hooks/useDelegatedBottomSheet'; +import { useOpenFromNativeBottomSheet } from '../../hooks/useDelegatingBottomSheet'; import { useDeviceScreen } from '../../hooks/useDeviceScreen'; import useFlag from '../../hooks/useFlag'; import useLastCallback from '../../hooks/useLastCallback'; @@ -26,33 +35,65 @@ import Warnings from './sections/Warnings'; import styles from './Main.module.scss'; +interface OwnProps { + isActive?: boolean; + onQrScanPress?: NoneToVoidFunction; +} + type StateProps = { currentTokenSlug?: string; - currentAccountId?: string; isStakingActive: boolean; isUnstakeRequested?: boolean; isTestnet?: boolean; isLedger?: boolean; + isStakingInfoModalOpen?: boolean; }; const STICKY_CARD_INTERSECTION_THRESHOLD = -3.75 * REM; +const STICKY_CARD_WITH_SAFE_AREA_INTERSECTION_THRESHOLD = -5.5 * REM; function Main({ - currentTokenSlug, currentAccountId, isStakingActive, isUnstakeRequested, isTestnet, isLedger, -}: StateProps) { + isActive, + currentTokenSlug, + isStakingActive, + isUnstakeRequested, + isTestnet, + isLedger, + onQrScanPress, + isStakingInfoModalOpen, +}: OwnProps & StateProps) { const { selectToken, startStaking, - fetchBackendStakingState, openBackupWalletModal, - setActiveContentTabIndex, + setActiveContentTab, + setSwapTokenOut, + changeTransferToken, + openStakingInfo, + closeStakingInfo, } = getActions(); // eslint-disable-next-line no-null/no-null const cardRef = useRef(null); + // eslint-disable-next-line no-null/no-null + const portraitContainerRef = useRef(null); const [canRenderStickyCard, setCanRenderStickyCard] = useState(false); - const [isStakingInfoOpened, openStakingInfo, closeStakingInfo] = useFlag(false); - const [isReceiveModalOpened, openReceiveModal, closeReceiveModal] = useFlag(false); + const [shouldRenderDarkStatusBar, setShouldRenderDarkStatusBar] = useState(false); + const [isReceiveModalOpened, openReceiveModal, closeReceiveModal] = useFlag(); + + useOpenFromMainBottomSheet('staking-info', openStakingInfo); + useOpenFromNativeBottomSheet('staking-info', openStakingInfo); + + useOpenFromMainBottomSheet('receive', openReceiveModal); + + const handleOpenStakingInfo = useLastCallback(() => { + if (IS_DELEGATED_BOTTOM_SHEET) { + BottomSheet.openInMain({ key: 'staking-info' }); + } else { + openStakingInfo(); + } + }); + const { isPortrait } = useDeviceScreen(); const { shouldRender: shouldRenderStickyCard, @@ -60,45 +101,74 @@ function Main({ } = useShowTransition(canRenderStickyCard); useEffect(() => { - if (currentAccountId && (isStakingActive || isUnstakeRequested)) { - fetchBackendStakingState(); - } - }, [fetchBackendStakingState, currentAccountId, isStakingActive, isUnstakeRequested]); + setStatusBarStyle(shouldRenderDarkStatusBar); + }, [shouldRenderDarkStatusBar]); useEffect(() => { if (currentTokenSlug) { - setActiveContentTabIndex({ index: ContentTab.Activity }); + setActiveContentTab({ tab: ContentTab.Activity }); + setSwapTokenOut({ tokenSlug: currentTokenSlug }); + changeTransferToken({ tokenSlug: currentTokenSlug }); } }, [currentTokenSlug]); useEffect(() => { - if (!isPortrait) { + if (!isPortrait || !isActive) { setCanRenderStickyCard(false); return undefined; } + const safeAreaTop = IS_CAPACITOR ? getStatusBarHeight() : getSafeAreaTop(); + const rootMarginTop = safeAreaTop > 0 + ? STICKY_CARD_WITH_SAFE_AREA_INTERSECTION_THRESHOLD + : STICKY_CARD_INTERSECTION_THRESHOLD; const observer = new IntersectionObserver((entries) => { const { isIntersecting, boundingClientRect: { left, width } } = entries[0]; setCanRenderStickyCard(entries.length > 0 && !isIntersecting && left >= 0 && left < width); - }, { rootMargin: `${STICKY_CARD_INTERSECTION_THRESHOLD}px 0px 0px` }); + }, { rootMargin: `${rootMarginTop}px 0px 0px` }); + const cardTopSideObserver = new IntersectionObserver((entries) => { + const { isIntersecting } = entries[0]; + + setShouldRenderDarkStatusBar(!isIntersecting); + }, { rootMargin: `${rootMarginTop / 2}px 0px 0px`, threshold: [1] }); const cardElement = cardRef.current; if (cardElement) { observer.observe(cardElement); + cardTopSideObserver.observe(cardElement); } return () => { if (cardElement) { observer.unobserve(cardElement); + cardTopSideObserver.unobserve(cardElement); } }; - }, [isPortrait]); + }, [isActive, isPortrait]); const handleTokenCardClose = useLastCallback(() => { selectToken({ slug: undefined }); - setActiveContentTabIndex({ index: ContentTab.Assets }); + setActiveContentTab({ tab: ContentTab.Assets }); }); + useEffect(() => { + if (!IS_TOUCH_ENV || !isPortrait || !portraitContainerRef.current || !currentTokenSlug) { + return undefined; + } + + return captureEvents(portraitContainerRef.current!, { + excludedClosestSelector: '.token-card', + onSwipe: (e, direction) => { + if (direction === SwipeDirection.Right) { + handleTokenCardClose(); + return true; + } + + return false; + }, + }); + }, [currentTokenSlug, handleTokenCardClose, isPortrait]); + const handleEarnClick = useLastCallback(() => { if (isStakingActive || isUnstakeRequested) { openStakingInfo(); @@ -109,11 +179,22 @@ function Main({ function renderPortraitLayout() { return ( -
+
- - {shouldRenderStickyCard && } + + {shouldRenderStickyCard && ( + + )}
- + - {isPortrait ? renderPortraitLayout() : renderLandscapeLayout()} + {!IS_DELEGATED_BOTTOM_SHEET && (isPortrait ? renderPortraitLayout() : renderLandscapeLayout())} - - + + @@ -161,18 +242,20 @@ function Main({ } export default memo( - withGlobal((global, ownProps, detachWhenChanged): StateProps => { - detachWhenChanged(global.currentAccountId); - const accountState = selectCurrentAccountState(global); - const account = selectCurrentAccount(global); - - return { - isStakingActive: Boolean(accountState?.stakingBalance) && !accountState?.isUnstakeRequested, - isUnstakeRequested: accountState?.isUnstakeRequested, - currentTokenSlug: accountState?.currentTokenSlug, - currentAccountId: global.currentAccountId, - isTestnet: global.settings.isTestnet, - isLedger: !!account?.ledger, - }; - })(Main), + withGlobal( + (global): StateProps => { + const accountState = selectCurrentAccountState(global); + const account = selectCurrentAccount(global); + + return { + isStakingActive: Boolean(accountState?.staking?.balance), + isUnstakeRequested: accountState?.staking?.isUnstakeRequested, + currentTokenSlug: accountState?.currentTokenSlug, + isTestnet: global.settings.isTestnet, + isLedger: !!account?.ledger, + isStakingInfoModalOpen: global.isStakingInfoModalOpen, + }; + }, + (global, _, stickToFirst) => stickToFirst(global.currentAccountId), + )(Main), ); diff --git a/src/components/main/modals/AddAccountModal.module.scss b/src/components/main/modals/AddAccountModal.module.scss index 777ca2f3..c673d06f 100644 --- a/src/components/main/modals/AddAccountModal.module.scss +++ b/src/components/main/modals/AddAccountModal.module.scss @@ -24,12 +24,17 @@ .button { width: 100%; min-width: 0 !important; - max-width: 100% !important; + max-width: 68vw !important; margin: 0 auto; &_single { width: 13.4375rem; } + + @media (max-width: 350px) { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } } .modalButtons { @@ -50,8 +55,11 @@ } .importButtons { - display: flex; + display: grid; + grid-auto-columns: minmax(max-content, 1fr); + grid-auto-flow: column; gap: 1rem; + justify-items: center; width: 100%; margin: 0.875rem 0 1rem; @@ -67,3 +75,12 @@ .sticker { margin: 0 auto 1.375rem; } + +.ledgerButton { + width: auto; + min-width: 8rem !important; + + @supports (width: stretch) { + width: stretch; + } +} diff --git a/src/components/main/modals/AddAccountModal.tsx b/src/components/main/modals/AddAccountModal.tsx index c789c1c2..6a2cd589 100644 --- a/src/components/main/modals/AddAccountModal.tsx +++ b/src/components/main/modals/AddAccountModal.tsx @@ -1,14 +1,17 @@ import React, { memo, useState } from '../../../lib/teact/teact'; -import { getActions, withGlobal } from '../../../global'; +import { getActions, getGlobal, withGlobal } from '../../../global'; import type { Account, HardwareConnectState } from '../../../global/types'; import type { LedgerWalletInfo } from '../../../util/ledger/types'; +import { ApiCommonError } from '../../../api/types'; import { ANIMATED_STICKER_BIG_SIZE_PX, MNEMONIC_COUNT } from '../../../config'; import renderText from '../../../global/helpers/renderText'; import { selectFirstNonHardwareAccount, selectNetworkAccounts } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; +import resolveModalTransitionName from '../../../util/resolveModalTransitionName'; import { IS_LEDGER_SUPPORTED } from '../../../util/windowEnvironment'; +import { callApi } from '../../../api'; import { ANIMATED_STICKERS_PATHS } from '../../ui/helpers/animatedAssets'; import useLang from '../../../hooks/useLang'; @@ -20,8 +23,8 @@ import AnimatedIconWithPreview from '../../ui/AnimatedIconWithPreview'; import Button from '../../ui/Button'; import Modal from '../../ui/Modal'; import ModalHeader from '../../ui/ModalHeader'; -import PasswordForm from '../../ui/PasswordForm'; import Transition from '../../ui/Transition'; +import AddAccountPasswordModal from './AddAccountPasswordModal'; import modalStyles from '../../ui/Modal.module.scss'; import styles from './AddAccountModal.module.scss'; @@ -58,7 +61,13 @@ function AddAccountModal({ isLedgerConnected, isTonAppConnected, }: StateProps) { - const { addAccount, clearAccountError, closeAddAccountModal } = getActions(); + const { + addAccount, + clearAccountError, + closeAddAccountModal, + afterSelectHardwareWallets, + showError, + } = getActions(); const lang = useLang(); const [renderingKey, setRenderingKey] = useState(RenderingState.Initial); @@ -75,7 +84,7 @@ function AddAccountModal({ setIsNewAccountImporting(false); }); - const handleNewAccountClick = useLastCallback(() => { + const handleNewAccountClick = useLastCallback(async () => { if (!firstNonHardwareAccount) { addAccount({ method: 'createAccount', @@ -84,8 +93,16 @@ function AddAccountModal({ return; } - setRenderingKey(RenderingState.Password); - setIsNewAccountImporting(false); + const isApiAvailable = await callApi('checkApiAvailability', { + accountId: getGlobal().currentAccountId!, + }); + + if (isApiAvailable) { + setRenderingKey(RenderingState.Password); + setIsNewAccountImporting(false); + } else { + showError({ error: ApiCommonError.ServerError }); + } }); const handleImportAccountClick = useLastCallback(() => { @@ -105,7 +122,16 @@ function AddAccountModal({ setRenderingKey(RenderingState.ConnectHardware); }); - const handleHardwareWalletConnected = useLastCallback(() => { + const handleAddLedgerWallet = useLastCallback(() => { + afterSelectHardwareWallets({ hardwareSelectedIndices: [hardwareWallets![0].index] }); + closeAddAccountModal(); + }); + + const handleHardwareWalletConnected = useLastCallback((isSingleWallet: boolean) => { + if (isSingleWallet) { + handleAddLedgerWallet(); + return; + } setRenderingKey(RenderingState.SelectAccountsHardware); }); @@ -151,7 +177,7 @@ function AddAccountModal({ {IS_LEDGER_SUPPORTED && ( - + +
@@ -162,12 +163,12 @@ function SignatureModal({ ; +}; + +enum EmailType { + Support, + Security, +} + +const CHANGELLY_PENDING_STATUSES = new Set(['new', 'waiting', 'confirming', 'exchanging', 'sending']); +const CHANGELLY_ERROR_STATUSES = new Set(['failed', 'expired', 'refunded', 'overdue']); +const ONCHAIN_ERROR_STATUSES = new Set(['failed', 'expired']); + +function SwapActivityModal({ activity, tokensBySlug }: StateProps) { + const { + startSwap, + closeActivityInfo, + } = getActions(); + + const lang = useLang(); + const { isPortrait } = useDeviceScreen(); + const animationLevel = getGlobal().settings.animationLevel; + const animationDuration = animationLevel === ANIMATION_LEVEL_MIN + ? 0 + : (isPortrait ? CLOSE_DURATION_PORTRAIT : CLOSE_DURATION) + ANIMATION_END_DELAY; + const renderedActivity = usePrevDuringAnimation(activity, animationDuration); + + const { txId, timestamp, networkFee = 0 } = renderedActivity ?? {}; + + let fromToken: ApiSwapAsset | undefined; + let toToken: ApiSwapAsset | undefined; + let fromAmount = 0; + let toAmount = 0; + let isPending = true; + let isError = false; + let isCexError = false; + let isCexHold = false; + let isCexWaiting = false; + let title = ''; + let errorMessage = ''; + let emailType: EmailType | undefined; + + if (renderedActivity) { + const { + status, from, to, cex, + } = renderedActivity; + fromToken = tokensBySlug?.[from]; + toToken = tokensBySlug?.[to]; + fromAmount = Number(renderedActivity.fromAmount); + toAmount = Number(renderedActivity.toAmount); + + const isFromTon = from === TON_TOKEN_SLUG; + + if (cex) { + isPending = CHANGELLY_PENDING_STATUSES.has(cex.status); + isCexError = CHANGELLY_ERROR_STATUSES.has(cex.status); + isCexHold = cex.status === 'hold'; + // Skip the 'waiting' status for transactions from TON to account for delayed status updates from changelly. + isCexWaiting = cex.status === 'waiting' && !isFromTon; + } else { + isPending = status === 'pending'; + isError = ONCHAIN_ERROR_STATUSES.has(status!); + } + + if (isPending) { + title = lang('Swapping'); + } else if (isCexHold) { + title = lang('Swap Hold'); + errorMessage = lang('Please contact security team to pass the KYC procedure.'); + emailType = EmailType.Security; + } else if (isCexError) { + const { status: cexStatus } = renderedActivity?.cex ?? {}; + if (cexStatus === 'expired' || cexStatus === 'overdue') { + title = lang('Swap Expired'); + errorMessage = lang('You have not sent the coins to the specified address.'); + } else if (cexStatus === 'refunded') { + title = lang('Swap Refunded'); + errorMessage = lang('Exchange failed and coins were refunded to your wallet.'); + } else { + title = lang('Swap Failed'); + errorMessage = lang('Please contact support and provide a transaction ID.'); + emailType = EmailType.Support; + } + } else if (isError) { + title = lang('Swap Failed'); + } else { + title = lang('Swapped'); + } + } + + const [, transactionHash] = (txId || '').split(':'); + const tonscanBaseUrl = TONSCAN_BASE_MAINNET_URL; + const tonscanTransactionUrl = transactionHash ? `${tonscanBaseUrl}tx/${transactionHash}` : undefined; + + const handleClose = useLastCallback(() => { + closeActivityInfo({ id: renderedActivity!.id }); + }); + + const handleSwapClick = useLastCallback(() => { + closeActivityInfo({ id: activity!.id }); + startSwap({ + tokenInSlug: activity!.from, + tokenOutSlug: activity!.to, + amountIn: fromAmount, + isPortrait, + }); + }); + + function renderHeader() { + return ( +
+
+ {title} + {isPending && ( + + )} +
+ {!!timestamp && ( +
+ {formatFullDay(lang.code!, timestamp)}, {formatTime(timestamp)} +
+ )} +
+ ); + } + + function renderFooterButton() { + let isButtonVisible = true; + let buttonText = 'Swap Again'; + + if (isCexWaiting) { + return ( + + ); + } + + if (isCexHold) { + isButtonVisible = false; + } else if (isCexError) { + const { status: cexStatus } = renderedActivity?.cex ?? {}; + if (cexStatus === 'expired' || cexStatus === 'refunded') { + buttonText = 'Try Again'; + } + } + + if (!isButtonVisible) { + return undefined; + } + + return ( + + ); + } + + function renderErrorMessage() { + const email = emailType === EmailType.Support + ? CHANGELLY_SUPPORT_EMAIL + : CHANGELLY_SECURITY_EMAIL; + + return ( +
+ {errorMessage} + {emailType !== undefined && {email}} +
+ ); + } + + function renderSwapInfo() { + const payinAddress = renderedActivity?.cex?.payinAddress; + + if (isCexWaiting) { + return ( +
+ {lang('$swap_changelly_to_ton_description1', { + value: ( + + {formatCurrencyExtended(Number(fromAmount), fromToken?.symbol ?? '', true)} + + ), + blockchain: ( + + {getBlockchainNetworkName(fromToken?.blockchain)} + + ), + time: , + })} + + +
+ ); + } + + if (isCexError || isCexHold) { + return ( +
+ {lang('Changelly Payment Address')} + +
+ ); + } + + return ( + <> + {payinAddress && ( +
+ {lang('Changelly Payment Address')} + +
+ )} +
+ {lang('Exchange rate')} + {renderCurrency(renderedActivity, fromToken, toToken)} +
+
+ + {lang('Blockchain fee')} + +
+ {formatCurrency(networkFee, TON_SYMBOL)} +
+
+ + ); + } + + function renderContent() { + return ( + <> + +
+ {renderSwapInfo()} + {renderErrorMessage()} +
+
+ {renderFooterButton()} +
+ + ); + } + + return ( + +
+ {tonscanTransactionUrl && ( + + + + )} + {renderContent()} +
+
+ ); +} + +export default memo( + withGlobal((global): StateProps => { + const accountState = selectCurrentAccountState(global); + + const id = accountState?.currentActivityId; + const activity = id ? accountState?.activities?.byId[id] : undefined; + + return { + activity: activity?.kind === 'swap' ? activity : undefined, + tokensBySlug: global.swapTokenInfo?.bySlug, + }; + })(SwapActivityModal), +); + +function renderCurrency(activity?: ApiSwapActivity, fromToken?: ApiSwapAsset, toToken?: ApiSwapAsset) { + const rate = getSwapRate(activity?.fromAmount, activity?.toAmount, fromToken, toToken); + + if (!rate) return undefined; + + return ( +
+ 1 {rate.firstCurrencySymbol}{' ≈ '} + {rate.price}{' '}{rate.secondCurrencySymbol} +
+ ); +} diff --git a/src/components/main/modals/TransactionModal.module.scss b/src/components/main/modals/TransactionModal.module.scss index 61c85827..fc9dbbdf 100644 --- a/src/components/main/modals/TransactionModal.module.scss +++ b/src/components/main/modals/TransactionModal.module.scss @@ -19,7 +19,7 @@ } .copyButtonWrapper { - margin-bottom: 1rem; + margin-bottom: 1.25rem; } .comment { @@ -98,7 +98,10 @@ .unstakeTime { position: relative; - max-width: 18rem; + display: flex; + flex-direction: column; + + max-width: 19rem; margin: 0.25rem auto 0; padding: 0.75rem; @@ -130,7 +133,6 @@ .transactionHeader { display: flex; flex-direction: column; - gap: 0.3125rem; } .headerTitle { @@ -139,6 +141,8 @@ align-items: center; justify-content: center; + margin-bottom: 0.25rem; + font-size: 1.0625rem; font-weight: 700; color: var(--color-black); @@ -168,7 +172,7 @@ display: flex; align-items: center; - padding-left: 1rem; + padding-left: 0.5rem; font-size: 0.8125rem; font-weight: 700; @@ -184,13 +188,14 @@ font-size: 1rem; font-weight: 600; color: var(--color-black); + word-break: break-all; background-color: var(--color-background-first); border-radius: 1rem; } .advancedTooltip { - cursor: pointer; + cursor: var(--custom-cursor, pointer); margin-left: 0.25rem; @@ -204,3 +209,51 @@ width: 18.9375rem; } + +.errorCexBlock { + display: flex; + flex-direction: column; + gap: 1rem; + align-items: center; + justify-content: center; + + margin-top: 0.75rem; +} + +.errorCexMessage { + font-size: 0.8125rem; + font-weight: 500; + line-height: 0.8125rem; + color: var(--color-gray-2); + text-align: center; +} + +.errorCexEmail { + font-size: 1.0625rem; + font-weight: 700; + line-height: 1.0625rem; + color: var(--color-blue); + text-align: center; +} + +.changellyInfoBlock { + display: flex; + flex-direction: column; + gap: 1rem; + align-items: center; +} + +.changellyDescription { + font-size: 0.8125rem; + font-weight: 500; + color: var(--color-gray-2); + text-align: center; +} + +.changellyTextField { + width: 100%; +} + +.changellyDescriptionBold { + font-weight: 700; +} diff --git a/src/components/main/modals/TransactionModal.tsx b/src/components/main/modals/TransactionModal.tsx index 5eeee63c..b50edf92 100644 --- a/src/components/main/modals/TransactionModal.tsx +++ b/src/components/main/modals/TransactionModal.tsx @@ -1,10 +1,12 @@ import React, { memo, useState } from '../../../lib/teact/teact'; import { getActions, getGlobal, withGlobal } from '../../../global'; -import type { ApiToken, ApiTransactionActivity } from '../../../api/types'; +import type { ApiStakingType, ApiToken, ApiTransactionActivity } from '../../../api/types'; import { - ANIMATION_END_DELAY, ANIMATION_LEVEL_MIN, + ANIMATION_END_DELAY, + ANIMATION_LEVEL_MIN, + IS_CAPACITOR, STAKING_CYCLE_DURATION_MS, TON_SYMBOL, TON_TOKEN_SLUG, @@ -14,11 +16,12 @@ import { import { bigStrToHuman, getIsTxIdLocal } from '../../../global/helpers'; import { selectCurrentAccountState } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; +import { vibrateOnSuccess } from '../../../util/capacitor'; import { formatFullDay, formatRelativeHumanDateTime, formatTime } from '../../../util/dateFormat'; +import resolveModalTransitionName from '../../../util/resolveModalTransitionName'; import { callApi } from '../../../api'; import { useDeviceScreen } from '../../../hooks/useDeviceScreen'; -import useFlag from '../../../hooks/useFlag'; import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; import usePrevDuringAnimation from '../../../hooks/usePrevDuringAnimation'; @@ -29,8 +32,10 @@ import TransactionAmount from '../../common/TransactionAmount'; import AmountWithFeeTextField from '../../ui/AmountWithFeeTextField'; import Button from '../../ui/Button'; import InteractiveTextField from '../../ui/InteractiveTextField'; -import Modal, { ANIMATION_DURATION, ANIMATION_DURATION_PORTRAIT } from '../../ui/Modal'; +import Modal, { CLOSE_DURATION, CLOSE_DURATION_PORTRAIT } from '../../ui/Modal'; +import ModalHeader from '../../ui/ModalHeader'; import PasswordForm from '../../ui/PasswordForm'; +import Transition from '../../ui/Transition'; import transferStyles from '../../transfer/Transfer.module.scss'; import modalStyles from '../../ui/Modal.module.scss'; @@ -45,7 +50,14 @@ type StateProps = { isTestnet?: boolean; startOfStakingCycle?: number; endOfStakingCycle?: number; + stakingType?: ApiStakingType; + isUnstakeRequested?: boolean; + isInstantUnstakeRequested?: boolean; }; +const enum SLIDES { + initial, + password, +} const EMPTY_HASH_VALUE = 'NOHASH'; @@ -56,19 +68,26 @@ function TransactionModal({ isTestnet, startOfStakingCycle, endOfStakingCycle, + stakingType, + isUnstakeRequested, + isInstantUnstakeRequested, }: StateProps) { const { startTransfer, startStaking, closeActivityInfo, + setIsPinPadPasswordAccepted, + clearIsPinPadPasswordAccepted, } = getActions(); const lang = useLang(); const { isPortrait } = useDeviceScreen(); + const [currentSlide, setCurrentSlide] = useState(SLIDES.initial); + const [nextKey, setNextKey] = useState(SLIDES.password); const animationLevel = getGlobal().settings.animationLevel; const animationDuration = animationLevel === ANIMATION_LEVEL_MIN ? 0 - : (isPortrait ? ANIMATION_DURATION_PORTRAIT : ANIMATION_DURATION) + ANIMATION_END_DELAY; + : (isPortrait ? CLOSE_DURATION_PORTRAIT : CLOSE_DURATION) + ANIMATION_END_DELAY; const renderedTransaction = usePrevDuringAnimation(transaction, animationDuration); const [unstakeDate, setUnstakeDate] = useState(Date.now() + STAKING_CYCLE_DURATION_MS); @@ -79,12 +98,12 @@ function TransactionModal({ comment, encryptedComment, fee, - txId, + id, isIncoming, slug, timestamp, } = renderedTransaction || {}; - const [, transactionHash] = (txId || '').split(':'); + const [, transactionHash] = (id || '').split(':'); const isStaking = Boolean(transaction?.type); const token = slug ? tokensBySlug?.[slug] : undefined; @@ -93,8 +112,8 @@ function TransactionModal({ const addressName = (address && savedAddresses?.[address]) || transaction?.metadata?.name; const isScam = Boolean(transaction?.metadata?.isScam); + const [isLoading, setIsLoading] = useState(false); const [decryptedComment, setDecryptedComment] = useState(); - const [isPasswordModalOpen, openPasswordModal, closePasswordModal] = useFlag(); const [passwordError, setPasswordError] = useState(); const tonscanBaseUrl = isTestnet ? TONSCAN_BASE_TESTNET_URL : TONSCAN_BASE_MAINNET_URL; @@ -103,7 +122,12 @@ function TransactionModal({ : undefined; const withUnstakeTimer = Boolean( - transaction?.type === 'unstakeRequest' && startOfStakingCycle && transaction.timestamp >= startOfStakingCycle, + (stakingType === 'liquid' ? isInstantUnstakeRequested || isUnstakeRequested : true) + && transaction?.type === 'unstakeRequest' + && startOfStakingCycle + && endOfStakingCycle + && transaction.timestamp >= startOfStakingCycle + && transaction.timestamp <= endOfStakingCycle, ); const { @@ -114,8 +138,6 @@ function TransactionModal({ useSyncEffect(() => { if (renderedTransaction) { setDecryptedComment(undefined); - } else { - closeActivityInfo(); } }, [renderedTransaction]); @@ -125,9 +147,20 @@ function TransactionModal({ } }, [endOfStakingCycle]); + const openPasswordSlide = useLastCallback(() => { + setCurrentSlide(SLIDES.password); + setNextKey(undefined); + }); + + const closePasswordSlide = useLastCallback(() => { + setCurrentSlide(SLIDES.initial); + setNextKey(SLIDES.password); + }); + const handleSendClick = useLastCallback(() => { - closeActivityInfo(); + closeActivityInfo({ id: id! }); startTransfer({ + isPortrait, tokenSlug: slug || TON_TOKEN_SLUG, toAddress: address, amount: Math.abs(amountHuman), @@ -136,11 +169,12 @@ function TransactionModal({ }); const handleStartStakingClick = useLastCallback(() => { - closeActivityInfo(); + closeActivityInfo({ id: id! }); startStaking(); }); const handlePasswordSubmit = useLastCallback(async (password: string) => { + setIsLoading(true); const result = await callApi( 'decryptComment', getGlobal().currentAccountId!, @@ -148,22 +182,36 @@ function TransactionModal({ fromAddress!, password, ); + setIsLoading(false); if (!result) { setPasswordError('Wrong password, please try again'); return; } - closePasswordModal(); + if (IS_CAPACITOR) { + setIsPinPadPasswordAccepted(); + await vibrateOnSuccess(true); + } + + closePasswordSlide(); setDecryptedComment(result); }); + const handleClose = useLastCallback(() => { + closeActivityInfo({ id: id! }); + if (IS_CAPACITOR) { + clearIsPinPadPasswordAccepted(); + } + }); + const clearPasswordError = useLastCallback(() => { setPasswordError(undefined); }); function renderHeader() { - const isLocal = txId && getIsTxIdLocal(txId); + const isLocal = id && getIsTxIdLocal(id); + const title = isIncoming ? lang('Received') : isLocal @@ -171,23 +219,38 @@ function TransactionModal({ : lang('Sent'); return ( -
-
- {title} - {isLocal && ( - +
+
+
+ {title} + {isLocal && ( + + )} + {isScam && {lang('Scam')}} +
+ {!!timestamp && ( +
+ {formatFullDay(lang.code!, timestamp)}, {formatTime(timestamp)} +
)} - {isScam && {lang('Scam')}}
- {!!timestamp && ( -
- {formatFullDay(lang.code!, timestamp)}, {formatTime(timestamp)} -
- )} +
); } @@ -224,31 +287,11 @@ function TransactionModal({ text={encryptedComment ? decryptedComment : comment} spoiler={spoiler} spoilerRevealText={encryptedComment ? lang('Decrypt') : lang('Display')} - spoilerCallback={openPasswordModal} + spoilerCallback={openPasswordSlide} copyNotification={lang('Comment was copied!')} className={styles.copyButtonWrapper} textClassName={styles.comment} /> - {encryptedComment && ( - - - - )} ); } @@ -306,27 +349,70 @@ function TransactionModal({ ); } + // eslint-disable-next-line consistent-return + function renderContent(isActive: boolean, isFrom: boolean, currentKey: number) { + switch (currentKey) { + case SLIDES.initial: + return ( + <> + {renderHeader()} +
+ {tonscanTransactionUrl && ( + + + + )} + {renderTransactionContent()} +
+ + ); + case SLIDES.password: + if (!encryptedComment) return undefined; + + return ( + <> + {!IS_CAPACITOR && } + + + ); + } + } + return ( -
- {tonscanTransactionUrl && ( - - - - )} - {renderTransactionContent()} -
+ + {renderContent} +
); } @@ -337,7 +423,10 @@ export default memo( const txId = accountState?.currentActivityId; const activity = txId ? accountState?.activities?.byId[txId] : undefined; - const { startOfCycle: startOfStakingCycle, endOfCycle: endOfStakingCycle } = accountState?.poolState || {}; + const { + start: startOfStakingCycle, + end: endOfStakingCycle, + } = accountState?.staking || {}; const savedAddresses = accountState?.savedAddresses; return { @@ -347,6 +436,9 @@ export default memo( isTestnet: global.settings.isTestnet, startOfStakingCycle, endOfStakingCycle, + stakingType: accountState?.staking?.type, + isUnstakeRequested: accountState?.staking?.isUnstakeRequested, + isInstantUnstakeRequested: accountState?.staking?.isInstantUnstakeRequested, }; })(TransactionModal), ); diff --git a/src/components/main/sections/Actions/LandscapeActions.module.scss b/src/components/main/sections/Actions/LandscapeActions.module.scss index a1752cf8..22c9990a 100644 --- a/src/components/main/sections/Actions/LandscapeActions.module.scss +++ b/src/components/main/sections/Actions/LandscapeActions.module.scss @@ -14,7 +14,7 @@ $tab-1-height: 348; .tabs { display: grid; - grid-template-columns: 1fr 1fr 1fr; + grid-template-columns: 1fr 1fr 1fr 1fr; width: 100%; @@ -50,10 +50,12 @@ $tab-1-height: 348; transition: color 150ms; - &:not(.active) { - &:hover, - &:focus { - color: var(--color-blue); + @media (hover: hover) { + &:not(.active) { + &:hover, + &:focus { + color: var(--color-blue); + } } } diff --git a/src/components/main/sections/Actions/LandscapeActions.tsx b/src/components/main/sections/Actions/LandscapeActions.tsx index 550ba024..92e61936 100644 --- a/src/components/main/sections/Actions/LandscapeActions.tsx +++ b/src/components/main/sections/Actions/LandscapeActions.tsx @@ -1,9 +1,7 @@ import React, { memo, useEffect, useMemo, useRef, } from '../../../../lib/teact/teact'; -import { - getActions, withGlobal, -} from '../../../../global'; +import { getActions, withGlobal } from '../../../../global'; import { ElectronEvent } from '../../../../electron/types'; import { ActiveTab } from '../../../../global/types'; @@ -19,12 +17,13 @@ import useLastCallback from '../../../../hooks/useLastCallback'; import StakingInfoContent from '../../../staking/StakingInfoContent'; import StakingInitial from '../../../staking/StakingInitial'; +import SwapInitial from '../../../swap/SwapInitial'; import TransferInitial from '../../../transfer/TransferInitial'; import Transition, { ACTIVE_SLIDE_CLASS_NAME, TO_SLIDE_CLASS_NAME } from '../../../ui/Transition'; import styles from './LandscapeActions.module.scss'; -const TABS = [ActiveTab.Receive, ActiveTab.Transfer, ActiveTab.Stake]; +const TABS = [ActiveTab.Receive, ActiveTab.Transfer, ActiveTab.Swap, ActiveTab.Stake]; interface OwnProps { hasStaking?: boolean; @@ -58,11 +57,18 @@ function LandscapeActions({ isStaking, ); + const isSwapAllowed = !(isTestnet || isLedger); + const isStakingAllowed = !(isTestnet || isLedger); + const areNotAllTabs = !isSwapAllowed || !isStakingAllowed; + useEffect(() => { - if ((isTestnet || isLedger) && [ActiveTab.Stake].includes(activeTabIndex)) { + if ( + (!isSwapAllowed && activeTabIndex === ActiveTab.Swap) + || (!isStakingAllowed && activeTabIndex === ActiveTab.Stake) + ) { setActiveTabIndex({ index: ActiveTab.Transfer }); } - }, [activeTabIndex, isTestnet, isLedger]); + }, [activeTabIndex, isTestnet, isLedger, isSwapAllowed, isStakingAllowed]); function renderCurrentTab(isActive: boolean) { switch (activeTabIndex) { @@ -76,6 +82,13 @@ function LandscapeActions({
); + case ActiveTab.Swap: + return ( +
+ +
+ ); + case ActiveTab.Stake: if (hasStaking || isUnstakeRequested) { return ( @@ -109,7 +122,7 @@ function LandscapeActions({ return (
- {!isTestnet && !isLedger && ( + {isSwapAllowed && ( +
setActiveTabIndex({ index: ActiveTab.Swap })} + > + + {lang('Swap')} + + +
+ )} + {isStakingAllowed && (
((global, ownProps, detachWhenChanged): StateProps => { - detachWhenChanged(global.currentAccountId); - - const accountState = selectAccountState(global, global.currentAccountId!) ?? {}; - - return { - activeTabIndex: accountState?.landscapeActionsActiveTabIndex, - isTestnet: global.settings.isTestnet, - }; - })(LandscapeActions), + withGlobal( + (global): StateProps => { + const accountState = selectAccountState(global, global.currentAccountId!) ?? {}; + + return { + activeTabIndex: accountState?.landscapeActionsActiveTabIndex, + isTestnet: global.settings.isTestnet, + }; + }, + (global, _, stickToFirst) => stickToFirst(global.currentAccountId), + )(LandscapeActions), ); diff --git a/src/components/main/sections/Actions/PortraitActions.module.scss b/src/components/main/sections/Actions/PortraitActions.module.scss index aea1d245..5703a0f6 100644 --- a/src/components/main/sections/Actions/PortraitActions.module.scss +++ b/src/components/main/sections/Actions/PortraitActions.module.scss @@ -38,9 +38,11 @@ transition: background-color 150ms, color 150ms; - &:hover, - &:focus { - color: var(--color-blue); + @media (hover: hover) { + &:hover, + &:focus { + color: var(--color-blue); + } } &:active { diff --git a/src/components/main/sections/Actions/PortraitActions.tsx b/src/components/main/sections/Actions/PortraitActions.tsx index 2f25c081..d3e7de6a 100644 --- a/src/components/main/sections/Actions/PortraitActions.tsx +++ b/src/components/main/sections/Actions/PortraitActions.tsx @@ -1,13 +1,16 @@ +import type { URLOpenListenerEvent } from '@capacitor/app'; +import { App as CapacitorApp } from '@capacitor/app'; import React, { memo, useEffect } from '../../../../lib/teact/teact'; import { getActions } from '../../../../global'; import { ElectronEvent } from '../../../../electron/types'; -import { TON_TOKEN_SLUG } from '../../../../config'; -import { bigStrToHuman } from '../../../../global/helpers'; import buildClassName from '../../../../util/buildClassName'; +import { clearLaunchUrl, getLaunchUrl } from '../../../../util/capacitor'; +import { processDeeplink } from '../../../../util/processDeeplink'; import useLang from '../../../../hooks/useLang'; +import useLastCallback from '../../../../hooks/useLastCallback'; import Button from '../../../ui/Button'; @@ -25,21 +28,39 @@ interface OwnProps { function PortraitActions({ hasStaking, isTestnet, isUnstakeRequested, onEarnClick, onReceiveClick, isLedger, }: OwnProps) { - const { startTransfer } = getActions(); + const { startTransfer, startSwap } = getActions(); const lang = useLang(); + const isSwapAllowed = !(isTestnet || isLedger); + const isStakingAllowed = !(isTestnet || isLedger); + useEffect(() => { - return window.electron?.on(ElectronEvent.DEEPLINK, (params: any) => { - startTransfer({ - tokenSlug: TON_TOKEN_SLUG, - toAddress: params.to, - amount: bigStrToHuman(params.amount), - comment: params.text, - }); + return window.electron?.on(ElectronEvent.DEEPLINK, ({ url }: { url: string }) => { + processDeeplink(url); }); }, [startTransfer]); + useEffect(() => { + const launchUrl = getLaunchUrl(); + if (launchUrl) { + processDeeplink(launchUrl); + clearLaunchUrl(); + } + + return CapacitorApp.addListener('appUrlOpen', (event: URLOpenListenerEvent) => { + processDeeplink(event.url); + }).remove; + }, [startTransfer]); + + const handleStartSwap = useLastCallback(() => { + startSwap({ isPortrait: true }); + }); + + const handleStartTransfer = useLastCallback(() => { + startTransfer({ isPortrait: true }); + }); + return (
@@ -47,11 +68,17 @@ function PortraitActions({ {lang('Receive')} - - {!isTestnet && !isLedger && ( + {isSwapAllowed && ( + + )} + {isStakingAllowed && ( + {shouldRenderSettingsButton && ( + + )} + {shouldRenderQrScannerButton && ( + + )} ); } @@ -212,17 +252,19 @@ function AccountSelector({ {isEdit && renderInput()} {shouldRender && renderAccountsChooser()} - ); } -export default memo(withGlobal((global): StateProps => { - const accounts = selectNetworkAccounts(global); - - return { - currentAccountId: global.currentAccountId!, - currentAccount: accounts?.[global.currentAccountId!], - accounts, - }; -})(AccountSelector)); +export default memo(withGlobal( + (global): StateProps => { + const accounts = selectNetworkAccounts(global); + + return { + currentAccountId: global.currentAccountId!, + currentAccount: accounts?.[global.currentAccountId!], + accounts, + }; + }, + (global, _, stickToFirst) => stickToFirst(global.currentAccountId), +)(AccountSelector)); diff --git a/src/components/main/sections/Card/Card.tsx b/src/components/main/sections/Card/Card.tsx index 2dbce5e2..d1961c28 100644 --- a/src/components/main/sections/Card/Card.tsx +++ b/src/components/main/sections/Card/Card.tsx @@ -1,31 +1,33 @@ import type { Ref } from 'react'; -import React, { - memo, useEffect, useMemo, -} from '../../../../lib/teact/teact'; +import React, { memo, useEffect, useMemo } from '../../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../../global'; +import type { ApiBaseCurrency } from '../../../../api/types'; import type { UserToken } from '../../../../global/types'; import { - DEFAULT_PRICE_CURRENCY, + IS_CAPACITOR, TON_TOKEN_SLUG, TONSCAN_BASE_MAINNET_URL, TONSCAN_BASE_TESTNET_URL, } from '../../../../config'; import { selectAccount, selectCurrentAccountState, selectCurrentAccountTokens } from '../../../../global/selectors'; import buildClassName from '../../../../util/buildClassName'; +import { vibrateOnSuccess } from '../../../../util/capacitor'; import captureEscKeyListener from '../../../../util/captureEscKeyListener'; import { copyTextToClipboard } from '../../../../util/clipboard'; -import { formatCurrency } from '../../../../util/formatNumber'; +import { formatCurrency, getShortCurrencySymbol } from '../../../../util/formatNumber'; import { shortenAddress } from '../../../../util/shortenAddress'; import { getTokenCardColor } from '../../helpers/card_colors'; import { buildTokenValues } from './helpers/buildTokenValues'; import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev'; +import useHistoryBack from '../../../../hooks/useHistoryBack'; import useLang from '../../../../hooks/useLang'; import useLastCallback from '../../../../hooks/useLastCallback'; import useShowTransition from '../../../../hooks/useShowTransition'; +import AnimatedCounter from '../../../ui/AnimatedCounter'; import Loading from '../../../ui/Loading'; import AccountSelector from './AccountSelector'; import TokenCard from './TokenCard'; @@ -34,8 +36,10 @@ import styles from './Card.module.scss'; interface OwnProps { ref?: Ref; + forceCloseAccountSelector?: boolean; onTokenCardClose: NoneToVoidFunction; onApyClick: NoneToVoidFunction; + onQrScanPress?: NoneToVoidFunction; } interface StateProps { @@ -44,6 +48,7 @@ interface StateProps { activeDappOrigin?: string; currentTokenSlug?: string; isTestnet?: boolean; + baseCurrency?: ApiBaseCurrency; } function Card({ @@ -52,15 +57,19 @@ function Card({ tokens, activeDappOrigin, currentTokenSlug, + forceCloseAccountSelector, onTokenCardClose, onApyClick, + onQrScanPress, isTestnet, + baseCurrency, }: OwnProps & StateProps) { const { showNotification } = getActions(); const lang = useLang(); const tonscanBaseUrl = isTestnet ? TONSCAN_BASE_TESTNET_URL : TONSCAN_BASE_MAINNET_URL; const tonscanAddressUrl = `${tonscanBaseUrl}address/${address}`; + const shortBaseSymbol = getShortCurrencySymbol(baseCurrency); const currentToken = useMemo(() => { return tokens ? tokens.find((token) => token.slug === currentTokenSlug) : undefined; @@ -102,6 +111,11 @@ function Card({ transitionClassNames: dappClassNames, } = useShowTransition(Boolean(dappDomain)); + useHistoryBack({ + isActive: Boolean(currentTokenSlug), + onBack: onTokenCardClose, + }); + useEffect( () => (shouldRenderTokenCard ? captureEscKeyListener(onTokenCardClose) : undefined), [shouldRenderTokenCard, onTokenCardClose], @@ -111,7 +125,10 @@ function Card({ if (!address) return; showNotification({ message: lang('Address was copied!') as string, icon: 'icon-copy' }); - copyTextToClipboard(address); + void copyTextToClipboard(address); + if (IS_CAPACITOR) { + void vibrateOnSuccess(); + } }); const { @@ -130,23 +147,45 @@ function Card({ return ( <>
- + {shouldRenderDapp && (
{renderingDappDomain}
)} -
- {DEFAULT_PRICE_CURRENCY} - {primaryWholePart} - {primaryFractionPart && .{primaryFractionPart}} -
+ { + shortBaseSymbol.length === 1 ? ( +
+ {shortBaseSymbol} + + {primaryFractionPart && ( + + + + )} +
+ ) : ( +
+ + {primaryFractionPart && ( + + + + )} + +  {shortBaseSymbol} + +
+ ) + } {primaryValue !== 0 && (
{changePrefix}   - {Math.abs(changePercent!)}% · {formatCurrency(Math.abs(changeValue!), DEFAULT_PRICE_CURRENCY)} + + {' · '} +
)}
@@ -190,16 +229,21 @@ function Card({ ); } -export default memo(withGlobal((global, ownProps, detachWhenChanged): StateProps => { - detachWhenChanged(global.currentAccountId); - const { address } = selectAccount(global, global.currentAccountId!) || {}; - const accountState = selectCurrentAccountState(global); - - return { - address, - tokens: selectCurrentAccountTokens(global), - activeDappOrigin: accountState?.activeDappOrigin, - currentTokenSlug: accountState?.currentTokenSlug, - isTestnet: global.settings.isTestnet, - }; -})(Card)); +export default memo( + withGlobal( + (global): StateProps => { + const { address } = selectAccount(global, global.currentAccountId!) || {}; + const accountState = selectCurrentAccountState(global); + + return { + address, + tokens: selectCurrentAccountTokens(global), + activeDappOrigin: accountState?.activeDappOrigin, + currentTokenSlug: accountState?.currentTokenSlug, + isTestnet: global.settings.isTestnet, + baseCurrency: global.settings.baseCurrency, + }; + }, + (global, _, stickToFirst) => stickToFirst(global.currentAccountId), + )(Card), +); diff --git a/src/components/main/sections/Card/StickyCard.module.scss b/src/components/main/sections/Card/StickyCard.module.scss index 0a24a770..63c52f63 100644 --- a/src/components/main/sections/Card/StickyCard.module.scss +++ b/src/components/main/sections/Card/StickyCard.module.scss @@ -1,3 +1,5 @@ +@import "../../../../styles/mixins"; + .root { position: fixed; top: 0; @@ -42,6 +44,17 @@ opacity: 1; } + + @supports (padding-top: env(safe-area-inset-top)) { + padding-top: env(safe-area-inset-top); + } + + @include respond-below(xs) { + // Fix for opera, dead zone of 37 pixels in extension window on windows + :global(html.is-windows.is-opera.is-extension) & { + padding-top: 1rem !important; + } + } } .content { @@ -55,6 +68,12 @@ max-width: 27rem; height: 3.75rem; margin: 0 auto; + + :global(html.with-safe-area-top) & { + box-sizing: content-box; + height: 1.5rem; + padding-bottom: 1.125rem; + } } .account { diff --git a/src/components/main/sections/Card/StickyCard.tsx b/src/components/main/sections/Card/StickyCard.tsx index b7d2a451..0ebfeb9e 100644 --- a/src/components/main/sections/Card/StickyCard.tsx +++ b/src/components/main/sections/Card/StickyCard.tsx @@ -1,11 +1,12 @@ import React, { memo, useMemo } from '../../../../lib/teact/teact'; import { withGlobal } from '../../../../global'; +import type { ApiBaseCurrency } from '../../../../api/types'; import type { UserToken } from '../../../../global/types'; -import { DEFAULT_PRICE_CURRENCY } from '../../../../config'; import { selectCurrentAccountTokens } from '../../../../global/selectors'; import buildClassName from '../../../../util/buildClassName'; +import { getShortCurrencySymbol } from '../../../../util/formatNumber'; import { buildTokenValues } from './helpers/buildTokenValues'; import AccountSelector from './AccountSelector'; @@ -14,17 +15,25 @@ import styles from './StickyCard.module.scss'; interface OwnProps { classNames?: string; + onQrScanPress?: NoneToVoidFunction; } interface StateProps { tokens?: UserToken[]; + baseCurrency?: ApiBaseCurrency; } -function StickyCard({ classNames, tokens }: OwnProps & StateProps) { +function StickyCard({ + classNames, + tokens, + onQrScanPress, + baseCurrency, +}: OwnProps & StateProps) { const values = useMemo(() => { return tokens ? buildTokenValues(tokens) : undefined; }, [tokens]); + const shortBaseSymbol = getShortCurrencySymbol(baseCurrency); const { primaryWholePart, primaryFractionPart } = values || {}; return ( @@ -33,10 +42,13 @@ function StickyCard({ classNames, tokens }: OwnProps & StateProps) {
- {DEFAULT_PRICE_CURRENCY} + {shortBaseSymbol} {primaryWholePart} {primaryFractionPart && .{primaryFractionPart}}
@@ -46,10 +58,13 @@ function StickyCard({ classNames, tokens }: OwnProps & StateProps) { ); } -export default memo(withGlobal((global, ownProps, detachWhenChanged): StateProps => { - detachWhenChanged(global.currentAccountId); - - return { - tokens: selectCurrentAccountTokens(global), - }; -})(StickyCard)); +export default memo( + withGlobal( + (global): StateProps => { + return { + tokens: selectCurrentAccountTokens(global), + }; + }, + (global, _, stickToFirst) => stickToFirst(global.currentAccountId), + )(StickyCard), +); diff --git a/src/components/main/sections/Card/TokenCard.tsx b/src/components/main/sections/Card/TokenCard.tsx index c8ef6367..d9f14cf3 100644 --- a/src/components/main/sections/Card/TokenCard.tsx +++ b/src/components/main/sections/Card/TokenCard.tsx @@ -1,19 +1,22 @@ import React, { memo, useState } from '../../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../../global'; +import type { ApiBaseCurrency } from '../../../../api/types'; import type { UserToken } from '../../../../global/types'; -import { DEFAULT_PRICE_CURRENCY, TON_TOKEN_SLUG } from '../../../../config'; +import { TON_TOKEN_SLUG } from '../../../../config'; import { selectCurrentAccountState } from '../../../../global/selectors'; import buildClassName from '../../../../util/buildClassName'; import { calcChangeValue } from '../../../../util/calcChangeValue'; import { formatShortDay } from '../../../../util/dateFormat'; -import { formatCurrency } from '../../../../util/formatNumber'; +import { formatCurrency, getShortCurrencySymbol } from '../../../../util/formatNumber'; import { round } from '../../../../util/round'; import { ASSET_LOGO_PATHS } from '../../../ui/helpers/assetLogos'; import { useDeviceScreen } from '../../../../hooks/useDeviceScreen'; +import useForceUpdate from '../../../../hooks/useForceUpdate'; import useLang from '../../../../hooks/useLang'; +import useTimeout from '../../../../hooks/useTimeout'; import TokenPriceChart from '../../../common/TokenPriceChart'; import Button from '../../../ui/Button'; @@ -34,6 +37,7 @@ interface OwnProps { interface StateProps { period?: keyof typeof HISTORY_PERIODS; apyValue: number; + baseCurrency?: ApiBaseCurrency; } const HISTORY_PERIODS = { @@ -43,11 +47,7 @@ const HISTORY_PERIODS = { } as const; const DEFAULT_PERIOD = '24h'; - -const COIN_MARKET_CAP_TOKENS: Record = { - toncoin: 'toncoin', - 'ton-tgr': 'tgr', -}; +const OFFLINE_TIMEOUT = 120000; // 2 minutes const CHART_DIMENSIONS = { width: 300, height: 64 }; @@ -59,11 +59,14 @@ function TokenCard({ apyValue, onApyClick, onClose, + baseCurrency, }: OwnProps & StateProps) { const { setCurrentTokenPeriod } = getActions(); const { isPortrait } = useDeviceScreen(); - const lang = useLang(); + const forceUpdate = useForceUpdate(); + + const shortBaseSymbol = getShortCurrencySymbol(baseCurrency); const currentHistoryPeriod = HISTORY_PERIODS[period].historyKey; const currentChangePeriod = HISTORY_PERIODS[period].changeKey; @@ -79,11 +82,15 @@ function TokenCard({ const history = currentHistoryPeriod in token ? token[currentHistoryPeriod] : undefined; + const tokenLastUpdatedAt = history?.length ? history[history.length - 1][0] * 1000 : undefined; const selectedHistoryPoint = history?.[selectedHistoryIndex]; const price = selectedHistoryPoint?.[1] || lastPrice; + const isLoading = !tokenLastUpdatedAt || (Date.now() - tokenLastUpdatedAt > OFFLINE_TIMEOUT); const dateStr = selectedHistoryPoint ? formatShortDay(lang.code!, selectedHistoryPoint[0] * 1000, true, true) - : lang('Now'); + : (isLoading && tokenLastUpdatedAt ? formatShortDay(lang.code!, tokenLastUpdatedAt, true, false) : lang('Now')); + + useTimeout(forceUpdate, isLoading ? undefined : OFFLINE_TIMEOUT, [tokenLastUpdatedAt]); const initialPrice = history?.[0]?.[1]; const change = initialPrice @@ -100,7 +107,7 @@ function TokenCard({ const withChange = Boolean(change !== undefined); const withHistory = Boolean(history?.length); const historyStartDay = withHistory ? new Date(history![0][0] * 1000) : undefined; - const withCmcButton = slug in COIN_MARKET_CAP_TOKENS; + const withCmcButton = Boolean(token.cmcSlug); function renderChart() { return ( @@ -117,7 +124,7 @@ function TokenCard({ } return ( -
+
+ ); +} + +export default memo(Swap); + +function renderCurrency(activity: ApiSwapActivity, fromToken?: ApiSwapAsset, toToken?: ApiSwapAsset) { + const rate = getSwapRate(activity.fromAmount, activity.toAmount, fromToken, toToken); + + if (!rate) return undefined; + + return ( +
+ {rate.firstCurrencySymbol}{' ≈ '} + + {rate.price}{' '}{rate.secondCurrencySymbol} + +
+ ); +} diff --git a/src/components/main/sections/Content/Token.module.scss b/src/components/main/sections/Content/Token.module.scss index 49c7bf4b..fc89a24f 100644 --- a/src/components/main/sections/Content/Token.module.scss +++ b/src/components/main/sections/Content/Token.module.scss @@ -38,8 +38,14 @@ } } - &:focus, - &:hover { + @media (hover: hover) { + &:focus, + &:hover { + background-color: var(--color-interactive-item-hover); + } + } + + &.active { background-color: var(--color-interactive-item-hover); } diff --git a/src/components/main/sections/Content/Token.tsx b/src/components/main/sections/Content/Token.tsx index 6645bf2d..76fb266e 100644 --- a/src/components/main/sections/Content/Token.tsx +++ b/src/components/main/sections/Content/Token.tsx @@ -1,11 +1,12 @@ import React, { memo } from '../../../../lib/teact/teact'; +import type { ApiBaseCurrency } from '../../../../api/types'; import type { UserToken } from '../../../../global/types'; -import { DEFAULT_PRICE_CURRENCY, TON_TOKEN_SLUG } from '../../../../config'; +import { TON_TOKEN_SLUG } from '../../../../config'; import buildClassName from '../../../../util/buildClassName'; import { calcChangeValue } from '../../../../util/calcChangeValue'; -import { formatCurrency } from '../../../../util/formatNumber'; +import { formatCurrency, getShortCurrencySymbol } from '../../../../util/formatNumber'; import { round } from '../../../../util/round'; import { ASSET_LOGO_PATHS } from '../../../ui/helpers/assetLogos'; @@ -24,7 +25,9 @@ interface OwnProps { isInvestorView?: boolean; classNames?: string; apyValue?: number; + isActive?: boolean; onClick: (slug: string) => void; + baseCurrency?: ApiBaseCurrency; } function Token({ @@ -34,7 +37,9 @@ function Token({ apyValue, isInvestorView, classNames, + isActive, onClick, + baseCurrency, }: OwnProps) { const { name, @@ -53,6 +58,8 @@ function Token({ const changeValue = Math.abs(round(calcChangeValue(value, change), 4)); const changePercent = Math.abs(round(change * 100, 2)); const withApy = Boolean(apyValue) && slug === TON_TOKEN_SLUG; + const fullClassName = buildClassName(styles.container, isActive && styles.active, classNames); + const shortBaseSymbol = getShortCurrencySymbol(baseCurrency); const { shouldRender: shouldRenderApy, @@ -86,7 +93,7 @@ function Token({ function renderInvestorView() { return ( -
- +
{renderChangeIcon()}% - +
@@ -128,7 +135,7 @@ function Token({ const canRenderApy = Boolean(apyValue) && slug === TON_TOKEN_SLUG; return ( -
diff --git a/src/components/main/sections/Content/Transaction.module.scss b/src/components/main/sections/Content/Transaction.module.scss index 34170bb6..5a58ae37 100644 --- a/src/components/main/sections/Content/Transaction.module.scss +++ b/src/components/main/sections/Content/Transaction.module.scss @@ -18,23 +18,27 @@ color: var(--color-black); text-align: left; - &:not(:last-of-type) { - &::after { - content: ''; - - position: absolute; - right: 1.5rem; - bottom: 0; - left: 3.625rem; - - height: 0.0625rem; - /* stylelint-disable-next-line plugin/whole-pixel */ - box-shadow: inset 0 -0.025rem 0 0 var(--color-separator); + &::after { + content: ''; + + position: absolute; + right: 1.5rem; + bottom: 0; + left: 3.625rem; + + height: 0.0625rem; + /* stylelint-disable-next-line plugin/whole-pixel */ + box-shadow: inset 0 -0.025rem 0 0 var(--color-separator); + } + + @media (hover: hover) { + &:focus, + &:hover { + background-color: var(--color-interactive-item-hover); } } - &:focus, - &:hover { + &.active { background-color: var(--color-interactive-item-hover); } @@ -44,6 +48,12 @@ } } +.itemLast { + &::after { + content: none; + } +} + .iconArrow { position: absolute; top: 0.8125rem; @@ -87,12 +97,40 @@ background-color: var(--color-activity-purple-background); } + + &_error { + color: var(--color-red); + + background: var(--color-activity-red-background) !important; + } + + &_swap { + font-size: 1.875rem; + + background-image: linear-gradient(135deg, #EDF1F6 13.89%, #EAFBF1 86.11%); + + :global(.theme-dark) & { + background-image: linear-gradient(135deg, #2E3947 13.89%, #1F3838 86.11%); + } + + &:not(.icon_error)::before { + color: transparent; + + background: linear-gradient(74deg, #53657B 29.67%, #1EC160 45.27%); + background-clip: text; + background-size: 100%; + + :global(.theme-dark) & { + background-image: linear-gradient(74deg, #A3B8CA 29.67%, #2CD36F 45.27%); + } + } + } } .iconWaiting { position: absolute; top: 2.25rem; - left: 2.375rem; + left: 2.125rem; font-size: 1rem; line-height: 1; @@ -119,6 +157,10 @@ color: var(--color-activity-green-text); } +.iconWaitingStake { + color: var(--color-activity-purple-text); +} + .leftBlock { grid-area: left; @@ -222,3 +264,44 @@ width: 2.375rem; margin-left: 0.25rem; } + +.swapAmount { + display: flex; + align-items: center; +} + +.swapArrowRight { + padding: 0 0.125rem; + + color: transparent; + + background: linear-gradient(90deg, #8399AE 0.98%, #2CD36F 99.02%); + background-clip: text; + background-size: 100%; + + :global(.theme-dark) & { + background-image: linear-gradient(90deg, #8799B3 0.98%, #2CD36F 99.02%); + } +} + +.swapSell { + color: var(--color-gray-2); +} + +.swapBuy { + color: var(--color-green); +} + +.swapError { + color: var(--color-red); +} + +.swapHold { + color: var(--color-gray-2); +} + +.isSwapErrorMessage { + font-size: 0.75rem; + font-weight: 400; + color: var(--color-red); +} diff --git a/src/components/main/sections/Content/Transaction.tsx b/src/components/main/sections/Content/Transaction.tsx index 8defd972..c2bd0103 100644 --- a/src/components/main/sections/Content/Transaction.tsx +++ b/src/components/main/sections/Content/Transaction.tsx @@ -23,6 +23,8 @@ type OwnProps = { ref?: Ref; tokensBySlug?: Record; transaction: ApiTransactionActivity; + isLast: boolean; + isActive: boolean; apyValue: number; savedAddresses?: Record; onClick: (id: string) => void; @@ -32,8 +34,10 @@ function Transaction({ ref, tokensBySlug, transaction, + isActive, apyValue, savedAddresses, + isLast, onClick, }: OwnProps) { const lang = useLang(); @@ -143,18 +147,28 @@ function Transaction({ ); } + const waitingIconClassName = buildClassName( + styles.iconWaiting, + isStaking && styles.iconWaitingStake, + 'icon-clock', + ); + return ( +
@@ -85,3 +116,33 @@ export default memo( }; })(Content), ); + +function initializeQrCode(cb: NoneToVoidFunction) { + import('qr-code-styling') + .then(({ default: QrCodeStyling }) => { + qrCode = new QrCodeStyling({ + width: QR_SIZE, + height: QR_SIZE, + image: './logo.svg', + margin: 0, + type: 'canvas', + dotsOptions: { + type: 'rounded', + }, + cornersSquareOptions: { + type: 'extra-rounded', + }, + imageOptions: { + imageSize: 0.4, + margin: 8, + crossOrigin: 'anonymous', + }, + qrOptions: { + errorCorrectionLevel: 'M', + }, + data: formatTransferUrl(''), + }); + + cb(); + }); +} diff --git a/src/components/receive/InvoiceModal.tsx b/src/components/receive/InvoiceModal.tsx index 16516f3b..85d6dfbc 100644 --- a/src/components/receive/InvoiceModal.tsx +++ b/src/components/receive/InvoiceModal.tsx @@ -1,6 +1,4 @@ -import React, { - memo, useMemo, useState, -} from '../../lib/teact/teact'; +import React, { memo, useMemo, useState } from '../../lib/teact/teact'; import { withGlobal } from '../../global'; import type { UserToken } from '../../global/types'; @@ -17,7 +15,6 @@ import { ASSET_LOGO_PATHS } from '../ui/helpers/assetLogos'; import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; -import Button from '../ui/Button'; import Dropdown from '../ui/Dropdown'; import Input from '../ui/Input'; import InteractiveTextField from '../ui/InteractiveTextField'; @@ -32,18 +29,14 @@ interface StateProps { } type OwnProps = { - backButtonText?: string; isOpen: boolean; - onBackButtonClick: () => void; onClose: () => void; }; function InvoiceModal({ address, - backButtonText = 'Back', isOpen, tokens, - onBackButtonClick, onClose, }: StateProps & OwnProps) { const lang = useLang(); @@ -95,13 +88,14 @@ function InvoiceModal({ return ( -
+
{renderText(lang('$receive_invoice_description'))}
-

+

{lang('Share this URL to receive TON')}

- - -
- -
+ ); } diff --git a/src/components/receive/QrModal.tsx b/src/components/receive/QrModal.tsx deleted file mode 100644 index 826ba286..00000000 --- a/src/components/receive/QrModal.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import QrCodeStyling from 'qr-code-styling'; -import React, { memo, useEffect, useRef } from '../../lib/teact/teact'; -import { withGlobal } from '../../global'; - -import { selectAccount } from '../../global/selectors'; -import buildClassName from '../../util/buildClassName'; -import { shortenAddress } from '../../util/shortenAddress'; -import formatTransferUrl from '../../util/ton/formatTransferUrl'; - -import useLang from '../../hooks/useLang'; - -import Button from '../ui/Button'; -import Modal from '../ui/Modal'; - -import styles from './ReceiveModal.module.scss'; - -const QR_SIZE = 600; -let qrCode: QrCodeStyling; - -type StateProps = { - address?: string; -}; - -type OwnProps = { - backButtonText?: string; - isOpen: boolean; - onBackButtonClick: () => void; - onClose: () => void; -}; - -function QrModal({ - address, backButtonText, isOpen, onBackButtonClick, onClose, -}: StateProps & OwnProps) { - const lang = useLang(); - // eslint-disable-next-line no-null/no-null - const qrCodeRef = useRef(null); - - useEffect(() => { - if (isOpen) { - if (!qrCode) { - qrCode = initializeQrCode(); - } - qrCode.append(qrCodeRef.current || undefined); - } - }, [isOpen]); - - useEffect(() => { - if (!address || !qrCode || !isOpen) { - return; - } - qrCode.update({ data: formatTransferUrl(address) }); - }, [address, isOpen]); - - return ( - -
-
-

{address && shortenAddress(address)}

- -
- -
-
- - ); -} - -export default memo( - withGlobal((global): StateProps => { - const address = selectAccount(global, global.currentAccountId!)?.address; - - return { - address, - }; - })(QrModal), -); - -function initializeQrCode() { - return new QrCodeStyling({ - width: QR_SIZE, - height: QR_SIZE, - image: './logo.svg', - margin: 0, - type: 'canvas', - dotsOptions: { - type: 'rounded', - }, - cornersSquareOptions: { - type: 'extra-rounded', - }, - imageOptions: { - imageSize: 0.4, - margin: 8, - crossOrigin: 'anonymous', - }, - qrOptions: { - errorCorrectionLevel: 'M', - }, - data: formatTransferUrl(''), - }); -} diff --git a/src/components/receive/ReceiveModal.module.scss b/src/components/receive/ReceiveModal.module.scss index 4503452d..77ed85a6 100644 --- a/src/components/receive/ReceiveModal.module.scss +++ b/src/components/receive/ReceiveModal.module.scss @@ -1,6 +1,10 @@ @import '../../styles/mixins'; .content { + display: flex; + flex-direction: column; + flex-grow: 1; + padding: 0 1rem 1rem; @supports (padding-bottom: env(safe-area-inset-bottom)) { @@ -8,29 +12,13 @@ } } -.contentQr { - margin-top: -0.25rem; -} +.contentTitle { + margin-bottom: 1.5rem; -.info { font-size: 0.9375rem; - font-weight: 600; - line-height: 1.0625rem; - color: var(--color-gray-1); + line-height: 1.1875rem; + color: var(--color-gray-2); text-align: center; - - &_push { - margin-bottom: 1.25rem; - } - - &_small { - margin-top: 0.75rem; - margin-bottom: 0; - } - - &.infoStatic { - padding-top: 0.5rem; - } } .qrCode { @@ -40,12 +28,13 @@ aspect-ratio: 1; width: 100%; - max-width: 20.5rem; - max-height: 20.5rem; - margin: 0 auto; + max-width: 11rem; + margin: 1rem auto 0; background-color: var(--color-white); - border-radius: var(--border-radius-default); + border-radius: var(--border-radius-card); + + transition: opacity 350ms ease-in-out; canvas { position: absolute; @@ -59,17 +48,23 @@ } } -.description { - margin: 1.5rem 0 0.375rem 0.5rem; +.qrCodeHidden { + opacity: 0; + + transition: none; +} + +.label { + margin: 0 0 0.5rem 0.5rem; font-size: 0.8125rem; font-weight: 700; line-height: 1; color: var(--color-gray-2); +} - &_forInvoice { - margin-top: 0.3125rem; - } +.labelForInvoice { + margin-top: 0.3125rem; } .copyButtonStatic { @@ -77,31 +72,9 @@ border: 1px solid var(--color-separator-input-stroke) !important; } -.buttons { - display: flex; - gap: 0.5rem; - justify-content: center; - - margin-top: 2rem; -} - -.qrButton { - width: auto !important; - min-width: 1.75rem !important; - padding: 0.5rem !important; - - font-size: 1.875rem !important; - - @include respond-above(xs) { - background-color: var(--color-gray-button-background-desktop) !important; - - &:hover { - background-color: var(--color-gray-button-background-desktop-hover) !important; - } - } -} - .invoiceButton { + max-width: 100% !important; + @include respond-above(xs) { background-color: var(--color-gray-button-background-desktop) !important; @@ -111,10 +84,6 @@ } } -.qrIcon { - max-width: 1.75rem; -} - .tokenDropdown { --offset-y-value: calc(100% - 3.25rem); --offset-x-value: 0; diff --git a/src/components/receive/ReceiveModal.tsx b/src/components/receive/ReceiveModal.tsx index aa2cfabb..3a1e6753 100644 --- a/src/components/receive/ReceiveModal.tsx +++ b/src/components/receive/ReceiveModal.tsx @@ -1,5 +1,11 @@ -import React, { memo } from '../../lib/teact/teact'; +import { BottomSheet } from 'native-bottom-sheet'; +import React, { memo, useEffect } from '../../lib/teact/teact'; +import { IS_DELEGATED_BOTTOM_SHEET } from '../../util/windowEnvironment'; + +import { useOpenFromMainBottomSheet } from '../../hooks/useDelegatedBottomSheet'; +import { useOpenFromNativeBottomSheet } from '../../hooks/useDelegatingBottomSheet'; +import { useDeviceScreen } from '../../hooks/useDeviceScreen'; import useFlag from '../../hooks/useFlag'; import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; @@ -7,7 +13,6 @@ import useLastCallback from '../../hooks/useLastCallback'; import Modal from '../ui/Modal'; import Content from './Content'; import InvoiceModal from './InvoiceModal'; -import QrModal from './QrModal'; import styles from './ReceiveModal.module.scss'; @@ -18,24 +23,49 @@ type Props = { function ReceiveModal({ isOpen, onClose }: Props) { const lang = useLang(); - const [isQrModalOpen, openQrModal, closeQrModal] = useFlag(false); + const { isPortrait } = useDeviceScreen(); const [isInvoiceModalOpen, openInvoiceModal, closeInvoiceModal] = useFlag(false); + useOpenFromNativeBottomSheet('invoice', openInvoiceModal); + useOpenFromMainBottomSheet('invoice', openInvoiceModal); + + useEffect(() => { + if (isOpen && !isPortrait) { + onClose(); + } + }, [isOpen, isPortrait, onClose]); + + const handleOpenInvoiceModal = useLastCallback(() => { + onClose(); + + if (IS_DELEGATED_BOTTOM_SHEET) { + BottomSheet.openInMain({ key: 'invoice' }); + } else { + openInvoiceModal(); + } + }); + const handleClose = useLastCallback(() => { onClose(); closeInvoiceModal(); - closeQrModal(); }); return ( <> - -
- -
+ + - - + ); } diff --git a/src/components/receive/ReceiveStatic.tsx b/src/components/receive/ReceiveStatic.tsx index 370cfa9d..a3eb7066 100644 --- a/src/components/receive/ReceiveStatic.tsx +++ b/src/components/receive/ReceiveStatic.tsx @@ -1,41 +1,23 @@ import React, { memo } from '../../lib/teact/teact'; import useFlag from '../../hooks/useFlag'; -import useLang from '../../hooks/useLang'; -import useLastCallback from '../../hooks/useLastCallback'; import Content from './Content'; import InvoiceModal from './InvoiceModal'; -import QrModal from './QrModal'; type Props = { className?: string; }; function ReceiveStatic({ className }: Props) { - const lang = useLang(); - const [isQrModalOpen, openQrModal, closeQrModal] = useFlag(false); const [isInvoiceModalOpen, openInvoiceModal, closeInvoiceModal] = useFlag(false); - const handleClose = useLastCallback(() => { - closeInvoiceModal(); - closeQrModal(); - }); - return (
- - +
); diff --git a/src/components/settings/SelectTokens.tsx b/src/components/settings/SelectTokens.tsx deleted file mode 100644 index e8fd5a18..00000000 --- a/src/components/settings/SelectTokens.tsx +++ /dev/null @@ -1,348 +0,0 @@ -import React, { - memo, useEffect, useMemo, useRef, useState, -} from '../../lib/teact/teact'; -import { getActions, withGlobal } from '../../global'; - -import type { UserToken } from '../../global/types'; - -import buildClassName from '../../util/buildClassName'; -import captureEscKeyListener from '../../util/captureEscKeyListener'; -import { formatCurrencyForBigValue, formatInteger } from '../../util/formatNumber'; -import { getIsAddressValid } from '../../util/getIsAddressValid'; -import { REM } from '../../util/windowEnvironment'; -import { ASSET_LOGO_PATHS } from '../ui/helpers/assetLogos'; - -import useLang from '../../hooks/useLang'; -import useLastCallback from '../../hooks/useLastCallback'; -import useScrolledState from '../../hooks/useScrolledState'; -import useShowTransition from '../../hooks/useShowTransition'; - -import Portal from '../ui/Portal'; -import Transition from '../ui/Transition'; - -import styles from './Settings.module.scss'; - -interface OwnProps { - isOpen: boolean; - type?: 'price' | 'balance'; - shouldFilter?: boolean; - tokens?: UserToken[]; - position?: { - top: number; - right: number; - left: number; - width: number; - }; - offset?: { - top: number; - left: number; - right: number; - bottom: number; - }; - onClose: NoneToVoidFunction; - onSelect: (token: UserToken) => void; -} - -interface StateProps { - token?: UserToken; - isLoading?: boolean; -} - -enum SearchState { - Popular, - Loading, - Token, - Empty, -} - -const SLIDE_ANIMATION_DURATION_MS = 250; - -const DEFAULT_OFFSET = { - top: 0.25 * REM, - right: 0.5 * REM, - bottom: REM, - left: 0, -}; - -function SelectTokens({ - token, - type = 'price', - shouldFilter = false, - tokens, - isOpen, - isLoading, - position, - offset = DEFAULT_OFFSET, - onClose, - onSelect, -}: OwnProps & StateProps) { - const { - importToken, - resetImportToken, - } = getActions(); - const lang = useLang(); - - const { shouldRender, transitionClassNames } = useShowTransition(isOpen); - const [searchValue, setSearchValue] = useState(''); - const [style, setStyle] = useState(); - const [renderingKey, setRenderingKey] = useState(SearchState.Popular); - - // eslint-disable-next-line no-null/no-null - const elementRef = useRef(null); - const isImportingRef = useRef(false); - - const { - handleScroll: handleContentScroll, - isAtBeginning: isContentNotScrolled, - } = useScrolledState(); - - useEffect(() => { - if (!elementRef.current) return; - - if (position) { - const { - top, right, - } = position; - - const { width: componentWidth, height: componentHeight } = elementRef.current.getBoundingClientRect(); - const isTop = window.innerHeight > top + componentHeight; - const verticalStyle = isTop - ? `top: ${top + offset.top}px;` - : `top: calc(100% - ${componentHeight + offset.bottom}px);`; - - setStyle(`${verticalStyle} left: ${(right - componentWidth) - offset.right}px;`); - } - }, [position, offset]); - - useEffect( - () => (shouldRender ? captureEscKeyListener(onClose) : undefined), - [onClose, shouldRender], - ); - - useEffect(() => { - setSearchValue(''); - resetImportToken(); - }, [shouldRender]); - - useEffect(() => { - if (getIsAddressValid(searchValue)) { - isImportingRef.current = true; - importToken({ address: searchValue }); - } else { - resetImportToken(); - } - }, [importToken, searchValue]); - - const filteredTokenList = useMemo(() => tokens?.filter((t) => !t.isDisabled).filter( - ({ symbol, keywords }) => { - const isSymbol = symbol.toLowerCase().includes(searchValue.toLowerCase()); - const isKeyword = keywords?.find((key) => key.toLowerCase().includes(searchValue.toLowerCase())); - return isSymbol || isKeyword; - }, - ) ?? [], [tokens, searchValue]); - - useEffect(() => { - const isImporting = isLoading || isImportingRef.current; - - if (isImporting && getIsAddressValid(searchValue)) { - isImportingRef.current = false; - setRenderingKey(SearchState.Loading); - } else if (token && getIsAddressValid(searchValue)) { - setRenderingKey(SearchState.Token); - } else if (filteredTokenList.length === 0) { - setRenderingKey(SearchState.Empty); - } else { - setRenderingKey(SearchState.Popular); - } - }, [token, isLoading, filteredTokenList, searchValue]); - - const handleTokenClick = useLastCallback((selectedToken: UserToken) => { - onClose(); - setTimeout(() => { - onSelect(selectedToken); - }, SLIDE_ANIMATION_DURATION_MS); - }); - - const popularTokenList = useMemo(() => filteredTokenList.map((t) => { - const image = t?.image ?? ASSET_LOGO_PATHS[t?.symbol.toLowerCase() as keyof typeof ASSET_LOGO_PATHS]; - const isAvailable = !shouldFilter; - const value = isAvailable - ? type === 'price' - ? formatCurrencyForBigValue(t.price) - : formatInteger(t.amount) - : lang('Unavailable'); - const handleClick = isAvailable ? () => handleTokenClick(t) : undefined; - return ( -
- -
- {t.symbol} - - {value} - -
-
- ); - }), [filteredTokenList, handleTokenClick, type, lang, shouldFilter]); - - // eslint-disable-next-line consistent-return - function renderContent(isActive: boolean, isFrom: boolean, currentKey: number) { - const { image, symbol } = token ?? {}; - - let content; - - switch (currentKey) { - case SearchState.Popular: - content = popularTokenList; - break; - case SearchState.Loading: - content = ( -
-
-
- - -
-
- ); - break; - case SearchState.Token: - content = ( -
handleTokenClick(token!)}> - {symbol} -
- {symbol} - {formatCurrencyForBigValue(0)} -
-
- ); - break; - case SearchState.Empty: - content = ( -
-
- -
-
- - {lang('Token Not Found')} - - - {lang('Try another keyword or address.')} - -
-
- ); - break; - } - - const isSingle = currentKey !== SearchState.Popular || popularTokenList.length === 1; - - return ( -
-
- {content} -
-
- ); - } - - const fullClassName = buildClassName( - styles.addTokenDialog, - transitionClassNames, - ); - - return shouldRender && ( - -
-
-
-
-
- - setSearchValue(e.target.value)} - value={searchValue} - /> -
-
- - {renderContent} - -
-
- - ); -} - -function LazyImage({ symbol, image, isAvailable }: { symbol: string; image: string; isAvailable?: boolean }) { - const [isLoading, setIsLoading] = useState(true); - const [isImageVisible, setIsImageVisible] = useState(true); - const [firstLetter] = symbol; - - return ( - <> - {isImageVisible && ( - {symbol} setIsLoading(false)} - onError={() => setIsImageVisible(false)} - /> - )} - {isLoading && ( -
- {firstLetter} -
- )} - - ); -} - -export default memo(withGlobal((global): StateProps => { - const { isLoading, token } = global.settings.importToken ?? {}; - - return { - isLoading, - token, - }; -})(SelectTokens)); diff --git a/src/components/settings/Settings.module.scss b/src/components/settings/Settings.module.scss index 229be886..d260b6b3 100644 --- a/src/components/settings/Settings.module.scss +++ b/src/components/settings/Settings.module.scss @@ -23,6 +23,14 @@ background-color: var(--color-background-second); } +@supports (padding-top: env(safe-area-inset-top)) { + html:global(:not(.is-native-bottom-sheet)) { + .transitionSlide { + padding-top: env(safe-area-inset-top); + } + } +} + .developerTitle { margin: 1rem auto 1.5rem; @@ -41,8 +49,7 @@ grid-template-columns: 1fr max-content 1fr; align-items: center; - margin-bottom: 2.25rem; - padding: 1.5rem 0.125rem 0; + padding: 1.5rem 0.125rem 1.375rem; font-size: 1.0625rem; font-weight: 700; @@ -54,6 +61,10 @@ padding-top: 2.3125rem; } } + + :global(html.with-safe-area-top) & { + padding-top: 0.75rem; + } } .languageHeader { @@ -76,6 +87,12 @@ font-size: 1.5rem; } +.headerBackInContent { + position: absolute; + top: 1.125rem; + left: 0.125rem; +} + .headerTitle { display: flex; justify-content: center; @@ -97,17 +114,24 @@ height: 100%; min-height: 0; - padding: 0 1rem 1rem; + padding: 0.75rem 1rem 1rem; @include adapt-padding-to-scrollbar(1rem); &_noScroll { overflow: visible; } + + @supports (padding-bottom: max(env(safe-area-inset-bottom), 1rem)) { + padding-bottom: max(env(safe-area-inset-bottom), 1rem); + } } -.contentInModal { - padding-top: 0.75rem; +.contentFullSize { + overflow: visible; + align-items: center; + + padding: 0 !important; } .blockTitle { @@ -162,14 +186,14 @@ color: var(--color-black); &_active { + pointer-events: none; + font-weight: 700; color: var(--color-blue); } } .themeIcon { - position: relative; - &::after { content: ''; @@ -311,9 +335,17 @@ padding: 0.875rem 1rem; } - &:focus, - &:hover { - background-color: var(--color-interactive-item-hover); + @media (hover: hover) { + &:focus, + &:hover { + background-color: var(--color-interactive-item-hover); + } + } + + @media (pointer: coarse) { + &:active { + background-color: var(--color-interactive-item-hover); + } } &:active { @@ -361,12 +393,6 @@ } } -.languageInfo { - display: flex; - flex-direction: column; - gap: 0.25rem; -} - .languageMain { font-size: 0.9375rem; font-weight: 600; @@ -451,21 +477,17 @@ color: var(--color-black); } -.dapp { - position: relative; - - &:not(:last-of-type)::after { - content: ''; +.dapp:not(:last-of-type)::after { + content: ''; - position: absolute; - right: 0; - bottom: 0; - left: 3.875rem; + position: absolute; + right: 0; + bottom: 0; + left: 3.875rem; - height: 0.0625rem; - /* stylelint-disable-next-line plugin/whole-pixel */ - box-shadow: inset 0 -0.025rem 0 0 var(--color-separator); - } + height: 0.0625rem; + /* stylelint-disable-next-line plugin/whole-pixel */ + box-shadow: inset 0 -0.025rem 0 0 var(--color-separator); } .backButton { @@ -480,6 +502,10 @@ margin: 0 auto 0.875rem; } +.stickerNativeBiometric { + margin-top: auto; +} + .title { margin-bottom: 1.25rem; @@ -566,7 +592,7 @@ } .tokenSortIcon { - cursor: pointer; + cursor: var(--custom-cursor, pointer); padding: 0 0.375rem; @@ -605,6 +631,8 @@ height: 0.1875rem; } +.dapp, +.themeIcon, .contentRelative, .sortableContainer { position: relative; @@ -617,7 +645,9 @@ height: 46rem; @supports (height: env(safe-area-inset-bottom)) { - height: calc(46rem + env(safe-area-inset-bottom)); + html:global(:not(.is-native-bottom-sheet)) { + height: calc(46rem + env(safe-area-inset-bottom)); + } } } @@ -625,7 +655,9 @@ height: 38rem; @supports (height: env(safe-area-inset-bottom)) { - height: calc(38rem + env(safe-area-inset-bottom)); + html:global(:not(.is-native-bottom-sheet)) { + height: calc(38rem + env(safe-area-inset-bottom)); + } } } @@ -646,270 +678,13 @@ } } -.addTokenTransition { - min-height: 4.5rem; -} - -.addTokenDialog { - position: absolute; - z-index: var(--z-menu-bubble); - transform: translateY(-0.375rem) !important; - - transition: var(--dropdown-transition) !important; - - :global(html.animation-level-0) &, - &:global(.open) { - transform: translateY(0) !important; - } - - &:global(.closing) { - transition: var(--dropdown-transition-backwards) !important; - - :global(html.animation-level-0) & { - transition: var(--no-animation-transition) !important; - } - } - - :global(html.animation-level-0) & { - transition: var(--no-animation-transition) !important; - } -} - -.addTokenBlock { - position: relative; - z-index: var(--z-menu-bubble); - - width: 19rem; - max-height: 15rem; - - background-color: var(--color-background-drop-down); - border-radius: var(--border-radius-default); - box-shadow: var(--default-shadow); -} - -.addTokenSearch { - position: relative; - z-index: 1; - - display: flex; - - padding: 0.5rem; - - background-color: var(--color-background-drop-down); - border-radius: var(--border-radius-default) var(--border-radius-default) 0 0; - - &_bordered { - /* stylelint-disable-next-line plugin/whole-pixel */ - box-shadow: 0 0.035rem 0 0 var(--color-separator); - } -} - -.addTokenInputWrapper { - display: flex; - align-items: center; - - width: 100%; - padding: 0.3125rem 0.375rem; - - font-size: 1.25rem; - line-height: 1; - color: var(--color-gray-3); - - background-color: var(--color-background-drop-down-search); - border-radius: var(--border-radius-default); -} - -.addTokenInput { - width: 100%; - - font-size: 0.9375rem; - color: var(--color-black); - - background: transparent; - border: none; - outline: none; - - appearance: none; - - &::placeholder { - font-weight: 600; - color: var(--color-gray-3); - } - - &:hover, - &:focus { - &::placeholder { - color: var(--color-interactive-input-text-hover-active); - } - } -} - -.addTokenContentWrapper { - overflow: hidden; - - max-height: 12rem; - - background-color: var(--color-background-drop-down); - border-radius: 0 0 var(--border-radius-default) var(--border-radius-default); -} - -.addTokenContent { - overflow-y: auto; - display: grid; - grid-gap: 0.5rem; - grid-template-columns: repeat(auto-fill, minmax(48%, 1fr)); - - max-height: 12rem; - padding: 0 0.5rem 0.5rem; - - &:global(.custom-scroll) { - overflow-x: hidden; - overflow-y: scroll; - - @include adapt-padding-to-scrollbar(0.5rem); - } - - &_single { - grid-template-columns: 1fr; - } -} - -.addTokenItem { - cursor: pointer; - - position: relative; - - overflow: hidden; - display: flex; - gap: 0.625rem; - align-items: center; - - height: 4rem; - padding-left: 0.75rem; - - border-radius: 0.625rem; - box-shadow: inset 0 0 0 0.0625rem var(--color-separator-input-stroke); - - &:hover { - background-color: var(--color-interactive-item-hover); - } -} - -.tokenInfo, -.addTokenText { +.languageInfo, +.tokenInfo { display: flex; flex-direction: column; gap: 0.25rem; } -.addTokenText_center { - align-items: center; - justify-content: center; - - width: 100%; -} - -.addTokenSymbol { - font-size: 0.9375rem; - font-weight: 600; - line-height: 1; - color: var(--color-black); - - &_loading { - width: 6.25rem; - height: 0.8125rem; - - background-color: var(--color-separator-input-stroke); - border-radius: var(--border-radius-default); - } - - &_gray, - &_disabled { - color: var(--color-gray-2); - } -} - -.addTokenPrice { - font-size: 0.75rem; - font-weight: 600; - line-height: 1; - color: var(--color-gray-2); - - &_loading { - width: 5rem; - height: 0.625rem; - - background-color: var(--color-separator-input-stroke); - border-radius: var(--border-radius-default); - } - - &_gray, - &_disabled { - color: var(--color-gray-3); - } -} - -.addTokenIconLetter { - font-size: 1.0625rem; - font-weight: 800; - color: var(--color-gray-3); - text-transform: capitalize; -} - -.addTokenIcon { - z-index: 2; - - flex-shrink: 0; - - width: 2.25rem; - height: 2.25rem; - - border-radius: 100%; - - &_loading { - background-color: var(--color-separator-input-stroke); - } - - &_empty { - display: flex; - align-items: center; - justify-content: center; - - box-shadow: inset 0 0 0 0.0625rem var(--color-gray-4); - } - - &_disabled { - opacity: 0.5; - } - - &_symbol { - position: absolute; - z-index: 1; - - display: flex; - align-items: center; - justify-content: center; - - box-shadow: inset 0 0 0 0.0625rem var(--color-gray-4); - } -} - -.emptyIcon { - margin-top: 0.3125rem; - - font-size: 0.9375rem; - color: var(--color-gray-3); -} - -.backdrop { - position: fixed; - z-index: var(--z-menu-backdrop); - top: -100vh; - right: -100vw; - bottom: -100vh; - left: -100vw; -} - .stickerAndTitle { display: flex; column-gap: 1rem; diff --git a/src/components/settings/Settings.tsx b/src/components/settings/Settings.tsx index 51cc071d..91e040f2 100644 --- a/src/components/settings/Settings.tsx +++ b/src/components/settings/Settings.tsx @@ -3,40 +3,53 @@ import React, { } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; -import type { ApiDapp } from '../../api/types'; -import type { - AnimationLevel, LangCode, Theme, UserToken, -} from '../../global/types'; +import { type GlobalState, SettingsState, type UserToken } from '../../global/types'; import { APP_NAME, APP_VERSION, + IS_CAPACITOR, IS_DAPP_SUPPORTED, - IS_ELECTRON, IS_EXTENSION, LANG_LIST, PROXY_HOSTS, TELEGRAM_WEB_URL, } from '../../config'; import { - selectAccountSettings, selectCurrentAccountTokens, selectPopularTokensWithoutAccountTokens, + selectAccountSettings, + selectCurrentAccountTokens, } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; +import { getIsNativeBiometricAuthSupported } from '../../util/capacitor'; import captureEscKeyListener from '../../util/captureEscKeyListener'; -import { captureSwipe, SwipeDirection } from '../../util/captureSwipe'; -import { IS_ANDROID, IS_LEDGER_SUPPORTED, IS_TOUCH_ENV } from '../../util/windowEnvironment'; +import resolveModalTransitionName from '../../util/resolveModalTransitionName'; +import { captureControlledSwipe } from '../../util/swipeController'; +import { + IS_BIOMETRIC_AUTH_SUPPORTED, + IS_DELEGATED_BOTTOM_SHEET, + IS_ELECTRON, + IS_LEDGER_SUPPORTED, + IS_TOUCH_ENV, +} from '../../util/windowEnvironment'; import useFlag from '../../hooks/useFlag'; +import useHistoryBack from '../../hooks/useHistoryBack'; import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; +import useModalTransitionKeys from '../../hooks/useModalTransitionKeys'; +import usePrevious2 from '../../hooks/usePrevious2'; import useScrolledState from '../../hooks/useScrolledState'; import useShowTransition from '../../hooks/useShowTransition'; +import { useStateRef } from '../../hooks/useStateRef'; import LogOutModal from '../main/modals/LogOutModal'; import Button from '../ui/Button'; import ModalHeader from '../ui/ModalHeader'; import Switcher from '../ui/Switcher'; import Transition from '../ui/Transition'; +import Biometrics from './biometrics/Biometrics'; +import NativeBiometricsToggle from './biometrics/NativeBiometricsToggle'; +import SettingsNativeBiometricsTurnOn from './biometrics/NativeBiometricsTurnOn'; import SettingsAbout from './SettingsAbout'; import SettingsAppearance from './SettingsAppearance'; import SettingsAssets from './SettingsAssets'; @@ -44,6 +57,7 @@ import SettingsDapps from './SettingsDapps'; import SettingsDeveloperOptions from './SettingsDeveloperOptions'; import SettingsDisclaimer from './SettingsDisclaimer'; import SettingsLanguage from './SettingsLanguage'; +import SettingsTokenList from './SettingsTokenList'; import modalStyles from '../ui/Modal.module.scss'; import styles from './Settings.module.scss'; @@ -52,6 +66,7 @@ import aboutImg from '../../assets/settings/settings_about.svg'; import appearanceImg from '../../assets/settings/settings_appearance.svg'; import assetsActivityImg from '../../assets/settings/settings_assets-activity.svg'; import backupSecretImg from '../../assets/settings/settings_backup-secret.svg'; +import biometricsImg from '../../assets/settings/settings_biometrics.svg'; import connectedDappsImg from '../../assets/settings/settings_connected-dapps.svg'; import disclaimerImg from '../../assets/settings/settings_disclaimer.svg'; import exitImg from '../../assets/settings/settings_exit.svg'; @@ -62,63 +77,47 @@ import tonLinksImg from '../../assets/settings/settings_ton-links.svg'; import tonMagicImg from '../../assets/settings/settings_ton-magic.svg'; import tonProxyImg from '../../assets/settings/settings_ton-proxy.svg'; -const enum RenderingState { - Initial, - Appearance, - Assets, - Dapps, - Language, - About, - Disclaimer, -} - type OwnProps = { isInsideModal?: boolean; }; type StateProps = { - theme: Theme; - animationLevel: AnimationLevel; - areTinyTransfersHidden?: boolean; - isInvestorViewEnabled?: boolean; - isTestnet?: boolean; - canPlaySounds?: boolean; - langCode: LangCode; - isTonProxyEnabled?: boolean; - isTonMagicEnabled?: boolean; - isDeeplinkHookEnabled?: boolean; - areTokensWithNoBalanceHidden?: boolean; - areTokensWithNoPriceHidden?: boolean; - isSortByValueEnabled?: boolean; - dapps: ApiDapp[]; + settings: GlobalState['settings']; + isOpen?: boolean; tokens?: UserToken[]; - popularTokens?: UserToken[]; orderedSlugs?: string[]; + isBiometricAuthEnabled: boolean; }; const AMOUNT_OF_CLICKS_FOR_DEVELOPERS_MODE = 5; function Settings({ - theme, - animationLevel, - areTinyTransfersHidden, - isTestnet, - isInvestorViewEnabled, - canPlaySounds, - langCode, - isTonProxyEnabled, - isTonMagicEnabled, - isDeeplinkHookEnabled, - areTokensWithNoBalanceHidden, - areTokensWithNoPriceHidden, - isSortByValueEnabled, - dapps, + settings: { + state, + theme, + animationLevel, + areTinyTransfersHidden, + isTestnet, + isInvestorViewEnabled, + canPlaySounds, + langCode, + isTonProxyEnabled, + isTonMagicEnabled, + isDeeplinkHookEnabled, + areTokensWithNoBalanceHidden, + areTokensWithNoPriceHidden, + isSortByValueEnabled, + dapps, + baseCurrency, + }, + isOpen = false, tokens, - popularTokens, orderedSlugs, isInsideModal, + isBiometricAuthEnabled, }: OwnProps & StateProps) { const { + setSettingsState, openBackupWalletModal, openHardwareWalletModal, closeSettings, @@ -127,13 +126,17 @@ function Settings({ toggleTonMagic, getDapps, initTokensOrder, + openBiometricsTurnOn, + openBiometricsTurnOffWarning, + clearIsPinPadPasswordAccepted, } = getActions(); const lang = useLang(); // eslint-disable-next-line no-null/no-null const transitionRef = useRef(null); + const { renderingKey, nextKey } = useModalTransitionKeys(state, isOpen); const [clicksAmount, setClicksAmount] = useState(isTestnet ? AMOUNT_OF_CLICKS_FOR_DEVELOPERS_MODE : 0); - const [renderingKey, setRenderingKey] = useState(RenderingState.Initial); + const prevRenderingKeyRef = useStateRef(usePrevious2(renderingKey)); const [isDeveloperModalOpen, openDeveloperModal, closeDeveloperModal] = useFlag(); const [isLogOutModalOpened, openLogOutModal, closeLogOutModal] = useFlag(); @@ -151,36 +154,59 @@ function Settings({ const { handleScroll: handleContentScroll, - isAtBeginning: isContentNotScrolled, + isScrolled, } = useScrolledState(); + const handleSlideAnimationStop = useLastCallback(() => { + if (prevRenderingKeyRef.current === SettingsState.NativeBiometricsTurnOn) { + clearIsPinPadPasswordAccepted(); + } + }); + + const handleCloseSettings = useLastCallback(() => { + closeSettings(undefined, { forceOnHeavyAnimation: true }); + }); + + useHistoryBack({ + isActive: !isInsideModal && renderingKey === SettingsState.Initial, + onBack: handleCloseSettings, + }); + const handleConnectedDappsOpen = useLastCallback(() => { getDapps(); - setRenderingKey(RenderingState.Dapps); + setSettingsState({ state: SettingsState.Dapps }); }); function handleAppearanceOpen() { - setRenderingKey(RenderingState.Appearance); + setSettingsState({ state: SettingsState.Appearance }); } function handleAssetsOpen() { - setRenderingKey(RenderingState.Assets); + setSettingsState({ state: SettingsState.Assets }); } function handleLanguageOpen() { - setRenderingKey(RenderingState.Language); + setSettingsState({ state: SettingsState.Language }); } function handleAboutOpen() { - setRenderingKey(RenderingState.About); + setSettingsState({ state: SettingsState.About }); } function handleDisclaimerOpen() { - setRenderingKey(RenderingState.Disclaimer); + setSettingsState({ state: SettingsState.Disclaimer }); } + const handleNativeBiometricsTurnOnOpen = useLastCallback(() => { + setSettingsState({ state: SettingsState.NativeBiometricsTurnOn }); + }); + const handleBackClick = useLastCallback(() => { - setRenderingKey(RenderingState.Initial); + setSettingsState({ state: SettingsState.Initial }); + }); + + const handleBackClickToAssets = useLastCallback(() => { + setSettingsState({ state: SettingsState.Assets }); }); const handleDeeplinkHookToggle = useLastCallback(() => { @@ -195,13 +221,45 @@ function Settings({ toggleTonMagic({ isEnabled: !isTonMagicEnabled }); }); + const handleBiometricAuthToggle = useLastCallback(() => { + if (isBiometricAuthEnabled) { + openBiometricsTurnOffWarning(); + } else { + openBiometricsTurnOn(); + } + }); + function handleOpenBackupWallet() { + if (IS_DELEGATED_BOTTOM_SHEET) { + handleCloseSettings(); + } + openBackupWalletModal(); } + const [isTrayIconEnabled, setIsTrayIconEnabled] = useState(false); + useEffect(() => { + window.electron?.getIsTrayIconEnabled().then(setIsTrayIconEnabled); + }, []); + + const handleTrayIconEnabledToggle = useLastCallback(() => { + setIsTrayIconEnabled(!isTrayIconEnabled); + window.electron?.setIsTrayIconEnabled(!isTrayIconEnabled); + }); + + const [isAutoUpdateEnabled, setIsAutoUpdateEnabled] = useState(false); + useEffect(() => { + window.electron?.getIsAutoUpdateEnabled().then(setIsAutoUpdateEnabled); + }, []); + + const handleAutoUpdateEnabledToggle = useLastCallback(() => { + setIsAutoUpdateEnabled(!isAutoUpdateEnabled); + window.electron?.setIsAutoUpdateEnabled(!isAutoUpdateEnabled); + }); + const handleBackOrCloseAction = useLastCallback(() => { - if (renderingKey === RenderingState.Initial) { - closeSettings(); + if (renderingKey === SettingsState.Initial) { + handleCloseSettings(); } else { handleBackClick(); } @@ -210,7 +268,7 @@ function Settings({ const handleCloseLogOutModal = useLastCallback((shouldCloseSettings: boolean) => { closeLogOutModal(); if (shouldCloseSettings) { - closeSettings(); + handleCloseSettings(); } }); @@ -236,15 +294,13 @@ function Settings({ return undefined; } - return captureSwipe(transitionRef.current!, (e, direction) => { - if (direction === SwipeDirection.Right) { - handleBackOrCloseAction(); - return true; - } - - return false; + return captureControlledSwipe(transitionRef.current!, { + onSwipeRightStart: IS_DELEGATED_BOTTOM_SHEET ? handleBackClick : handleBackOrCloseAction, + onCancel: () => { + setSettingsState({ state: prevRenderingKeyRef.current! }); + }, }); - }, [handleBackOrCloseAction]); + }, [handleBackClick, handleBackOrCloseAction, prevRenderingKeyRef]); function renderHandleDeeplinkButton() { return ( @@ -267,13 +323,13 @@ function Settings({ {isInsideModal ? ( ) : ( -
- @@ -282,9 +338,28 @@ function Settings({ )}
+ {getIsNativeBiometricAuthSupported() && ( + + )} + {IS_BIOMETRIC_AUTH_SUPPORTED && ( +
+
+ {lang('Biometric + {lang('Biometric Authentication')} + + +
+
+ )} {IS_EXTENSION && (
{PROXY_HOSTS && ( @@ -339,7 +414,7 @@ function Settings({
- {(IS_DAPP_SUPPORTED) && ( + {IS_DAPP_SUPPORTED && (
{lang('Connected {lang('Connected Dapps')} @@ -410,23 +485,28 @@ function Settings({ // eslint-disable-next-line consistent-return function renderContent(isActive: boolean, isFrom: boolean, currentKey: number) { switch (currentKey) { - case RenderingState.Initial: + case SettingsState.Initial: return renderSettings(); - case RenderingState.Appearance: + case SettingsState.Appearance: return ( ); - case RenderingState.Assets: + case SettingsState.Assets: return ( ); - case RenderingState.Dapps: + case SettingsState.Dapps: return ( ); - case RenderingState.Language: - return ; - case RenderingState.About: - return ; - case RenderingState.Disclaimer: + case SettingsState.Language: + return ( + + ); + case SettingsState.About: + return ; + case SettingsState.Disclaimer: return ( ); + case SettingsState.NativeBiometricsTurnOn: + return ( + + ); + case SettingsState.SelectTokenList: + return ( + + ); } } @@ -465,56 +567,33 @@ function Settings({
{renderContent} + {IS_BIOMETRIC_AUTH_SUPPORTED && }
); } -export default memo(withGlobal((global): StateProps => { - const { - theme, - animationLevel, - areTinyTransfersHidden, - isTestnet, - isInvestorViewEnabled, - canPlaySounds, - langCode, - isTonMagicEnabled, - isTonProxyEnabled, - isDeeplinkHookEnabled, - areTokensWithNoBalanceHidden, - areTokensWithNoPriceHidden, - isSortByValueEnabled, - dapps, - } = global.settings; +export default memo(withGlobal((global): StateProps => { + const { authConfig } = global.settings; const { orderedSlugs } = selectAccountSettings(global, global.currentAccountId!) ?? {}; return { - theme, - animationLevel, - areTinyTransfersHidden, - isTestnet, - isInvestorViewEnabled, - canPlaySounds, - langCode, - isTonMagicEnabled, - isTonProxyEnabled, - isDeeplinkHookEnabled, - areTokensWithNoBalanceHidden, - areTokensWithNoPriceHidden, - isSortByValueEnabled, - dapps, + settings: global.settings, + isOpen: global.areSettingsOpen, tokens: selectCurrentAccountTokens(global), - popularTokens: selectPopularTokensWithoutAccountTokens(global), orderedSlugs, + isBiometricAuthEnabled: !!authConfig && authConfig.kind !== 'password', }; })(Settings)); diff --git a/src/components/settings/SettingsAbout.tsx b/src/components/settings/SettingsAbout.tsx index e940e074..6ec73202 100644 --- a/src/components/settings/SettingsAbout.tsx +++ b/src/components/settings/SettingsAbout.tsx @@ -4,6 +4,7 @@ import { APP_NAME, APP_VERSION, IS_EXTENSION } from '../../config'; import renderText from '../../global/helpers/renderText'; import buildClassName from '../../util/buildClassName'; +import useHistoryBack from '../../hooks/useHistoryBack'; import useLang from '../../hooks/useLang'; import useScrolledState from '../../hooks/useScrolledState'; @@ -16,16 +17,22 @@ import styles from './Settings.module.scss'; import logoSrc from '../../assets/logo.svg'; interface OwnProps { + isActive?: boolean; handleBackClick: () => void; isInsideModal?: boolean; } -function SettingsAbout({ handleBackClick, isInsideModal }: OwnProps) { +function SettingsAbout({ isActive, handleBackClick, isInsideModal }: OwnProps) { const lang = useLang(); + useHistoryBack({ + isActive, + onBack: handleBackClick, + }); + const { handleScroll: handleContentScroll, - isAtBeginning: isContentNotScrolled, + isScrolled, } = useScrolledState(); return ( @@ -33,12 +40,12 @@ function SettingsAbout({ handleBackClick, isInsideModal }: OwnProps) { {isInsideModal ? ( ) : ( -
+
)} -
+

{lang('Theme')}

@@ -139,6 +166,28 @@ function SettingsAppearance({ checked={canPlaySounds} />
+ {IS_ELECTRON && IS_WINDOWS && ( +
+ {lang('Display Tray Icon')} + + +
+ )} + {IS_ELECTRON && ( +
+ {lang('Enable Auto-Updates')} + + +
+ )}
diff --git a/src/components/settings/SettingsAssets.tsx b/src/components/settings/SettingsAssets.tsx index b923230b..440c2e0a 100644 --- a/src/components/settings/SettingsAssets.tsx +++ b/src/components/settings/SettingsAssets.tsx @@ -1,13 +1,16 @@ -import React, { memo } from '../../lib/teact/teact'; +import React, { memo, useRef, useState } from '../../lib/teact/teact'; import { getActions } from '../../global'; +import type { ApiBaseCurrency } from '../../api/types'; import type { UserToken } from '../../global/types'; import { + DEFAULT_PRICE_CURRENCY, TINY_TRANSFER_MAX_COST, TON_SYMBOL, } from '../../config'; import buildClassName from '../../util/buildClassName'; +import useHistoryBack from '../../hooks/useHistoryBack'; import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; import useScrolledState from '../../hooks/useScrolledState'; @@ -22,8 +25,8 @@ import SettingsTokens from './SettingsTokens'; import styles from './Settings.module.scss'; interface OwnProps { + isActive?: boolean; tokens?: UserToken[]; - popularTokens?: UserToken[]; orderedSlugs?: string[]; areTinyTransfersHidden?: boolean; isInvestorViewEnabled?: boolean; @@ -31,17 +34,40 @@ interface OwnProps { areTokensWithNoPriceHidden?: boolean; isSortByValueEnabled?: boolean; isInsideModal?: boolean; - handleBackClick: () => void; + handleBackClick: NoneToVoidFunction; + baseCurrency?: ApiBaseCurrency; } -const CURRENCY_OPTIONS = [{ - value: 'usd', - name: 'US Dollar', -}]; +const CURRENCY_OPTIONS = [ + { + value: 'USD', + name: 'US Dollar', + }, + { + value: 'EUR', + name: 'Euro', + }, + { + value: 'RUB', + name: 'Ruble', + }, + { + value: 'CNY', + name: 'Yuan', + }, + { + value: 'BTC', + name: 'Bitcoin', + }, + { + value: 'TON', + name: 'Toncoin', + }, +]; function SettingsAssets({ + isActive, tokens, - popularTokens, orderedSlugs, areTinyTransfersHidden, isInvestorViewEnabled, @@ -50,6 +76,7 @@ function SettingsAssets({ isSortByValueEnabled, handleBackClick, isInsideModal, + baseCurrency, }: OwnProps) { const { toggleTinyTransfersHidden, @@ -57,12 +84,21 @@ function SettingsAssets({ toggleTokensWithNoBalance, toggleTokensWithNoPrice, toggleSortByValue, + changeBaseCurrency, } = getActions(); const lang = useLang(); + // eslint-disable-next-line no-null/no-null + const scrollContainerRef = useRef(null); + + useHistoryBack({ + isActive, + onBack: handleBackClick, + }); + const { handleScroll: handleContentScroll, - isAtBeginning: isContentNotScrolled, + isScrolled, } = useScrolledState(); const handleTinyTransfersHiddenToggle = useLastCallback(() => { @@ -85,17 +121,26 @@ function SettingsAssets({ toggleSortByValue({ isEnabled: !isSortByValueEnabled }); }); + const [localBaseCurrency, setLocalBaseCurrency] = useState(baseCurrency); + + const handleBaseCurrencyChange = useLastCallback((currency: string) => { + if (currency !== baseCurrency) { + setLocalBaseCurrency(currency as ApiBaseCurrency); + changeBaseCurrency({ currency: currency as ApiBaseCurrency }); + } + }); + return (
{isInsideModal ? ( ) : ( -
+
)}
@@ -190,10 +237,11 @@ function SettingsAssets({
diff --git a/src/components/settings/SettingsDapps.tsx b/src/components/settings/SettingsDapps.tsx index e7a39e42..ca4abc55 100644 --- a/src/components/settings/SettingsDapps.tsx +++ b/src/components/settings/SettingsDapps.tsx @@ -7,6 +7,7 @@ import buildClassName from '../../util/buildClassName'; import { ANIMATED_STICKERS_PATHS } from '../ui/helpers/animatedAssets'; import useFlag from '../../hooks/useFlag'; +import useHistoryBack from '../../hooks/useHistoryBack'; import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; import useScrolledState from '../../hooks/useScrolledState'; @@ -16,6 +17,7 @@ import DisconnectDappModal from '../main/modals/DisconnectDappModal'; import AnimatedIconWithPreview from '../ui/AnimatedIconWithPreview'; import Button from '../ui/Button'; import ModalHeader from '../ui/ModalHeader'; +import Transition from '../ui/Transition'; import styles from './Settings.module.scss'; @@ -37,9 +39,14 @@ function SettingsDapps({ const [isDisconnectModalOpen, openDisconnectModal, closeDisconnectModal] = useFlag(); const [dappToDelete, setDappToDelete] = useState(); + useHistoryBack({ + isActive, + onBack: handleBackClick, + }); + const { handleScroll: handleContentScroll, - isAtBeginning: isContentNotScrolled, + isScrolled, } = useScrolledState(); const handleDisconnectDapp = useLastCallback((origin: string) => { @@ -104,7 +111,6 @@ function SettingsDapps({ tgsUrl={ANIMATED_STICKERS_PATHS.noData} previewUrl={ANIMATED_STICKERS_PATHS.noDataPreview} size={ANIMATED_STICKER_BIG_SIZE_PX} - className={styles.sticker} noLoop={false} nonInteractive /> @@ -122,12 +128,12 @@ function SettingsDapps({ {isInsideModal ? ( ) : ( -
+
)}
- {content} + + {content} +
diff --git a/src/components/settings/SettingsDisclaimer.tsx b/src/components/settings/SettingsDisclaimer.tsx index ae614185..7e0b6cda 100644 --- a/src/components/settings/SettingsDisclaimer.tsx +++ b/src/components/settings/SettingsDisclaimer.tsx @@ -5,6 +5,7 @@ import renderText from '../../global/helpers/renderText'; import buildClassName from '../../util/buildClassName'; import { ANIMATED_STICKERS_PATHS } from '../ui/helpers/animatedAssets'; +import useHistoryBack from '../../hooks/useHistoryBack'; import useLang from '../../hooks/useLang'; import useScrolledState from '../../hooks/useScrolledState'; @@ -23,9 +24,14 @@ interface OwnProps { function SettingsDisclaimer({ isActive, handleBackClick, isInsideModal }: OwnProps) { const lang = useLang(); + useHistoryBack({ + isActive, + onBack: handleBackClick, + }); + const { handleScroll: handleContentScroll, - isAtBeginning: isContentNotScrolled, + isScrolled, } = useScrolledState(); return ( @@ -33,11 +39,11 @@ function SettingsDisclaimer({ isActive, handleBackClick, isInsideModal }: OwnPro {isInsideModal ? ( ) : ( -
+
)} -
+
{renderLanguages()}
diff --git a/src/components/settings/SettingsModal.tsx b/src/components/settings/SettingsModal.tsx index e8197774..65367ae8 100644 --- a/src/components/settings/SettingsModal.tsx +++ b/src/components/settings/SettingsModal.tsx @@ -1,7 +1,8 @@ import React, { memo } from '../../lib/teact/teact'; -import { IS_ELECTRON, IS_EXTENSION } from '../../config'; +import { IS_EXTENSION } from '../../config'; import buildClassName from '../../util/buildClassName'; +import { IS_ELECTRON } from '../../util/windowEnvironment'; import Modal from '../ui/Modal'; @@ -23,9 +24,11 @@ function SettingsModal({ children, isOpen, onClose }: OwnProps) { {children} diff --git a/src/components/settings/SettingsTokenList.tsx b/src/components/settings/SettingsTokenList.tsx new file mode 100644 index 00000000..25ec48bd --- /dev/null +++ b/src/components/settings/SettingsTokenList.tsx @@ -0,0 +1,35 @@ +import React, { memo } from '../../lib/teact/teact'; + +import useHistoryBack from '../../hooks/useHistoryBack'; + +import TokenSelector from '../common/TokenSelector'; + +import styles from './Settings.module.scss'; + +interface OwnProps { + isActive?: boolean; + handleBackClick: NoneToVoidFunction; +} + +function SettingsTokenList({ + isActive, + handleBackClick, +}: OwnProps) { + useHistoryBack({ + isActive, + onBack: handleBackClick, + }); + + return ( +
+ +
+ ); +} + +export default memo(SettingsTokenList); diff --git a/src/components/settings/SettingsTokens.tsx b/src/components/settings/SettingsTokens.tsx index 1b28c1e4..8509144d 100644 --- a/src/components/settings/SettingsTokens.tsx +++ b/src/components/settings/SettingsTokens.tsx @@ -1,17 +1,18 @@ +import type { RefObject } from 'react'; import React, { memo, useEffect, useRef, useState, } from '../../lib/teact/teact'; import { getActions } from '../../global'; -import type { UserToken } from '../../global/types'; +import type { ApiBaseCurrency } from '../../api/types'; +import { SettingsState, type UserToken } from '../../global/types'; -import { DEFAULT_PRICE_CURRENCY, TON_TOKEN_SLUG } from '../../config'; +import { TON_TOKEN_SLUG } from '../../config'; import buildClassName from '../../util/buildClassName'; -import { formatCurrency } from '../../util/formatNumber'; +import { formatCurrency, getShortCurrencySymbol } from '../../util/formatNumber'; import { isBetween } from '../../util/math'; import { ASSET_LOGO_PATHS } from '../ui/helpers/assetLogos'; -import useFlag from '../../hooks/useFlag'; import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; @@ -19,7 +20,6 @@ import DeleteTokenModal from '../main/modals/DeleteTokenModal'; import AnimatedCounter from '../ui/AnimatedCounter'; import Draggable from '../ui/Draggable'; import Switcher from '../ui/Switcher'; -import SelectTokens from './SelectTokens'; import styles from './Settings.module.scss'; @@ -29,44 +29,43 @@ interface SortState { draggedIndex?: number; } -interface Position { - top: number; - right: number; -} - interface OwnProps { + parentContainer: RefObject; tokens?: UserToken[]; - popularTokens?: UserToken[]; orderedSlugs?: string[]; isSortByValueEnabled?: boolean; + baseCurrency?: ApiBaseCurrency; } const TOKEN_HEIGHT_PX = 64; const TOP_OFFSET = 48; function SettingsTokens({ - tokens, popularTokens, orderedSlugs, isSortByValueEnabled, + parentContainer, + tokens, + orderedSlugs, + isSortByValueEnabled, + baseCurrency, }: OwnProps) { const { + openSettingsWithState, sortTokens, - toggleDisabledToken, - addToken, + toggleExceptionToken, } = getActions(); const lang = useLang(); + const shortBaseSymbol = getShortCurrencySymbol(baseCurrency); // eslint-disable-next-line no-null/no-null const tokensRef = useRef(null); // eslint-disable-next-line no-null/no-null const sortableContainerRef = useRef(null); - const [isAddTokenModalOpen, openAddTokenModal, closeAddTokenModal] = useFlag(); const [tokenToDelete, setTokenToDelete] = useState(); const [state, setState] = useState({ orderedTokenSlugs: orderedSlugs, dragOrderTokenSlugs: orderedSlugs, draggedIndex: undefined, }); - const [sortableContainerPosition, setSortableContainerPosition] = useState(); useEffect(() => { if (!arraysAreEqual(orderedSlugs, state.orderedTokenSlugs)) { @@ -78,15 +77,8 @@ function SettingsTokens({ } }, [orderedSlugs, state.orderedTokenSlugs]); - const handleOpenAddTokenModal = useLastCallback(() => { - if (sortableContainerRef.current) { - const { top, right } = sortableContainerRef.current.getBoundingClientRect(); - setSortableContainerPosition({ - top, right, - }); - } - - openAddTokenModal(); + const handleOpenAddTokenPage = useLastCallback(() => { + openSettingsWithState({ state: SettingsState.SelectTokenList }); }); const handleDrag = useLastCallback((translation: { x: number; y: number }, id: string | number) => { @@ -120,10 +112,12 @@ function SettingsTokens({ }); }); - const handleDisabledToken = useLastCallback((slug: string) => { + const handleExceptionToken = useLastCallback((slug: string, e: React.MouseEvent | React.TouchEvent) => { if (slug === TON_TOKEN_SLUG) return; - toggleDisabledToken({ slug }); + e.preventDefault(); + e.stopPropagation(); + toggleExceptionToken({ slug }); }); const handleDeleteToken = useLastCallback((token: UserToken, e: React.MouseEvent) => { @@ -131,10 +125,6 @@ function SettingsTokens({ setTokenToDelete(token); }); - const handleTokenSelect = useLastCallback((token: UserToken) => { - addToken({ token }); - }); - function renderToken(token: UserToken, index: number) { const { symbol, image, name, amount, price, slug, isDisabled, @@ -145,8 +135,8 @@ function SettingsTokens({ const totalAmount = amount * price; const isDragged = state.draggedIndex === index; - const draggedTop = getOrderIndex(slug, state.orderedTokenSlugs); - const top = getOrderIndex(slug, state.dragOrderTokenSlugs); + const draggedTop = isSortByValueEnabled ? getOffsetByIndex(index) : getOffsetBySlug(slug, state.orderedTokenSlugs); + const top = isSortByValueEnabled ? getOffsetByIndex(index) : getOffsetBySlug(slug, state.dragOrderTokenSlugs); const style = `top: ${isDragged ? draggedTop : top}px;`; const knobStyle = 'left: 1rem;'; @@ -167,8 +157,9 @@ function SettingsTokens({ className={buildClassName(styles.item, styles.item_token)} offset={{ top: TOP_OFFSET }} parentRef={tokensRef} + scrollRef={parentContainer} // eslint-disable-next-line react/jsx-no-bind - onClick={() => handleDisabledToken(slug)} + onClick={(e) => handleExceptionToken(slug, e)} >
- + {isDeleteButtonVisible && ( @@ -196,8 +187,6 @@ function SettingsTokens({ className={styles.menuSwitcher} label={lang('Investor View')} checked={!isDisabled} - // eslint-disable-next-line react/jsx-no-bind - onCheck={() => handleDisabledToken(slug)} /> )} @@ -213,21 +202,13 @@ function SettingsTokens({ style={`height: ${(tokens?.length ?? 0) * TOKEN_HEIGHT_PX + TOP_OFFSET}px`} ref={tokensRef} > -
+
{lang('Add Token')}
{tokens?.map(renderToken)}
- -
@@ -239,9 +220,13 @@ function arraysAreEqual(arr1: T[] = [], arr2: T[] = []) { return arr1.length === arr2.length && arr1.every((value, index) => value === arr2[index]); } -function getOrderIndex(slug: string, list: string[] = []) { +function getOffsetBySlug(slug: string, list: string[] = []) { const realIndex = list.indexOf(slug); const index = realIndex === -1 ? list.length : realIndex; + return getOffsetByIndex(index); +} + +function getOffsetByIndex(index: number) { return index * TOKEN_HEIGHT_PX + TOP_OFFSET; } diff --git a/src/components/settings/biometrics/Biometrics.module.scss b/src/components/settings/biometrics/Biometrics.module.scss new file mode 100644 index 00000000..e552357f --- /dev/null +++ b/src/components/settings/biometrics/Biometrics.module.scss @@ -0,0 +1,47 @@ +.modalDialog { + height: 37rem; + + @supports (height: env(safe-area-inset-bottom)) { + height: calc(37rem + env(safe-area-inset-bottom)); + } + + @media (min-width: 416.01px) { + max-width: 24rem; + } +} + +.sticker { + margin: 0 auto 1.25rem; +} + +.stickerHuge { + margin-top: 2rem; +} + +.step { + align-self: center; + + margin-bottom: 1rem; + padding: 0.75rem; + + font-size: 0.9375rem; + font-weight: 600; + color: var(--light-gray-1); + + background-color: var(--color-gray-button-background-light); + border-radius: var(--border-radius-buttons); +} + +.error { + align-self: center; + + padding: 0.375rem 0.5rem; + + font-size: 1.0625rem; + font-weight: 700; + line-height: 1; + color: var(--color-transaction-red-text); + + background-color: var(--color-transaction-red-background); + border-radius: var(--border-radius-tiny); +} diff --git a/src/components/settings/biometrics/Biometrics.tsx b/src/components/settings/biometrics/Biometrics.tsx new file mode 100644 index 00000000..30b21190 --- /dev/null +++ b/src/components/settings/biometrics/Biometrics.tsx @@ -0,0 +1,53 @@ +import React, { memo } from '../../../lib/teact/teact'; +import { getActions, withGlobal } from '../../../global'; + +import { BiometricsState } from '../../../global/types'; + +import BiometricsTurnOff from './TurnOff'; +import BiometricsTurnOffWarning from './TurnOffWarning'; +import BiometricsTurnOn from './TurnOn'; + +interface StateProps { + state: BiometricsState; + error?: string; +} + +function Biometrics({ state, error }: StateProps) { + const { closeBiometricSettings } = getActions(); + + const isTurnOnBiometricsOpened = state === BiometricsState.TurnOnPasswordConfirmation + || state === BiometricsState.TurnOnRegistration + || state === BiometricsState.TurnOnVerification + || state === BiometricsState.TurnOnComplete; + const isTurnOffBiometricsOpened = state === BiometricsState.TurnOffBiometricConfirmation + || state === BiometricsState.TurnOffCreatePassword + || state === BiometricsState.TurnOffComplete; + const isTurnOffBiometricsWarningOpened = state === BiometricsState.TurnOffWarning; + + return ( + <> + + + + + ); +} + +export default memo(withGlobal((global) => { + const { biometrics: { state, error } } = global; + + return { + state, + error, + }; +})(Biometrics)); diff --git a/src/components/settings/biometrics/NativeBiometricsToggle.tsx b/src/components/settings/biometrics/NativeBiometricsToggle.tsx new file mode 100644 index 00000000..97653245 --- /dev/null +++ b/src/components/settings/biometrics/NativeBiometricsToggle.tsx @@ -0,0 +1,123 @@ +import { Dialog } from '@capacitor/dialog'; +import React, { memo } from '../../../lib/teact/teact'; +import { getActions, withGlobal } from '../../../global'; + +import renderText from '../../../global/helpers/renderText'; +import { getIsFaceIdAvailable, getIsTouchIdAvailable } from '../../../util/capacitor'; +import { IS_DELEGATED_BOTTOM_SHEET } from '../../../util/windowEnvironment'; + +import useFlag from '../../../hooks/useFlag'; +import useLang from '../../../hooks/useLang'; +import useLastCallback from '../../../hooks/useLastCallback'; +import useSyncEffect from '../../../hooks/useSyncEffect'; + +import Button from '../../ui/Button'; +import Modal from '../../ui/Modal'; +import Switcher from '../../ui/Switcher'; + +import modalStyles from '../../ui/Modal.module.scss'; +import styles from '../Settings.module.scss'; + +import biometricsImg from '../../../assets/settings/settings_biometrics.svg'; + +interface OwnProps { + onEnable: NoneToVoidFunction; +} + +interface StateProps { + isBiometricAuthEnabled: boolean; +} + +function NativeBiometricsToggle({ isBiometricAuthEnabled, onEnable }: OwnProps & StateProps) { + const { disableNativeBiometrics } = getActions(); + const isFaceId = getIsFaceIdAvailable(); + const isTouchId = getIsTouchIdAvailable(); + + const lang = useLang(); + const [isWarningModalOpen, openWarningModal, closeWarningModal] = useFlag(); + const switcherTitle = isFaceId ? 'Face ID' : (isTouchId ? 'Touch ID' : lang('Biometric Authentication')); + const warningTitle = isFaceId + ? 'Turn Off Face ID?' + : (isTouchId ? 'Turn Off Touch ID?' : 'Turn Off Biometrics?'); + const warningDescription = isFaceId + ? 'Are you sure you want to disable Face ID?' + : (isTouchId ? 'Are you sure you want to disable Touch ID?' : 'Are you sure you want to disable biometrics?'); + + const handleConfirmDisableBiometrics = useLastCallback(() => { + closeWarningModal(); + disableNativeBiometrics(); + }); + + useSyncEffect(() => { + if (!IS_DELEGATED_BOTTOM_SHEET) return; + + if (isWarningModalOpen) { + Dialog.confirm({ + title: lang(warningTitle), + message: lang(warningDescription), + okButtonTitle: lang('Yes'), + }) + .then(({ value }) => { + if (value) { + handleConfirmDisableBiometrics(); + } + }) + .finally(closeWarningModal); + } + }, [handleConfirmDisableBiometrics, isWarningModalOpen, lang, warningDescription, warningTitle]); + + const handleBiometricAuthToggle = useLastCallback(() => { + if (isBiometricAuthEnabled) { + openWarningModal(); + } else { + onEnable(); + } + }); + + function renderDisableNativeBiometricsWarning() { + if (IS_DELEGATED_BOTTOM_SHEET) return undefined; + + return ( + +

+ {renderText(lang(warningDescription))} +

+ +
+ + +
+
+ ); + } + + return ( +
+
+ + {switcherTitle} + + +
+ {renderDisableNativeBiometricsWarning()} +
+ ); +} + +export default memo(withGlobal((global): StateProps => { + const { authConfig } = global.settings; + + return { + isBiometricAuthEnabled: !!authConfig && authConfig.kind === 'native-biometrics', + }; +})(NativeBiometricsToggle)); diff --git a/src/components/settings/biometrics/NativeBiometricsTurnOn.tsx b/src/components/settings/biometrics/NativeBiometricsTurnOn.tsx new file mode 100644 index 00000000..e17605af --- /dev/null +++ b/src/components/settings/biometrics/NativeBiometricsTurnOn.tsx @@ -0,0 +1,116 @@ +import React, { memo, useEffect, useState } from '../../../lib/teact/teact'; +import { getActions, withGlobal } from '../../../global'; + +import { PIN_LENGTH } from '../../../config'; +import buildClassName from '../../../util/buildClassName'; +import { ANIMATED_STICKERS_PATHS } from '../../ui/helpers/animatedAssets'; + +import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps'; +import useHistoryBack from '../../../hooks/useHistoryBack'; +import useLang from '../../../hooks/useLang'; +import useLastCallback from '../../../hooks/useLastCallback'; + +import AnimatedIconWithPreview from '../../ui/AnimatedIconWithPreview'; +import Button from '../../ui/Button'; +import PinPad from '../../ui/PinPad'; + +import styles from '../Settings.module.scss'; + +interface OwnProps { + isActive?: boolean; + handleBackClick: NoneToVoidFunction; +} + +interface StateProps { + isPinPadPasswordAccepted?: boolean; + error?: string; + isNativeBiometricsEnabled?: boolean; +} + +function NativeBiometricsTurnOn({ + isActive, + isPinPadPasswordAccepted, + error, + isNativeBiometricsEnabled, + handleBackClick, +}: OwnProps & StateProps) { + const { enableNativeBiometrics, clearNativeBiometricsError } = getActions(); + + const lang = useLang(); + const [pin, setPin] = useState(''); + const pinPadType = pin.length !== PIN_LENGTH + ? undefined + : (isPinPadPasswordAccepted ? 'success' : (error ? 'error' : undefined)); + const pinTitle = isPinPadPasswordAccepted + ? 'Correct' + : (error && pin.length === PIN_LENGTH ? error : 'Enter code'); + + useEffect(() => { + if (!isActive) return; + + setPin(''); + }, [isActive]); + + useEffectWithPrevDeps(([prevIsEnabled]) => { + if (isNativeBiometricsEnabled && !prevIsEnabled) { + handleBackClick(); + } + }, [isNativeBiometricsEnabled, handleBackClick]); + + useHistoryBack({ + isActive, + onBack: handleBackClick, + }); + + const handleSubmit = useLastCallback((password: string) => { + enableNativeBiometrics({ password }); + }); + + return ( +
+
+ + + + +
+
+ ); +} + +export default memo(withGlobal((global): StateProps => { + const { + nativeBiometricsError, + isPinPadPasswordAccepted, + settings: { authConfig }, + } = global; + + return { + isPinPadPasswordAccepted, + error: nativeBiometricsError, + isNativeBiometricsEnabled: !!authConfig && authConfig.kind === 'native-biometrics', + }; +})(NativeBiometricsTurnOn)); diff --git a/src/components/settings/biometrics/TurnOff.tsx b/src/components/settings/biometrics/TurnOff.tsx new file mode 100644 index 00000000..f89e64e4 --- /dev/null +++ b/src/components/settings/biometrics/TurnOff.tsx @@ -0,0 +1,146 @@ +import React, { memo } from '../../../lib/teact/teact'; +import { getActions } from '../../../global'; + +import { BiometricsState } from '../../../global/types'; + +import { ANIMATED_STICKER_HUGE_SIZE_PX } from '../../../config'; +import buildClassName from '../../../util/buildClassName'; +import resolveModalTransitionName from '../../../util/resolveModalTransitionName'; +import { ANIMATED_STICKERS_PATHS } from '../../ui/helpers/animatedAssets'; + +import useLang from '../../../hooks/useLang'; +import useLastCallback from '../../../hooks/useLastCallback'; +import useModalTransitionKeys from '../../../hooks/useModalTransitionKeys'; + +import AnimatedIconWithPreview from '../../ui/AnimatedIconWithPreview'; +import Button from '../../ui/Button'; +import CreatePasswordForm from '../../ui/CreatePasswordForm'; +import Modal from '../../ui/Modal'; +import ModalHeader from '../../ui/ModalHeader'; +import Transition from '../../ui/Transition'; + +import modalStyles from '../../ui/Modal.module.scss'; +import styles from './Biometrics.module.scss'; + +interface OwnProps { + isOpen: boolean; + state: BiometricsState; + error?: string; + onClose: NoneToVoidFunction; +} + +const STICKER_SIZE = 180; + +function TurnOff({ + isOpen, state, error, onClose, +}: OwnProps) { + const { disableBiometrics } = getActions(); + + const lang = useLang(); + const { renderingKey, nextKey, updateNextKey } = useModalTransitionKeys(state, isOpen); + + const handleSubmit = useLastCallback((password: string, isPasswordNumeric?: boolean) => { + disableBiometrics({ password, isPasswordNumeric }); + }); + + // eslint-disable-next-line consistent-return + function renderContent(isActive: boolean, isFrom: boolean, currentKey: number) { + switch (currentKey) { + case BiometricsState.TurnOffBiometricConfirmation: + return ( + <> + +
+ +
{lang('Please verify your identity.')}
+ {error &&
{lang(error)}
} + +
+ +
+
+ + ); + + case BiometricsState.TurnOffCreatePassword: + return ( + <> + +
+ + +
+ + ); + + case BiometricsState.TurnOffComplete: + return ( + <> + +
+ + +
+ +
+
+ + ); + } + } + + return ( + + + {renderContent} + + + ); +} + +export default memo(TurnOff); diff --git a/src/components/settings/biometrics/TurnOffWarning.tsx b/src/components/settings/biometrics/TurnOffWarning.tsx new file mode 100644 index 00000000..a4011013 --- /dev/null +++ b/src/components/settings/biometrics/TurnOffWarning.tsx @@ -0,0 +1,46 @@ +import React, { memo } from '../../../lib/teact/teact'; +import { getActions } from '../../../global'; + +import buildClassName from '../../../util/buildClassName'; + +import useLang from '../../../hooks/useLang'; + +import Button from '../../ui/Button'; +import Modal from '../../ui/Modal'; + +import modalStyles from '../../ui/Modal.module.scss'; + +interface OwnProps { + isOpen: boolean; + onClose: NoneToVoidFunction; +} + +function TurnOffWaning({ isOpen, onClose }: OwnProps) { + const { openBiometricsTurnOff } = getActions(); + + const lang = useLang(); + + return ( + +

+ {lang('If you turn off biometric protection, you will need to create a password.')} +

+ +
+ + +
+
+ ); +} + +export default memo(TurnOffWaning); diff --git a/src/components/settings/biometrics/TurnOn.tsx b/src/components/settings/biometrics/TurnOn.tsx new file mode 100644 index 00000000..b699448b --- /dev/null +++ b/src/components/settings/biometrics/TurnOn.tsx @@ -0,0 +1,189 @@ +import React, { memo, useEffect, useState } from '../../../lib/teact/teact'; +import { getActions } from '../../../global'; + +import { BiometricsState } from '../../../global/types'; + +import { ANIMATED_STICKER_HUGE_SIZE_PX } from '../../../config'; +import buildClassName from '../../../util/buildClassName'; +import resolveModalTransitionName from '../../../util/resolveModalTransitionName'; +import { IS_ELECTRON } from '../../../util/windowEnvironment'; +import { ANIMATED_STICKERS_PATHS } from '../../ui/helpers/animatedAssets'; + +import useLang from '../../../hooks/useLang'; +import useLastCallback from '../../../hooks/useLastCallback'; +import useModalTransitionKeys from '../../../hooks/useModalTransitionKeys'; + +import AnimatedIconWithPreview from '../../ui/AnimatedIconWithPreview'; +import Button from '../../ui/Button'; +import Modal from '../../ui/Modal'; +import ModalHeader from '../../ui/ModalHeader'; +import PasswordForm from '../../ui/PasswordForm'; +import Transition from '../../ui/Transition'; + +import modalStyles from '../../ui/Modal.module.scss'; +import styles from './Biometrics.module.scss'; + +interface OwnProps { + isOpen: boolean; + state: BiometricsState; + error?: string; + onClose: NoneToVoidFunction; +} + +const STICKER_SIZE = 180; + +function TurnOn({ + isOpen, state, error, onClose, +}: OwnProps) { + const { enableBiometrics } = getActions(); + + const lang = useLang(); + const [localError, setLocalError] = useState(); + const { renderingKey, nextKey, updateNextKey } = useModalTransitionKeys(state, isOpen); + const shouldDisablePasswordForm = Boolean(state !== BiometricsState.TurnOnPasswordConfirmation); + + useEffect(() => { + if (isOpen) { + setLocalError(''); + } + }, [isOpen]); + + const handleClearError = useLastCallback(() => { + setLocalError(undefined); + }); + + const handleSubmit = useLastCallback((password: string) => { + if (shouldDisablePasswordForm) { + return; + } + + try { + enableBiometrics({ password }); + } catch (err: any) { + setLocalError(err.message || 'Unknown error'); + } + }); + + // eslint-disable-next-line consistent-return + function renderContent(isActive: boolean, isFrom: boolean, currentKey: number) { + switch (currentKey) { + case BiometricsState.TurnOnPasswordConfirmation: + return ( + <> + + + + ); + + case BiometricsState.TurnOnRegistration: + return ( + <> + +
+ + +
{lang('Step 1 of 2. Registration')}
+ +
+ +
+
+ + ); + + case BiometricsState.TurnOnVerification: + return ( + <> + +
+ + +
+ {lang(IS_ELECTRON ? 'Verification' : 'Step 2 of 2. Verification')} +
+ +
+ +
+
+ + ); + + case BiometricsState.TurnOnComplete: + return ( + <> + +
+ + +
+ +
+
+ + ); + } + } + + return ( + + + {renderContent} + + + ); +} + +export default memo(TurnOn); diff --git a/src/components/staking/StakeModal.tsx b/src/components/staking/StakeModal.tsx index 586922e2..5f1ad252 100644 --- a/src/components/staking/StakeModal.tsx +++ b/src/components/staking/StakeModal.tsx @@ -1,17 +1,19 @@ -import React, { memo, useMemo } from '../../lib/teact/teact'; +import React, { memo, useMemo, useState } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; import type { GlobalState, UserToken } from '../../global/types'; import { StakingState } from '../../global/types'; -import { TON_TOKEN_SLUG } from '../../config'; +import { IS_CAPACITOR, TON_TOKEN_SLUG } from '../../config'; import { selectCurrentAccountTokens } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; +import { formatCurrency } from '../../util/formatNumber'; +import resolveModalTransitionName from '../../util/resolveModalTransitionName'; +import { ASSET_LOGO_PATHS } from '../ui/helpers/assetLogos'; import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; import useModalTransitionKeys from '../../hooks/useModalTransitionKeys'; -import usePrevious from '../../hooks/usePrevious'; import TransferResult from '../common/TransferResult'; import Button from '../ui/Button'; @@ -57,18 +59,19 @@ function StakeModal({ const lang = useLang(); const isOpen = IS_OPEN_STATES.has(state); const tonToken = useMemo(() => tokens?.find(({ slug }) => slug === TON_TOKEN_SLUG), [tokens]); - const renderedTokenBalance = usePrevious(tonToken?.amount, true); - const renderedStakingAmount = usePrevious(amount, true); + const [renderedStakingAmount, setRenderedStakingAmount] = useState(amount); const { renderingKey, nextKey, updateNextKey } = useModalTransitionKeys(state, isOpen); const handleBackClick = useLastCallback(() => { if (state === StakingState.StakePassword) { + clearStakingError(); setStakingScreen({ state: StakingState.StakeInitial }); } }); const handleTransferSubmit = useLastCallback((password: string) => { + setRenderedStakingAmount(amount); submitStakingPassword({ password }); }); @@ -77,21 +80,38 @@ function StakeModal({ cancelStaking(); }); + function renderStakingShortInfo() { + if (!tonToken || !amount) return undefined; + + const logoPath = tonToken.image || ASSET_LOGO_PATHS[tonToken.symbol.toLowerCase() as keyof typeof ASSET_LOGO_PATHS]; + + return ( +
+ {tonToken.symbol} + {formatCurrency(amount, tonToken.symbol)} +
+ ); + } + function renderPassword(isActive: boolean) { return ( <> - + {!IS_CAPACITOR && } + > + {IS_CAPACITOR && renderStakingShortInfo()} + ); } @@ -107,7 +127,7 @@ function StakeModal({ playAnimation={isActive} amount={renderedStakingAmount} noSign - balance={renderedTokenBalance} + balance={tonToken?.amount ?? 0} operationAmount={amount ? -amount : undefined} firstButtonText={lang('View')} secondButtonText={lang('Stake More')} @@ -144,15 +164,17 @@ function StakeModal({ return ( { if (isActive) { - fetchBackendStakingState(); + fetchStakingHistory(); } - }, [fetchBackendStakingState, isActive]); + }, [fetchStakingHistory, isActive]); const handleStakeClick = useLastCallback(() => { onClose?.(); @@ -110,7 +112,7 @@ function StakingInfoContent({ {lang('$total', { value: ( - {formatCurrency(stakingHistory!.totalProfit, TON_SYMBOL)} + {formatCurrency(totalProfit, TON_SYMBOL)} ), })} @@ -123,7 +125,7 @@ function StakingInfoContent({ !isStatic && height >= HISTORY_SCROLL_APPEARANCE_HEIGHT_PX && 'custom-scroll', )} > - {stakingHistory?.profitHistory.map((record) => ( + {stakingHistory?.map((record) => ( ((global): StateProps => { const accountState = selectCurrentAccountState(global); return { - amount: accountState?.stakingBalance || 0, - apyValue: accountState?.poolState?.lastApy || 0, + amount: accountState?.staking?.balance || 0, + apyValue: accountState?.staking?.apy || 0, + totalProfit: accountState?.staking?.totalProfit ?? 0, stakingHistory: accountState?.stakingHistory, tokens: selectCurrentAccountTokens(global), - isUnstakeRequested: accountState?.isUnstakeRequested, - endOfStakingCycle: accountState?.poolState?.endOfCycle, + isUnstakeRequested: accountState?.staking?.isUnstakeRequested, + endOfStakingCycle: accountState?.staking?.end, }; })(StakingInfoContent)); diff --git a/src/components/staking/StakingInfoModal.tsx b/src/components/staking/StakingInfoModal.tsx index 789411e3..7910cd00 100644 --- a/src/components/staking/StakingInfoModal.tsx +++ b/src/components/staking/StakingInfoModal.tsx @@ -28,7 +28,7 @@ function StakingInfoModal({ isUnstakeRequested, onClose, }: OwnProps & StateProps) { - const { fetchBackendStakingState } = getActions(); + const { fetchStakingHistory } = getActions(); const forceUpdate = useForceUpdate(); @@ -36,15 +36,16 @@ function StakingInfoModal({ useEffect(() => { if (isOpen) { - fetchBackendStakingState(); + fetchStakingHistory(); } - }, [fetchBackendStakingState, isOpen]); + }, [fetchStakingHistory, isOpen]); return ( @@ -55,6 +56,6 @@ export default memo(withGlobal((global): StateProps => { const accountState = selectCurrentAccountState(global); return { - isUnstakeRequested: accountState?.isUnstakeRequested, + isUnstakeRequested: accountState?.staking?.isUnstakeRequested, }; })(StakingInfoModal)); diff --git a/src/components/staking/StakingInitial.tsx b/src/components/staking/StakingInitial.tsx index 78ca8fb4..5e187945 100644 --- a/src/components/staking/StakingInitial.tsx +++ b/src/components/staking/StakingInitial.tsx @@ -1,3 +1,4 @@ +import { Dialog } from '@capacitor/dialog'; import React, { memo, useEffect, useMemo, useState, } from '../../lib/teact/teact'; @@ -8,22 +9,28 @@ import type { UserToken } from '../../global/types'; import { ANIMATED_STICKER_MIDDLE_SIZE_PX, ANIMATED_STICKER_SMALL_SIZE_PX, + DEFAULT_DECIMAL_PLACES, + DEFAULT_FEE, MIN_BALANCE_FOR_UNSTAKE, + STAKING_FORWARD_AMOUNT, + STAKING_MIN_AMOUNT, TON_TOKEN_SLUG, } from '../../config'; import { bigStrToHuman } from '../../global/helpers'; import renderText from '../../global/helpers/renderText'; import { selectCurrentAccountState, selectCurrentAccountTokens } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; -import { formatCurrency, formatCurrencyExtended } from '../../util/formatNumber'; +import { formatCurrency, formatCurrencySimple } from '../../util/formatNumber'; import { floor } from '../../util/round'; import { throttle } from '../../util/schedulers'; +import { IS_DELEGATED_BOTTOM_SHEET } from '../../util/windowEnvironment'; import { ANIMATED_STICKERS_PATHS } from '../ui/helpers/animatedAssets'; import { ASSET_LOGO_PATHS } from '../ui/helpers/assetLogos'; import useFlag from '../../hooks/useFlag'; import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; +import useSyncEffect from '../../hooks/useSyncEffect'; import AnimatedIconWithPreview from '../ui/AnimatedIconWithPreview'; import Button from '../ui/Button'; @@ -46,7 +53,6 @@ interface StateProps { tokens?: UserToken[]; fee?: string; stakingBalance: number; - stakingMinAmount: number; apyValue: number; } @@ -64,7 +70,6 @@ function StakingInitial({ tokens, fee, stakingBalance, - stakingMinAmount, apyValue, }: OwnProps & StateProps) { const { submitStakingInitial, fetchStakingFee } = getActions(); @@ -78,9 +83,11 @@ function StakingInitial({ const [shouldUseAllBalance, setShouldUseAllBalance] = useState(false); const { - amount: balance, decimals, symbol, + amount: balance, symbol, } = useMemo(() => tokens?.find(({ slug }) => slug === TON_TOKEN_SLUG), [tokens]) || {}; const hasAmountError = Boolean(isInsufficientBalance || apiError); + const calculatedFee = fee ? bigStrToHuman(fee) * RESERVED_FEE_FACTOR : DEFAULT_FEE; + const decimals = DEFAULT_DECIMAL_PLACES; const validateAndSetAmount = useLastCallback((newAmount: number | undefined, noReset = false) => { if (!noReset) { @@ -99,9 +106,9 @@ function StakingInitial({ return; } - if (!balance || newAmount > balance) { + if (!balance || newAmount + STAKING_MIN_AMOUNT + calculatedFee > balance) { setIsInsufficientBalance(true); - } else if (balance + stakingBalance < stakingMinAmount) { + } else if (balance + stakingBalance < STAKING_MIN_AMOUNT) { setIsNotEnough(true); } @@ -110,14 +117,13 @@ function StakingInitial({ useEffect(() => { if (shouldUseAllBalance && balance) { - const calculatedFee = fee && shouldUseAllBalance ? bigStrToHuman(fee, decimals) : 0; - const newAmount = balance - calculatedFee * RESERVED_FEE_FACTOR; + const newAmount = balance - STAKING_FORWARD_AMOUNT - calculatedFee; validateAndSetAmount(newAmount, true); } else { validateAndSetAmount(amount, true); } - }, [amount, balance, decimals, fee, shouldUseAllBalance, validateAndSetAmount]); + }, [amount, balance, fee, shouldUseAllBalance, validateAndSetAmount, calculatedFee]); useEffect(() => { if (!amount) { @@ -131,8 +137,24 @@ function StakingInitial({ }); }, [amount, fetchStakingFee]); + useSyncEffect(() => { + if (!IS_DELEGATED_BOTTOM_SHEET) return; + + if (isStakingInfoModalOpen) { + Dialog.alert({ + title: lang('Why staking is safe?'), + message: [ + `1. ${lang('$safe_staking_description1')}`, + `2. ${lang('$safe_staking_description2')}`, + `3. ${lang('$safe_staking_description3')}`, + ].join('\n\n').replace(/\*\*/g, ''), + }) + .then(closeStakingInfoModal); + } + }, [isStakingInfoModalOpen, lang]); + const handleAmountBlur = useLastCallback(() => { - if (amount && amount + stakingBalance < stakingMinAmount) { + if (amount && amount + stakingBalance < STAKING_MIN_AMOUNT) { setIsNotEnough(true); } }); @@ -157,11 +179,12 @@ function StakingInitial({ } validateAndSetAmount(amount - MIN_BALANCE_FOR_UNSTAKE); + setShouldUseAllBalance(false); }); const canSubmit = amount && balance && !isNotEnough && amount <= balance - && (amount + stakingBalance >= stakingMinAmount); + && (amount + stakingBalance >= STAKING_MIN_AMOUNT); const handleSubmit = useLastCallback((e) => { e.preventDefault(); @@ -187,16 +210,18 @@ function StakingInitial({ } const isFullBalanceSelected = balance && amount - && (balance >= amount && balance - amount <= MIN_BALANCE_FOR_UNSTAKE); - const balanceLink = lang('$balance_is', { + && (balance >= amount && Number((balance - amount).toFixed(2)) < MIN_BALANCE_FOR_UNSTAKE); // TODO $decimals + + const balanceLink = lang('$max_balance', { balance: ( {balance !== undefined - ? formatCurrencyExtended(floor(balance, STAKING_DECIMAL), symbol, true) + ? formatCurrencySimple(balance, symbol, decimals) : lang('Loading...')} ), }); + const minusOneLink = (
{formatCurrency(-Math.round(MIN_BALANCE_FOR_UNSTAKE), symbol)} @@ -231,7 +256,7 @@ function StakingInitial({ lang('$min_value', { value: ( - {formatCurrency(stakingMinAmount, 'TON')} + {formatCurrency(STAKING_MIN_AMOUNT, 'TON')} ), }) @@ -241,6 +266,8 @@ function StakingInitial({ } function renderStakingSafeModal() { + if (IS_DELEGATED_BOTTOM_SHEET) return undefined; + return ( +
@@ -157,12 +190,13 @@ function UnstakeModal({ )}
- + @@ -175,18 +209,21 @@ function UnstakeModal({ function renderPassword(isActive: boolean) { return ( <> - + {!IS_CAPACITOR && } + > + {IS_CAPACITOR && renderUnstakingShortInfo()} + ); } @@ -194,7 +231,10 @@ function UnstakeModal({ function renderComplete(isActive: boolean) { return ( <> - +
-
- - {lang('$unstaking_when_receive', { - time: {formatRelativeHumanDateTime(lang.code, unstakeDate)}, - })} -
+ {renderedIsInstantUnstake && ( +
+ + {lang('$unstaking_when_receive', { + time: {formatRelativeHumanDateTime(lang.code, unstakeDate)}, + })} +
+ )}
@@ -237,15 +279,17 @@ function UnstakeModal({ return ( { return { ...global.staking, tokens, - stakingBalance: currentAccountState?.stakingBalance, - endOfStakingCycle: currentAccountState?.poolState?.endOfCycle, + stakingType: currentAccountState?.staking?.type, + stakingBalance: currentAccountState?.staking?.balance, + endOfStakingCycle: currentAccountState?.staking?.end, + stakingInfo: global.stakingInfo, }; })(UnstakeModal)); diff --git a/src/components/swap/Swap.module.scss b/src/components/swap/Swap.module.scss new file mode 100644 index 00000000..b4a3d936 --- /dev/null +++ b/src/components/swap/Swap.module.scss @@ -0,0 +1,795 @@ +@import "../../styles/mixins"; + +.modalDialog { + overflow: hidden; + + height: 35rem; + + @supports (height: env(safe-area-inset-bottom)) { + height: calc(35rem + env(safe-area-inset-bottom)); + } +} + +.scrollContent { + overflow-x: hidden; + overflow-y: scroll; + display: flex; + flex-direction: column; + + height: 100%; + min-height: 0; + padding: 0 1rem 1rem; + + @include adapt-padding-to-scrollbar(1rem); + + @supports (padding-bottom: env(safe-area-inset-bottom)) { + padding-bottom: max(env(safe-area-inset-bottom), 1rem); + } +} + +.amountInput { + margin-bottom: 0.8125rem; +} + +.inputLabel { + margin-bottom: 0.3125rem; +} + +.amountInputInner { + padding-right: 0.5rem; +} + +.balanceContainer, +.priceContainer, +.advancedSlippageContainer { + position: relative; + z-index: 1; + + width: 100%; +} + +.tokenPrice, +.balance { + position: absolute; + top: -0.125rem; + right: 0.5rem; + + display: flex; + gap: 0.25rem; + + font-size: 0.8125rem; + color: var(--color-gray-1); +} + +.tokenPrice { + font-weight: 400; +} + +.tokenPriceBold { + font-weight: 600; +} + +.balanceLink { + cursor: var(--custom-cursor, pointer); + + font-weight: 600; + color: var(--color-blue); + text-decoration: underline; + text-decoration-style: dotted; + + @media (hover: hover) { + &:hover, + &:focus { + text-decoration: none; + } + } +} + +.content { + position: relative; + + display: flex; + flex-direction: column; +} + +.inputContainer { + display: flex; + flex-direction: column; +} + +.slippageLabel { + display: flex; + align-items: center; +} + +.tokenSelectorWrapper { + align-self: center; + + width: auto; + height: 3rem; +} + +.tokenSelectorSlide { + display: flex; + justify-content: flex-end; +} + +.tokenSelector { + cursor: var(--custom-cursor, pointer); + + display: flex; + gap: 0.375rem; + align-items: center; + align-self: center; + justify-content: center; + + height: 3rem; + padding: 0.75rem !important; + + color: var(--color-input-button-text); + + background-color: var(--color-input-button-background); + border: none; + border-radius: var(--border-radius-small); + outline: none; + + @media (hover: hover) { + &:hover { + background-color: var(--color-input-button-background-hover); + } + } + + @media (pointer: coarse) { + &:active { + background-color: var(--color-input-button-background-hover); + } + } +} + +.tokenSelectorIcon { + font-size: 1rem; + color: var(--color-gray-3); +} + +.tokenIcon { + width: 1.5rem; + height: 1.5rem; + + border-radius: 50%; +} + +.tokenIconWrapper { + position: relative; +} + +.tokenBlockchainIcon { + position: absolute; + z-index: 1; + top: 0.875rem; + right: -0.25rem; + + width: 0.75rem; + height: 0.75rem; + + border-radius: 50%; + + /* stylelint-disable-next-line plugin/whole-pixel */ + box-shadow: 0 0 0 0.0938rem var(--color-input-button-background), + inset 0 0 0 0.125rem var(--color-input-button-background); +} + +.tokenContent { + display: flex; + align-items: center; + + font-size: 0.9375rem; + font-weight: 700; +} + +.swapButtonWrapper { + cursor: var(--custom-cursor, pointer); + + position: absolute; + z-index: 1; + top: 50%; + left: 50%; + transform: translate(-50%, -45%); + + display: flex; + align-items: center; + justify-content: center; + + width: 2.75rem; + height: 2.75rem; + + background-color: var(--color-background-second); + border-radius: 50%; +} + +.swapButtonWrapperStatic { + background-color: var(--color-background-first); +} + +.swapButton { + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + + width: 2.25rem; + height: 2.25rem; + + font-size: 0.875rem; + color: var(--color-blue-button-text); + + background-color: var(--color-blue-button-background); + border-radius: 50%; + + transition: background-color 150ms; + + @media (hover: hover) { + &:hover { + background-color: var(--color-blue-button-background-hover); + } + } +} + +.feeWrapper { + display: flex; + justify-content: center; + + height: 1rem; +} + +.feeContent { + display: flex; + gap: 0.25rem; + align-items: center; + justify-content: center; +} + +.feeContentClickable { + cursor: var(--custom-cursor, pointer); +} + +.feeText { + font-size: 0.8125rem; + font-weight: 600; + color: var(--color-gray-2); +} + +.feeIcon { + font-size: 0.75rem; + color: var(--color-gray-4); +} + +.footerBlock { + display: flex; + flex-direction: column; + gap: 1rem; + align-items: center; + + margin-top: auto; +} + +.footerBlockStatic { + margin-top: 2rem; +} + +.dot { + width: 0.125rem; + height: 0.125rem; + margin: 0 0.25rem; + + font-style: normal; + line-height: 1rem; + + background-color: var(--color-blue); + border-radius: 50%; +} + +.advancedTitle { + display: flex; + justify-content: center; + + margin: 1rem 0 1.5rem; + + font-size: 1.0625rem; + font-weight: 700; + color: var(--color-black); +} + +.advancedBlock { + display: flex; + flex-direction: column; + gap: 1rem; + + margin-bottom: 1.25rem; + padding: 1rem; + + background-color: var(--color-background-first); + border-radius: var(--border-radius-default); +} + +.advancedRow { + display: flex; + justify-content: space-between; +} + +.advancedDescription { + display: flex; + align-items: center; + + font-size: 0.8125rem; + font-weight: 600; + color: var(--color-gray-1); +} + +.advancedTooltip { + cursor: var(--custom-cursor, pointer); + + margin-left: 0.25rem; + + color: var(--color-gray-4); + + transition: color 150ms; +} + +.advancedTooltipContainer { + z-index: 1; + + width: 18.9375rem; +} + +.advancedTooltipMessage { + display: flex; + flex-direction: column; + gap: 1rem; + + font-size: 0.75rem; +} + +.advancedValue { + font-size: 0.8125rem; + font-weight: 700; + color: var(--color-black); +} + +.advancedSlippageError { + position: absolute; + bottom: -0.25rem; + + display: flex; + justify-content: space-between; + + width: 100%; + min-height: 1rem; + padding: 0 0.5rem; + + font-size: 0.75rem; + line-height: 1rem; + color: var(--color-red); +} + +.advancedSlippage { + position: absolute; + top: 0; + right: 0.5rem; + + display: flex; + align-items: center; + + font-size: 0.8125rem; + font-weight: 600; + line-height: 0.8125rem; + color: var(--color-blue); +} + +.advancedInput { + position: relative; + + margin-bottom: 0.5rem; +} + +.advancedInputValue { + font-size: 1rem; + + &.isEmpty::before { + content: "0"; + + font-size: 1rem; + } +} + +.advancedError { + color: var(--color-red); +} + +.footerButtonWrapper { + height: 2.75rem; +} + +.footerButton { + width: 100%; + max-width: 100% !important; +} + +.priceImpact { + cursor: var(--custom-cursor, pointer); + + display: flex; + gap: 1rem; + align-items: center; + + padding: 1rem; + + background-color: var(--color-background-first); + border-radius: var(--border-radius-default); +} + +.priceImpactStatic { + padding: 0; +} + +.priceImpactSticker { + width: 4.3125rem; + height: 4.3125rem; +} + +.priceImpactContent { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.priceImpactTitle { + position: relative; + + font-size: 0.9375rem; + font-weight: 700; + color: var(--color-red); + + @media (hover: hover) { + &:hover { + color: var(--color-red-button-background-hover) + } + } +} + +.priceImpactArrow { + position: absolute; + bottom: 0.0625rem; + + font-size: 0.75rem; +} + +.priceImpactDescription { + font-size: 0.9375rem; + font-weight: 500; + color: var(--color-gray-1); +} + +.swapCornerTop, +.swapCornerBottom { + position: absolute; + bottom: 0; + left: 50%; + transform: translate(-50%, 0); + + overflow: hidden; + + width: 3rem; + height: 0.6875rem; + + &::after, + &::before { + content: ""; + + position: absolute; + top: 1.8125rem; + left: 50%; + transform: translate(-50%, -50%); + + width: 3rem; + height: 3rem; + + background: var(--color-background-first); + border-radius: 50% 50% 0 0; + } + + &::before { + transition: box-shadow 150ms; + + :global(html.animation-level-0) & { + transition: none !important; + } + } +} + +.swapCornerStaticTop, +.swapCornerStaticBottom { + &::before { + box-shadow: 0 0 0 0.0625rem var(--color-separator-input-stroke); + } +} + +.swapCornerBottom, +.swapCornerStaticBottom { + top: 1.125rem; + transform: rotate(180deg) translate(50%, 0); +} + +.swapCornerTop { + &::before { + top: 1.75rem + } +} + +.swapCornerStaticTop { + bottom: 0; + + height: 0.75rem; + + &::before { + top: 1.8125rem + } +} + +.selectBlockchainBlock { + align-items: center; +} + +.inputAddressWrapper { + margin: 0; +} + +.inputAddress { + width: 100%; + margin-top: 1.5rem; +} + +.blockchainHintWrapper { + display: flex; + justify-content: center; + + margin-top: 1rem; + padding: 0 2rem; +} + +.blockchainHintText { + font-size: 0.8125rem; + font-weight: 500; + color: var(--color-gray-2); + text-align: center; +} + +.blockchainHintTextError { + font-size: 0.9375rem; + color: var(--color-red); +} + +.inputButton { + position: absolute; + top: 0.5rem; + right: 0.5rem; + + display: flex; + align-items: center; + justify-content: center; + + width: 2rem; + height: 2rem; + + font-size: 1.25rem; + color: var(--color-gray-3); + + background-color: var(--color-background-first); + border-radius: var(--border-radius-small) !important; + + @media (hover: hover) { + &:hover, + &:focus { + color: var(--color-input-button-text); + + background-color: var(--color-input-button-background); + } + } + + + @media (pointer: coarse) { + &:active { + color: var(--color-input-button-text); + + background-color: var(--color-input-button-background); + // Optimization + transition: none; + } + } +} + +.changellyInfo { + display: flex; + gap: 1rem; + align-items: center; + + padding: 1rem; + + background-color: var(--color-background-first); + border-radius: var(--border-radius-default); +} + +.changellyInfoStatic { + padding: 0; +} + +.changellyInfoContent { + display: flex; + flex-direction: column; + gap: 0.875rem; +} + +.changellyInfoTitle { + display: flex; + gap: 0.25rem; + align-items: center; + justify-content: center; + + font-size: 0.8125rem; + font-weight: 700; + line-height: 0.8125rem; + text-align: center; +} +.changellyInfoDescription { + font-size: 0.75rem; + font-weight: 500; + line-height: 0.9375rem; + text-align: center; +} + +.changellyIcon { + font-size: 1rem; + color: var(--color-green); +} + +.blockchainButtons { + padding: 0; +} + +.changellyInfoBlock { + display: flex; + flex-direction: column; + gap: 1rem; + align-items: center; + + margin-top: 2rem; +} + +.changellyDescription { + font-size: 0.8125rem; + font-weight: 500; + color: var(--color-gray-2); + text-align: center; +} + +.changellyTextField { + width: 100%; +} + +.changellyImportantRed { + font-size: 1.0625rem; + font-weight: 700; + color: var(--color-red); + text-align: center; +} + +.changellyDescriptionBold { + font-weight: 700; +} + +.arrowContainer, +.arrowContainerInverted { + position: relative; +} + +.arrowContainerInverted { + transform: rotate(180deg); +} + +$translate: 1.75em; +$animation-time: 350ms; + +@keyframes arrow-disappear { + from { + transform: none; + + opacity: 1; + } + + to { + transform: translateY(-$translate); + + opacity: 0; + } +} + +@keyframes arrow-appear { + from { + transform: translateY($translate); + + opacity: 0; + } + + to { + transform: none; + + opacity: 1; + } +} + +.arrow { + visibility: hidden; +} + +.arrowNew, +.arrowOld { + position: absolute; + top: 0.0625rem; + left: 0; +} + +.animateDisappear { + animation: $animation-time ease-in-out arrow-disappear forwards; +} + +.animateAppear { + animation: $animation-time ease-in-out arrow-appear forwards; +} + +.swapShortInfo { + display: flex; + gap: 0.25rem; + align-items: center; + + max-width: calc(100% - 2rem); + height: 2rem; + padding: 0 0.375rem; + + font-size: 0.9375rem; + font-weight: 500; + color: var(--color-activity-blue); + + background-color: var(--color-activity-blue-background); + border-radius: 1rem; +} + +.swapShortAmount { + font-weight: 700; +} + +.swapShortValue { + overflow: hidden; + + text-overflow: ellipsis; + white-space: nowrap; +} + +.swapShortInfoTokenIcon { + width: 1.25rem; + height: 1.25rem; + + border-radius: 50%; +} + +.swapShortInfoBlockchainIcon { + position: absolute; + z-index: 1; + top: 0.75rem; + left: 0.75rem; + + width: 0.5625rem; + height: 0.5625rem; + + border-radius: 50%; + box-shadow: 0 0 0 0.0625rem var(--color-activity-blue-background), + 0 0 0 0.0625rem var(--color-background-second); +} diff --git a/src/components/swap/SwapBlockchain.tsx b/src/components/swap/SwapBlockchain.tsx new file mode 100644 index 00000000..44eb8e5b --- /dev/null +++ b/src/components/swap/SwapBlockchain.tsx @@ -0,0 +1,235 @@ +import type { TeactNode } from '../../lib/teact/teact'; +import React, { + memo, useRef, useState, +} from '../../lib/teact/teact'; +import { getActions } from '../../global'; + +import { SwapState, SwapType, type UserSwapToken } from '../../global/types'; + +import { ANIMATED_STICKER_BIG_SIZE_PX, IS_FIREFOX_EXTENSION } from '../../config'; +import buildClassName from '../../util/buildClassName'; +import { shortenAddress } from '../../util/shortenAddress'; +import getBlockchainNetworkName from '../../util/swap/getBlockchainNetworkName'; +import { IS_FIREFOX } from '../../util/windowEnvironment'; +import { callApi } from '../../api'; +import { ANIMATED_STICKERS_PATHS } from '../ui/helpers/animatedAssets'; + +import { useDeviceScreen } from '../../hooks/useDeviceScreen'; +import useFlag from '../../hooks/useFlag'; +import useHistoryBack from '../../hooks/useHistoryBack'; +import useLang from '../../hooks/useLang'; +import useLastCallback from '../../hooks/useLastCallback'; + +import AnimatedIconWithPreview from '../ui/AnimatedIconWithPreview'; +import Button from '../ui/Button'; +import Input from '../ui/Input'; +import ModalHeader from '../ui/ModalHeader'; +import Transition from '../ui/Transition'; + +import modalStyles from '../ui/Modal.module.scss'; +import styles from './Swap.module.scss'; + +interface OwnProps { + isActive: boolean; + swapType?: SwapType; + toAddress?: string; + tokenIn?: UserSwapToken; + tokenOut?: UserSwapToken; +} + +const SHORT_ADDRESS_SHIFT = 14; +const MIN_ADDRESS_LENGTH_TO_SHORTEN = SHORT_ADDRESS_SHIFT * 2; + +function SwapBlockchain({ + isActive, + swapType, + toAddress = '', + tokenIn, + tokenOut, +}: OwnProps) { + const { + cancelSwap, setSwapCexAddress, showNotification, setSwapScreen, + } = getActions(); + const lang = useLang(); + const { isPortrait } = useDeviceScreen(); + + // eslint-disable-next-line no-null/no-null + const toAddressRef = useRef(null); + + // Note: As of 27-11-2023, Firefox does not support readText() + const [shouldRenderPasteButton, setShouldRenderPasteButton] = useState(!(IS_FIREFOX || IS_FIREFOX_EXTENSION)); + const [isAddressFocused, markAddressFocused, unmarkAddressFocused] = useFlag(); + const [hasToAddressError, setHasToAddressError] = useState(false); + const [canContinue, setCanContinue] = useState(swapType !== SwapType.CrosschainFromTon); + + const toAddressShort = toAddress.length > MIN_ADDRESS_LENGTH_TO_SHORTEN + ? shortenAddress(toAddress, SHORT_ADDRESS_SHIFT) || '' + : toAddress; + + const addressPlaceholder = (lang('Receiving address in blockchain', { + blockchain: getBlockchainNetworkName(tokenOut?.blockchain), + }) as TeactNode[]).join(''); + + const handleCancelClick = useLastCallback(() => { + setHasToAddressError(false); + setCanContinue(false); + setSwapScreen({ state: isPortrait ? SwapState.Initial : SwapState.None }); + }); + + useHistoryBack({ + isActive, + onBack: handleCancelClick, + }); + + const handleAddressFocus = useLastCallback(() => { + const el = toAddressRef.current!; + + // `selectionStart` is only updated in the next frame after `focus` event + requestAnimationFrame(() => { + const caretPosition = el.selectionStart!; + markAddressFocused(); + + // Restore caret position after input field value has been focused and expanded + requestAnimationFrame(() => { + const newCaretPosition = caretPosition <= SHORT_ADDRESS_SHIFT + 3 + ? caretPosition + : Math.max(0, el.value.length - (toAddressShort.length - caretPosition)); + + el.setSelectionRange(newCaretPosition, newCaretPosition); + if (newCaretPosition > SHORT_ADDRESS_SHIFT * 2) { + el.scrollLeft = el.scrollWidth - el.clientWidth; + } + }); + }); + }); + + const validateToAddress = useLastCallback(async (address: string) => { + const response = await callApi('swapCexValidateAddress', { + slug: tokenOut?.slug!, + address, + }); + + if (!response) { + setHasToAddressError(false); + setCanContinue(false); + return; + } + + setHasToAddressError(!response.result); + setCanContinue(response.result); + }); + + const handleAddressBlur = useLastCallback(() => { + unmarkAddressFocused(); + }); + + const handleAddressInput = useLastCallback((newToAddress: string) => { + validateToAddress(newToAddress.trim()); + setSwapCexAddress({ toAddress: newToAddress.trim() }); + }); + + const handlePasteClick = useLastCallback(() => { + navigator.clipboard + .readText() + .then((clipboardText) => { + setSwapCexAddress({ toAddress: clipboardText.trim() }); + validateToAddress(clipboardText.trim()); + }) + .catch(() => { + showNotification({ + message: lang('Error reading clipboard') as string, + }); + setShouldRenderPasteButton(false); + }); + }); + + const submitPassword = useLastCallback(() => { + setSwapScreen({ state: SwapState.Password }); + }); + + function renderInfo() { + const text = hasToAddressError + ? lang('Incorrect address.') + : lang('Please provide an address of your wallet in another blockchain to receive bought tokens.'); + + return ( + + + {text} + + + ); + } + + function renderInputAddress() { + if (swapType !== SwapType.CrosschainFromTon) return undefined; + + return ( + <> +
+ + {shouldRenderPasteButton && toAddress === '' && ( + + )} + +
+ {renderInfo()} + + ); + } + + const title = (lang('$swap_from_to', { + from: tokenIn?.symbol, + to: tokenOut?.symbol, + }) as TeactNode[]).join(''); + + return ( + <> + +
+ + + {renderInputAddress()} + +
+ + +
+
+ + ); +} + +export default memo(SwapBlockchain); diff --git a/src/components/swap/SwapComplete.tsx b/src/components/swap/SwapComplete.tsx new file mode 100644 index 00000000..96c773e3 --- /dev/null +++ b/src/components/swap/SwapComplete.tsx @@ -0,0 +1,76 @@ +import React, { memo } from '../../lib/teact/teact'; + +import type { SwapType, UserSwapToken } from '../../global/types'; + +import buildClassName from '../../util/buildClassName'; + +import useHistoryBack from '../../hooks/useHistoryBack'; +import useLang from '../../hooks/useLang'; + +import SwapResult from '../common/SwapResult'; +import Button from '../ui/Button'; +import ModalHeader from '../ui/ModalHeader'; + +import modalStyles from '../ui/Modal.module.scss'; +import styles from './Swap.module.scss'; + +interface OwnProps { + isActive: boolean; + tokenIn?: UserSwapToken; + tokenOut?: UserSwapToken; + amountIn?: number; + amountOut?: number; + swapType?: SwapType; + toAddress?: string; + onInfoClick: NoneToVoidFunction; + onStartSwap: NoneToVoidFunction; + onClose: NoneToVoidFunction; +} + +function SwapComplete({ + isActive, + tokenIn, + tokenOut, + amountIn, + amountOut, + swapType, + toAddress, + onInfoClick, + onStartSwap, + onClose, +}: OwnProps) { + const lang = useLang(); + + useHistoryBack({ + isActive, + onBack: onClose, + }); + + return ( + <> + + +
+ + +
+ +
+
+ + ); +} + +export default memo(SwapComplete); diff --git a/src/components/swap/SwapInitial.tsx b/src/components/swap/SwapInitial.tsx new file mode 100644 index 00000000..2f8786d5 --- /dev/null +++ b/src/components/swap/SwapInitial.tsx @@ -0,0 +1,626 @@ +import { BottomSheet } from 'native-bottom-sheet'; +import React, { + memo, useEffect, useMemo, useRef, useState, +} from '../../lib/teact/teact'; +import { + getActions, getGlobal, withGlobal, +} from '../../global'; + +import type { GlobalState, UserSwapToken } from '../../global/types'; +import { + SwapInputSource, SwapState, SwapType, +} from '../../global/types'; + +import { + ANIMATED_STICKER_TINY_SIZE_PX, + ANIMATION_LEVEL_MAX, + CHANGELLY_AML_KYC, + CHANGELLY_PRIVACY_POLICY, + CHANGELLY_TERMS_OF_USE, + JUSDT_TOKEN_SLUG, + JWBTC_TOKEN_SLUG, + TON_SYMBOL, + TON_TOKEN_SLUG, +} from '../../config'; +import { selectCurrentAccountState, selectSwapTokens } from '../../global/selectors'; +import buildClassName from '../../util/buildClassName'; +import { formatCurrency, formatCurrencySimple } from '../../util/formatNumber'; +import getSwapRate from '../../util/swap/getSwapRate'; +import { IS_DELEGATED_BOTTOM_SHEET } from '../../util/windowEnvironment'; +import { ANIMATED_STICKERS_PATHS } from '../ui/helpers/animatedAssets'; + +import useDebouncedCallback from '../../hooks/useDebouncedCallback'; +import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps'; +import useFlag from '../../hooks/useFlag'; +import useLang from '../../hooks/useLang'; +import useLastCallback from '../../hooks/useLastCallback'; +import usePrevious from '../../hooks/usePrevious'; +import usePrevious2 from '../../hooks/usePrevious2'; +import useSyncEffect from '../../hooks/useSyncEffect'; + +import AnimatedIconWithPreview from '../ui/AnimatedIconWithPreview'; +import RichNumberInput from '../ui/RichNumberInput'; +import Transition from '../ui/Transition'; +import SwapSelectToken from './components/SwapSelectToken'; +import SwapSubmitButton from './components/SwapSubmitButton'; +import SwapSettingsModal, { MAX_PRICE_IMPACT_VALUE } from './SwapSettingsModal'; + +import modalStyles from '../ui/Modal.module.scss'; +import styles from './Swap.module.scss'; + +interface OwnProps { + isStatic?: boolean; + isActive?: boolean; +} + +interface StateProps { + currentSwap: GlobalState['currentSwap']; + accountId?: string; + tokens?: UserSwapToken[]; + cardTokenSlug?: string; +} + +const ESTIMATE_REQUEST_INTERVAL = 15_000; +const ESTIMATE_REQUEST_DEBOUNCE_TIME = 500; +const DEFAULT_SWAP_FEE = 0.5; + +function SwapInitial({ + currentSwap: { + tokenInSlug = TON_TOKEN_SLUG, + tokenOutSlug = JWBTC_TOKEN_SLUG, + amountIn, + amountOut, + errorType, + isEstimating, + shouldEstimate, + networkFee = 0, + realNetworkFee = 0, + priceImpact = 0, + inputSource, + swapType, + limits, + isLoading, + pairs, + }, + accountId, + cardTokenSlug, + tokens, + isActive, + isStatic, +}: OwnProps & StateProps) { + const { + setDefaultSwapParams, + setSwapAmountIn, + setSwapAmountOut, + switchSwapTokens, + estimateSwap, + estimateSwapCex, + setSwapScreen, + loadSwapPairs, + setSwapType, + setSwapCexAddress, + } = getActions(); + const lang = useLang(); + + // eslint-disable-next-line no-null/no-null + const inputInRef = useRef(null); + // eslint-disable-next-line no-null/no-null + const inputOutRef = useRef(null); + + // eslint-disable-next-line no-null/no-null + const estimateIntervalId = useRef(null); + + const [isSettingsModalOpen, openSettingsModal, closeSettingsModal] = useFlag(false); + + const [hasAmountInError, setHasAmountInError] = useState(false); + + const tokenInTransitionKey = useTokenTransitionKey(tokenInSlug); + + const accountIdPrev = usePrevious(accountId, true); + + const tokenIn = useMemo( + () => tokens?.find((token) => token.slug === tokenInSlug), + [tokenInSlug, tokens], + ); + + const tokenOut = useMemo( + () => tokens?.find((token) => token.slug === tokenOutSlug), + [tokenOutSlug, tokens], + ); + + const TON = useMemo( + () => tokens?.find((token) => token.slug === TON_TOKEN_SLUG) ?? { amount: 0 }, + [tokens], + ); + + const isTokenInTON = tokenInSlug === TON_TOKEN_SLUG; + const totalTonAmount = useMemo( + () => { + if (!tokenIn || !amountIn) { + return 0; + } + if (isTokenInTON) { + return amountIn + networkFee; + } + return networkFee; + }, + [tokenIn, amountIn, isTokenInTON, networkFee], + ); + + const isErrorExist = errorType !== undefined; + const isEnoughTON = TON.amount > totalTonAmount; + // eslint-disable-next-line max-len + const isCorrectAmountIn = (amountIn && tokenIn?.amount && amountIn > 0 && amountIn <= tokenIn?.amount && isEnoughTON) || swapType === SwapType.CrosschainToTon; + const isCorrectAmountOut = amountOut && amountOut > 0; + const canSubmit = Boolean(isCorrectAmountIn && isCorrectAmountOut && !isEstimating && !isErrorExist); + const isPriceImpactError = priceImpact >= MAX_PRICE_IMPACT_VALUE; + + const isCrosschain = swapType === SwapType.CrosschainFromTon || swapType === SwapType.CrosschainToTon; + + const isReverseProhibited = useMemo(() => { + return isCrosschain || pairs?.bySlug?.[tokenInSlug]?.[tokenOutSlug]?.isReverseProhibited; + }, [isCrosschain, pairs, tokenInSlug, tokenOutSlug]); + + const handleEstimateSwap = useLastCallback((shouldBlock: boolean) => { + if (isCrosschain) { + estimateSwapCex({ shouldBlock }); + return; + } + estimateSwap({ shouldBlock }); + }); + + const debounceEstimateSwap = useDebouncedCallback( + handleEstimateSwap, [handleEstimateSwap], ESTIMATE_REQUEST_DEBOUNCE_TIME, true, + ); + + const createEstimateTimer = useLastCallback(() => { + estimateIntervalId.current = window.setInterval(() => { + debounceEstimateSwap(false); + }, ESTIMATE_REQUEST_INTERVAL); + }); + + useEffect(() => { + if (cardTokenSlug === TON_TOKEN_SLUG) { + setDefaultSwapParams({ tokenInSlug: JUSDT_TOKEN_SLUG, tokenOutSlug: cardTokenSlug }); + } else { + setDefaultSwapParams({ tokenOutSlug: cardTokenSlug }); + } + }, [cardTokenSlug]); + + useEffect(() => { + const clearEstimateTimer = () => estimateIntervalId.current && window.clearInterval(estimateIntervalId.current); + + if (shouldEstimate) { + clearEstimateTimer(); + debounceEstimateSwap(true); + } + + createEstimateTimer(); + + return clearEstimateTimer; + }, [shouldEstimate, debounceEstimateSwap, createEstimateTimer]); + + useEffect(() => { + const shouldForceUpdate = accountId !== accountIdPrev; + + if (tokenInSlug) { + loadSwapPairs({ tokenSlug: tokenInSlug, shouldForceUpdate }); + } + if (tokenOutSlug) { + loadSwapPairs({ tokenSlug: tokenOutSlug, shouldForceUpdate }); + } + }, [tokenInSlug, tokenOutSlug, accountId, accountIdPrev]); + + useEffect(() => { + if (tokenIn?.blockchain === 'ton' && tokenOut?.blockchain !== 'ton') { + setSwapType({ type: SwapType.CrosschainFromTon }); + return; + } else if (tokenOut?.blockchain === 'ton' && tokenIn?.blockchain !== 'ton') { + setSwapType({ type: SwapType.CrosschainToTon }); + return; + } + setSwapType({ type: SwapType.OnChain }); + }, [tokenIn, tokenOut]); + + const validateAmountIn = useLastCallback((amount: number | undefined) => { + if (swapType === SwapType.CrosschainToTon) { + setHasAmountInError(false); + return; + } + + const hasError = amount !== undefined && ( + Number.isNaN(amount) || amount < 0 + || (tokenIn?.amount !== undefined && amount > tokenIn.amount) + ); + + setHasAmountInError(hasError); + }); + + useEffect(() => { + validateAmountIn(amountIn); + }, [amountIn, tokenIn, validateAmountIn, swapType]); + + useEffectWithPrevDeps(([prevIsOpen]) => { + if (!IS_DELEGATED_BOTTOM_SHEET) return; + + if (isSettingsModalOpen || prevIsOpen) { + BottomSheet.setSelfSize({ size: isSettingsModalOpen ? 'full' : 'half' }); + } + }, [isSettingsModalOpen]); + + const handleAmountInChange = useLastCallback( + (amount: number | undefined, noReset = false) => { + if (!noReset) { + setHasAmountInError(false); + } + + if (amount === undefined) { + setSwapAmountIn({ amount: undefined }); + return; + } + + validateAmountIn(amount); + setSwapAmountIn({ amount }); + }, + ); + + const handleAmountOutChange = useLastCallback( + (amount: number | undefined) => { + setSwapAmountOut({ amount }); + }, + ); + + const handleMaxAmountClick = useLastCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + + if (!tokenIn?.amount) { + return; + } + + const amountWithFee = tokenIn.amount > DEFAULT_SWAP_FEE + ? tokenIn.amount - DEFAULT_SWAP_FEE + : tokenIn.amount; + const newAmount = isTokenInTON ? amountWithFee : tokenIn.amount; + + handleAmountInChange(newAmount); + }, + ); + + const handleSubmit = useLastCallback((e) => { + e.preventDefault(); + + if (!canSubmit) { + return; + } + + if (isCrosschain) { + setSwapCexAddress({ toAddress: '' }); + if (swapType === SwapType.CrosschainToTon) { + setSwapScreen({ state: SwapState.Password }); + } else { + setSwapScreen({ state: SwapState.Blockchain }); + } + return; + } + + setSwapScreen({ state: SwapState.Password }); + }); + + const handleSwitchTokens = useLastCallback(() => { + switchSwapTokens(); + }); + + function renderBalance() { + const isBalanceVisible = tokenIn && swapType !== SwapType.CrosschainToTon; + + return ( + + {isBalanceVisible && ( +
+ + {lang('$max_balance', { + balance: ( +
+ {formatCurrencySimple(tokenIn.amount, tokenIn?.symbol, tokenIn?.decimals)} +
+ ), + })} +
+
+ )} +
+ ); + } + + function renderPrice() { + const isPriceVisible = Boolean(amountIn && amountOut); + const shouldBeRendered = isPriceVisible && !isEstimating; + const rate = getSwapRate( + amountIn ? String(amountIn) : undefined, + amountOut ? String(amountOut) : undefined, + tokenIn, + tokenOut, + true, + ); + + if (!rate) return undefined; + + return ( + +
+ {shouldBeRendered && ( + + {rate.firstCurrencySymbol}{' ≈ '} + + {rate.price}{' '}{rate.secondCurrencySymbol} + + + )} +
+
+ ); + } + + function renderPriceImpactWarning() { + if (!priceImpact || !isPriceImpactError || isCrosschain) { + return undefined; + } + + return ( +
+ +
+ + {lang('The exchange rate is below market value!', { value: `${priceImpact}%` })} + + + + {lang('We do not recommend to perform an exchange, try to specify a lower amount.')} + +
+
+ ); + } + + function renderChangellyInfo() { + if (!isCrosschain) { + return undefined; + } + + return ( +
+ ); + } + + function renderFee() { + if (swapType === SwapType.CrosschainToTon) return undefined; + + const isFeeEqualZero = realNetworkFee === 0; + const text = lang(isFeeEqualZero ? '$fee_value' : '$fee_value_almost_equal', { + fee: formatCurrency(realNetworkFee, TON_SYMBOL), + }); + + return ( + +
+ {text} + { + isCrosschain + ? undefined + : + } +
+
+ ); + } + + return ( + <> +
+
+
+ {renderBalance()} + + + +
+ +
+
+ +
+
+ +
+ {renderPrice()} + + + +
+
+ +
+ {renderFee()} + + {renderPriceImpactWarning()} + {renderChangellyInfo()} + + +
+
+ + + ); +} + +export default memo( + withGlobal( + (global): StateProps => { + const accountState = selectCurrentAccountState(global); + + return { + accountId: global.currentAccountId, + currentSwap: global.currentSwap, + tokens: selectSwapTokens(global), + cardTokenSlug: accountState?.currentTokenSlug, + }; + }, + (global, _, stickToFirst) => stickToFirst(global.currentAccountId), + )(SwapInitial), +); + +function useTokenTransitionKey(tokenSlug: string) { + const transitionKeyRef = useRef(0); + + useSyncEffect(() => { + transitionKeyRef.current++; + }, [tokenSlug]); + + return transitionKeyRef.current; +} + +function AnimatedArrows({ value }: { value?: string }) { + const animationLevel = getGlobal().settings.animationLevel; + const prevValue = usePrevious2(value); + + const shouldAnimate = (animationLevel === ANIMATION_LEVEL_MAX) && prevValue && prevValue !== value; + const [hasAnimation, setHasAnimation] = useState(false); + + useEffect(() => { + if (!shouldAnimate) return undefined; + + setHasAnimation(true); + + const timeoutId = window.setTimeout(() => { + setHasAnimation(false); + }, 350); + + return () => window.clearTimeout(timeoutId); + }, [animationLevel, prevValue, shouldAnimate, value]); + + function renderArrow(isInverted?: boolean) { + return ( +
+
+ +
+
+ +
+
+ +
+
+ ); + } + + return ( + <> + {renderArrow()} + {renderArrow(true)} + + ); +} diff --git a/src/components/swap/SwapModal.tsx b/src/components/swap/SwapModal.tsx new file mode 100644 index 00000000..fd12333f --- /dev/null +++ b/src/components/swap/SwapModal.tsx @@ -0,0 +1,306 @@ +import React, { + memo, useEffect, useMemo, useState, +} from '../../lib/teact/teact'; +import { getActions, withGlobal } from '../../global'; + +import type { ApiActivity } from '../../api/types'; +import type { GlobalState, UserSwapToken } from '../../global/types'; +import { SwapState, SwapType } from '../../global/types'; + +import { IS_CAPACITOR } from '../../config'; +import { selectCurrentAccountState, selectSwapTokens } from '../../global/selectors'; +import buildClassName from '../../util/buildClassName'; +import { formatCurrencyExtended } from '../../util/formatNumber'; +import resolveModalTransitionName from '../../util/resolveModalTransitionName'; +import getBlockchainNetworkIcon from '../../util/swap/getBlockchainNetworkIcon'; +import { ASSET_LOGO_PATHS } from '../ui/helpers/assetLogos'; + +import { useDeviceScreen } from '../../hooks/useDeviceScreen'; +import useLang from '../../hooks/useLang'; +import useLastCallback from '../../hooks/useLastCallback'; +import useModalTransitionKeys from '../../hooks/useModalTransitionKeys'; + +import TokenSelector from '../common/TokenSelector'; +import Modal from '../ui/Modal'; +import ModalHeader from '../ui/ModalHeader'; +import Transition from '../ui/Transition'; +import SwapBlockchain from './SwapBlockchain'; +import SwapComplete from './SwapComplete'; +import SwapInitial from './SwapInitial'; +import SwapPassword from './SwapPassword'; +import SwapWaitTokens from './SwapWaitTokens'; + +import modalStyles from '../ui/Modal.module.scss'; +import styles from './Swap.module.scss'; + +interface StateProps { + currentSwap: GlobalState['currentSwap']; + swapTokens?: UserSwapToken[]; + activityById?: Record; +} + +function SwapModal({ + currentSwap: { + state, + tokenInSlug, + tokenOutSlug, + amountIn = 0, + amountOut = 0, + isLoading, + error, + activityId, + swapType, + toAddress, + payinAddress, + }, + swapTokens, + activityById, +}: StateProps) { + const { + startSwap, + cancelSwap, + setSwapScreen, + submitSwap, + showActivityInfo, + submitSwapCexFromTon, + submitSwapCexToTon, + } = getActions(); + const lang = useLang(); + const { isPortrait } = useDeviceScreen(); + + const isOpen = state !== SwapState.None; + const { renderingKey, nextKey, updateNextKey } = useModalTransitionKeys(state, isOpen); + + const tokenIn = useMemo( + () => swapTokens?.find((token) => token.slug === tokenInSlug), + [tokenInSlug, swapTokens], + ); + + const tokenOut = useMemo( + () => swapTokens?.find((token) => token.slug === tokenOutSlug), + [swapTokens, tokenOutSlug], + ); + + const [renderedTransactionAmountIn, setRenderedTransactionAmountIn] = useState(amountIn); + const [renderedTransactionAmountOut, setRenderedTransactionAmountOut] = useState(amountOut); + + useEffect(() => { + if (!isOpen || !activityById || !activityId || swapType !== SwapType.CrosschainToTon) return; + + const activity = activityById[activityId]; + + if (activity.kind === 'swap') { + const status = activity.cex?.status; + if (status === 'exchanging' || status === 'confirming') { + setSwapScreen({ state: SwapState.Complete }); + } + } + }, [activityById, activityId, isOpen, swapType]); + + const handleTransferSubmit = useLastCallback((password: string) => { + setRenderedTransactionAmountIn(amountIn); + setRenderedTransactionAmountOut(amountOut); + + if (swapType === SwapType.OnChain) { + submitSwap({ password }); + return; + } + + if (swapType === SwapType.CrosschainToTon) { + submitSwapCexToTon({ password }); + } else { + submitSwapCexFromTon({ password }); + } + }); + + const handleBackClick = useLastCallback(() => { + if (state === SwapState.Password) { + if (swapType === SwapType.CrosschainFromTon) { + setSwapScreen({ state: SwapState.Blockchain }); + } else { + setSwapScreen({ state: isPortrait ? SwapState.Initial : SwapState.None }); + } + return; + } + + if (state === SwapState.SelectTokenTo || state === SwapState.SelectTokenFrom) { + setSwapScreen({ state: isPortrait ? SwapState.Initial : SwapState.None }); + } + + if (state === SwapState.Blockchain) { + setSwapScreen({ state: isPortrait ? SwapState.Initial : SwapState.None }); + } + }); + + const handleTransactionInfoClick = useLastCallback(() => { + cancelSwap({ shouldReset: true }); + showActivityInfo({ id: activityId! }); + }); + + const handleModalClose = useLastCallback(() => { + cancelSwap({ shouldReset: isPortrait }); + updateNextKey(); + }); + + const handleModalCloseWithReset = useLastCallback(() => { + cancelSwap({ shouldReset: true }); + }); + + const handleStartSwap = useLastCallback(() => { + startSwap({ isPortrait }); + }); + + function renderSwapShortInfo() { + if (!tokenIn || !tokenOut || !amountIn || !amountOut) return undefined; + + const logoIn = tokenIn.image ?? ASSET_LOGO_PATHS[tokenIn.symbol.toLowerCase() as keyof typeof ASSET_LOGO_PATHS]; + const logoOut = tokenOut.image ?? ASSET_LOGO_PATHS[tokenOut.symbol.toLowerCase() as keyof typeof ASSET_LOGO_PATHS]; + + return ( +
+
+ {tokenIn.symbol} + {tokenIn.blockchain && ( + {tokenIn.blockchain} + )} +
+ + {lang('%amount_from% to %amount_to%', { + amount_from: ( + + {formatCurrencyExtended(amountIn, tokenIn.symbol ?? '', true)} + ), + amount_to: ( + + {formatCurrencyExtended(amountOut, tokenOut.symbol ?? '', true)} + ), + })} + +
+ {tokenOut.symbol} + {tokenOut.blockchain && ( + {tokenOut.blockchain} + )} +
+
+ ); + } + + // eslint-disable-next-line consistent-return + function renderContent(isActive: boolean, isFrom: boolean, currentKey: number) { + switch (currentKey) { + case SwapState.Initial: + return ( + <> + + + + ); + case SwapState.Blockchain: + return ( + + ); + case SwapState.WaitTokens: + return ( + + ); + case SwapState.Password: + return ( + + {IS_CAPACITOR && renderSwapShortInfo()} + + ); + case SwapState.Complete: + return ( + + ); + case SwapState.SelectTokenFrom: + case SwapState.SelectTokenTo: + return ( + + ); + } + } + + return ( + + + {renderContent} + + + ); +} + +export default memo(withGlobal((global): StateProps => { + const accountState = selectCurrentAccountState(global); + const activityById = accountState?.activities?.byId; + + return { + currentSwap: global.currentSwap, + swapTokens: selectSwapTokens(global), + activityById, + }; +})(SwapModal)); diff --git a/src/components/swap/SwapPassword.tsx b/src/components/swap/SwapPassword.tsx new file mode 100644 index 00000000..f72fc4f2 --- /dev/null +++ b/src/components/swap/SwapPassword.tsx @@ -0,0 +1,60 @@ +import React, { memo, type TeactNode } from '../../lib/teact/teact'; +import { getActions } from '../../global'; + +import { IS_CAPACITOR } from '../../config'; + +import useHistoryBack from '../../hooks/useHistoryBack'; +import useLang from '../../hooks/useLang'; + +import ModalHeader from '../ui/ModalHeader'; +import PasswordForm from '../ui/PasswordForm'; + +interface OwnProps { + isActive: boolean; + isLoading?: boolean; + error?: string; + children?: TeactNode; + onSubmit: (password: string) => void; + onBack: NoneToVoidFunction; +} + +function SwapPassword({ + isActive, + isLoading, + error, + children, + onSubmit, + onBack, +}: OwnProps) { + const { cancelSwap, clearSwapError } = getActions(); + + const lang = useLang(); + + useHistoryBack({ + isActive, + onBack, + }); + + return ( + <> + {!IS_CAPACITOR && } + + {children} + + + ); +} + +export default memo(SwapPassword); diff --git a/src/components/swap/SwapSettingsModal.tsx b/src/components/swap/SwapSettingsModal.tsx new file mode 100644 index 00000000..85d36b72 --- /dev/null +++ b/src/components/swap/SwapSettingsModal.tsx @@ -0,0 +1,239 @@ +import React, { memo, useState } from '../../lib/teact/teact'; +import { getActions, withGlobal } from '../../global'; + +import type { UserSwapToken, UserToken } from '../../global/types'; + +import { TON_SYMBOL } from '../../config'; +import buildClassName from '../../util/buildClassName'; +import { formatCurrency } from '../../util/formatNumber'; + +import useFlag from '../../hooks/useFlag'; +import useLang from '../../hooks/useLang'; +import useLastCallback from '../../hooks/useLastCallback'; + +import Button from '../ui/Button'; +import IconWithTooltip from '../ui/IconWithTooltip'; +import Modal from '../ui/Modal'; +import RichNumberInput from '../ui/RichNumberInput'; + +import modalStyles from '../ui/Modal.module.scss'; +import styles from './Swap.module.scss'; + +interface OwnProps { + isOpen: boolean; + tokenOut?: UserToken | UserSwapToken; + fee?: number; + onClose: () => void; +} + +interface StateProps { + slippage: number; + priceImpact?: number; + amountOutMin?: string; +} + +const SLIPPAGE_VALUES = [0.1, 0.5, 1, 5]; +const MAX_SLIPPAGE_VALUE = 50; + +export const MAX_PRICE_IMPACT_VALUE = 5; + +function SwapSettingsModal({ + isOpen, + onClose, + tokenOut, + slippage, + priceImpact = 0, + fee = 0, + amountOutMin = '0', +}: OwnProps & StateProps) { + const { setSlippage } = getActions(); + const lang = useLang(); + + const [isSlippageFocused, markSlippageFocused, unmarkSlippageFocused] = useFlag(); + const [hasError, setHasError] = useState(false); + + const [currentSlippage, setCurrentSlippage] = useState(slippage); + + const priceImpactError = priceImpact >= MAX_PRICE_IMPACT_VALUE; + const slippageError = currentSlippage === undefined || currentSlippage > MAX_SLIPPAGE_VALUE; + + const handleSave = useLastCallback(() => { + setSlippage({ slippage: currentSlippage! }); + onClose(); + }); + + const resetModal = useLastCallback(() => { + setCurrentSlippage(slippage); + }); + + function renderSlippageValues() { + const slippageList = SLIPPAGE_VALUES.map((value, index) => { + return ( + <> +
setCurrentSlippage(value)} + className={styles.balanceLink} + >{value}% +
+ {index + 1 !== SLIPPAGE_VALUES.length && } + + ); + }); + + return ( +
+ {slippageList} +
+ ); + } + + function renderSlippageError() { + const error = currentSlippage === undefined + ? lang('Slippage not specified') + : currentSlippage > MAX_SLIPPAGE_VALUE + ? lang('Slippage too high') + : ''; + + setHasError(!!error); + + return ( +
+ {lang(error)} +
+ ); + } + + function renderSlippageLabel() { + return ( + <> + {lang('Slippage')} + + {lang('$swap_slippage_tooltip1')} + {lang('$swap_slippage_tooltip2')} +
+ )} + tooltipClassName={styles.advancedTooltipContainer} + iconClassName={buildClassName( + styles.advancedTooltip, slippageError && styles.advancedError, + )} + /> + + ); + } + + return ( + +
+ {lang('Swap Details')} +
+
+ {renderSlippageValues()} + + {renderSlippageError()} +
+
+
+ + {lang('Blockchain fee')} + + + ≈ {formatCurrency(fee, TON_SYMBOL)} + +
+
+ + {lang('Price Impact')} + + {lang('$swap_price_impact_tooltip1')} + {lang('$swap_price_impact_tooltip2')} +
+ )} + tooltipClassName={styles.advancedTooltipContainer} + iconClassName={buildClassName( + styles.advancedTooltip, priceImpactError && styles.advancedError, + )} + /> + + {priceImpact}% + +
+
+ + {lang('Minimum Received')} + + {lang('$swap_minimum_received_tooltip1')} + {lang('$swap_minimum_received_tooltip2')} +
+ )} + tooltipClassName={styles.advancedTooltipContainer} + iconClassName={styles.advancedTooltip} + /> + + {tokenOut && ( + { + formatCurrency(Number(amountOutMin), tokenOut.symbol) + } + + )} +
+
+
+ + +
+ + ); +} + +export default memo( + withGlobal((global): StateProps => { + const { + slippage, priceImpact, amountOutMin, + } = global.currentSwap; + + return { + slippage, + priceImpact, + amountOutMin, + }; + })(SwapSettingsModal), +); diff --git a/src/components/swap/SwapWaitTokens.tsx b/src/components/swap/SwapWaitTokens.tsx new file mode 100644 index 00000000..6d3666cd --- /dev/null +++ b/src/components/swap/SwapWaitTokens.tsx @@ -0,0 +1,133 @@ +import React, { memo, useMemo, useState } from '../../lib/teact/teact'; + +import type { UserSwapToken } from '../../global/types'; + +import { CHANGELLY_WAITING_DEADLINE } from '../../config'; +import buildClassName from '../../util/buildClassName'; +import { formatCurrencyExtended } from '../../util/formatNumber'; +import getBlockchainNetworkName from '../../util/swap/getBlockchainNetworkName'; + +import useHistoryBack from '../../hooks/useHistoryBack'; +import useLang from '../../hooks/useLang'; +import useLastCallback from '../../hooks/useLastCallback'; + +import Countdown from '../common/Countdown'; +import SwapTokensInfo from '../common/SwapTokensInfo'; +import Button from '../ui/Button'; +import InteractiveTextField from '../ui/InteractiveTextField'; +import ModalHeader from '../ui/ModalHeader'; +import Transition from '../ui/Transition'; + +import modalStyles from '../ui/Modal.module.scss'; +import styles from './Swap.module.scss'; + +interface OwnProps { + isActive: boolean; + tokenIn?: UserSwapToken; + tokenOut?: UserSwapToken; + amountIn?: number; + amountOut?: number; + payinAddress?: string; + onClose: NoneToVoidFunction; +} + +function SwapWaitTokens({ + isActive, + tokenIn, + tokenOut, + amountIn, + amountOut, + payinAddress, + onClose, +}: OwnProps) { + const lang = useLang(); + + const [isExpired, setIsExpired] = useState(false); + + const timestamp = useMemo(() => Date.now(), []); + + useHistoryBack({ + isActive, + onBack: onClose, + }); + + const handleTimeout = useLastCallback(() => { + setIsExpired(true); + }); + + function renderInfo() { + if (isExpired) { + return ( +
+ + {lang('The time for sending coins is over')} + + + {lang('Please wait a few moments...')} + +
+ ); + } + + return ( +
+ {lang('$swap_changelly_to_ton_description1', { + value: ( + + {formatCurrencyExtended(Number(amountIn), tokenIn?.symbol ?? '', true)} + + ), + blockchain: ( + + {getBlockchainNetworkName(tokenIn?.blockchain)} + + ), + time: , + })} + + +
+ ); + } + + return ( + <> + + +
+ + + + {renderInfo()} + + +
+ +
+
+ + ); +} + +export default memo(SwapWaitTokens); diff --git a/src/components/swap/components/SwapSelectToken.tsx b/src/components/swap/components/SwapSelectToken.tsx new file mode 100644 index 00000000..cb6f87a1 --- /dev/null +++ b/src/components/swap/components/SwapSelectToken.tsx @@ -0,0 +1,109 @@ +import React, { + memo, + useLayoutEffect, + useRef, +} from '../../../lib/teact/teact'; +import { setExtraStyles } from '../../../lib/teact/teact-dom'; +import { getActions } from '../../../global'; + +import { SwapState, type UserSwapToken } from '../../../global/types'; + +import { TON_BLOCKCHAIN } from '../../../config'; +import buildClassName from '../../../util/buildClassName'; +import getBlockchainNetworkIcon from '../../../util/swap/getBlockchainNetworkIcon'; +import { REM } from '../../../util/windowEnvironment'; +import { ASSET_LOGO_PATHS } from '../../ui/helpers/assetLogos'; + +import useLastCallback from '../../../hooks/useLastCallback'; +import useSyncEffect from '../../../hooks/useSyncEffect'; + +import Transition from '../../ui/Transition'; + +import styles from '../Swap.module.scss'; + +interface OwnProps { + token?: UserSwapToken; + shouldFilter?: boolean; +} + +function SwapSelectToken({ token, shouldFilter }: OwnProps) { + const { setSwapScreen } = getActions(); + + const { transitionRef } = useSelectorWidth(token); + + const handleOpenSelectTokenModal = useLastCallback(() => { + setSwapScreen({ + state: shouldFilter ? SwapState.SelectTokenTo : SwapState.SelectTokenFrom, + }); + }); + + const buttonTransitionKeyRef = useRef(0); + const buttonStateStr = `${token?.symbol}_${token?.blockchain}`; + + useSyncEffect(() => { + buttonTransitionKeyRef.current++; + }, [buttonStateStr]); + + function renderToken(tokenToRender: UserSwapToken | undefined) { + const image = ASSET_LOGO_PATHS[ + tokenToRender?.symbol.toLowerCase() as keyof typeof ASSET_LOGO_PATHS + ] ?? tokenToRender?.image; + const blockchain = tokenToRender?.blockchain ?? TON_BLOCKCHAIN; + + return ( + + + + ); + } + + return renderToken(token); +} + +export default memo(SwapSelectToken); + +const BUTTON_WIDTH = 70 / REM; +const CHARACTER_WIDTH = 10; + +function useSelectorWidth(token?: UserSwapToken) { + // eslint-disable-next-line no-null/no-null + const transitionRef = useRef(null); + + useLayoutEffect(() => { + if (!transitionRef.current || !token) return; + + const symbolWidth = (token.symbol.length * CHARACTER_WIDTH) / REM; + const minWidth = `${symbolWidth + BUTTON_WIDTH}rem`; + + setExtraStyles(transitionRef.current, { + minWidth, + }); + }, [token]); + + return { + transitionRef, + }; +} diff --git a/src/components/swap/components/SwapSubmitButton.tsx b/src/components/swap/components/SwapSubmitButton.tsx new file mode 100644 index 00000000..8d2d8a7a --- /dev/null +++ b/src/components/swap/components/SwapSubmitButton.tsx @@ -0,0 +1,114 @@ +import React, { + memo, useRef, +} from '../../../lib/teact/teact'; + +import type { UserSwapToken } from '../../../global/types'; +import { SwapErrorType, SwapType } from '../../../global/types'; + +import { formatCurrencySimple } from '../../../util/formatNumber'; + +import useLang from '../../../hooks/useLang'; +import useSyncEffect from '../../../hooks/useSyncEffect'; + +import Button from '../../ui/Button'; +import Transition from '../../ui/Transition'; + +import styles from '../Swap.module.scss'; + +interface OwnProps { + tokenIn?: UserSwapToken; + tokenOut?: UserSwapToken; + amountIn?: number; + amountOut?: number; + swapType?: SwapType; + isEstimating?: boolean; + isSending?: boolean; + isEnoughTON?: boolean; + isPriceImpactError?: boolean; + canSubmit?: boolean; + errorType?: SwapErrorType; + limits?: { + fromMin?: string; + fromMax?: string; + }; +} + +function SwapSubmitButton({ + tokenIn, + tokenOut, + amountIn, + amountOut, + swapType, + isEstimating, + isSending, + isEnoughTON, + isPriceImpactError, + canSubmit, + errorType, + limits, +}: OwnProps) { + const lang = useLang(); + + const isErrorExist = errorType !== undefined; + const shouldSendingBeVisible = isSending && swapType === SwapType.CrosschainToTon; + const isDisabled = !canSubmit || shouldSendingBeVisible; + const isLoading = isEstimating || shouldSendingBeVisible; + + const errorMsgByType = { + [SwapErrorType.InvalidPair]: lang('Invalid Pair'), + [SwapErrorType.NotEnoughLiquidity]: lang('Insufficient liquidity'), + [SwapErrorType.ChangellyMinSwap]: lang('Minimum amount', { + value: formatCurrencySimple(Number(limits?.fromMin ?? 0), tokenIn?.symbol ?? '', tokenIn?.decimals), + }), + [SwapErrorType.ChangellyMaxSwap]: lang('Maximum amount', { + value: formatCurrencySimple(Number(limits?.fromMax ?? 0), tokenIn?.symbol ?? '', tokenIn?.decimals), + }), + }; + + const isTouched = Boolean(amountIn || amountOut); + + let text = lang('$swap_from_to', { + from: tokenIn?.symbol, + to: tokenOut?.symbol, + }); + + if (isTouched && isErrorExist) { + text = errorMsgByType[errorType]; + } else if (isTouched && !isEnoughTON && swapType !== SwapType.CrosschainToTon) { + text = lang('Not enough TON'); + } + + let shouldShowError = !isEstimating && ((isPriceImpactError || isErrorExist || !isEnoughTON)); + + if (swapType === SwapType.CrosschainToTon) { + shouldShowError = !isEstimating && ((isPriceImpactError || isErrorExist)); + } + + const buttonTransitionKeyRef = useRef(0); + const buttonStateStr = `${text}_${!canSubmit}_${isTouched && shouldShowError}`; + + useSyncEffect(() => { + buttonTransitionKeyRef.current++; + }, [buttonStateStr]); + + return ( + + + + ); +} + +export default memo(SwapSubmitButton); diff --git a/src/components/transfer/Transfer.module.scss b/src/components/transfer/Transfer.module.scss index ed1394b2..712dac29 100644 --- a/src/components/transfer/Transfer.module.scss +++ b/src/components/transfer/Transfer.module.scss @@ -1,10 +1,10 @@ @import "../../styles/mixins"; .modalDialog { - height: 35rem; + height: 35.5rem; @supports (height: env(safe-area-inset-bottom)) { - height: calc(35rem + env(safe-area-inset-bottom)); + height: calc(35.5rem + env(safe-area-inset-bottom)); } } @@ -25,18 +25,27 @@ border-radius: var(--border-radius-small) !important; - &:hover, - &:focus, &:active { color: var(--color-input-button-text); background-color: var(--color-input-button-background); - } - &:active { // Optimization transition: none; } + + @media (hover: hover) { + &:hover, + &:focus { + color: var(--color-input-button-text); + + background-color: var(--color-input-button-background); + } + } +} + +.inputButtonShifted { + right: 2.5rem; } .amountInput { @@ -150,7 +159,7 @@ gap: 0.25rem; align-items: center; - margin-bottom: 0.3125rem; + margin-bottom: 0.5rem; font-weight: 700; color: var(--color-gray-2); @@ -175,7 +184,7 @@ } .addressWidget { - margin-bottom: 1rem; + margin-bottom: 1.25rem; } .inputReadOnly { @@ -184,7 +193,7 @@ display: flex; box-sizing: border-box; - margin-bottom: 1rem; + margin-bottom: 1.25rem; padding: 0.875rem 0.75rem; font-size: 1rem; @@ -204,6 +213,10 @@ } } +.commentInputWrapper { + margin-bottom: 0 !important; +} + .sticker { margin: 0 auto 1.25rem; @@ -237,10 +250,6 @@ animation: button-loading-spinner 1s linear infinite; } -.buttonSubmit { - margin-top: 2rem; -} - .savedAddressItem { cursor: var(--custom-cursor, pointer); @@ -392,6 +401,10 @@ padding: 0.5rem 0.6875rem 0.375rem; } +.inputWithIcon { + padding-right: 2.375rem; +} + textarea.inputStatic { /* default input padding minus 1px accounted for border width */ padding: 0.875rem 0.75rem 0.8125rem; @@ -432,4 +445,31 @@ textarea.inputStatic { z-index: 1; width: 18.9375rem; -} \ No newline at end of file +} + +.transferShortInfo { + display: flex; + gap: 0.25rem; + align-items: center; + + height: 2rem; + padding: 0 0.5rem 0 0.375rem; + + font-size: 0.9375rem; + font-weight: 500; + color: var(--color-activity-blue); + + background-color: var(--color-activity-blue-background); + border-radius: 1rem; +} + +.bold { + font-weight: 700; +} + +.tokenIcon { + width: 1.25rem; + height: 1.25rem; + + border-radius: 50%; +} diff --git a/src/components/transfer/TransferComplete.tsx b/src/components/transfer/TransferComplete.tsx new file mode 100644 index 00000000..45b90c4c --- /dev/null +++ b/src/components/transfer/TransferComplete.tsx @@ -0,0 +1,96 @@ +import React, { memo } from '../../lib/teact/teact'; +import { getActions } from '../../global'; + +import { TON_TOKEN_SLUG } from '../../config'; +import { bigStrToHuman } from '../../global/helpers'; + +import { useDeviceScreen } from '../../hooks/useDeviceScreen'; +import useHistoryBack from '../../hooks/useHistoryBack'; +import useLang from '../../hooks/useLang'; +import useLastCallback from '../../hooks/useLastCallback'; + +import TransferResult from '../common/TransferResult'; +import Button from '../ui/Button'; +import ModalHeader from '../ui/ModalHeader'; + +import modalStyles from '../ui/Modal.module.scss'; + +interface OwnProps { + isActive?: boolean; + amount?: number; + symbol: string; + balance?: number; + fee?: string; + operationAmount?: number; + txId?: string; + tokenSlug?: string; + toAddress?: string; + comment?: string; + onInfoClick: NoneToVoidFunction; + onClose: NoneToVoidFunction; +} + +const AMOUNT_PRECISION = 4; + +function TransferComplete({ + isActive, + amount, + symbol, + balance, + fee, + operationAmount, + txId, + tokenSlug, + toAddress, + comment, + onInfoClick, + onClose, +}: OwnProps) { + const { startTransfer } = getActions(); + + const lang = useLang(); + const { isPortrait } = useDeviceScreen(); + + useHistoryBack({ + isActive, + onBack: onClose, + }); + + const handleTransactionRepeatClick = useLastCallback(() => { + startTransfer({ + isPortrait, + tokenSlug: tokenSlug || TON_TOKEN_SLUG, + toAddress, + amount, + comment, + }); + }); + + return ( + <> + + +
+ + +
+ +
+
+ + ); +} + +export default memo(TransferComplete); diff --git a/src/components/transfer/TransferConfirm.tsx b/src/components/transfer/TransferConfirm.tsx new file mode 100644 index 00000000..358d3801 --- /dev/null +++ b/src/components/transfer/TransferConfirm.tsx @@ -0,0 +1,140 @@ +import React, { memo } from '../../lib/teact/teact'; +import { getActions, withGlobal } from '../../global'; + +import type { GlobalState } from '../../global/types'; + +import { ANIMATED_STICKER_SMALL_SIZE_PX } from '../../config'; +import { bigStrToHuman } from '../../global/helpers'; +import buildClassName from '../../util/buildClassName'; +import { ANIMATED_STICKERS_PATHS } from '../ui/helpers/animatedAssets'; + +import useHistoryBack from '../../hooks/useHistoryBack'; +import useLang from '../../hooks/useLang'; + +import AmountWithFeeTextField from '../ui/AmountWithFeeTextField'; +import AnimatedIconWithPreview from '../ui/AnimatedIconWithPreview'; +import Button from '../ui/Button'; +import IconWithTooltip from '../ui/IconWithTooltip'; +import InteractiveTextField from '../ui/InteractiveTextField'; +import ModalHeader from '../ui/ModalHeader'; + +import modalStyles from '../ui/Modal.module.scss'; +import styles from './Transfer.module.scss'; + +interface OwnProps { + isActive: boolean; + savedAddresses?: Record; + symbol: string; + onBack: NoneToVoidFunction; + onClose: NoneToVoidFunction; +} + +interface StateProps { + currentTransfer: GlobalState['currentTransfer']; +} + +function TransferConfirm({ + currentTransfer: { + amount, + toAddress, + resolvedAddress, + fee, + comment, + shouldEncrypt, + promiseId, + isLoading, + toAddressName, + isToNewAddress, + }, symbol, isActive, savedAddresses, onBack, onClose, +}: OwnProps & StateProps) { + const { submitTransferConfirm } = getActions(); + + const lang = useLang(); + + const addressName = savedAddresses?.[toAddress!] || toAddressName; + + useHistoryBack({ + isActive, + onBack, + }); + + function renderComment() { + if (!comment) { + return undefined; + } + + return ( + <> +
{shouldEncrypt ? lang('Encrypted Message') : lang('Comment')}
+
+ {comment} +
+ + ); + } + + return ( + <> + +
+ +
+ {lang('Receiving Address')} + + {isToNewAddress && ( + + )} +
+ + + + + {renderComment()} + +
+ {promiseId ? ( + + ) : ( + + )} + +
+
+ + ); +} + +export default memo(withGlobal((global): StateProps => { + return { + currentTransfer: global.currentTransfer, + }; +})(TransferConfirm)); diff --git a/src/components/transfer/TransferInitial.tsx b/src/components/transfer/TransferInitial.tsx index 6cad1536..19aa4d9a 100644 --- a/src/components/transfer/TransferInitial.tsx +++ b/src/components/transfer/TransferInitial.tsx @@ -1,15 +1,16 @@ +import type { URLOpenListenerEvent } from '@capacitor/app'; +import { App as CapacitorApp } from '@capacitor/app'; import React, { memo, useEffect, useMemo, useRef, useState, } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; +import type { ApiBaseCurrency } from '../../api/types'; import type { UserToken } from '../../global/types'; import type { DropdownItem } from '../ui/Dropdown'; import { ElectronEvent } from '../../electron/types'; -import { - DEFAULT_PRICE_CURRENCY, TON_SYMBOL, TON_TOKEN_SLUG, -} from '../../config'; +import { IS_FIREFOX_EXTENSION, TON_SYMBOL, TON_TOKEN_SLUG } from '../../config'; import { bigStrToHuman } from '../../global/helpers'; import { selectCurrentAccountState, @@ -17,13 +18,20 @@ import { selectIsHardwareAccount, } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; +import { clearLaunchUrl, getLaunchUrl } from '../../util/capacitor'; import dns from '../../util/dns'; -import { formatCurrency, formatCurrencyExtended } from '../../util/formatNumber'; +import { + formatCurrency, + formatCurrencyExtended, + formatCurrencySimple, + getShortCurrencySymbol, +} from '../../util/formatNumber'; import { getIsAddressValid } from '../../util/getIsAddressValid'; import { throttle } from '../../util/schedulers'; import { shortenAddress } from '../../util/shortenAddress'; import stopEvent from '../../util/stopEvent'; -import { IS_TOUCH_ENV } from '../../util/windowEnvironment'; +import { parseTonDeeplink } from '../../util/ton/deeplinks'; +import { IS_FIREFOX, IS_TOUCH_ENV } from '../../util/windowEnvironment'; import { ASSET_LOGO_PATHS } from '../ui/helpers/assetLogos'; import useCurrentOrPrev from '../../hooks/useCurrentOrPrev'; @@ -45,6 +53,8 @@ import styles from './Transfer.module.scss'; interface OwnProps { isStatic?: boolean; + onQrScanPress?: NoneToVoidFunction; + onCommentChange?: NoneToVoidFunction; } interface StateProps { @@ -52,14 +62,14 @@ interface StateProps { amount?: number; comment?: string; shouldEncrypt?: boolean; + isLoading?: boolean; fee?: string; tokenSlug?: string; tokens?: UserToken[]; savedAddresses?: Record; - isLoading?: boolean; - onCommentChange?: NoneToVoidFunction; isEncryptedCommentSupported: boolean; isCommentSupported: boolean; + baseCurrency?: ApiBaseCurrency; } const COMMENT_MAX_SIZE_BYTES = 5000; @@ -82,10 +92,12 @@ function TransferInitial({ tokens, fee, savedAddresses, - isLoading, - onCommentChange, isEncryptedCommentSupported, isCommentSupported, + isLoading, + onCommentChange, + onQrScanPress, + baseCurrency, }: OwnProps & StateProps) { const { submitTransferInitial, @@ -96,6 +108,7 @@ function TransferInitial({ setTransferToAddress, setTransferComment, setTransferShouldEncrypt, + cancelTransfer, } = getActions(); // eslint-disable-next-line no-null/no-null @@ -103,7 +116,8 @@ function TransferInitial({ const lang = useLang(); - const [shouldRenderPasteButton, setShouldRenderPasteButton] = useState(true); + // Note: As of 27-11-2023, Firefox does not support readText() + const [shouldRenderPasteButton, setShouldRenderPasteButton] = useState(!(IS_FIREFOX || IS_FIREFOX_EXTENSION)); const [isAddressFocused, markAddressFocused, unmarkAddressFocused] = useFlag(); const [isSavedAddressesOpen, openSavedAddresses, closeSavedAddresses] = useFlag(); const [savedAddressForDeletion, setSavedAddressForDeletion] = useState(); @@ -126,6 +140,9 @@ function TransferInitial({ const amountInCurrency = price && amount && !Number.isNaN(amount) ? amount * price : undefined; const renderingAmountInCurrency = useCurrentOrPrev(amountInCurrency, true); const renderingFee = useCurrentOrPrev(fee, true); + const withPasteButton = shouldRenderPasteButton && toAddress === ''; + const withQrScanButton = Boolean(onQrScanPress); + const shortBaseSymbol = getShortCurrencySymbol(baseCurrency); const { shouldRender: shouldRenderCurrency, transitionClassNames: currencyClassNames } = useShowTransition( Boolean(amountInCurrency), @@ -201,13 +218,32 @@ function TransferInitial({ }); }, [amount, comment, fetchFee, hasToAddressError, isAddressValid, toAddress, tokenSlug]); + const processDeeplink = useLastCallback((url: string) => { + const params = parseTonDeeplink(url); + if (!params) return; + + setTransferToAddress({ toAddress: params.to }); + setTransferAmount({ amount: params.amount ? bigStrToHuman(params.amount) : undefined }); + setTransferComment({ comment: params.comment }); + }); + useEffect(() => { - return window.electron?.on(ElectronEvent.DEEPLINK, (params: any) => { - setTransferToAddress({ toAddress: params.to }); - setTransferAmount({ amount: bigStrToHuman(params.amount) }); - setTransferComment({ comment: params.text }); + return window.electron?.on(ElectronEvent.DEEPLINK, ({ url }: { url: string }) => { + processDeeplink(url); }); - }, []); + }, [processDeeplink]); + + useEffect(() => { + const launchUrl = getLaunchUrl(); + if (launchUrl) { + processDeeplink(launchUrl); + clearLaunchUrl(); + } + + return CapacitorApp.addListener('appUrlOpen', (event: URLOpenListenerEvent) => { + processDeeplink(event.url); + }).remove; + }, [processDeeplink]); const handleTokenChange = useLastCallback( (slug: string) => { @@ -262,6 +298,11 @@ function TransferInitial({ setTransferToAddress({ toAddress: newToAddress }); }); + const handleQrScanClick = useLastCallback(() => { + cancelTransfer(); + onQrScanPress!(); + }); + const handlePasteClick = useLastCallback(() => { navigator.clipboard .readText() @@ -396,10 +437,10 @@ function TransferInitial({ return (
- {lang('$balance_is', { + {lang('$max_balance', { balance: ( - {balance !== undefined ? formatCurrencyExtended(balance, symbol, true) : lang('Loading...')} + {balance !== undefined ? formatCurrencySimple(balance, symbol, decimals) : lang('Loading...')} ), })} @@ -422,7 +463,7 @@ function TransferInitial({ function renderCurrencyValue() { return ( - ≈ {formatCurrency(renderingAmountInCurrency || 0, DEFAULT_PRICE_CURRENCY)} + ≈ {formatCurrency(renderingAmountInCurrency || 0, shortBaseSymbol)} ); } @@ -446,7 +487,7 @@ function TransferInitial({
- {shouldRenderPasteButton && toAddress === '' && ( + {withQrScanButton && ( + + )} + {withPasteButton && ( @@ -490,17 +541,18 @@ function TransferInitial({ {isCommentSupported && ( )} -
+
@@ -516,35 +568,37 @@ function TransferInitial({ } export default memo( - withGlobal((global, ownProps, detachWhenChanged): StateProps => { - detachWhenChanged(global.currentAccountId); - - const { - toAddress, - amount, - comment, - shouldEncrypt, - fee, - tokenSlug, - isLoading, - } = global.currentTransfer; + withGlobal( + (global): StateProps => { + const { + toAddress, + amount, + comment, + shouldEncrypt, + fee, + tokenSlug, + isLoading, + } = global.currentTransfer; - const isLedger = selectIsHardwareAccount(global); + const isLedger = selectIsHardwareAccount(global); + const accountState = selectCurrentAccountState(global); - return { - toAddress, - amount, - comment, - shouldEncrypt, - fee, - tokenSlug, - tokens: selectCurrentAccountTokens(global), - isLoading, - savedAddresses: selectCurrentAccountState(global)?.savedAddresses, - isEncryptedCommentSupported: !isLedger, - isCommentSupported: !tokenSlug || tokenSlug === TON_TOKEN_SLUG || !isLedger, - }; - })(TransferInitial), + return { + toAddress, + amount, + comment, + shouldEncrypt, + fee, + tokenSlug, + tokens: selectCurrentAccountTokens(global), + savedAddresses: accountState?.savedAddresses, + isEncryptedCommentSupported: !isLedger, + isCommentSupported: !tokenSlug || tokenSlug === TON_TOKEN_SLUG || !isLedger, + isLoading, + }; + }, + (global, _, stickToFirst) => stickToFirst(global.currentAccountId), + )(TransferInitial), ); function trimStringByMaxBytes(str: string, maxBytes: number) { diff --git a/src/components/transfer/TransferModal.tsx b/src/components/transfer/TransferModal.tsx index 5759dff4..c4bec414 100644 --- a/src/components/transfer/TransferModal.tsx +++ b/src/components/transfer/TransferModal.tsx @@ -1,38 +1,44 @@ -import React, { memo, useEffect, useMemo } from '../../lib/teact/teact'; +import React, { + memo, useEffect, useMemo, useState, +} from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; import type { GlobalState, HardwareConnectState, UserToken } from '../../global/types'; import { TransferState } from '../../global/types'; -import { ANIMATED_STICKER_SMALL_SIZE_PX, TON_TOKEN_SLUG } from '../../config'; -import { bigStrToHuman } from '../../global/helpers'; +import { IS_CAPACITOR } from '../../config'; import { selectCurrentAccountState, selectCurrentAccountTokens } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import captureKeyboardListeners from '../../util/captureKeyboardListeners'; -import { ANIMATED_STICKERS_PATHS } from '../ui/helpers/animatedAssets'; +import { formatCurrency } from '../../util/formatNumber'; +import resolveModalTransitionName from '../../util/resolveModalTransitionName'; +import { shortenAddress } from '../../util/shortenAddress'; +import { ASSET_LOGO_PATHS } from '../ui/helpers/assetLogos'; +import { useDeviceScreen } from '../../hooks/useDeviceScreen'; import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; import useModalTransitionKeys from '../../hooks/useModalTransitionKeys'; import usePrevious from '../../hooks/usePrevious'; +import useWindowSize from '../../hooks/useWindowSize'; -import TransferResult from '../common/TransferResult'; import LedgerConfirmOperation from '../ledger/LedgerConfirmOperation'; import LedgerConnect from '../ledger/LedgerConnect'; -import AmountWithFeeTextField from '../ui/AmountWithFeeTextField'; -import AnimatedIconWithPreview from '../ui/AnimatedIconWithPreview'; -import Button from '../ui/Button'; -import IconWithTooltip from '../ui/IconWithTooltip'; -import InteractiveTextField from '../ui/InteractiveTextField'; import Modal from '../ui/Modal'; import ModalHeader from '../ui/ModalHeader'; -import PasswordForm from '../ui/PasswordForm'; import Transition from '../ui/Transition'; +import TransferComplete from './TransferComplete'; +import TransferConfirm from './TransferConfirm'; import TransferInitial from './TransferInitial'; +import TransferPassword from './TransferPassword'; import modalStyles from '../ui/Modal.module.scss'; import styles from './Transfer.module.scss'; +interface OwnProps { + onQrScanPress?: NoneToVoidFunction; +} + interface StateProps { currentTransfer: GlobalState['currentTransfer']; tokens?: UserToken[]; @@ -42,42 +48,37 @@ interface StateProps { isTonAppConnected?: boolean; } -const AMOUNT_PRECISION = 4; +const SCREEN_HEIGHT_FOR_FORCE_FULLSIZE_NBS = 762; // Computed empirically function TransferModal({ currentTransfer: { state, amount, toAddress, - resolvedAddress, fee, comment, - shouldEncrypt, - promiseId, error, isLoading, txId, tokenSlug, - toAddressName, - isToNewAddress, - }, tokens, savedAddresses, hardwareState, isLedgerConnected, isTonAppConnected, -}: StateProps) { + }, tokens, savedAddresses, hardwareState, isLedgerConnected, isTonAppConnected, onQrScanPress, +}: OwnProps & StateProps) { const { submitTransferConfirm, submitTransferPassword, submitTransferHardware, setTransferScreen, - clearTransferError, cancelTransfer, showActivityInfo, - startTransfer, } = getActions(); const lang = useLang(); + const { isPortrait } = useDeviceScreen(); const isOpen = state !== TransferState.None; + const { screenHeight } = useWindowSize(); const selectedToken = useMemo(() => tokens?.find((token) => token.slug === tokenSlug), [tokenSlug, tokens]); - const renderedTokenBalance = usePrevious(selectedToken?.amount, true); + const [renderedTokenBalance, setRenderedTokenBalance] = useState(selectedToken?.amount); const renderedTransactionAmount = usePrevious(amount, true); const symbol = selectedToken?.symbol || ''; @@ -94,6 +95,8 @@ function TransferModal({ ), [state, submitTransferConfirm]); const handleTransferSubmit = useLastCallback((password: string) => { + setRenderedTokenBalance(selectedToken?.amount); + submitTransferPassword({ password }); }); @@ -107,143 +110,36 @@ function TransferModal({ }); const handleTransactionInfoClick = useLastCallback(() => { - cancelTransfer(); - showActivityInfo({ id: txId }); - }); - - const handleTransactionRepeatClick = useLastCallback(() => { - startTransfer({ - tokenSlug: tokenSlug || TON_TOKEN_SLUG, - toAddress, - amount, - comment, - }); + cancelTransfer({ shouldReset: true }); + showActivityInfo({ id: txId! }); }); const handleModalClose = useLastCallback(() => { - cancelTransfer(); + cancelTransfer({ shouldReset: isPortrait }); updateNextKey(); }); - function renderComment() { - if (!comment) { - return undefined; - } - - return ( - <> -
{shouldEncrypt ? lang('Encrypted Message') : lang('Comment')}
-
{comment}
- - ); - } - - function renderConfirm(isActive: boolean) { - const addressName = savedAddresses?.[toAddress!] || toAddressName; - - return ( - <> - -
- -
- {lang('Receiving Address')} - - {isToNewAddress && ( - - )} -
- - - - - {renderComment()} + const handleModalCloseWithReset = useLastCallback(() => { + cancelTransfer({ shouldReset: true }); + }); -
- {promiseId ? ( - - ) : ( - - )} - -
-
- - ); - } + const handleLedgerConnect = useLastCallback(() => { + submitTransferHardware(); + }); - function renderPassword(isActive: boolean) { - return ( - <> - - - - ); - } + function renderTransferShortInfo() { + const logoPath = selectedToken?.image || ASSET_LOGO_PATHS[symbol.toLowerCase() as keyof typeof ASSET_LOGO_PATHS]; - function renderComplete(isActive: boolean) { return ( - <> - - -
- - -
- -
-
- +
+ {symbol} + + {lang('%amount% to %address%', { + amount: {formatCurrency(amount!, symbol)}, + address: {shortenAddress(toAddress!)}, + })} + +
); } @@ -253,22 +149,41 @@ function TransferModal({ case TransferState.Initial: return ( <> - - + + ); case TransferState.Confirm: - return renderConfirm(isActive); + return ( + + ); case TransferState.Password: - return renderPassword(isActive); + return ( + + {IS_CAPACITOR ? renderTransferShortInfo() : undefined} + + ); case TransferState.ConnectHardware: return ( ); case TransferState.ConfirmHardware: @@ -276,26 +191,42 @@ function TransferModal({ ); case TransferState.Complete: - return renderComplete(isActive); + return ( + + ); } } return ( { +export default memo(withGlobal((global): StateProps => { const accountState = selectCurrentAccountState(global); const { diff --git a/src/components/transfer/TransferPassword.tsx b/src/components/transfer/TransferPassword.tsx new file mode 100644 index 00000000..eb8020a1 --- /dev/null +++ b/src/components/transfer/TransferPassword.tsx @@ -0,0 +1,56 @@ +import React, { memo, type TeactNode } from '../../lib/teact/teact'; +import { getActions } from '../../global'; + +import useHistoryBack from '../../hooks/useHistoryBack'; +import useLang from '../../hooks/useLang'; + +import ModalHeader from '../ui/ModalHeader'; +import PasswordForm from '../ui/PasswordForm'; + +interface OwnProps { + isActive: boolean; + isLoading?: boolean; + error?: string; + children?: TeactNode; + onSubmit: (password: string) => void; + onCancel: NoneToVoidFunction; +} + +function TransferPassword({ + isActive, isLoading, error, children, onSubmit, onCancel, +}: OwnProps) { + const { + cancelTransfer, + clearTransferError, + } = getActions(); + + const lang = useLang(); + + useHistoryBack({ + isActive, + onBack: onCancel, + }); + + return ( + <> + {!children && } + + {children} + + + ); +} + +export default memo(TransferPassword); diff --git a/src/components/ui/AmountWithFeeTextField.module.scss b/src/components/ui/AmountWithFeeTextField.module.scss index a32e8108..f8b2be3c 100644 --- a/src/components/ui/AmountWithFeeTextField.module.scss +++ b/src/components/ui/AmountWithFeeTextField.module.scss @@ -4,7 +4,7 @@ display: flex; box-sizing: border-box; - margin-bottom: 1rem; + margin-bottom: 1.25rem; padding: 0.875rem 0.75rem; font-size: 1rem; @@ -28,7 +28,7 @@ .label, .feeLabel { - margin-bottom: 0.3125rem; + margin-bottom: 0.5rem; padding: 0 0.5rem; font-size: 0.8125rem; @@ -40,7 +40,7 @@ .feeLabel { position: absolute; - top: -1.1875rem; + top: -1.375rem; right: 0.3125rem; } diff --git a/src/components/ui/AnimatedSticker.tsx b/src/components/ui/AnimatedSticker.tsx index c4865120..ed8ee16e 100644 --- a/src/components/ui/AnimatedSticker.tsx +++ b/src/components/ui/AnimatedSticker.tsx @@ -6,12 +6,12 @@ import React, { import type RLottieInstance from '../../lib/rlottie/RLottie'; -import { IS_ELECTRON } from '../../config'; import { requestMeasure } from '../../lib/fasterdom/fasterdom'; import { ensureRLottie, getRLottie } from '../../lib/rlottie/RLottie.async'; import buildClassName from '../../util/buildClassName'; import buildStyle from '../../util/buildStyle'; import generateUniqueId from '../../util/generateUniqueId'; +import { IS_ELECTRON } from '../../util/windowEnvironment'; import useBackgroundMode, { isBackgroundModeActive } from '../../hooks/useBackgroundMode'; import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps'; diff --git a/src/components/ui/Button.module.scss b/src/components/ui/Button.module.scss index c39d6be9..4a97cb12 100644 --- a/src/components/ui/Button.module.scss +++ b/src/components/ui/Button.module.scss @@ -10,6 +10,10 @@ text-decoration: none; white-space: nowrap; + &.loadingInit::after { + border-top-color: var(--color-gray-button-text); + } + background: none; border: 0; border-radius: 0; @@ -49,11 +53,13 @@ background-color: var(--color-gray-button-background); border-radius: var(--border-radius-buttons); - &:hover, - &:focus { - color: var(--color-gray-button-text-hover); + @media (hover: hover) { + &:hover, + &:focus { + color: var(--color-gray-button-text-hover); - background-color: var(--color-gray-button-background-hover); + background-color: var(--color-gray-button-background-hover); + } } &[disabled] { @@ -72,12 +78,17 @@ transition: color 150ms; - &:hover, - &:focus, &:active { color: var(--color-blue-button-background-hover); } + @media (hover: hover) { + &:hover, + &:focus{ + color: var(--color-blue-button-background-hover); + } + } + &[disabled] { opacity: 0.4; } @@ -92,11 +103,17 @@ background-color: var(--color-blue-button-background); - &:hover, - &:focus { - color: var(--color-blue-button-text-hover); + &.loadingInit::after { + border-top-color: var(--color-blue-button-text); + } + + @media (hover: hover) { + &:hover, + &:focus { + color: var(--color-blue-button-text-hover); - background-color: var(--color-blue-button-background-hover); + background-color: var(--color-blue-button-background-hover); + } } } @@ -120,11 +137,17 @@ background-color: var(--color-red-button-background); - &:hover, - &:focus { - color: var(--color-red-button-text-hover); + &.loadingInit::after { + border-top-color: var(--color-red-button-text); + } - background-color: var(--color-red-button-background-hover); + @media (hover: hover) { + &:hover, + &:focus { + color: var(--color-red-button-text-hover); + + background-color: var(--color-red-button-background-hover); + } } &[disabled] { @@ -140,9 +163,11 @@ transition: color 150ms; - &:hover, - &:focus { - color: var(--color-red-button-background-hover); + @media (hover: hover) { + &:hover, + &:focus { + color: var(--color-red-button-background-hover); + } } :global(html.animation-level-0) & { @@ -173,7 +198,7 @@ } } -.loading { +.loadingInit { position: relative; &::after { @@ -189,17 +214,28 @@ height: 1rem; margin: auto; + opacity: 0; border: 0.25rem solid transparent; - border-top-color: var(--color-white); border-radius: 50%; - animation: button-loading-spinner 1s linear infinite; + transition: opacity 150ms; } } -.buttonText { - visibility: hidden; - opacity: 0; +.loadingStart { + color: rgba(0,0,0,0) !important; + + &::after { + content: ""; + + opacity: 1; + } +} + +.loadingAnimation { + &::after { + animation: button-loading-spinner 1s linear infinite; + } } @keyframes button-loading-spinner { diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index df987475..79517d76 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -4,6 +4,7 @@ import React, { memo, useState } from '../../lib/teact/teact'; import buildClassName from '../../util/buildClassName'; import useLastCallback from '../../hooks/useLastCallback'; +import useShowTransition from '../../hooks/useShowTransition'; import styles from './Button.module.scss'; @@ -29,6 +30,8 @@ type OwnProps = { // Longest animation duration const CLICKED_TIMEOUT = 400; +const LOADING_CLOSE_DURATION = 200; + function Button({ ref, children, @@ -49,6 +52,10 @@ function Button({ }: OwnProps) { const [isClicked, setIsClicked] = useState(false); + const { + shouldRender: shouldRenderLoading, + } = useShowTransition(isLoading, undefined, undefined, undefined, undefined, LOADING_CLOSE_DURATION); + const handleClick = useLastCallback(() => { if (!isDisabled && onClick) { onClick(); @@ -60,6 +67,12 @@ function Button({ }, CLICKED_TIMEOUT); }); + const loadingClassName = buildClassName( + isLoading !== undefined && styles.loadingInit, + isLoading && styles.loadingStart, + shouldRenderLoading && styles.loadingAnimation, + ); + return ( ); } diff --git a/src/components/ui/ConfettiContainer.module.scss b/src/components/ui/ConfettiContainer.module.scss new file mode 100644 index 00000000..93984cb9 --- /dev/null +++ b/src/components/ui/ConfettiContainer.module.scss @@ -0,0 +1,13 @@ +.root { + pointer-events: none; + touch-action: none; + + position: fixed; + z-index: var(--z-confetti); + top: 0; + right: 0; + bottom: 0; + left: 0; + + overflow: hidden; +} diff --git a/src/components/ui/ConfettiContainer.tsx b/src/components/ui/ConfettiContainer.tsx new file mode 100644 index 00000000..ff21e6ca --- /dev/null +++ b/src/components/ui/ConfettiContainer.tsx @@ -0,0 +1,199 @@ +import React, { memo, useRef } from '../../lib/teact/teact'; +import { withGlobal } from '../../global'; + +import { requestMeasure } from '../../lib/fasterdom/fasterdom'; + +import { useDeviceScreen } from '../../hooks/useDeviceScreen'; +import useForceUpdate from '../../hooks/useForceUpdate'; +import useLastCallback from '../../hooks/useLastCallback'; +import useSyncEffect from '../../hooks/useSyncEffect'; +import useWindowSize from '../../hooks/useWindowSize'; + +import styles from './ConfettiContainer.module.scss'; + +type StateProps = { + lastRequestedAt?: number; +}; + +interface Confetti { + pos: { + x: number; + y: number; + }; + velocity: { + x: number; + y: number; + }; + size: number; + color: string; + flicker: number; + flickerFrequency: number; + rotation: number; + lastDrawnAt: number; + frameCount: number; +} + +const CONFETTI_FADEOUT_TIMEOUT = 10000; +const DEFAULT_CONFETTI_SIZE = 10; +const CONFETTI_COLORS = ['#E8BC2C', '#D0049E', '#02CBFE', '#5723FD', '#FE8C27', '#6CB859']; + +function ConfettiContainer({ lastRequestedAt }: StateProps) { + // eslint-disable-next-line no-null/no-null + const canvasRef = useRef(null); + const confettiRef = useRef([]); + const isRafStartedRef = useRef(false); + const windowSize = useWindowSize(); + const forceUpdate = useForceUpdate(); + const { isPortrait } = useDeviceScreen(); + + const defaultConfettiAmount = isPortrait ? 50 : 100; + + const invokeGenerateConfetti = useLastCallback((w: number, h: number) => { + generateConfetti(confettiRef, w, h, defaultConfettiAmount); + }); + + const updateCanvas = useLastCallback(() => { + if (!canvasRef.current || !isRafStartedRef.current) { + return; + } + const canvas = canvasRef.current; + + const ctx = canvas.getContext('2d'); + if (!ctx) { + return; + } + + const { width: canvasWidth, height: canvasHeight } = canvas; + ctx.clearRect(0, 0, canvasWidth, canvasHeight); + + const confettiToRemove: Set = new Set([]); + confettiRef.current.forEach((c, i) => { + const { + pos, + velocity, + size, + color, + flicker, + flickerFrequency, + rotation, + lastDrawnAt, + frameCount, + } = c; + const diff = (Date.now() - lastDrawnAt) / 1000; + + const newPos = { + x: pos.x + velocity.x * diff, + y: pos.y + velocity.y * diff, + }; + + const newVelocity = { + x: velocity.x * 0.98, // Air Resistance + y: velocity.y += diff * 1000, // Gravity + }; + + const newFlicker = size * Math.abs(Math.sin(frameCount * flickerFrequency)); + const newRotation = 5 * frameCount * flickerFrequency * (Math.PI / 180); + + const newFrameCount = frameCount + 1; + const newLastDrawnAt = Date.now(); + + const shouldRemove = newPos.y > canvasHeight + c.size; + if (shouldRemove) { + confettiToRemove.add(c); + return; + } + + confettiRef.current[i] = { + ...c, + pos: newPos, + velocity: newVelocity, + flicker: newFlicker, + rotation: newRotation, + lastDrawnAt: newLastDrawnAt, + frameCount: newFrameCount, + }; + ctx.fillStyle = color; + ctx.beginPath(); + ctx.ellipse( + pos.x, + pos.y, + size, + flicker, + rotation, + 0, + 2 * Math.PI, + ); + ctx.fill(); + }); + + confettiRef.current = confettiRef.current.filter((c) => !confettiToRemove.has(c)); + if (confettiRef.current.length) { + requestMeasure(updateCanvas); + } else { + isRafStartedRef.current = false; + } + }); + + useSyncEffect(([prevConfettiTime]) => { + let hideTimeout: number; + if (lastRequestedAt && prevConfettiTime !== lastRequestedAt) { + invokeGenerateConfetti(windowSize.width, windowSize.height); + hideTimeout = window.setTimeout(forceUpdate, CONFETTI_FADEOUT_TIMEOUT); + + if (!isRafStartedRef.current) { + isRafStartedRef.current = true; + requestMeasure(updateCanvas); + } + } + + return () => { + window.clearTimeout(hideTimeout); + }; + // eslint-disable-next-line react-hooks-static-deps/exhaustive-deps -- Old timeout should be cleared only if new confetti is generated + }, [lastRequestedAt, forceUpdate, updateCanvas]); + + if (!lastRequestedAt || Date.now() - lastRequestedAt > CONFETTI_FADEOUT_TIMEOUT) { + return undefined; + } + return ( +
+ +
+ ); +} + +export default memo(withGlobal((global): StateProps => { + return { + lastRequestedAt: global.confettiRequestedAt, + }; +})(ConfettiContainer)); + +function generateConfetti(confettiRef: { current: Confetti[] }, width : number, height: number, amount: number) { + for (let i = 0; i < amount; i++) { + const leftSide = i % 2; + const pos = { + x: width * (leftSide ? -0.1 : 1.1), + y: height * 0.75, + }; + const randomX = Math.random() * width * 1.5; + const randomY = -height / 2 - Math.random() * height; + const velocity = { + x: leftSide ? randomX : randomX * -1, + y: randomY, + }; + + const randomColor = CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)]; + const size = DEFAULT_CONFETTI_SIZE; + confettiRef.current.push({ + pos, + size, + color: randomColor, + velocity, + flicker: size, + flickerFrequency: Math.random() * 0.2, + rotation: 0, + lastDrawnAt: Date.now(), + frameCount: 0, + }); + } +} diff --git a/src/components/ui/CreatePasswordForm.tsx b/src/components/ui/CreatePasswordForm.tsx new file mode 100644 index 00000000..ff045f5f --- /dev/null +++ b/src/components/ui/CreatePasswordForm.tsx @@ -0,0 +1,258 @@ +import React, { + memo, useEffect, useRef, useState, +} from '../../lib/teact/teact'; + +import { PIN_LENGTH } from '../../config'; +import buildClassName from '../../util/buildClassName'; +import { IS_ANDROID, IS_IOS } from '../../util/windowEnvironment'; + +import useFlag from '../../hooks/useFlag'; +import useFocusAfterAnimation from '../../hooks/useFocusAfterAnimation'; +import useLang from '../../hooks/useLang'; +import useLastCallback from '../../hooks/useLastCallback'; +import { usePasswordValidation } from '../../hooks/usePasswordValidation'; + +import Button from './Button'; +import Input from './Input'; +import Modal from './Modal'; + +import styles from '../auth/Auth.module.scss'; +import modalStyles from './Modal.module.scss'; + +interface OwnProps { + isActive?: boolean; + isLoading?: boolean; + formId: string; + onCancel: NoneToVoidFunction; + onSubmit: (password: string, isPasswordNumeric: boolean) => void; +} + +function CreatePasswordForm({ + isActive, isLoading, formId, onCancel, onSubmit, +}: OwnProps) { + const lang = useLang(); + const isMobile = IS_IOS || IS_ANDROID; + + // eslint-disable-next-line no-null/no-null + const firstInputRef = useRef(null); + + const [isJustSubmitted, setIsJustSubmitted] = useState(false); + const [firstPassword, setFirstPassword] = useState(''); + const [secondPassword, setSecondPassword] = useState(''); + const [isPasswordFocused, markPasswordFocused, unmarkPasswordFocused] = useFlag(false); + const [isWeakPasswordModalOpen, openWeakPasswordModal, closeWeakPasswordModal] = useFlag(false); + + const [hasError, setHasError] = useState(false); + const [isPasswordsNotEqual, setIsPasswordsNotEqual] = useState(false); + const [isSecondPasswordFocused, markSecondPasswordFocused, unmarkSecondPasswordFocused] = useFlag(false); + const canSubmit = isActive && firstPassword.length > 0 && secondPassword.length > 0 && !hasError; + + const shouldRenderError = hasError && !isPasswordFocused; + + const validation = usePasswordValidation({ + firstPassword, + secondPassword, + isOnlyNumbers: isMobile, + requiredLength: isMobile ? PIN_LENGTH : undefined, + }); + + useFocusAfterAnimation(firstInputRef, !isActive); + + useEffect(() => { + setIsPasswordsNotEqual(false); + if (firstPassword === '' || !isActive || isPasswordFocused) { + setHasError(false); + return; + } + + const { noEqual, invalidLength } = validation; + + if ((!isSecondPasswordFocused || isJustSubmitted) && noEqual && secondPassword !== '') { + setHasError(true); + setIsPasswordsNotEqual(true); + } else if (!noEqual || secondPassword === '' || (isSecondPasswordFocused && !isJustSubmitted)) { + setHasError(false); + } + if (isMobile && invalidLength && !isJustSubmitted) { + setHasError(true); + } + }, [ + isActive, firstPassword, secondPassword, validation, isSecondPasswordFocused, isPasswordFocused, isJustSubmitted, + isMobile, + ]); + + const handleFirstPasswordChange = useLastCallback((value: string) => { + setFirstPassword(value); + if (isJustSubmitted) { + setIsJustSubmitted(false); + } + }); + + const handleSecondPasswordChange = useLastCallback((value: string) => { + setSecondPassword(value); + if (isJustSubmitted) { + setIsJustSubmitted(false); + } + }); + + const handleSubmit = useLastCallback((e: React.FormEvent) => { + e.preventDefault(); + e.stopPropagation(); + + if (!canSubmit) { + return; + } + + if (firstPassword !== secondPassword) { + setIsJustSubmitted(true); + setHasError(true); + setIsPasswordsNotEqual(true); + return; + } + + const isWeakPassword = Object.values(validation).find((rule) => rule); + + if (!isMobile && isWeakPassword && !isWeakPasswordModalOpen) { + openWeakPasswordModal(); + return; + } + + if (isWeakPasswordModalOpen) { + closeWeakPasswordModal(); + } + onSubmit(firstPassword, isMobile); + }); + + function renderErrors() { + if (isPasswordsNotEqual) { + return ( +
+ {lang('Passwords must be equal.')} +
+ ); + } + + const { + invalidLength, + noUpperCase, + noLowerCase, + noNumber, + noSpecialChar, + } = validation; + + if (isMobile) { + return ( +
+ + {lang('Password must contain %length% digits.', { length: PIN_LENGTH })} + +
+ ); + } + + return ( +
+ {lang('To protect your wallet as much as possible, use a password with')} + + {' '}{lang('$auth_password_rule_8chars')}, + + + {' '}{lang('$auth_password_rule_one_small_char')}, + + + {' '}{lang('$auth_password_rule_one_capital_char')}, + + + {' '}{lang('$auth_password_rule_one_digit')}, + + + {' '}{lang('$auth_password_rule_one_special_char')} + . +
+ ); + } + + return ( + <> + +
+ + +
+ + {renderErrors()} + +
+ + +
+ + + +

+ {lang('Your have entered an insecure password, which can be easily guessed by scammers.')} +

+

+ {lang('Continue or change password to something more secure?')} +

+
+ + +
+
+ + ); +} + +export default memo(CreatePasswordForm); + +function getValidationRuleClass(shouldRenderError: boolean, ruleHasError: boolean) { + return buildClassName( + styles.passwordRule, + !ruleHasError ? styles.valid : shouldRenderError ? styles.invalid : undefined, + ); +} diff --git a/src/components/ui/Draggable.tsx b/src/components/ui/Draggable.tsx index 2922f9ec..a6b02105 100644 --- a/src/components/ui/Draggable.tsx +++ b/src/components/ui/Draggable.tsx @@ -5,6 +5,7 @@ import React, { import buildClassName from '../../util/buildClassName'; import buildStyle from '../../util/buildStyle'; +import { clamp } from '../../util/math'; import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; @@ -25,6 +26,7 @@ type Offset = { type DraggableState = { isDragging: boolean; + scrollTop: number; origin: TPoint; translation: TPoint; width?: number; @@ -41,8 +43,9 @@ type OwnProps = { isDisabled?: boolean; offset?: Offset; parentRef?: RefObject; + scrollRef?: RefObject; className?: string; - onClick: NoneToVoidFunction; + onClick: (e: React.MouseEvent | React.TouchEvent) => void; }; const ZERO_POINT: TPoint = { x: 0, y: 0 }; @@ -54,6 +57,8 @@ const DEFAULT_OFFSET: Offset = { right: 0, }; +const EDGE_THRESHOLD = 150; + function Draggable({ children, id, @@ -64,6 +69,7 @@ function Draggable({ isDisabled, offset = DEFAULT_OFFSET, parentRef, + scrollRef, className, onClick, }: OwnProps) { @@ -73,61 +79,110 @@ function Draggable({ // eslint-disable-next-line no-null/no-null const buttonRef = useRef(null); + const scrollIntervalId = useRef(); + const [state, setState] = useState({ isDragging: false, + scrollTop: 0, origin: ZERO_POINT, translation: ZERO_POINT, }); - const handleMouseDown = (e: React.MouseEvent) => { - const { x, y } = getClientCoordinate(e); + const lastMousePosition = useRef({ x: 0, y: 0 }); + + const updateDraggablePosition = () => { + if (!state.isDragging || !ref.current || !parentRef?.current || !scrollRef?.current) return; + + const translation = calculateConstrainedTranslation( + state, + lastMousePosition.current, + scrollRef.current.scrollTop, + ref.current, + parentRef.current, + offset, + ); + + setState((current) => ({ + ...current, + translation, + })); + + onDrag(translation, id); + }; + + const stopContinuousScroll = () => { + if (scrollIntervalId.current !== undefined) { + cancelAnimationFrame(scrollIntervalId.current); + scrollIntervalId.current = undefined; + } + }; + + const startContinuousScroll = (scrollContainer: HTMLElement, speed: number) => { + const animateScroll = () => { + scrollContainer.scrollBy(0, speed); + updateDraggablePosition(); + scrollIntervalId.current = requestAnimationFrame(animateScroll); + }; + + stopContinuousScroll(); + animateScroll(); + }; + + const setInitialState = (e: React.MouseEvent | TouchEvent) => { + const origin = getClientCoordinate(e); + const scrollTop = scrollRef?.current?.scrollTop ?? 0; setState({ ...state, isDragging: true, - origin: { x, y }, + origin, + scrollTop, width: ref.current?.offsetWidth, height: ref.current?.offsetHeight, }); }; + const handleMouseDown = (e: React.MouseEvent) => { + setInitialState(e); + }; + const handleTouchStart = useLastCallback((e: TouchEvent) => { e.stopPropagation(); e.preventDefault(); - const { x, y } = getClientCoordinate(e); - - setState({ - ...state, - isDragging: true, - origin: { x, y }, - width: ref.current?.offsetWidth, - height: ref.current?.offsetHeight, - }); + setInitialState(e); }); const handleMouseMove = useLastCallback((e: MouseEvent | TouchEvent) => { - const { x, y } = getClientCoordinate(e); - - const translation = { - x: x - state.origin.x, - y: y - state.origin.y, - }; + if (!ref.current || !parentRef?.current || !scrollRef?.current) return; - if (parentRef && parentRef?.current && ref.current) { - const { - top = 0, right = 0, bottom = 0, left = 0, - } = offset; - const parentRect = parentRef.current.getBoundingClientRect(); - const draggableRect = ref.current.getBoundingClientRect(); - - const minX = -ref.current.offsetLeft + left; - const maxX = parentRect.width - draggableRect.width - ref.current.offsetLeft + right; - - const minY = -ref.current.offsetTop + top; - const maxY = parentRect.height - draggableRect.height - ref.current.offsetTop + bottom; + const { x, y } = getClientCoordinate(e); + lastMousePosition.current = { x, y }; + + const translation = calculateConstrainedTranslation( + state, + lastMousePosition.current, + scrollRef.current.scrollTop, + ref.current, + parentRef.current, + offset, + ); - translation.x = Math.max(minX, Math.min(translation.x, maxX)); - translation.y = Math.max(minY, Math.min(translation.y, maxY)); + const scrollRect = scrollRef.current.getBoundingClientRect(); + const distanceFromTop = y - scrollRect.top; + const distanceFromBottom = scrollRect.bottom - y; + + if (distanceFromTop < EDGE_THRESHOLD) { + startContinuousScroll( + scrollRef.current, + -scaledEase((EDGE_THRESHOLD - distanceFromTop) / EDGE_THRESHOLD), + ); + } else if (distanceFromBottom < EDGE_THRESHOLD) { + startContinuousScroll( + scrollRef.current, + scaledEase((EDGE_THRESHOLD - distanceFromBottom) / EDGE_THRESHOLD), + ); + } else { + stopContinuousScroll(); } setState((current) => ({ @@ -149,23 +204,12 @@ function Draggable({ onDragEnd(); }); - useEffect(() => { - if (state.isDragging && isDisabled) { - setState((current) => ({ - ...current, - isDragging: false, - width: undefined, - height: undefined, - })); - } - }, [isDisabled, state.isDragging]); - - const handleClick = useLastCallback(() => { + const handleClick = useLastCallback((e: React.MouseEvent | React.TouchEvent) => { if (state.isDragging) { return; } - onClick(); + onClick(e); }); useEffect(() => { @@ -182,6 +226,7 @@ function Draggable({ window.addEventListener('touchcancel', handleMouseUp); window.addEventListener('mouseup', handleMouseUp); } else { + stopContinuousScroll(); window.removeEventListener('touchmove', handleMouseMove); window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('touchend', handleMouseUp); @@ -196,6 +241,7 @@ function Draggable({ return () => { if (state.isDragging) { + stopContinuousScroll(); window.removeEventListener('touchmove', handleMouseMove); window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('touchend', handleMouseUp); @@ -207,7 +253,7 @@ function Draggable({ } } }; - }, [handleMouseMove, handleMouseUp, handleTouchStart, state.isDragging, buttonRef]); + }, [handleMouseMove, handleMouseUp, handleTouchStart, state.isDragging, buttonRef, isDisabled]); const fullClassName = buildClassName(styles.container, className, state.isDragging && styles.isDragging); @@ -257,3 +303,38 @@ function getClientCoordinate(e: MouseEvent | TouchEvent | React.MouseEvent | Rea return { x, y }; } + +function scaledEase(n: number) { + return 5 * (n ** 3) + 1; +} + +function calculateConstrainedTranslation( + state: DraggableState, + lastMousePosition: TPoint, + lastScrollTop: number, + draggableElement: HTMLDivElement, + parentElement: HTMLDivElement, + offset: Offset, +) { + const translation = { + x: lastMousePosition.x - state.origin.x, + y: lastMousePosition.y - state.origin.y + lastScrollTop - state.scrollTop, + }; + + const { + top = 0, right = 0, bottom = 0, left = 0, + } = offset; + + const parentRect = parentElement.getBoundingClientRect(); + const draggableRect = draggableElement.getBoundingClientRect(); + + const minX = -draggableElement.offsetLeft + left; + const maxX = parentRect.width - draggableRect.width - draggableElement.offsetLeft + right; + const minY = -draggableElement.offsetTop + top; + const maxY = parentRect.height - draggableRect.height - draggableElement.offsetTop + bottom; + + return { + x: clamp(translation.x, minX, maxX), + y: clamp(translation.y, minY, maxY), + }; +} diff --git a/src/components/ui/Dropdown.module.scss b/src/components/ui/Dropdown.module.scss index 88cd097b..ddce8fdd 100644 --- a/src/components/ui/Dropdown.module.scss +++ b/src/components/ui/Dropdown.module.scss @@ -56,9 +56,17 @@ transition: none !important; } - &:hover, - &:focus { - background-color: var(--color-input-button-background-hover); + @media (hover: hover) { + &:hover, + &:focus { + background-color: var(--color-input-button-background-hover); + } + } + + @media (pointer: coarse) { + &:active { + background-color: var(--color-input-button-background-hover); + } } } @@ -162,3 +170,14 @@ line-height: 1rem; } } + +.dropdownButtonWrapper { + display: flex; + + width: 5rem; + height: 1rem; +} + +.spinner { + --spinner-size: 1rem; +} diff --git a/src/components/ui/Dropdown.tsx b/src/components/ui/Dropdown.tsx index 2a7502cc..c9c293b5 100644 --- a/src/components/ui/Dropdown.tsx +++ b/src/components/ui/Dropdown.tsx @@ -5,6 +5,7 @@ import buildClassName from '../../util/buildClassName'; import useFlag from '../../hooks/useFlag'; import useLang from '../../hooks/useLang'; +import Loading from './Loading'; import Menu from './Menu'; import styles from './Dropdown.module.scss'; @@ -27,6 +28,7 @@ interface OwnProps { disabled?: boolean; shouldTranslateOptions?: boolean; onChange?: (value: string) => void; + isLoading?: boolean; } const DEFAULT_ARROW = 'caret'; @@ -44,6 +46,7 @@ function Dropdown({ disabled, shouldTranslateOptions, onChange, + isLoading = false, }: OwnProps): TeactJsx { const lang = useLang(); const [isMenuOpen, openMenu, closeMenu] = useFlag(); @@ -88,23 +91,29 @@ function Dropdown({ onClick={isFullyInteractive && !disabled && withMenu ? openMenu : undefined} > {label && {label}} - + + {isLoading ? ( + + ) : ( + + )} + {withMenu && ( diff --git a/src/components/ui/IconWithTooltip.module.scss b/src/components/ui/IconWithTooltip.module.scss index 0be2131d..446f8018 100644 --- a/src/components/ui/IconWithTooltip.module.scss +++ b/src/components/ui/IconWithTooltip.module.scss @@ -46,5 +46,5 @@ } .icon { - cursor: pointer; + cursor: var(--custom-cursor, pointer); } \ No newline at end of file diff --git a/src/components/ui/InfiniteScroll.tsx b/src/components/ui/InfiniteScroll.tsx new file mode 100644 index 00000000..7a3c9e76 --- /dev/null +++ b/src/components/ui/InfiniteScroll.tsx @@ -0,0 +1,279 @@ +import type { RefObject, UIEvent } from 'react'; +import type { FC } from '../../lib/teact/teact'; +import React, { + useEffect, useLayoutEffect, useMemo, useRef, +} from '../../lib/teact/teact'; + +import { LoadMoreDirection } from '../../global/types'; + +import { requestForcedReflow } from '../../lib/fasterdom/fasterdom'; +import buildStyle from '../../util/buildStyle'; +import resetScroll from '../../util/resetScroll'; +import { debounce } from '../../util/schedulers'; +import { IS_ANDROID } from '../../util/windowEnvironment'; + +import useLastCallback from '../../hooks/useLastCallback'; + +type OwnProps = { + ref?: RefObject; + scrollRef?: RefObject; + style?: string; + className?: string; + items?: any[]; + itemSelector?: string; + preloadBackwards?: number; + sensitiveArea?: number; + withAbsolutePositioning?: boolean; + maxHeight?: number; + noScrollRestore?: boolean; + noScrollRestoreOnTop?: boolean; + noFastList?: boolean; + cacheBuster?: any; + beforeChildren?: React.ReactNode; + children: React.ReactNode; + onLoadMore?: ({ direction }: { direction: LoadMoreDirection }) => void; + onScroll?: (e: UIEvent) => void; + onWheel?: (e: React.WheelEvent) => void; + onClick?: (e: React.MouseEvent) => void; + onKeyDown?: (e: React.KeyboardEvent) => void; + onDragOver?: (e: React.DragEvent) => void; + onDragLeave?: (e: React.DragEvent) => void; +}; + +const DEFAULT_LIST_SELECTOR = '.ListItem'; +const DEFAULT_PRELOAD_BACKWARDS = 20; +const DEFAULT_SENSITIVE_AREA = 800; + +const InfiniteScroll: FC = ({ + ref, + scrollRef, + style, + className, + items, + itemSelector = DEFAULT_LIST_SELECTOR, + preloadBackwards = DEFAULT_PRELOAD_BACKWARDS, + sensitiveArea = DEFAULT_SENSITIVE_AREA, + withAbsolutePositioning, + maxHeight, + // Used to turn off restoring scroll position (e.g. for frequently re-ordered chat or user lists) + noScrollRestore = false, + noScrollRestoreOnTop = false, + noFastList, + // Used to re-query `listItemElements` if rendering is delayed by transition + cacheBuster, + beforeChildren, + children, + onLoadMore, + onScroll, + onWheel, + onClick, + onKeyDown, + onDragOver, + onDragLeave, +}: OwnProps) => { + // eslint-disable-next-line no-null/no-null + let containerRef = useRef(null); + if (ref) { + containerRef = ref; + } + + const stateRef = useRef<{ + listItemElements?: NodeListOf; + isScrollTopJustUpdated?: boolean; + currentAnchor?: HTMLDivElement | undefined; + currentAnchorTop?: number; + }>({}); + + const [loadMoreBackwards, loadMoreForwards] = useMemo(() => { + if (!onLoadMore) { + return []; + } + + return [ + debounce(() => { + onLoadMore({ direction: LoadMoreDirection.Backwards }); + }, 1000, true, false), + debounce(() => { + onLoadMore({ direction: LoadMoreDirection.Forwards }); + }, 1000, true, false), + ]; + // eslint-disable-next-line react-hooks-static-deps/exhaustive-deps + }, [onLoadMore, items]); + + // Initial preload + useEffect(() => { + if (!loadMoreBackwards) { + return; + } + + if (preloadBackwards > 0 && (!items || items.length < preloadBackwards)) { + loadMoreBackwards(); + return; + } + + const { scrollHeight, clientHeight } = scrollRef?.current ?? containerRef.current!; + if (clientHeight && scrollHeight <= clientHeight) { + loadMoreBackwards(); + } + }, [items, loadMoreBackwards, preloadBackwards, scrollRef]); + + // Restore `scrollTop` after adding items + useLayoutEffect(() => { + requestForcedReflow(() => { + const container = scrollRef?.current ?? containerRef.current!; + const state = stateRef.current; + + state.listItemElements = container.querySelectorAll(itemSelector); + + let newScrollTop: number; + + if (state.currentAnchor && Array.from(state.listItemElements).includes(state.currentAnchor)) { + const { scrollTop } = container; + const newAnchorTop = state.currentAnchor!.getBoundingClientRect().top; + newScrollTop = scrollTop + (newAnchorTop - state.currentAnchorTop!); + } else { + const nextAnchor = state.listItemElements[0]; + if (nextAnchor) { + state.currentAnchor = nextAnchor; + state.currentAnchorTop = nextAnchor.getBoundingClientRect().top; + } + } + + if (withAbsolutePositioning || noScrollRestore) { + return undefined; + } + + const { scrollTop } = container; + if (noScrollRestoreOnTop && scrollTop === 0) { + return undefined; + } + + return () => { + resetScroll(container, newScrollTop); + + state.isScrollTopJustUpdated = true; + }; + }); + }, [items, itemSelector, noScrollRestore, noScrollRestoreOnTop, cacheBuster, withAbsolutePositioning, scrollRef]); + + const handleScroll = useLastCallback((e: UIEvent) => { + if (loadMoreForwards && loadMoreBackwards) { + const { + isScrollTopJustUpdated, currentAnchor, currentAnchorTop, + } = stateRef.current; + const listItemElements = stateRef.current.listItemElements!; + + if (isScrollTopJustUpdated) { + stateRef.current.isScrollTopJustUpdated = false; + return; + } + + const listLength = listItemElements.length; + const container = scrollRef?.current ?? containerRef.current!; + const { scrollTop, scrollHeight, offsetHeight } = container; + const top = listLength ? listItemElements[0].offsetTop : 0; + const isNearTop = scrollTop <= top + sensitiveArea; + const bottom = listLength + ? listItemElements[listLength - 1].offsetTop + listItemElements[listLength - 1].offsetHeight + : scrollHeight; + const isNearBottom = bottom - (scrollTop + offsetHeight) <= sensitiveArea; + let isUpdated = false; + + if (isNearTop) { + const nextAnchor = listItemElements[0]; + if (nextAnchor) { + const nextAnchorTop = nextAnchor.getBoundingClientRect().top; + const newAnchorTop = currentAnchor?.offsetParent && currentAnchor !== nextAnchor + ? currentAnchor.getBoundingClientRect().top + : nextAnchorTop; + const isMovingUp = ( + currentAnchor && currentAnchorTop !== undefined && newAnchorTop > currentAnchorTop + ); + + if (isMovingUp) { + stateRef.current.currentAnchor = nextAnchor; + stateRef.current.currentAnchorTop = nextAnchorTop; + isUpdated = true; + loadMoreForwards(); + } + } + } + + if (isNearBottom) { + const nextAnchor = listItemElements[listLength - 1]; + if (nextAnchor) { + const nextAnchorTop = nextAnchor.getBoundingClientRect().top; + const newAnchorTop = currentAnchor?.offsetParent && currentAnchor !== nextAnchor + ? currentAnchor.getBoundingClientRect().top + : nextAnchorTop; + const isMovingDown = ( + currentAnchor && currentAnchorTop !== undefined && newAnchorTop < currentAnchorTop + ); + + if (isMovingDown) { + stateRef.current.currentAnchor = nextAnchor; + stateRef.current.currentAnchorTop = nextAnchorTop; + isUpdated = true; + loadMoreBackwards(); + } + } + } + + if (!isUpdated) { + if (currentAnchor?.offsetParent) { + stateRef.current.currentAnchorTop = currentAnchor.getBoundingClientRect().top; + } else { + const nextAnchor = listItemElements[0]; + + if (nextAnchor) { + stateRef.current.currentAnchor = nextAnchor; + stateRef.current.currentAnchorTop = nextAnchor.getBoundingClientRect().top; + } + } + } + } + + if (onScroll) { + onScroll(e); + } + }); + + useLayoutEffect(() => { + const scrollContainerRef = scrollRef?.current; + const handleNativeScroll = (e: Event) => handleScroll(e as unknown as UIEvent); + if (scrollContainerRef) { + scrollContainerRef.addEventListener('scroll', handleNativeScroll); + } + + return () => { + scrollContainerRef?.removeEventListener('scroll', handleNativeScroll); + }; + }, [handleScroll, scrollRef]); + + return ( +
+ {beforeChildren} + {withAbsolutePositioning && items?.length ? ( +
+ {children} +
+ ) : children} +
+ ); +}; + +export default InfiniteScroll; diff --git a/src/components/ui/Input.module.scss b/src/components/ui/Input.module.scss index 17d26366..2e5f69e5 100644 --- a/src/components/ui/Input.module.scss +++ b/src/components/ui/Input.module.scss @@ -1,7 +1,7 @@ .wrapper { position: relative; - margin-bottom: 1rem; + margin-bottom: 1.25rem; } .input { @@ -117,6 +117,8 @@ line-height: 1; white-space: nowrap; + transition: color 150ms; + /* Fix for ContentEditable multiline "issues" */ br { display: none; @@ -162,6 +164,11 @@ } } +.isLoading { + font-size: 3rem; + color: var(--color-gray-4); +} + textarea.input { resize: none; @@ -172,6 +179,10 @@ textarea.input { line-height: 1.3125rem; } +.inputWrapperStatic { + box-shadow: inset 0 0 0 0.0625rem var(--color-separator-input-stroke); +} + .input__wrapper { display: flex; @@ -242,7 +253,7 @@ textarea.input { display: block; - margin-bottom: 0.3125rem; + margin-bottom: 0.5rem; padding: 0 0.5rem; font-size: 0.8125rem; @@ -286,3 +297,15 @@ textarea.input { white-space: normal; } } + +.swapCorner { + &::before { + box-shadow: 0 0 0 0.125rem var(--color-blue) !important; + } + + &_error { + &::before { + box-shadow: 0 0 0 0.125rem var(--color-red) !important; + } + } +} diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx index 8f640375..df5c5a4c 100644 --- a/src/components/ui/Input.tsx +++ b/src/components/ui/Input.tsx @@ -18,6 +18,7 @@ type OwnProps = { label?: TeactNode; placeholder?: string; value?: string | number; + inputMode?: 'numeric' | 'text' | 'search'; maxLength?: number; isRequired?: boolean; isControlled?: boolean; @@ -31,8 +32,8 @@ type OwnProps = { children?: TeactNode; onInput: (value: string, inputArg?: any) => void; onKeyDown?: (e: KeyboardEvent) => void; - onFocus?: NoneToVoidFunction; - onBlur?: NoneToVoidFunction; + onFocus?: (e: React.FocusEvent) => void; + onBlur?: (e: React.FocusEvent) => void; }; function Input({ @@ -40,6 +41,7 @@ function Input({ id, label, placeholder, + inputMode, isRequired, isControlled, isMultiline, @@ -127,6 +129,7 @@ function Input({ className={inputFullClass} type={finalType} value={value} + inputMode={inputMode} maxLength={maxLength} autoComplete={autoComplete} onInput={handleInput} diff --git a/src/components/ui/InteractiveTextField.tsx b/src/components/ui/InteractiveTextField.tsx index 0c69dcda..915e6344 100644 --- a/src/components/ui/InteractiveTextField.tsx +++ b/src/components/ui/InteractiveTextField.tsx @@ -3,9 +3,10 @@ import React, { } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; -import { TONSCAN_BASE_MAINNET_URL, TONSCAN_BASE_TESTNET_URL } from '../../config'; +import { IS_CAPACITOR, TONSCAN_BASE_MAINNET_URL, TONSCAN_BASE_TESTNET_URL } from '../../config'; import { selectCurrentAccountState } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; +import { vibrateOnSuccess } from '../../util/capacitor'; import captureKeyboardListeners from '../../util/captureKeyboardListeners'; import { copyTextToClipboard } from '../../util/clipboard'; import { shortenAddress } from '../../util/shortenAddress'; @@ -35,6 +36,7 @@ interface OwnProps { className?: string; textClassName?: string; noSavedAddress?: boolean; + noExplorer?: boolean; } interface StateProps { @@ -53,6 +55,7 @@ function InteractiveTextField({ spoilerCallback, copyNotification, noSavedAddress, + noExplorer, className, textClassName, isAddressAlreadySaved, @@ -83,7 +86,7 @@ function InteractiveTextField({ } addSavedAddress({ address, name: savedAddressName }); - showNotification({ message: 'Address was saved!', icon: 'icon-star' }); + showNotification({ message: lang('Address was saved!'), icon: 'icon-star' }); closeSaveAddressModal(); }); @@ -99,7 +102,10 @@ function InteractiveTextField({ const handleCopy = useLastCallback(() => { showNotification({ message: copyNotification, icon: 'icon-copy' }); - copyTextToClipboard(address || text || ''); + void copyTextToClipboard(address || text || ''); + if (IS_CAPACITOR) { + void vibrateOnSuccess(); + } }); const handleRevealSpoiler = useLastCallback(() => { @@ -209,7 +215,7 @@ function InteractiveTextField({ )} - {address && ( + {!noExplorer && address && ( = ({ // eslint-disable-next-line no-null/no-null const menuRef = useRef(null); + useHistoryBack({ + isActive: Boolean(isOpen && onClose), + onBack: onClose!, + shouldBeReplaced: true, + }); + const { transitionClassNames, } = useShowTransition( diff --git a/src/components/ui/MenuItem.module.scss b/src/components/ui/MenuItem.module.scss index e7b59957..9ea67b30 100644 --- a/src/components/ui/MenuItem.module.scss +++ b/src/components/ui/MenuItem.module.scss @@ -44,12 +44,23 @@ } } - &:hover, - &:focus { - color: var(--color-blue); - text-decoration: none; + @media (hover: hover) { + &:hover, + &:focus { + color: var(--color-blue); + text-decoration: none; - background-color: var(--color-interactive-popup-menu-hover); + background-color: var(--color-interactive-popup-menu-hover); + } + } + + @media (pointer: coarse) { + &:active { + color: var(--color-blue); + text-decoration: none; + + background-color: var(--color-interactive-popup-menu-hover); + } } &:active { diff --git a/src/components/ui/Modal.module.scss b/src/components/ui/Modal.module.scss index 122d3981..669bc13c 100644 --- a/src/components/ui/Modal.module.scss +++ b/src/components/ui/Modal.module.scss @@ -6,44 +6,178 @@ position: relative; z-index: var(--z-modal); - &.slideUpAnimation { - --transition: 500ms cubic-bezier(0.3, 0.8, 0.2, 1); - } - &.error { .dialog { max-width: 23rem; } } - &:global(.open) .backdrop, + .dialog { + transform: translateY(-1rem); + + opacity: 0; + + transition: transform var(--transition), opacity var(--transition); + + :global(html.animation-level-0) & { + transform: translateY(0); + + transition: var(--no-animation-transition); + } + } + &:global(.open) .dialog { + transform: translateY(0); + opacity: 1; } - &:not(.slideUpAnimation):global(.open) .dialog { - transform: translate3d(0, 0, 0); + &:global(.closing) .dialog { + transform: translateY(1rem); } - :global(html:not(.animation-level-0)) &:not(.slideUpAnimation):global(.closing) .dialog { - transform: translate3d(0, 1rem, 0); + .backdrop { + opacity: 0; + + transition: opacity var(--transition); + + :global(html.animation-level-0) & { + transition: var(--no-animation-transition); + } } - &.slideUpAnimation:global(.shown) { - .container { - overflow: hidden; + &:global(.open) .backdrop { + opacity: 1; + } +} + +.delegatingToNative { + display: none; +} + +.slideUpAnimation { + --transition: 500ms cubic-bezier(0.3, 0.8, 0.2, 1); + + .container { + overflow: hidden; + align-items: flex-end; + + padding: 0; + } + + // Disable catching events on closing modals + &:global(.closing) .container { + pointer-events: none; + } + + .dialog { + transform: translateY(100%); + + max-width: 28rem; + max-height: calc(95 * var(--vh, 1vh)); + margin: 0; + + background: var(--color-background-second); + border-radius: var(--border-radius-default) var(--border-radius-default) 0 0; + + transition: transform var(--transition); + + :global(html:not(.animation-level-0)) & { + opacity: 1; + } + + :global(html.animation-level-0) & { + transition: var(--no-animation-transition); } + > :global(.Transition-slideLayers), + > :global(.Transition-slideLayersBackwards) { + border-radius: var(--border-radius-default) var(--border-radius-default) 0 0; + } + } + + &:global(.open) .dialog { + transform: translateY(0); + } + + &:global(.closing) .dialog { + transform: translateY(100%); + } + + :global(html.animation-level-0) &:global(.closing) { + .dialog { + display: none; + + // The transition for the `display` property is needed to prevent blocking events + /* stylelint-disable-next-line plugin/no-low-performance-animation-properties */ + transition: var(--no-animation-transition), display 0ms 200ms !important; + } + + .backdrop { + display: none; + } + } + + // Mimic the animation from the system Settings section on Android + :global(html.is-android) &:not(.forceBottomSheet) { + --transition: 500ms cubic-bezier(0.16, 1, 0.3, 1); + .dialog { - transform: translateY(100%); + transform: translateY(15%); + + height: 100%; + max-height: 100%; + + opacity: 0; + border-radius: 0; + + > :global(.Transition-slideLayers), + > :global(.Transition-slideLayersBackwards) { + border-radius: 0; + } + } + + &:global(.open) .dialog { + transform: translateY(0); + + opacity: 1; + + transition: transform var(--transition), opacity var(--transition); } - &:global(.shown.open) { + &:global(.closing) { .dialog { - transform: translateY(0); + transform: translateY(3%); + + transition: transform 200ms ease-in, opacity 100ms 100ms ease-out !important; } } } + + :global(html.is-native-bottom-sheet) & { + .container { + align-items: flex-start; + + background: var(--color-background-second); + } + + .dialog { + max-height: none; + + border-radius: 0; + box-shadow: none; + + transition: none; + } + + &:global(.closing) .dialog { + transform: translateY(0); + } + + .backdrop { + display: none; + } + } } .container { @@ -58,12 +192,6 @@ justify-content: center; padding: 1rem; - - .slideUpAnimation & { - align-items: flex-end; - - padding: 0; - } } .backdrop { @@ -74,14 +202,7 @@ bottom: 0; left: 0; - opacity: 0; background-color: var(--color-tint); - - transition: opacity var(--transition); - - :global(html.animation-level-0) & { - transition: var(--no-animation-transition) !important; - } } .noBackdrop { @@ -90,7 +211,6 @@ .dialog { position: relative; - transform: translate3d(0, -1rem, 0); display: inline-flex; flex-direction: column; @@ -101,47 +221,10 @@ max-height: 100%; margin: 2rem auto; - opacity: 0; background-color: var(--color-background-window); border-radius: var(--border-radius-default); box-shadow: var(--default-shadow); - transition: transform var(--transition), opacity var(--transition); - - .slideUpAnimation & { - transform: none; - - max-width: 28rem; - max-height: calc(95 * var(--vh, 1vh)); - margin: 0; - - background: var(--color-background-second); - border-radius: var(--border-radius-default) var(--border-radius-default) 0 0; - - transition: transform var(--transition); - - :global(html:not(.animation-level-0)) & { - opacity: 1; - } - - :global(html.animation-level-0) & { - transition: var(--no-animation-transition) !important; - } - - > :global(.Transition-slideLayers), - > :global(.Transition-slideLayersBackwards) { - border-radius: var(--border-radius-default) var(--border-radius-default) 0 0; - } - } - - :global(html.animation-level-0) & { - transform: translate3d(0, 0, 0) !important; - - opacity: 0; - - transition: var(--no-animation-transition) !important; - } - .content > :global(.Transition-slideLayers), .content > :global(.Transition-slideLayersBackwards) { overflow: hidden; @@ -169,8 +252,6 @@ color: var(--color-black); - transition: box-shadow 200ms; - &_noClose { grid-template-areas: "title title title"; @@ -183,13 +264,7 @@ } &_wideContent { - grid-template-columns: 0.25fr 1fr 0.25fr; - } - - &_bordered { - z-index: 1; - /* stylelint-disable-next-line plugin/whole-pixel */ - box-shadow: 0 0.035rem 0 0 var(--color-separator); + grid-template-columns: 0.25fr 1.5fr 0.25fr; } &_back { @@ -200,6 +275,7 @@ align-items: center; margin-left: -0.875rem; + padding: 0 0.5rem; font-size: 1.0625rem; color: var(--color-blue); @@ -306,23 +382,38 @@ } .buttons { - display: flex; + display: grid; + grid-auto-columns: minmax(max-content, 1fr); + grid-auto-flow: column; gap: 1rem; - justify-content: center; + justify-items: center; - margin-top: auto; + margin-top: auto; // Used to pull down when modal has fixed height + padding-top: 2rem; } -.button { - min-width: 9rem !important; +.buttonsInsideContentWithScroll { + margin-bottom: -1rem; + padding-bottom: 1rem; + + @supports (padding-bottom: env(safe-area-inset-bottom)) { + margin-bottom: calc(-1 * max(env(safe-area-inset-bottom), 1rem)); + padding-bottom: max(env(safe-area-inset-bottom), 1rem); + } } -.customCancelButton { - min-width: 7rem !important; +.buttonsNoExtraSpace { + padding-top: 0; } -.customSubmitButton { - max-width: 100%; +.button { + width: 100%; + min-width: 9rem !important; + max-width: 68vw !important; +} + +.shortButton { + min-width: 6rem !important; } .transition { diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx index 0a7350ef..658f5e8f 100644 --- a/src/components/ui/Modal.tsx +++ b/src/components/ui/Modal.tsx @@ -1,22 +1,24 @@ import type { RefObject } from 'react'; -import type { - TeactNode, -} from '../../lib/teact/teact'; -import React, { useEffect, useRef } from '../../lib/teact/teact'; +import type { BottomSheetKeys } from 'native-bottom-sheet'; +import type { TeactNode } from '../../lib/teact/teact'; +import React, { useEffect, useLayoutEffect, useRef } from '../../lib/teact/teact'; import { ANIMATION_END_DELAY, IS_EXTENSION } from '../../config'; import buildClassName from '../../util/buildClassName'; +import { captureEvents, SwipeDirection } from '../../util/captureEvents'; import captureKeyboardListeners from '../../util/captureKeyboardListeners'; -import { captureSwipe, SwipeDirection } from '../../util/captureSwipe'; +import { getIsSwipeToCloseDisabled } from '../../util/modalSwipeManager'; import trapFocus from '../../util/trapFocus'; -import { IS_TOUCH_ENV } from '../../util/windowEnvironment'; +import { IS_ANDROID, IS_DELEGATED_BOTTOM_SHEET, IS_TOUCH_ENV } from '../../util/windowEnvironment'; import windowSize from '../../util/windowSize'; +import freezeWhenClosed from '../../hooks/freezeWhenClosed'; +import { useDelegatedBottomSheet } from '../../hooks/useDelegatedBottomSheet'; +import { useDelegatingBottomSheet } from '../../hooks/useDelegatingBottomSheet'; import { useDeviceScreen } from '../../hooks/useDeviceScreen'; -import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps'; import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'; +import useHistoryBack from '../../hooks/useHistoryBack'; import useLang from '../../hooks/useLang'; -import useLastCallback from '../../hooks/useLastCallback'; import useShowTransition from '../../hooks/useShowTransition'; import Button from './Button'; @@ -31,6 +33,10 @@ type OwnProps = { contentClassName?: string; isOpen?: boolean; isCompact?: boolean; + nativeBottomSheetKey?: BottomSheetKeys; + forceFullNative?: boolean; // Always open in "full" size + noResetFullNativeOnBlur?: boolean; // Don't reset "full" size on blur + forceBottomSheet?: boolean; noBackdrop?: boolean; noBackdropClose?: boolean; header?: any; @@ -42,12 +48,8 @@ type OwnProps = { dialogRef?: RefObject; }; -type StateProps = { - shouldSkipHistoryAnimations?: boolean; -}; - -export const ANIMATION_DURATION = 350; -export const ANIMATION_DURATION_PORTRAIT = 500; +export const CLOSE_DURATION = 350; +export const CLOSE_DURATION_PORTRAIT = IS_ANDROID ? 200 : 500; function Modal({ dialogRef, @@ -57,6 +59,10 @@ function Modal({ contentClassName, isOpen, isCompact, + nativeBottomSheetKey, + forceFullNative, + forceBottomSheet, + noResetFullNativeOnBlur, noBackdrop, noBackdropClose, header, @@ -65,63 +71,75 @@ function Modal({ onClose, onCloseAnimationEnd, onEnter, - shouldSkipHistoryAnimations, -}: OwnProps & StateProps): TeactJsx { +}: OwnProps): TeactJsx { + const lang = useLang(); + // eslint-disable-next-line no-null/no-null + const modalRef = useRef(null); + + // eslint-disable-next-line no-null/no-null + const localDialogRef = useRef(null); + dialogRef ||= localDialogRef; + const { isPortrait } = useDeviceScreen(); - const animationDuration = isPortrait ? ANIMATION_DURATION_PORTRAIT : ANIMATION_DURATION; + const animationDuration = isPortrait ? CLOSE_DURATION_PORTRAIT : CLOSE_DURATION; const { shouldRender, transitionClassNames } = useShowTransition( isOpen, onCloseAnimationEnd, - shouldSkipHistoryAnimations, + undefined, false, - shouldSkipHistoryAnimations, + undefined, animationDuration + ANIMATION_END_DELAY, ); - const lang = useLang(); - // eslint-disable-next-line no-null/no-null - const modalRef = useRef(null); const isSlideUp = !isCompact && isPortrait; - const handleClose = useLastCallback((e: KeyboardEvent) => { - if (IS_EXTENSION) { - e.preventDefault(); - } - - onClose(); + useHistoryBack({ + isActive: isOpen, + onBack: onClose, }); useEffect( - () => (isOpen ? captureKeyboardListeners({ onEsc: handleClose, onEnter }) : undefined), - [handleClose, isOpen, onEnter], + () => (isOpen ? captureKeyboardListeners({ + onEnter, + onEsc: (e: KeyboardEvent) => { + if (IS_EXTENSION) { + e.preventDefault(); + } + + onClose(); + }, + }) : undefined), + [isOpen, onClose, onEnter], ); useEffect(() => (isOpen && modalRef.current ? trapFocus(modalRef.current) : undefined), [isOpen]); - useEffectWithPrevDeps( - ([prevIsOpen]) => { - if (isOpen || (!isOpen && prevIsOpen !== undefined)) { - dispatchHeavyAnimationEvent(animationDuration + ANIMATION_END_DELAY); - } - }, - [animationDuration, isOpen], - ); + useLayoutEffect(() => ( + isOpen ? dispatchHeavyAnimationEvent(animationDuration + ANIMATION_END_DELAY) : undefined + ), [animationDuration, isOpen]); useEffect(() => { - if (!IS_TOUCH_ENV || !isOpen || !isPortrait || !isSlideUp) { + if (!IS_TOUCH_ENV || !isOpen || !isPortrait || !isSlideUp || IS_DELEGATED_BOTTOM_SHEET) { return undefined; } - return captureSwipe(modalRef.current!, (e, direction) => { - if (direction === SwipeDirection.Down && !windowSize.getIsKeyboardVisible()) { - onClose(); - return true; - } + return captureEvents(modalRef.current!, { + onSwipe: (e, direction) => { + if (direction === SwipeDirection.Down && !windowSize.getIsKeyboardVisible() && !getIsSwipeToCloseDisabled()) { + onClose(); + return true; + } - return false; + return false; + }, }); }, [isOpen, isPortrait, isSlideUp, onClose]); + const isDelegatingToNative = useDelegatingBottomSheet(nativeBottomSheetKey, isPortrait, isOpen, onClose); + useDelegatedBottomSheet( + nativeBottomSheetKey, isOpen, onClose, dialogRef, forceFullNative, noResetFullNativeOnBlur, + ); + if (!shouldRender) { return undefined; } @@ -136,7 +154,9 @@ function Modal({ } return ( -
+
{title}
{hasCloseButton && ( + )} +
+ + {!isSmallHeight &&
{lang(title)}
} + {children} +
+ + + + ); + } + return (
+ {isBiometricAuthEnabled ? ( +
+ {lang(operationType === 'transfer' + ? 'Please confirm transaction using biometrics' : 'Please confirm operation using biometrics')} +
+ ) : ( + + )} + {localError && ( +
{lang(localError)}
+ )} + {help && !error && ( +
{help}
+ )}
{onCancel && ( - )} - + {!isBiometricAuthEnabled && ( + + )}
); } -export default memo(PasswordForm); +export default memo(withGlobal((global): StateProps => { + const { isPasswordNumeric, authConfig } = global.settings; + const isBiometricAuthEnabled = !!authConfig && authConfig.kind !== 'password'; + const isNativeBiometricAuthEnabled = !!authConfig && authConfig.kind === 'native-biometrics'; + + return { + isPasswordNumeric, + isBiometricAuthEnabled, + isNativeBiometricAuthEnabled, + authConfig, + }; +})(PasswordForm)); diff --git a/src/components/ui/PinPad.module.scss b/src/components/ui/PinPad.module.scss new file mode 100644 index 00000000..d5264a22 --- /dev/null +++ b/src/components/ui/PinPad.module.scss @@ -0,0 +1,280 @@ +.root { + width: 100%; + margin-top: auto; + padding: 1.5rem 1rem 1rem; + + background-color: var(--color-background-first); + + @supports (padding-bottom: max(1rem, env(safe-area-inset-bottom))) { + padding-bottom: max(1rem, env(safe-area-inset-bottom)); + } +} + +.title { + margin-bottom: 2rem; + + font-size: 1.0625rem; + font-weight: 700; + color: var(--color-gray-1); + text-align: center; + + transition: color 200ms; +} + +.dots { + --fill-color: var(--color-gray-4); + + display: flex; + align-items: center; + justify-content: center; + + margin-bottom: 1.5rem; +} + +.dotsError { + animation-name: shakeAnimation; + animation-duration: 200ms; + animation-timing-function: ease; + animation-iteration-count: 2; +} + +.dotsLoading { + position: relative; + + &::after { + content: ''; + + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + + width: 1.5rem; + height: 1.5rem; + + opacity: 0; + background-image: var(--spinner-green-data); + background-repeat: no-repeat; + background-size: 100%; + + animation: spin 1000ms linear 750ms infinite, + appear 1000ms linear 750ms forwards; + } +} + +.dot { + width: 0.75rem; + height: 0.75rem; + + background-color: var(--fill-color); + border-radius: 50%; + + transition: background-color 200ms, transform 300ms; + + .dotsLoading > & { + &:nth-child(1) { + animation: dotLoadingAnimation 1000ms linear forwards, firstDotLoadingAnimation 1000ms linear forwards; + } + &:nth-child(2) { + animation: dotLoadingAnimation 1000ms linear forwards, secondDotLoadingAnimation 1000ms linear forwards; + } + &:nth-child(3) { + animation: dotLoadingAnimation 1000ms linear forwards, thirdDotLoadingAnimation 1000ms linear forwards; + } + &:nth-child(4) { + animation: dotLoadingAnimation 1000ms linear forwards, fourthDotLoadingAnimation 1000ms linear forwards; + } + } +} + +.dotFilled { + --fill-color: var(--color-blue); + + animation: scaleAnimation 300ms linear forwards; +} + +.dot + .dot { + margin-left: 0.625rem; +} + +.grid { + display: grid; + grid-template-columns: repeat(3, 1fr); +} + +.button { + cursor: var(--custom-cursor, pointer); + user-select: none; + + position: relative; + + display: flex; + align-items: center; + justify-content: center; + + height: 5rem; + + font-size: 2rem; + font-weight: 700; + line-height: 2rem; + text-align: center; + + transition: opacity 200ms; + + @media (max-height: 43.5rem) { + height: 4rem; + } + + &::after { + content: ''; + + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + + aspect-ratio: 1; + height: 100%; + + opacity: 0; + background: var(--color-tint); + border-radius: 50%; + + transition: opacity 200ms; + } +} + +.buttonActive { + &::after { + opacity: 0.2; + } +} + +.buttonHidden { + pointer-events: none; + + opacity: 0; +} + +.error { + --fill-color: var(--color-red); + + color: var(--color-red); +} + +.success { + --fill-color: var(--color-green); + + color: var(--color-green); +} + +@keyframes scaleAnimation { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.4); + } + 100% { + transform: scale(1); + } +} + +@keyframes shakeAnimation { + 0% { + transform: translateX(0); + } + 25% { + transform: translateX(0.75rem); + } + 75% { + transform: translateX(-0.75rem); + } + 100% { + transform: translateX(0); + } +} + +@keyframes dotLoadingAnimation { + 0% { + transform: translateX(0) scale(1); + + opacity: 1; + } + 25% { + transform: translateX(0) scale(1.4); + } + 50% { + transform: translateX(0) scale(1); + } + 100% { + opacity: 0; + } +} + +@keyframes firstDotLoadingAnimation { + 50% { + transform: translateX(0); + } + 75% { + transform: translateX(2.0625rem); + } + 100% { + transform: translateX(2.0625rem); + } +} + +@keyframes secondDotLoadingAnimation { + 50% { + transform: translateX(0); + } + 75% { + transform: translateX(0.6875rem); + } + 100% { + transform: translateX(0.6875rem); + } +} + +@keyframes thirdDotLoadingAnimation { + 50% { + transform: translateX(0); + } + 75% { + transform: translateX(-0.6875rem); + } + 100% { + transform: translateX(-0.6875rem); + } +} + +@keyframes fourthDotLoadingAnimation { + 50% { + transform: translateX(0); + } + 75% { + transform: translateX(-2.0625rem); + } + 100% { + transform: translateX(-2.0625rem); + } +} + +@keyframes spin { + from { + transform: translate(-50%, -0.375rem) rotate(0deg); + } + + to { + transform: translate(-50%, -0.375rem) rotate(360deg); + } +} + +@keyframes appear { + from { + opacity: 0; + } + to { + opacity: 1; + } +} diff --git a/src/components/ui/PinPad.tsx b/src/components/ui/PinPad.tsx new file mode 100644 index 00000000..51aeadbc --- /dev/null +++ b/src/components/ui/PinPad.tsx @@ -0,0 +1,152 @@ +import React, { memo, useEffect } from '../../lib/teact/teact'; +import { withGlobal } from '../../global'; + +import type { GlobalState } from '../../global/types'; + +import buildClassName from '../../util/buildClassName'; +import { getIsFaceIdAvailable, vibrateOnError } from '../../util/capacitor'; + +import useLastCallback from '../../hooks/useLastCallback'; + +import PinPadButton from './PinPadButton'; + +import styles from './PinPad.module.scss'; + +interface OwnProps { + title: string; + type?: 'error' | 'success'; + value: string; + length?: number; + className?: string; + onBiometricsClick?: NoneToVoidFunction; + onChange: (value: string) => void; + onClearError?: NoneToVoidFunction; + onSubmit: (pin: string) => void; +} + +type StateProps = Pick & { + isPinPadPasswordAccepted?: boolean; +}; + +const DEFAULT_PIN_LENGTH = 4; +const RESET_STATE_DELAY_MS = 1500; + +function PinPad({ + title, + type, + value, + length = DEFAULT_PIN_LENGTH, + onBiometricsClick, + isPinPadPasswordAccepted, + className, + onChange, + onClearError, + onSubmit, +}: OwnProps & StateProps) { + const isFaceId = getIsFaceIdAvailable(); + const canRenderBackspace = value.length > 0; + const isDisablePinButtons = value.length === length || Boolean(type); + const isSuccess = type === 'success' || isPinPadPasswordAccepted; + const titleClassName = buildClassName( + styles.title, + type === 'error' && styles.error, + isSuccess && styles.success, + ); + + useEffect(() => { + if (type !== 'error') return undefined; + + const timeoutId = window.setTimeout(() => { + if (value.length === length) { + onChange(''); + } + onClearError?.(); + }, RESET_STATE_DELAY_MS); + void vibrateOnError(); + + return () => { + window.clearTimeout(timeoutId); + }; + }, [length, onChange, onClearError, type, value.length]); + + const handleClick = useLastCallback((char: string) => { + const newValue = `${value}${char}`; + onChange(newValue); + + if (newValue.length === length) { + onSubmit(newValue); + } + }); + + const handleBackspaceClick = useLastCallback(() => { + onClearError?.(); + if (!value.length) return; + + onChange(value.slice(0, -1)); + }); + + function renderDots() { + const dotsClassName = buildClassName( + styles.dots, + type === 'error' && styles.dotsError, + isSuccess && styles.dotsLoading, + ); + + return ( +
+ {Array.from({ length }, (_, i) => ( +
+ ))} +
+ ); + } + + return ( +
+
{title}
+ {renderDots()} + +
+ + + + + + + + + + {!onBiometricsClick ? : ( + + + + )} + + + + +
+
+ ); +} + +export default memo(withGlobal( + (global) => { + const { isPinPadPasswordAccepted } = global; + return { + isPinPadPasswordAccepted, + }; + }, +)(PinPad)); diff --git a/src/components/ui/PinPadButton.tsx b/src/components/ui/PinPadButton.tsx new file mode 100644 index 00000000..214a7229 --- /dev/null +++ b/src/components/ui/PinPadButton.tsx @@ -0,0 +1,43 @@ +import React, { memo, useState } from '../../lib/teact/teact'; + +import buildClassName from '../../util/buildClassName'; +import { vibrate } from '../../util/capacitor'; + +import styles from './PinPad.module.scss'; + +interface OwnProps { + value?: any; + children?: React.ReactNode; + className?: string; + isDisabled?: boolean; + onClick?: (value?: any) => void; +} + +const CLICKED_TIMEOUT_MS = 200; + +function PinPadButton({ + value, children, className, isDisabled, onClick, +}: OwnProps) { + const [isClicked, setIsClicked] = useState(false); + + const handleClick = () => { + void vibrate(); + onClick?.(value); + setIsClicked(true); + setTimeout(() => { + setIsClicked(false); + }, CLICKED_TIMEOUT_MS); + }; + return ( +
+ {value || children} +
+ ); +} + +export default memo(PinPadButton); diff --git a/src/components/ui/RichNumberInput.tsx b/src/components/ui/RichNumberInput.tsx index 1ddad20d..a4368094 100644 --- a/src/components/ui/RichNumberInput.tsx +++ b/src/components/ui/RichNumberInput.tsx @@ -1,11 +1,10 @@ import type { TeactNode } from '../../lib/teact/teact'; import React, { - memo, useEffect, useRef, + memo, useLayoutEffect, useRef, useState, } from '../../lib/teact/teact'; -import { FRACTION_DIGITS } from '../../config'; +import { DEFAULT_DECIMAL_PLACES, FRACTION_DIGITS } from '../../config'; import { Big } from '../../lib/big.js'; -import { requestMutation } from '../../lib/fasterdom/fasterdom'; import buildClassName from '../../util/buildClassName'; import { round } from '../../util/round'; import { saveCaretPosition } from '../../util/saveCaretPosition'; @@ -21,6 +20,7 @@ type OwnProps = { labelText?: React.ReactNode; value?: number; hasError?: boolean; + isLoading?: boolean; suffix?: string; className?: string; inputClassName?: string; @@ -34,12 +34,14 @@ type OwnProps = { onPressEnter?: (e: React.KeyboardEvent) => void; decimals?: number; disabled?: boolean; + isStatic?: boolean; }; function RichNumberInput({ id, labelText, hasError, + isLoading = false, suffix, value, children, @@ -53,39 +55,53 @@ function RichNumberInput({ onFocus, onPressEnter, decimals = FRACTION_DIGITS, - disabled, + disabled = false, + isStatic = false, }: OwnProps) { // eslint-disable-next-line no-null/no-null const inputRef = useRef(null); const lang = useLang(); const [hasFocus, markHasFocus, unmarkHasFocus] = useFlag(false); + const [isContentEditable, setContentEditable] = useState(!disabled); - const updateHtml = useLastCallback((parts?: RegExpMatchArray) => { - const input = inputRef.current!; + const handleLoadingHtml = useLastCallback((input: HTMLInputElement, parts?: RegExpMatchArray) => { const newHtml = parts ? buildContentHtml(parts, suffix, decimals) : ''; + input.innerHTML = newHtml; + setContentEditable(false); + return newHtml; + }); + + const handleNumberHtml = useLastCallback((input: HTMLInputElement, parts?: RegExpMatchArray) => { + const newHtml = parts ? buildContentHtml(parts, suffix, decimals) : ''; const restoreCaretPosition = document.activeElement === inputRef.current ? saveCaretPosition(input, decimals) : undefined; + input.innerHTML = newHtml; + setContentEditable(!disabled); restoreCaretPosition?.(); - // Trick to remove pseudo-element with placeholder in this tick - requestMutation(() => { - input.classList.toggle(styles.isEmpty, !newHtml.length); - }); + return newHtml; + }); + + const updateHtml = useLastCallback((parts?: RegExpMatchArray) => { + const input = inputRef.current!; + const content = isLoading ? handleLoadingHtml(input, parts) : handleNumberHtml(input, parts); + + input.classList.toggle(styles.isEmpty, !content.length); }); - useEffect(() => { - const newValue = castValue(value, decimals); + useLayoutEffect(() => { + const newValue = castValue(value); - const parts = getParts(String(newValue), decimals); + const parts = getParts(String(newValue)); updateHtml(parts); if (value !== newValue) { onChange?.(newValue); } - }, [decimals, onChange, updateHtml, value, suffix]); + }, [decimals, onChange, updateHtml, value, suffix, isLoading, disabled]); function handleChange(e: React.FormEvent) { const inputValue = e.currentTarget.innerText.trim(); @@ -126,6 +142,7 @@ function RichNumberInput({ const inputWrapperFullClass = buildClassName( styles.input__wrapper, + isStatic && styles.inputWrapperStatic, hasError && styles.error, hasFocus && styles.input__wrapper_hasFocus, inputClassName, @@ -135,6 +152,7 @@ function RichNumberInput({ styles.input_rich, !value && styles.isEmpty, valueClassName, + isLoading && styles.isLoading, ); const labelTextClassName = buildClassName( styles.label, @@ -143,6 +161,8 @@ function RichNumberInput({ ); const cornerFullClass = buildClassName( cornerClassName, + hasFocus && styles.swapCorner, + hasError && styles.swapCorner_error, ); return ( @@ -160,13 +180,14 @@ function RichNumberInput({ {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
{children} - {cornerClassName &&
}
+ {cornerClassName &&
}
); } -function getParts(value: string, decimals: number) { +function getParts(value: string, decimals = DEFAULT_DECIMAL_PLACES) { const regex = getInputRegex(decimals); // Correct problem with numbers like 1e-8 if (value.includes('e-')) { @@ -195,7 +216,7 @@ export function getInputRegex(decimals: number) { return new RegExp(`^(\\d+)([.,])?(\\d{1,${decimals}})?`); } -function castValue(value?: number | string, decimals?: number) { +function castValue(value?: number | string, decimals = DEFAULT_DECIMAL_PLACES) { return value && Number.isFinite(Number(value)) ? round(value, decimals, Big.roundDown) : undefined; } diff --git a/src/components/ui/Switcher.tsx b/src/components/ui/Switcher.tsx index d13211e7..a3aa5d82 100644 --- a/src/components/ui/Switcher.tsx +++ b/src/components/ui/Switcher.tsx @@ -46,6 +46,7 @@ function Switcher({ checked={checked} className={styles.input} onChange={handleChange} + teactExperimentControlled={!onChange && !onCheck} /> diff --git a/src/components/ui/TabList.tsx b/src/components/ui/TabList.tsx index d637a001..22a0e76f 100644 --- a/src/components/ui/TabList.tsx +++ b/src/components/ui/TabList.tsx @@ -12,6 +12,7 @@ import Tab from './Tab'; import styles from './TabList.module.scss'; export type TabWithProperties = { + id: number; title: string; className?: string; }; @@ -74,7 +75,7 @@ function TabList({ previousActiveTab={previousActiveTab} className={tab?.className} onClick={onSwitchTab} - clickArg={i} + clickArg={tab.id} /> ))}
diff --git a/src/components/ui/Transition.scss b/src/components/ui/Transition.scss index a84969e2..0e04dff1 100644 --- a/src/components/ui/Transition.scss +++ b/src/components/ui/Transition.scss @@ -17,6 +17,11 @@ left: 0; } + &-from, + &-from .custom-scroll { + pointer-events: none !important; + } + &-inactive { display: none !important; // Best performance when animating container //transform: scale(0); // Shortest initial delay @@ -181,6 +186,44 @@ } } + &-slideFadeAndroid { + --background-color: var(--color-background-second); + + > .Transition_slide { + z-index: 0; + + background: var(--background-color); + } + + > .Transition_slide-to { + transform: translateX(1.5rem); + transform-origin: left; + + opacity: 0; + + animation: fade-in-opacity var(--slide-transition), slide-fade-in-move-android var(--slide-transition); + } + } + + &-slideFadeAndroidBackwards { + --background-color: var(--color-background-second); + + > .Transition_slide { + z-index: 0; + + background: var(--background-color); + } + + > .Transition_slide-from { + transform: translateX(0); + + opacity: 1; + + animation: fade-in-backwards-opacity var(--slide-transition), + slide-fade-in-backwards-move-android var(--slide-transition); + } + } + &-zoomFade { > .Transition_slide-from { transform: scale(1); @@ -198,7 +241,7 @@ // We can omit `transform: scale(1.1);` here because `opacity` is 0. // We need to for proper position calculation in `InfiniteScroll`. - animation: fade-in-opacity 0.15s ease, zoomFade-in-move 0.15s ease; + animation: fade-in-opacity 0.15s ease, zoom-fade-in-move 0.15s ease; } } @@ -206,13 +249,13 @@ > .Transition_slide-from { transform: scale(1); - animation: fade-in-backwards-opacity 0.1s ease, zoomFade-in-backwards-move 0.15s ease; + animation: fade-in-backwards-opacity 0.1s ease, zoom-fade-in-backwards-move 0.15s ease; } > .Transition_slide-to { transform: scale(0.95); - animation: fade-out-backwards-opacity 0.15s ease, zoomFade-out-backwards-move 0.15s ease; + animation: fade-out-backwards-opacity 0.15s ease, zoom-fade-out-backwards-move 0.15s ease; } } @@ -277,7 +320,7 @@ } > .Transition_slide-from { - animation: slide-layers-out var(--layer-transition); + animation: slide-layers-out var(--layer-transition-behind); } } @@ -293,9 +336,10 @@ > .Transition_slide-to { transform: translateX(-20%); - opacity: 0.75; + opacity: calc(1 - var(--layer-blackout-opacity)); - animation: slide-layers-out-backwards var(--layer-transition); + animation: slide-layers-out-backwards var(--layer-transition-behind); + animation-duration: 450ms; } > .Transition_slide-from { @@ -565,7 +609,25 @@ } } -@keyframes zoomFade-in-move { +@keyframes slide-fade-in-move-android { + 0% { + transform: translateX(20%); + } + 100% { + transform: translateX(0); + } +} + +@keyframes slide-fade-in-backwards-move-android { + 0% { + transform: translateX(0); + } + 100% { + transform: translateX(15%); + } +} + +@keyframes zoom-fade-in-move { 0% { transform: scale(1.1); } @@ -574,7 +636,7 @@ } } -@keyframes zoomFade-in-backwards-move { +@keyframes zoom-fade-in-backwards-move { 0% { transform: scale(1); } @@ -583,7 +645,7 @@ } } -@keyframes zoomFade-out-backwards-move { +@keyframes zoom-fade-out-backwards-move { 0% { transform: scale(0.95); } diff --git a/src/components/ui/Transition.tsx b/src/components/ui/Transition.tsx index de70af17..7ddda35f 100644 --- a/src/components/ui/Transition.tsx +++ b/src/components/ui/Transition.tsx @@ -1,15 +1,16 @@ import type { RefObject } from 'react'; import React, { useEffect, useLayoutEffect, useRef } from '../../lib/teact/teact'; -import { addExtraClass, removeExtraClass, toggleExtraClass } from '../../lib/teact/teact-dom'; +import { + addExtraClass, removeExtraClass, setExtraStyles, toggleExtraClass, +} from '../../lib/teact/teact-dom'; import { getGlobal } from '../../global'; -import type { GlobalState } from '../../global/types'; - import { ANIMATION_LEVEL_MIN } from '../../config'; import { requestForcedReflow, requestMutation } from '../../lib/fasterdom/fasterdom'; import buildClassName from '../../util/buildClassName'; import { waitForAnimationEnd, waitForTransitionEnd } from '../../util/cssAnimationEndListeners'; import forceReflow from '../../util/forceReflow'; +import { allowSwipeControlForTransition } from '../../util/swipeController'; import useForceUpdate from '../../hooks/useForceUpdate'; import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'; @@ -20,12 +21,13 @@ import './Transition.scss'; type AnimationName = ( 'none' | 'slide' | 'slideRtl' | 'slideFade' | 'zoomFade' | 'slideLayers' | 'fade' | 'pushSlide' | 'reveal' | 'slideOptimized' | 'slideOptimizedRtl' | 'semiFade' - | 'slideVertical' | 'slideVerticalFade' + | 'slideVertical' | 'slideVerticalFade' | 'slideFadeAndroid' ); -export type ChildrenFn = (isActive: boolean, isFrom: boolean, currentKey: number) => React.ReactNode; +export type ChildrenFn = (isActive: boolean, isFrom: boolean, currentKey: number, activeKey: number) => React.ReactNode; export type TransitionProps = { ref?: RefObject; activeKey: number; + prevKey?: number; nextKey?: number; name: AnimationName; direction?: 'auto' | 'inverse' | 1 | -1; @@ -39,6 +41,7 @@ export type TransitionProps = { id?: string; className?: string; slideClassName?: string; + withSwipeControl?: boolean; onStart?: NoneToVoidFunction; onStop?: NoneToVoidFunction; children: React.ReactNode | ChildrenFn; @@ -60,6 +63,7 @@ function Transition({ ref, activeKey, nextKey, + prevKey, name, direction = 'auto', renderCount, @@ -71,6 +75,7 @@ function Transition({ id, className, slideClassName, + withSwipeControl, onStart, onStop, children, @@ -78,6 +83,7 @@ function Transition({ const currentKeyRef = useRef(); // No need for a container to update on change const { animationLevel } = getGlobal().settings; + const shouldDisableAnimation = animationLevel === ANIMATION_LEVEL_MIN; // eslint-disable-next-line no-null/no-null let containerRef = useRef(null); @@ -88,14 +94,19 @@ function Transition({ const rendersRef = useRef>({}); const prevActiveKey = usePrevious(activeKey); const forceUpdate = useForceUpdate(); + const isAnimatingRef = useRef(false); + const isSwipeJustCancelledRef = useRef(false); - const activeKeyChanged = prevActiveKey !== undefined && activeKey !== prevActiveKey; + const hasActiveKeyChanged = prevActiveKey !== undefined && activeKey !== prevActiveKey; - if (!renderCount && activeKeyChanged) { + if (!renderCount && hasActiveKeyChanged) { rendersRef.current = { [prevActiveKey]: rendersRef.current[prevActiveKey] }; } rendersRef.current[activeKey] = children; + if (prevKey) { + rendersRef.current[prevKey] = children; + } if (nextKey) { rendersRef.current[nextKey] = children; } @@ -124,7 +135,6 @@ function Transition({ const keys = Object.keys(rendersRef.current).map(Number); const prevActiveIndex = renderCount ? prevActiveKey : keys.indexOf(prevActiveKey); const activeIndex = renderCount ? activeKey : keys.indexOf(activeKey); - const nextIndex = nextKey ? (renderCount ? nextKey : keys.indexOf(nextKey)) : -1; const childNodes = Array.from(container.childNodes); if (!childNodes.length) { @@ -140,21 +150,25 @@ function Transition({ } }); - if (!activeKeyChanged) { - const activeChild = childNodes[activeIndex]; - if (activeChild instanceof HTMLElement) { - addExtraClass(activeChild, CLASSES.active); - - if (isSlideOptimized) { - activeChild.style.transition = 'none'; - activeChild.style.transform = 'translate3d(0, 0, 0)'; - } + if (!hasActiveKeyChanged) { + if (isAnimatingRef.current) { + return; } - const nextChild = nextIndex !== -1 && nextIndex !== activeIndex && childNodes[nextIndex] as HTMLElement; - if (nextChild instanceof HTMLElement) { - addExtraClass(nextChild, CLASSES.inactive); - } + childElements.forEach((childElement) => { + if (childElement === childNodes[activeIndex]) { + addExtraClass(childElement, CLASSES.active); + + if (isSlideOptimized) { + setExtraStyles(childElement, { + transition: 'none', + transform: 'translate3d(0, 0, 0)', + }); + } + } else if (!isSlideOptimized) { + addExtraClass(childElement, CLASSES.inactive); + } + }); return; } @@ -162,13 +176,18 @@ function Transition({ currentKeyRef.current = activeKey; if (isSlideOptimized) { + if (!childNodes[activeIndex]) { + return; + } + performSlideOptimized( - animationLevel, + shouldDisableAnimation, name, isBackwards, cleanup, activeKey, currentKeyRef, + isAnimatingRef, container, childNodes[activeIndex], childNodes[prevActiveIndex], @@ -180,7 +199,11 @@ function Transition({ return; } - if (name === 'none' || animationLevel === ANIMATION_LEVEL_MIN) { + if (name === 'none' || shouldDisableAnimation || isSwipeJustCancelledRef.current) { + if (isSwipeJustCancelledRef.current) { + isSwipeJustCancelledRef.current = false; + } + childNodes.forEach((node, i) => { if (node instanceof HTMLElement) { removeExtraClass(node, CLASSES.from); @@ -204,6 +227,7 @@ function Transition({ } }); + isAnimatingRef.current = true; const dispatchHeavyAnimationStop = dispatchHeavyAnimationEvent(); onStart?.(); @@ -233,24 +257,41 @@ function Transition({ if (shouldRestoreHeight) { if (activeElement) { - activeElement.style.height = 'auto'; - container.style.height = `${clientHeight}px`; + setExtraStyles(activeElement, { height: 'auto' }); + setExtraStyles(container, { height: `${clientHeight}px` }); } } onStop?.(); dispatchHeavyAnimationStop(); + isAnimatingRef.current = false; cleanup(); }); } - const watchedNode = name === 'reveal' && isBackwards + const watchedNode = (name === 'reveal' || name === 'slideFadeAndroid') && isBackwards ? childNodes[prevActiveIndex] : childNodes[activeIndex]; if (watchedNode) { - waitForAnimationEnd(watchedNode, onAnimationEnd, undefined, FALLBACK_ANIMATION_END); + if (withSwipeControl && childNodes[prevActiveIndex]) { + const giveUpAnimationEnd = waitForAnimationEnd(watchedNode, onAnimationEnd); + + allowSwipeControlForTransition( + childNodes[prevActiveIndex] as HTMLElement, + childNodes[activeIndex] as HTMLElement, + () => { + giveUpAnimationEnd(); + isSwipeJustCancelledRef.current = true; + onStop?.(); + dispatchHeavyAnimationStop(); + isAnimatingRef.current = false; + }, + ); + } else { + waitForAnimationEnd(watchedNode, onAnimationEnd, undefined, FALLBACK_ANIMATION_END); + } } else { onAnimationEnd(); } @@ -258,7 +299,7 @@ function Transition({ activeKey, nextKey, prevActiveKey, - activeKeyChanged, + hasActiveKeyChanged, isBackwards, name, onStart, @@ -268,8 +309,9 @@ function Transition({ shouldCleanup, slideClassName, cleanupExceptionKey, - animationLevel, + shouldDisableAnimation, forceUpdate, + withSwipeControl, ]); useEffect(() => { @@ -290,9 +332,11 @@ function Transition({ } requestMutation(() => { - activeElement.style.height = 'auto'; - container.style.height = `${clientHeight}px`; - container.style.flexBasis = `${clientHeight}px`; + setExtraStyles(activeElement, { height: 'auto' }); + setExtraStyles(container, { + height: `${clientHeight}px`, + flexBasis: `${clientHeight}px`, + }); }); }, [shouldRestoreHeight, children]); @@ -305,15 +349,13 @@ function Transition({ return undefined; } - const rendered = typeof render === 'function' ? render(key === activeKey, key === prevActiveKey, key) : render; + const rendered = typeof render === 'function' + ? render(key === activeKey, key === prevActiveKey, key, activeKey) + : render; - return (shouldWrap && key !== wrapExceptionKey) || asFastList ? ( -
- {rendered} -
- ) : ( - rendered - ); + return (shouldWrap && key !== wrapExceptionKey) || asFastList + ?
{rendered}
+ : rendered; }); return ( @@ -331,12 +373,13 @@ function Transition({ export default Transition; function performSlideOptimized( - animationLevel: GlobalState['settings']['animationLevel'], + shouldDisableAnimation: boolean, name: 'slideOptimized' | 'slideOptimizedRtl', isBackwards: boolean, cleanup: NoneToVoidFunction, activeKey: number, currentKeyRef: { current: number | undefined }, + isAnimatingRef: { current: boolean | undefined }, container: HTMLElement, toSlide: ChildNode, fromSlide?: ChildNode, @@ -344,20 +387,24 @@ function performSlideOptimized( onStart?: NoneToVoidFunction, onStop?: NoneToVoidFunction, ) { - if (animationLevel === ANIMATION_LEVEL_MIN) { + if (shouldDisableAnimation) { toggleExtraClass(container, `Transition-${name}`, !isBackwards); toggleExtraClass(container, `Transition-${name}Backwards`, isBackwards); if (fromSlide instanceof HTMLElement) { - fromSlide.style.transition = 'none'; - fromSlide.style.transform = ''; removeExtraClass(fromSlide, CLASSES.active); + setExtraStyles(fromSlide, { + transition: 'none', + transform: '', + }); } if (toSlide instanceof HTMLElement) { - toSlide.style.transition = 'none'; - toSlide.style.transform = 'translate3d(0, 0, 0)'; addExtraClass(toSlide, CLASSES.active); + setExtraStyles(toSlide, { + transition: 'none', + transform: 'translate3d(0, 0, 0)', + }); } cleanup(); @@ -369,21 +416,25 @@ function performSlideOptimized( isBackwards = !isBackwards; } + isAnimatingRef.current = true; const dispatchHeavyAnimationStop = dispatchHeavyAnimationEvent(); - onStart?.(); toggleExtraClass(container, `Transition-${name}`, !isBackwards); toggleExtraClass(container, `Transition-${name}Backwards`, isBackwards); if (fromSlide instanceof HTMLElement) { - fromSlide.style.transition = 'none'; - fromSlide.style.transform = 'translate3d(0, 0, 0)'; + setExtraStyles(fromSlide, { + transition: 'none', + transform: 'translate3d(0, 0, 0)', + }); } if (toSlide instanceof HTMLElement) { - toSlide.style.transition = 'none'; - toSlide.style.transform = `translate3d(${isBackwards ? '-' : ''}100%, 0, 0)`; + setExtraStyles(toSlide, { + transition: 'none', + transform: `translate3d(${isBackwards ? '-' : ''}100%, 0, 0)`, + }); } requestForcedReflow(() => { @@ -393,15 +444,19 @@ function performSlideOptimized( return () => { if (fromSlide instanceof HTMLElement) { - fromSlide.style.transition = ''; - fromSlide.style.transform = `translate3d(${isBackwards ? '' : '-'}100%, 0, 0)`; removeExtraClass(fromSlide, CLASSES.active); + setExtraStyles(fromSlide, { + transition: '', + transform: `translate3d(${isBackwards ? '' : '-'}100%, 0, 0)`, + }); } if (toSlide instanceof HTMLElement) { - toSlide.style.transition = ''; - toSlide.style.transform = 'translate3d(0, 0, 0)'; addExtraClass(toSlide, CLASSES.active); + setExtraStyles(toSlide, { + transition: '', + transform: 'translate3d(0, 0, 0)', + }); } }; }); @@ -415,17 +470,21 @@ function performSlideOptimized( } if (fromSlide instanceof HTMLElement) { - fromSlide.style.transition = 'none'; - fromSlide.style.transform = ''; + setExtraStyles(fromSlide, { + transition: 'none', + transform: '', + }); } if (shouldRestoreHeight && clientHeight && toSlide instanceof HTMLElement) { - toSlide.style.height = 'auto'; - container.style.height = `${clientHeight}px`; + setExtraStyles(toSlide, { height: 'auto' }); + setExtraStyles(container, { height: `${clientHeight}px` }); } onStop?.(); dispatchHeavyAnimationStop(); + isAnimatingRef.current = false; + cleanup(); }); }); diff --git a/src/components/ui/helpers/animatedAssets.ts b/src/components/ui/helpers/animatedAssets.ts index b1125900..c281a779 100644 --- a/src/components/ui/helpers/animatedAssets.ts +++ b/src/components/ui/helpers/animatedAssets.ts @@ -1,5 +1,6 @@ import bill from '../../../assets/lottie/duck_bill.tgs'; import forge from '../../../assets/lottie/duck_forges.tgs'; +import guard from '../../../assets/lottie/duck_guard.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'; @@ -8,8 +9,10 @@ import snitch from '../../../assets/lottie/duck_snitch.tgs'; import thumbUp from '../../../assets/lottie/duck_thumb.tgs'; import holdTon from '../../../assets/lottie/duck_ton.tgs'; import wait from '../../../assets/lottie/duck_wait.tgs'; +import yeee from '../../../assets/lottie/duck_yeee.tgs'; import billPreview from '../../../assets/lottiePreview/duck_bill.png'; import forgePreview from '../../../assets/lottiePreview/duck_forges.png'; +import guardPreview from '../../../assets/lottiePreview/duck_guard.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'; @@ -18,6 +21,7 @@ import snitchPreview from '../../../assets/lottiePreview/duck_snitch.png'; import thumbUpPreview from '../../../assets/lottiePreview/duck_thumb.png'; import holdTonPreview from '../../../assets/lottiePreview/duck_ton.png'; import waitPreview from '../../../assets/lottiePreview/duck_wait.png'; +import yeeePreview from '../../../assets/lottiePreview/duck_yeee.png'; export const ANIMATED_STICKERS_PATHS = { hello, @@ -30,6 +34,8 @@ export const ANIMATED_STICKERS_PATHS = { forge, wait, run, + yeee, + guard, helloPreview, snitchPreview, billPreview, @@ -40,4 +46,6 @@ export const ANIMATED_STICKERS_PATHS = { forgePreview, waitPreview, runPreview, + yeeePreview, + guardPreview, }; diff --git a/src/components/ui/helpers/assetLogos.ts b/src/components/ui/helpers/assetLogos.ts index 0198f964..17213020 100644 --- a/src/components/ui/helpers/assetLogos.ts +++ b/src/components/ui/helpers/assetLogos.ts @@ -1,5 +1,5 @@ -import btc from '../../../assets/coins/btc.png'; -import ton from '../../../assets/coins/ton.png'; +import btc from '../../../assets/coins/coin_btc.png'; +import ton from '../../../assets/coins/coin_ton.png'; export const ASSET_LOGO_PATHS = { ton, diff --git a/src/config.ts b/src/config.ts index 28b751a2..bf498965 100644 --- a/src/config.ts +++ b/src/config.ts @@ -12,17 +12,24 @@ export const IS_TEST = APP_ENV === 'test'; export const IS_PERF = APP_ENV === 'perf'; export const IS_EXTENSION = process.env.IS_EXTENSION === '1'; export const IS_FIREFOX_EXTENSION = process.env.IS_FIREFOX_EXTENSION === '1'; -export const IS_ELECTRON = process.env.IS_ELECTRON === '1'; -export const IS_DAPP_SUPPORTED = IS_EXTENSION || IS_ELECTRON; -export const IS_SSE_SUPPORTED = IS_ELECTRON; +export const IS_ELECTRON_BUILD = process.env.IS_ELECTRON_BUILD === '1'; +export const IS_CAPACITOR = process.env.IS_CAPACITOR === '1'; +export const IS_DAPP_SUPPORTED = IS_EXTENSION || IS_ELECTRON_BUILD || IS_CAPACITOR; +export const IS_SSE_SUPPORTED = IS_ELECTRON_BUILD || IS_CAPACITOR; export const ELECTRON_HOST_URL = 'https://dumb-host'; export const INACTIVE_MARKER = '[Inactive]'; +export const PRODUCTION_URL = 'https://mytonwallet.app'; +export const BASE_URL = process.env.BASE_URL; -export const STRICTERDOM_ENABLED = DEBUG && !IS_ELECTRON; +export const STRICTERDOM_ENABLED = DEBUG && !IS_ELECTRON_BUILD; export const DEBUG_ALERT_MSG = 'Shoot!\nSomething went wrong, please see the error details in Dev Tools Console.'; +export const PIN_LENGTH = 4; +export const NATIVE_BIOMETRICS_USERNAME = 'MyTonWallet'; +export const NATIVE_BIOMETRICS_SERVER = 'https://mytonwallet.app'; + export const MNEMONIC_COUNT = 24; export const MNEMONIC_CHECK_COUNT = 3; @@ -35,8 +42,8 @@ export const ANIMATED_STICKER_SMALL_SIZE_PX = 110; export const ANIMATED_STICKER_MIDDLE_SIZE_PX = 120; export const ANIMATED_STICKER_DEFAULT_PX = 150; export const ANIMATED_STICKER_BIG_SIZE_PX = 156; +export const ANIMATED_STICKER_HUGE_SIZE_PX = 192; -export const DEFAULT_PRICE_CURRENCY = '$'; export const TON_SYMBOL = 'TON'; export const DEFAULT_LANDSCAPE_ACTION_TAB_ID = 1; @@ -45,22 +52,6 @@ export const DEFAULT_DECIMAL_PLACES = 9; export const DEFAULT_SLIPPAGE_VALUE = 0.5; -export const TOKEN_INFO = { - toncoin: { - name: 'Toncoin', - symbol: TON_SYMBOL, - slug: 'toncoin', - quote: { - price: 1.95, - percentChange1h: 0, - percentChange24h: 0, - percentChange7d: 0, - percentChange30d: 0, - }, - decimals: DEFAULT_DECIMAL_PLACES, - }, -}; - export const GLOBAL_STATE_CACHE_DISABLED = false; export const GLOBAL_STATE_CACHE_KEY = 'mytonwallet-global-state'; @@ -72,13 +63,22 @@ export const THEME_DEFAULT = 'system'; export const MAIN_ACCOUNT_ID = '0-ton-mainnet'; -export const TONHTTPAPI_MAINNET_URL = process.env.TONHTTPAPI_MAINNET_URL || 'https://toncenter.com/api/v2/jsonRPC'; -export const TONHTTPAPI_MAINNET_API_KEY = (IS_ELECTRON && process.env.ELECTRON_TONHTTPAPI_MAINNET_API_KEY) +export const TONHTTPAPI_MAINNET_URL = process.env.TONHTTPAPI_MAINNET_URL + || 'https://tonhttpapi.mytonwallet.org/api/v2/jsonRPC'; +export const TONHTTPAPI_MAINNET_API_KEY = (IS_ELECTRON_BUILD && process.env.ELECTRON_TONHTTPAPI_MAINNET_API_KEY) || process.env.TONHTTPAPI_MAINNET_API_KEY; +export const TONINDEXER_MAINNET_URL = process.env.TONINDEXER_MAINNET_URL + || 'https://tonhttpapi.mytonwallet.org/api/v3'; +export const TONAPIIO_MAINNET_URL = process.env.TONAPIIO_MAINNET_URL || 'https://tonapiio.mytonwallet.org'; + export const TONHTTPAPI_TESTNET_URL = process.env.TONHTTPAPI_TESTNET_URL - || 'https://testnet.toncenter.com/api/v2/jsonRPC'; -export const TONHTTPAPI_TESTNET_API_KEY = (IS_ELECTRON && process.env.ELECTRON_TONHTTPAPI_TESTNET_API_KEY) + || 'https://tonhttpapi-testnet.mytonwallet.org/api/v2/jsonRPC'; +export const TONHTTPAPI_TESTNET_API_KEY = (IS_ELECTRON_BUILD && process.env.ELECTRON_TONHTTPAPI_TESTNET_API_KEY) || process.env.TONHTTPAPI_TESTNET_API_KEY; +export const TONINDEXER_TESTNET_URL = process.env.TONINDEXER_TESTNET_URL + || 'https://tonhttpapi-testnet.mytonwallet.org/api/v3'; +export const TONAPIIO_TESTNET_URL = process.env.TONAPIIO_TESTNET_URL || 'https://tonapiio-testnet.mytonwallet.org'; + export const BRILLIANT_API_BASE_URL = process.env.BRILLIANT_API_BASE_URL || 'https://mytonwallet-api.herokuapp.com'; export const FRACTION_DIGITS = 9; @@ -91,14 +91,22 @@ export const TONSCAN_BASE_TESTNET_URL = 'https://testnet.tonscan.org/'; export const GETGEMS_BASE_MAINNET_URL = 'https://getgems.io/'; export const GETGEMS_BASE_TESTNET_URL = 'https://testnet.getgems.io/'; +export const CHANGELLY_SUPPORT_EMAIL = 'support@changelly.com'; +export const CHANGELLY_SECURITY_EMAIL = 'security@changelly.com'; +export const CHANGELLY_TERMS_OF_USE = 'https://changelly.com/terms-of-use'; +export const CHANGELLY_PRIVACY_POLICY = 'https://changelly.com/privacy-policy'; +export const CHANGELLY_AML_KYC = 'https://changelly.com/aml-kyc'; +export const CHANGELLY_WAITING_DEADLINE = 3 * 60 * 60 * 1000; // 3 hour + export const TON_TOKEN_SLUG = 'toncoin'; export const JWBTC_TOKEN_SLUG = 'ton-eqdcbkghmc'; +export const JUSDT_TOKEN_SLUG = 'ton-eqbynbo23y'; export const PROXY_HOSTS = process.env.PROXY_HOSTS; export const TINY_TRANSFER_MAX_COST = 0.01; -export const LANG_CACHE_NAME = 'mtw-lang-31'; +export const LANG_CACHE_NAME = 'mtw-lang-53'; export const LANG_LIST: LangItem[] = [{ langCode: 'en', @@ -127,7 +135,72 @@ export const LANG_LIST: LangItem[] = [{ rtl: false, }]; -export const STAKING_CYCLE_DURATION_MS = 129600000; // 36 hours +export const STAKING_CYCLE_DURATION_MS = 131072000; // 36.4 hours export const MIN_BALANCE_FOR_UNSTAKE = 1.02; +export const STAKING_FORWARD_AMOUNT = 1; +export const DEFAULT_FEE = 0.01; export const STAKING_POOLS = process.env.STAKING_POOLS ? process.env.STAKING_POOLS.split(' ') : []; +export const LIQUID_POOL = process.env.LIQUID_POOL || 'EQD2_4d91M4TVbEBVyBF8J1UwpMJc361LKVCz6bBlffMW05o'; +export const LIQUID_JETTON = process.env.LIQUID_JETTON || 'EQCqC6EhRJ_tpWngKxL6dV0k6DSnRUrs9GSVkLbfdCqsj6TE'; +export const STAKING_MIN_AMOUNT = 1; + +export const TON_PROTOCOL = 'ton://'; +export const TONCONNECT_PROTOCOL = 'tc://'; +export const TONCONNECT_UNIVERSAL_URL = 'https://connect.mytonwallet.org'; + +export const TOKEN_INFO = { + toncoin: { + name: 'Toncoin', + symbol: TON_SYMBOL, + slug: TON_TOKEN_SLUG, + cmcSlug: TON_TOKEN_SLUG, + quote: { + price: 1.95, + percentChange1h: 0, + percentChange24h: 0, + percentChange7d: 0, + percentChange30d: 0, + }, + decimals: DEFAULT_DECIMAL_PLACES, + }, +}; + +export const TON_BLOCKCHAIN = 'ton'; + +export const INIT_SWAP_ASSETS = { + toncoin: { + name: 'Toncoin', + symbol: TON_SYMBOL, + blockchain: TON_BLOCKCHAIN, + slug: TON_TOKEN_SLUG, + decimals: DEFAULT_DECIMAL_PLACES, + }, + 'ton-eqdcbkghmc': { + name: 'jWBTC', + symbol: 'jWBTC', + blockchain: TON_BLOCKCHAIN, + slug: 'ton-eqdcbkghmc', + decimals: 8, + // eslint-disable-next-line max-len + image: 'https://cache.tonapi.io/imgproxy/LaFKdzahVX9epWT067gyVLd8aCa1lFrZd7Rp9siViEE/rs:fill:200:200:1/g:no/aHR0cHM6Ly9icmlkZ2UudG9uLm9yZy90b2tlbi8xLzB4MjI2MGZhYzVlNTU0MmE3NzNhYTQ0ZmJjZmVkZjdjMTkzYmMyYzU5OS5wbmc.webp', + contract: 'EQDcBkGHmC4pTf34x3Gm05XvepO5w60DNxZ-XT4I6-UGG5L5', + keywords: ['bitcoin'], + }, +}; + +export const MULTITAB_DATA_CHANNEL_NAME = 'mtw-multitab'; +export const ACTIVE_TAB_STORAGE_KEY = 'mtw-active-tab'; + +export const INDEXED_DB_NAME = 'keyval-store'; +export const INDEXED_DB_STORE_NAME = 'keyval'; + +export const MIN_ASSETS_TAB_VIEW = 5; + +export const DEFAULT_PRICE_CURRENCY = 'USD'; +export const SHORT_CURRENCY_SYMBOL_MAP = { + USD: '$', + EUR: '€', + RUB: '₽', + CNY: '¥', +}; diff --git a/src/electron/autoUpdates.ts b/src/electron/autoUpdates.ts index e8162edd..7dd5cbc6 100644 --- a/src/electron/autoUpdates.ts +++ b/src/electron/autoUpdates.ts @@ -1,43 +1,55 @@ -import { ipcMain } from 'electron'; +import { app, ipcMain, net } from 'electron'; import type { ProgressInfo, UpdateInfo } from 'electron-updater'; import { autoUpdater, CancellationToken } from 'electron-updater'; import { ElectronAction, ElectronEvent } from './types'; -import { forceQuit, IS_MAC_OS, mainWindow } from './utils'; +import { PRODUCTION_URL } from '../config'; +import getIsAppUpdateNeeded from '../util/getIsAppUpdateNeeded'; +import { pause } from '../util/schedulers'; +import { + forceQuit, IS_MAC_OS, IS_PREVIEW, IS_WINDOWS, mainWindow, store, +} from './utils'; +export const AUTO_UPDATE_SETTING_KEY = 'autoUpdate'; + +const ELECTRON_APP_VERSION_URL = 'electronVersion.txt'; const CHECK_UPDATE_INTERVAL = 5 * 60 * 1000; let cancellationToken: CancellationToken = new CancellationToken(); -let interval: NodeJS.Timer; +let isUpdateCheckStarted = false; export function setupAutoUpdates() { - if (!interval) { - autoUpdater.autoDownload = true; - autoUpdater.autoInstallOnAppQuit = true; - autoUpdater.checkForUpdates(); + if (isUpdateCheckStarted) { + return; + } - interval = setInterval(() => autoUpdater.checkForUpdates(), CHECK_UPDATE_INTERVAL); + isUpdateCheckStarted = true; + autoUpdater.autoDownload = true; + autoUpdater.autoInstallOnAppQuit = true; - ipcMain.handle(ElectronAction.DOWNLOAD_UPDATE, () => { - autoUpdater.downloadUpdate(cancellationToken).catch((error) => { - mainWindow.webContents.send(ElectronEvent.UPDATE_ERROR, error); - }); - }); - ipcMain.handle(ElectronAction.CANCEL_UPDATE, () => { - cancellationToken.cancel(); - cancellationToken = new CancellationToken(); - }); - ipcMain.handle(ElectronAction.INSTALL_UPDATE, () => { - if (IS_MAC_OS) { - forceQuit.enable(); - } + checkForUpdates(); - return autoUpdater.quitAndInstall(); + ipcMain.handle(ElectronAction.DOWNLOAD_UPDATE, () => { + autoUpdater.downloadUpdate(cancellationToken).catch((error) => { + mainWindow.webContents.send(ElectronEvent.UPDATE_ERROR, error); }); - } + }); + ipcMain.handle(ElectronAction.INSTALL_UPDATE, () => { + if (IS_MAC_OS || IS_WINDOWS) { + forceQuit.enable(); + } + + return autoUpdater.quitAndInstall(); + }); + ipcMain.handle(ElectronAction.CANCEL_UPDATE, () => { + cancellationToken.cancel(); + cancellationToken = new CancellationToken(); + }); - autoUpdater.on('error', (error: Error) => mainWindow.webContents.send(ElectronEvent.UPDATE_ERROR, error)); + autoUpdater.on('error', (error: Error) => { + mainWindow.webContents.send(ElectronEvent.UPDATE_ERROR, error); + }); autoUpdater.on('update-available', (info: UpdateInfo) => { mainWindow.webContents.send(ElectronEvent.UPDATE_AVAILABLE, info); }); @@ -48,3 +60,51 @@ export function setupAutoUpdates() { mainWindow.webContents.send(ElectronEvent.UPDATE_DOWNLOADED, info); }); } + +export function getIsAutoUpdateEnabled() { + return !IS_PREVIEW && store.get(AUTO_UPDATE_SETTING_KEY); +} + +async function checkForUpdates(): Promise { + while (true) { // eslint-disable-line no-constant-condition + if (await shouldPerformAutoUpdate()) { + if (getIsAutoUpdateEnabled()) { + autoUpdater.checkForUpdates(); + + return; + } + + mainWindow.webContents.send(ElectronEvent.UPDATE_DOWNLOADED); + } + + await pause(CHECK_UPDATE_INTERVAL); + } +} + +function shouldPerformAutoUpdate(): Promise { + return new Promise((resolve) => { + const request = net.request(`${PRODUCTION_URL}/${ELECTRON_APP_VERSION_URL}?${Date.now()}`); + + request.on('response', (response) => { + let contents = ''; + + response.on('end', () => { + resolve(getIsAppUpdateNeeded(contents, app.getVersion())); + }); + + response.on('data', (data: Buffer) => { + contents = `${contents}${String(data)}`; + }); + + response.on('error', () => { + resolve(false); + }); + }); + + request.on('error', () => { + resolve(false); + }); + + request.end(); + }); +} diff --git a/src/electron/config.yml b/src/electron/config.yml index 58c68bdd..97d36277 100644 --- a/src/electron/config.yml +++ b/src/electron/config.yml @@ -7,6 +7,7 @@ extraMetadata: files: - "dist" - "package.json" + - "public/icon-electron-windows.ico" - "!dist/get" - "!dist/**/statoscope-build-statistics.json" - "!dist/**/statoscope-report.html" diff --git a/src/electron/deeplink.ts b/src/electron/deeplink.ts index 95bde6f3..7ce36d79 100644 --- a/src/electron/deeplink.ts +++ b/src/electron/deeplink.ts @@ -67,6 +67,10 @@ export function initDeeplink() { processDeeplink(); if (mainWindow) { + if (!mainWindow.isVisible()) { + mainWindow.show(); + } + if (mainWindow.isMinimized()) { mainWindow.restore(); } @@ -82,11 +86,8 @@ export function processDeeplink() { } if (isTonTransferDeeplink(deeplinkUrl)) { - const parsed = new URL(deeplinkUrl); mainWindow.webContents.send(ElectronEvent.DEEPLINK, { - to: parsed.pathname.replace(/^.*\//g, ''), - amount: Number(parsed.searchParams.get('amount')), - text: parsed.searchParams.get('text'), + url: deeplinkUrl, }); } else if (isTonConnectDeeplink(deeplinkUrl)) { mainWindow.webContents.send(ElectronEvent.DEEPLINK_TONCONNECT, { diff --git a/src/electron/main.ts b/src/electron/main.ts index bde4f2ed..689bf4df 100644 --- a/src/electron/main.ts +++ b/src/electron/main.ts @@ -5,8 +5,9 @@ import contextMenu from 'electron-context-menu'; import path from 'path'; import { initDeeplink } from './deeplink'; +import { setupSecrets } from './secrets'; import { IS_MAC_OS } from './utils'; -import { createWindow, setupCloseHandlers } from './window'; +import { createWindow, setupCloseHandlers, setupElectronActionHandlers } from './window'; initDeeplink(); @@ -24,5 +25,7 @@ app.on('ready', () => { } createWindow(); + setupElectronActionHandlers(); setupCloseHandlers(); + setupSecrets(); }); diff --git a/src/electron/preload.ts b/src/electron/preload.ts index 4cab3a56..6c3cd5f1 100644 --- a/src/electron/preload.ts +++ b/src/electron/preload.ts @@ -18,6 +18,17 @@ const electronApi: ElectronApi = { toggleDeeplinkHandler: (isEnabled: boolean) => ipcRenderer.invoke(ElectronAction.TOGGLE_DEEPLINK_HANDLER, isEnabled), + getIsTouchIdSupported: () => ipcRenderer.invoke(ElectronAction.GET_IS_TOUCH_ID_SUPPORTED), + encryptPassword: (password: string) => ipcRenderer.invoke(ElectronAction.ENCRYPT_PASSWORD, password), + decryptPassword: (encrypted: string) => ipcRenderer.invoke(ElectronAction.DECRYPT_PASSWORD, encrypted), + + setIsTrayIconEnabled: (value: boolean) => ipcRenderer.invoke(ElectronAction.SET_IS_TRAY_ICON_ENABLED, value), + getIsTrayIconEnabled: () => ipcRenderer.invoke(ElectronAction.GET_IS_TRAY_ICON_ENABLED), + setIsAutoUpdateEnabled: (value: boolean) => ipcRenderer.invoke(ElectronAction.SET_IS_AUTO_UPDATE_ENABLED, value), + getIsAutoUpdateEnabled: () => ipcRenderer.invoke(ElectronAction.GET_IS_AUTO_UPDATE_ENABLED), + + restoreStorage: () => ipcRenderer.invoke(ElectronAction.RESTORE_STORAGE), + on: (eventName: ElectronEvent, callback) => { const subscription = (event: IpcRendererEvent, ...args: any) => callback(...args); diff --git a/src/electron/secrets.ts b/src/electron/secrets.ts new file mode 100644 index 00000000..38147859 --- /dev/null +++ b/src/electron/secrets.ts @@ -0,0 +1,20 @@ +import { ipcMain, safeStorage, systemPreferences } from 'electron'; + +import { ElectronAction } from './types'; + +export function setupSecrets() { + ipcMain.handle(ElectronAction.GET_IS_TOUCH_ID_SUPPORTED, () => { + return safeStorage.isEncryptionAvailable() && systemPreferences.canPromptTouchID(); + }); + ipcMain.handle(ElectronAction.ENCRYPT_PASSWORD, (e, password: string) => { + return safeStorage.encryptString(password).toString('base64'); + }); + ipcMain.handle(ElectronAction.DECRYPT_PASSWORD, async (e, encrypted: string) => { + try { + await systemPreferences.promptTouchID('confirm an operation in MyTonWallet'); + return safeStorage.decryptString(Buffer.from(encrypted, 'base64')); + } catch (err) { + return undefined; + } + }); +} diff --git a/src/electron/storageUtils.ts b/src/electron/storageUtils.ts new file mode 100644 index 00000000..879c6622 --- /dev/null +++ b/src/electron/storageUtils.ts @@ -0,0 +1,152 @@ +import type { StorageKey } from '../api/storages/types'; + +import { ACTIVE_TAB_STORAGE_KEY, INDEXED_DB_NAME, INDEXED_DB_STORE_NAME } from '../config'; +import { checkIsWebContentsUrlAllowed, mainWindow } from './utils'; + +let localStorage: Record | undefined; +let idb: { key: StorageKey; value: any }[] | undefined; + +export function captureStorage(): Promise<[void, void]> { + return Promise.all([captureLocalStorage(), captureIdb()]); +} + +export function restoreStorage(): Promise<[void, void]> { + return Promise.all([restoreLocalStorage(), restoreIdb()]); +} + +async function captureLocalStorage(): Promise { + const contents = mainWindow.webContents; + const contentsUrl = contents.getURL(); + + if (!checkIsWebContentsUrlAllowed(contentsUrl)) { + return; + } + + localStorage = await contents.executeJavaScript('({ ...localStorage });'); +} + +async function captureIdb(): Promise { + const contents = mainWindow.webContents; + const contentsUrl = contents.getURL(); + + if (!checkIsWebContentsUrlAllowed(contentsUrl)) { + return; + } + + idb = await contents.executeJavaScript(` + new Promise((resolve) => { + const request = window.indexedDB.open('${INDEXED_DB_NAME}'); + + request.onupgradeneeded = (event) => { + event.target.transaction.abort(); + resolve(); + } + + request.onsuccess = (event) => { + const result = []; + + const db = event.target.result; + const transaction = db.transaction(['${INDEXED_DB_STORE_NAME}'], 'readonly'); + const store = transaction.objectStore('${INDEXED_DB_STORE_NAME}'); + + store.openCursor().onsuccess = (e) => { + const cursor = e.target.result; + if (cursor) { + result.push({ key: cursor.key, value: cursor.value }); + cursor.continue(); + } else { + resolve(result); + } + }; + + transaction.oncomplete = () => { + db.close(); + }; + + transaction.onerror = () => { + resolve(); + }; + } + + request.onerror = () => { + resolve(); + }; + }); + `); +} + +export async function restoreLocalStorage(): Promise { + if (!localStorage) { + return; + } + + const contents = mainWindow.webContents; + const contentsUrl = contents.getURL(); + + if (!checkIsWebContentsUrlAllowed(contentsUrl)) { + return; + } + + await contents.executeJavaScript( + Object.keys(localStorage) + .filter((key: string) => key !== ACTIVE_TAB_STORAGE_KEY) + .map((key: string) => `localStorage.setItem('${key}', JSON.stringify(${localStorage![key]}))`) + .join(';'), + ); + + localStorage = undefined; +} + +export async function restoreIdb(): Promise { + if (!idb) { + return; + } + + const contents = mainWindow.webContents; + const contentsUrl = contents.getURL(); + + if (!checkIsWebContentsUrlAllowed(contentsUrl)) { + return; + } + + await contents.executeJavaScript(` + new Promise((resolve) => { + const request = window.indexedDB.open('${INDEXED_DB_NAME}'); + + request.onupgradeneeded = (event) => { + const db = event.target.result; + + if (!db.objectStoreNames.contains('${INDEXED_DB_STORE_NAME}')) { + db.createObjectStore('${INDEXED_DB_STORE_NAME}'); + } + } + + request.onsuccess = (event) => { + const result = {}; + + const db = event.target.result; + const transaction = db.transaction(['${INDEXED_DB_STORE_NAME}'], 'readwrite'); + const store = transaction.objectStore('${INDEXED_DB_STORE_NAME}'); + + ${JSON.stringify(idb)}.forEach(item => { + store.put(item.value, item.key); + }); + + transaction.oncomplete = () => { + db.close(); + resolve(); + }; + + transaction.onerror = () => { + resolve(); + }; + } + + request.onerror = () => { + resolve(); + }; + }); + `); + + idb = undefined; +} diff --git a/src/electron/tray.ts b/src/electron/tray.ts new file mode 100644 index 00000000..1d385ec8 --- /dev/null +++ b/src/electron/tray.ts @@ -0,0 +1,123 @@ +import type { BrowserWindow } from 'electron'; +import { + app, Menu, nativeImage, Tray, +} from 'electron'; +import path from 'path'; + +import { forceQuit, mainWindow, store } from './utils'; + +const TRAY_ICON_SETTINGS_KEY = 'trayIcon'; +const WINDOW_BLUR_TIMEOUT = 800; + +interface TrayHelper { + instance?: Tray; + lastFocusedWindow?: BrowserWindow; + lastFocusedWindowTimer?: NodeJS.Timeout; + handleWindowFocus: () => void; + handleWindowBlur: () => void; + handleWindowClose: () => void; + setupListeners: () => void; + removeListeners: () => void; + create: () => void; + enable: () => void; + disable: () => void; + isEnabled: boolean; +} + +const tray: TrayHelper = { + handleWindowFocus() { + clearTimeout(this.lastFocusedWindowTimer as unknown as NodeJS.Timeout); + this.lastFocusedWindow = mainWindow; + }, + + handleWindowBlur() { + this.lastFocusedWindowTimer = setTimeout(() => { + if (this.lastFocusedWindow === mainWindow) { + this.lastFocusedWindow = undefined; + } + }, WINDOW_BLUR_TIMEOUT); + }, + + handleWindowClose() { + this.lastFocusedWindow = undefined; + }, + + setupListeners() { + this.handleWindowFocus = this.handleWindowFocus.bind(this); + this.handleWindowBlur = this.handleWindowBlur.bind(this); + this.handleWindowClose = this.handleWindowClose.bind(this); + + mainWindow.on('focus', this.handleWindowFocus); + mainWindow.on('blur', this.handleWindowBlur); + mainWindow.on('close', this.handleWindowClose); + }, + + removeListeners() { + mainWindow.removeListener('focus', this.handleWindowFocus); + mainWindow.removeListener('blur', this.handleWindowBlur); + mainWindow.removeListener('close', this.handleWindowClose); + }, + + create() { + if (this.instance) { + return; + } + + this.setupListeners(); + + const icon = nativeImage.createFromPath(path.resolve(__dirname, '../public/icon-electron-windows.ico')); + const title = app.getName(); + + this.instance = new Tray(icon); + + const handleOpenFromTray = () => { + if (!mainWindow.isVisible()) { + mainWindow.show(); + } else { + mainWindow.focus(); + } + }; + + const handleCloseFromTray = () => { + forceQuit.enable(); + app.quit(); + }; + + const handleTrayClick = () => { + if (this.lastFocusedWindow) { + mainWindow.hide(); + this.lastFocusedWindow = undefined; + } else { + handleOpenFromTray(); + } + }; + + const contextMenu = Menu.buildFromTemplate([ + { label: `Open ${title}`, click: handleOpenFromTray }, + { label: `Quit ${title}`, click: handleCloseFromTray }, + ]); + + this.instance.on('click', handleTrayClick); + this.instance.setContextMenu(contextMenu); + this.instance.setToolTip(title); + this.instance.setTitle(title); + }, + + enable() { + store.set(TRAY_ICON_SETTINGS_KEY, true); + this.create(); + }, + + disable() { + store.set(TRAY_ICON_SETTINGS_KEY, false); + this.instance?.destroy(); + this.instance = undefined; + this.removeListeners(); + }, + + get isEnabled(): boolean { + return store.get(TRAY_ICON_SETTINGS_KEY, true) as boolean; + }, +}; + +export default tray; diff --git a/src/electron/types.ts b/src/electron/types.ts index 2b9e32c5..86627327 100644 --- a/src/electron/types.ts +++ b/src/electron/types.ts @@ -20,6 +20,17 @@ export enum ElectronAction { HANDLE_DOUBLE_CLICK = 'handle-double-click', TOGGLE_DEEPLINK_HANDLER = 'toggle-deeplink-handler', + + GET_IS_TOUCH_ID_SUPPORTED = 'get-is-touch-id-supported', + ENCRYPT_PASSWORD = 'encrypt-password', + DECRYPT_PASSWORD = 'decrypt-password', + + SET_IS_TRAY_ICON_ENABLED = 'set-is-tray-icon-enabled', + GET_IS_TRAY_ICON_ENABLED = 'get-is-tray-icon-enabled', + SET_IS_AUTO_UPDATE_ENABLED = 'set-is-auto-update-enabled', + GET_IS_AUTO_UPDATE_ENABLED = 'get-is-auto-update-enabled', + + RESTORE_STORAGE = 'restore-storage', } export interface ElectronApi { @@ -36,6 +47,17 @@ export interface ElectronApi { toggleDeeplinkHandler: (isEnabled: boolean) => Promise; + getIsTouchIdSupported: () => Promise; + encryptPassword: (password: string) => Promise; + decryptPassword: (encrypted: string) => Promise; + + setIsTrayIconEnabled: (value: boolean) => Promise; + getIsTrayIconEnabled: () => Promise; + setIsAutoUpdateEnabled: (value: boolean) => Promise; + getIsAutoUpdateEnabled: () => Promise; + + restoreStorage: () => Promise; + on: (eventName: ElectronEvent, callback: any) => VoidFunction; } diff --git a/src/electron/utils.ts b/src/electron/utils.ts index 28f0e8df..835cff7e 100644 --- a/src/electron/utils.ts +++ b/src/electron/utils.ts @@ -1,10 +1,37 @@ import type { BrowserWindow } from 'electron'; +import { app } from 'electron'; +import Store from 'electron-store'; +import fs from 'fs'; + +import { BASE_URL, PRODUCTION_URL } from '../config'; + +const ALLOWED_URL_ORIGINS = [BASE_URL!, PRODUCTION_URL].map((url) => (new URL(url).origin)); + +export function checkIsWebContentsUrlAllowed(url: string): boolean { + if (!app.isPackaged) { + return true; + } + + const parsedUrl = new URL(url); + + if (parsedUrl.pathname === encodeURI(`${__dirname}/index.html`)) { + return true; + } + + return ALLOWED_URL_ORIGINS.includes(parsedUrl.origin); +} + +export const WINDOW_STATE_FILE = 'window-state.json'; export const IS_MAC_OS = process.platform === 'darwin'; export const IS_WINDOWS = process.platform === 'win32'; export const IS_LINUX = process.platform === 'linux'; +export const IS_PREVIEW = process.env.IS_PREVIEW === 'true'; +export const IS_FIRST_RUN = !fs.existsSync(`${app.getPath('userData')}/${WINDOW_STATE_FILE}`); export let mainWindow: BrowserWindow; // eslint-disable-line import/no-mutable-exports +export const store: Store = new Store(); + export function setMainWindow(window: BrowserWindow) { mainWindow = window; } diff --git a/src/electron/window.ts b/src/electron/window.ts index 253a8502..09c0148c 100644 --- a/src/electron/window.ts +++ b/src/electron/window.ts @@ -6,16 +6,21 @@ import path from 'path'; import { ElectronAction } from './types'; -import { setupAutoUpdates } from './autoUpdates'; +import { APP_ENV, BASE_URL } from '../config'; +import { AUTO_UPDATE_SETTING_KEY, getIsAutoUpdateEnabled, setupAutoUpdates } from './autoUpdates'; import { processDeeplink } from './deeplink'; +import { captureStorage, restoreStorage } from './storageUtils'; +import tray from './tray'; import { - forceQuit, IS_MAC_OS, mainWindow, setMainWindow, + checkIsWebContentsUrlAllowed, forceQuit, IS_FIRST_RUN, IS_MAC_OS, IS_PREVIEW, IS_WINDOWS, + mainWindow, setMainWindow, store, WINDOW_STATE_FILE, } from './utils'; const ALLOWED_DEVICE_ORIGINS = ['http://localhost:4321', 'file://']; export function createWindow() { const windowState = windowStateKeeper({ + file: WINDOW_STATE_FILE, defaultWidth: 980, defaultHeight: 788, }); @@ -31,7 +36,7 @@ export function createWindow() { title: 'MyTonWallet', webPreferences: { preload: path.join(__dirname, 'preload.js'), - devTools: process.env.APP_ENV !== 'production', + devTools: APP_ENV !== 'production', }, titleBarStyle: 'hidden', ...(IS_MAC_OS && { @@ -56,17 +61,54 @@ export function createWindow() { return deviceType === 'hid' && ALLOWED_DEVICE_ORIGINS.includes(origin); }); - if (app.isPackaged) { - mainWindow.loadURL(`file://${__dirname}/index.html`); - } else { - mainWindow.loadURL('http://localhost:4321'); - mainWindow.webContents.openDevTools(); - } + window.webContents.on('will-navigate', (event, newUrl) => { + if (!checkIsWebContentsUrlAllowed(newUrl)) { + event.preventDefault(); + } + }); if (!IS_MAC_OS) { setupWindowsTitleBar(); } + if (IS_WINDOWS && tray.isEnabled) { + tray.create(); + } + + mainWindow.webContents.once('dom-ready', async () => { + processDeeplink(); + + if (APP_ENV === 'production') { + setupAutoUpdates(); + } + + if (!IS_FIRST_RUN && getIsAutoUpdateEnabled() === undefined) { + store.set(AUTO_UPDATE_SETTING_KEY, true); + await captureStorage(); + loadWindowUrl(); + } + + mainWindow.show(); + }); + + loadWindowUrl(); +} + +function loadWindowUrl(): void { + if (!app.isPackaged) { + mainWindow.loadURL('http://localhost:4321'); + mainWindow.webContents.openDevTools(); + } else if (getIsAutoUpdateEnabled()) { + mainWindow.loadURL(BASE_URL!); + } else if (getIsAutoUpdateEnabled() === undefined && IS_FIRST_RUN) { + store.set(AUTO_UPDATE_SETTING_KEY, true); + mainWindow.loadURL(BASE_URL!); + } else { + mainWindow.loadURL(`file://${__dirname}/index.html`); + } +} + +export function setupElectronActionHandlers() { ipcMain.handle(ElectronAction.HANDLE_DOUBLE_CLICK, () => { const doubleClickAction = systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string'); @@ -81,19 +123,36 @@ export function createWindow() { } }); - mainWindow.webContents.once('dom-ready', () => { - mainWindow.show(); - processDeeplink(); + ipcMain.handle(ElectronAction.SET_IS_TRAY_ICON_ENABLED, (_, isTrayIconEnabled: boolean) => { + if (isTrayIconEnabled) { + tray.enable(); + } else { + tray.disable(); + } + }); - if (process.env.APP_ENV === 'production') { - setupAutoUpdates(); + ipcMain.handle(ElectronAction.GET_IS_TRAY_ICON_ENABLED, () => tray.isEnabled); + + ipcMain.handle(ElectronAction.SET_IS_AUTO_UPDATE_ENABLED, async (_, isAutoUpdateEnabled: boolean) => { + if (IS_PREVIEW) { + return; } + + store.set(AUTO_UPDATE_SETTING_KEY, isAutoUpdateEnabled); + await captureStorage(); + loadWindowUrl(); + }); + + ipcMain.handle(ElectronAction.GET_IS_AUTO_UPDATE_ENABLED, () => { + return store.get(AUTO_UPDATE_SETTING_KEY, true); }); + + ipcMain.handle(ElectronAction.RESTORE_STORAGE, () => restoreStorage()); } export function setupCloseHandlers() { mainWindow.on('close', (event: Event) => { - if (IS_MAC_OS) { + if (IS_MAC_OS || (IS_WINDOWS && tray.isEnabled)) { if (forceQuit.isEnabled) { app.exit(0); forceQuit.disable(); @@ -135,7 +194,7 @@ export function setupCloseHandlers() { function setupWindowsTitleBar() { mainWindow.removeMenu(); - ipcMain.handle(ElectronAction.CLOSE, () => mainWindow.destroy()); + ipcMain.handle(ElectronAction.CLOSE, () => mainWindow.close()); ipcMain.handle(ElectronAction.MINIMIZE, () => mainWindow.minimize()); ipcMain.handle(ElectronAction.MAXIMIZE, () => mainWindow.maximize()); ipcMain.handle(ElectronAction.UNMAXIMIZE, () => mainWindow.unmaximize()); diff --git a/src/extension/pageScript/deeplinkHook.ts b/src/extension/pageScript/deeplinkHook.ts index 086180ff..37ecbc02 100644 --- a/src/extension/pageScript/deeplinkHook.ts +++ b/src/extension/pageScript/deeplinkHook.ts @@ -1,3 +1,4 @@ +import { parseTonDeeplink } from '../../util/ton/deeplinks'; import { callApi } from '../../api/providers/extension/connectorForPageScript'; const originalOpenFn = window.open; @@ -36,24 +37,9 @@ function patchedOpenFn(url?: string | URL, ...args: any[]) { } function tryHandleDeeplink(value: any) { - if (typeof value !== 'string' || !value.startsWith('ton://transfer/')) { - return false; - } + const params = parseTonDeeplink(value); + if (!params) return false; - try { - const url = new URL(value); - const params = { - to: url.pathname.replace('//transfer/', ''), - amount: url.searchParams.get('amount'), - text: url.searchParams.get('text'), - }; - void callApi('prepareTransaction', { - to: params.to, - amount: params.amount, - comment: params.text, - }); - return true; - } catch (err) { - return false; - } + void callApi('prepareTransaction', params); + return true; } diff --git a/src/global/actions/api/auth.ts b/src/global/actions/api/auth.ts index eccf5a51..22f8b710 100644 --- a/src/global/actions/api/auth.ts +++ b/src/global/actions/api/auth.ts @@ -1,18 +1,38 @@ -import { AppState, AuthState, HardwareConnectState } from '../../types'; +import { NativeBiometric } from '@capgo/capacitor-native-biometric'; -import { MNEMONIC_CHECK_COUNT, MNEMONIC_COUNT } from '../../../config'; +import { ApiCommonError } from '../../../api/types'; +import { + AppState, AuthState, BiometricsState, HardwareConnectState, +} from '../../types'; + +import { + APP_NAME, IS_CAPACITOR, MNEMONIC_CHECK_COUNT, MNEMONIC_COUNT, +} from '../../../config'; import { parseAccountId } from '../../../util/account'; +import authApi from '../../../util/authApi'; +import webAuthn from '../../../util/authApi/webAuthn'; +import { + getIsBiometricAuthSupported, + getIsNativeBiometricAuthSupported, + vibrateOnError, + vibrateOnSuccess, +} from '../../../util/capacitor'; import { cloneDeep } from '../../../util/iteratees'; import { pause } from '../../../util/schedulers'; +import { IS_DELEGATED_BOTTOM_SHEET, IS_ELECTRON } from '../../../util/windowEnvironment'; import { callApi } from '../../../api'; import { addActionHandler, getGlobal, setGlobal } from '../..'; import { INITIAL_STATE } from '../../initialState'; import { + clearCurrentSwap, clearCurrentTransfer, + clearIsPinPadPasswordAccepted, createAccount, updateAuth, + updateBiometrics, updateCurrentAccountState, updateHardware, + updateIsPinPadPasswordAccepted, updateSettings, } from '../../reducers'; import { @@ -20,11 +40,15 @@ import { selectCurrentNetwork, selectFirstNonHardwareAccount, selectIsOneAccount, + selectLastLedgerAccountIndex, selectNetworkAccountsMemoized, selectNewestTxIds, } from '../../selectors'; +import { callActionInMain } from '../../../hooks/useDelegatedBottomSheet'; + const CREATING_DURATION = 3300; +const NATIVE_BIOMETRICS_PAUSE_MS = 750; addActionHandler('restartAuth', (global) => { if (global.currentAccountId) { @@ -46,6 +70,19 @@ addActionHandler('startCreatingWallet', async (global, actions) => { const isFirstAccount = !Object.values(accounts).length; const methodState = isFirstAccount ? AuthState.creatingWallet : AuthState.createBackup; + const network = selectCurrentNetwork(global); + const checkResult = await callApi('checkApiAvailability', { + blockchainKey: 'ton', + network, + }); + + if (!checkResult) { + actions.showError({ error: ApiCommonError.ServerError }); + return; + } + + global = getGlobal(); + const promiseCalls = [ callApi('generateMnemonic'), ...(isFirstAccount ? [pause(CREATING_DURATION)] : []), @@ -75,10 +112,51 @@ addActionHandler('startCreatingWallet', async (global, actions) => { return; } - setGlobal(updateAuth(global, { state: AuthState.createPassword })); + setGlobal(updateAuth(global, { + state: getIsBiometricAuthSupported() + ? (getIsNativeBiometricAuthSupported() ? AuthState.createPin : AuthState.createBiometrics) + : AuthState.createPassword, + })); + + if (isFirstAccount) { + actions.requestConfetti(); + if (IS_CAPACITOR) { + void vibrateOnSuccess(); + } + } }); -addActionHandler('afterCreatePassword', (global, actions, { password }) => { +addActionHandler('createPin', (global, actions, { pin, isImporting }) => { + global = updateAuth(global, { + state: isImporting ? AuthState.importWalletConfirmPin : AuthState.confirmPin, + password: pin, + }); + setGlobal(global); +}); + +addActionHandler('confirmPin', (global, actions, { isImporting }) => { + global = updateAuth(global, { + state: isImporting ? AuthState.importWalletCreateNativeBiometrics : AuthState.createNativeBiometrics, + }); + setGlobal(global); +}); + +addActionHandler('cancelConfirmPin', (global, actions, { isImporting }) => { + global = updateAuth(global, { + state: isImporting ? AuthState.importWalletCreatePin : AuthState.createPin, + }); + setGlobal(global); +}); + +addActionHandler('cancelDisclaimer', (global) => { + setGlobal(updateAuth(global, { + state: getIsBiometricAuthSupported() + ? (getIsNativeBiometricAuthSupported() ? AuthState.createPin : AuthState.createBiometrics) + : AuthState.createPassword, + })); +}); + +addActionHandler('afterCreatePassword', (global, actions, { password, isPasswordNumeric }) => { setGlobal(updateAuth(global, { isLoading: true })); const { method } = getGlobal().auth; @@ -91,10 +169,102 @@ addActionHandler('afterCreatePassword', (global, actions, { password }) => { return; } - actions.createAccount({ password, isImporting }); + actions.createAccount({ password, isImporting, isPasswordNumeric }); +}); + +addActionHandler('afterCreateBiometrics', async (global, actions) => { + const withCredential = !IS_ELECTRON; + global = updateAuth(global, { + isLoading: true, + error: undefined, + biometricsStep: withCredential ? 1 : undefined, + }); + setGlobal(global); + + try { + const credential = withCredential + ? await webAuthn.createCredential() + : undefined; + global = getGlobal(); + global = updateAuth(global, { biometricsStep: withCredential ? 2 : undefined }); + setGlobal(global); + const result = await authApi.setupBiometrics({ credential }); + + global = getGlobal(); + global = updateAuth(global, { + isLoading: false, + biometricsStep: undefined, + }); + + if (!result) { + global = updateAuth(global, { error: 'Biometric setup failed' }); + setGlobal(global); + + return; + } + + global = updateSettings(global, { authConfig: result.config }); + setGlobal(global); + + actions.afterCreatePassword({ password: result.password }); + } catch (err: any) { + const error = err?.message.includes('privacy-considerations-client') + ? 'Biometric setup failed' + : (err?.message || 'Biometric setup failed'); + global = getGlobal(); + global = updateAuth(global, { + isLoading: false, + error, + biometricsStep: undefined, + }); + setGlobal(global); + } +}); + +addActionHandler('afterCreateNativeBiometrics', async (global, actions) => { + global = updateAuth(global, { + isLoading: true, + error: undefined, + }); + setGlobal(global); + + try { + const { password } = global.auth; + const result = await authApi.setupNativeBiometrics(password!); + + global = getGlobal(); + global = updateAuth(global, { isLoading: false }); + global = updateSettings(global, { authConfig: result.config }); + setGlobal(global); + + actions.afterCreatePassword({ password: password!, isPasswordNumeric: true }); + } catch (err: any) { + const error = err?.message.includes('privacy-considerations-client') + ? 'Biometric setup failed' + : (err?.message || 'Biometric setup failed'); + global = getGlobal(); + global = updateAuth(global, { + isLoading: false, + error, + }); + setGlobal(global); + } +}); + +addActionHandler('skipCreateNativeBiometrics', (global, actions) => { + const { password } = global.auth; + + global = updateAuth(global, { isLoading: false, error: undefined }); + global = updateSettings(global, { + authConfig: { kind: 'password' }, + isPasswordNumeric: true, + }); + setGlobal(global); + + actions.afterCreatePassword({ password: password!, isPasswordNumeric: true }); }); -addActionHandler('createAccount', async (global, actions, { password, isImporting }) => { +addActionHandler('createAccount', async (global, actions, { password, isImporting, isPasswordNumeric }) => { setGlobal(updateAuth(global, { isLoading: true })); const network = selectCurrentNetwork(getGlobal()); @@ -108,40 +278,40 @@ addActionHandler('createAccount', async (global, actions, { password, isImportin global = getGlobal(); - if (!result) { + if (!result || 'error' in result) { setGlobal(updateAuth(global, { isLoading: undefined })); + actions.showError({ error: result?.error }); return; } + const { accountId, address } = result; global = updateAuth(global, { + address, + accountId, isLoading: undefined, password: undefined, + ...(isPasswordNumeric && { isPasswordNumeric: true }), }); - const { accountId, address } = result; if (isImporting) { - global = { ...global, currentAccountId: accountId }; - global = updateAuth(global, { - state: AuthState.ready, - }); - global = createAccount(global, accountId, address); - setGlobal(global); - - actions.afterSignIn(); - if (selectIsOneAccount(global)) { - actions.resetApiSettings(); + const hasAccounts = Object.keys(selectAccounts(global) || {}).length > 0; + if (hasAccounts) { + setGlobal(global); + actions.afterConfirmDisclaimer(); + + return; + } else { + global = updateAuth(global, { state: AuthState.disclaimer }); } } else { const accounts = selectAccounts(global) ?? {}; const isFirstAccount = !Object.values(accounts).length; global = updateAuth(global, { state: isFirstAccount ? AuthState.disclaimerAndBackup : AuthState.createBackup, - accountId, - address, }); - - setGlobal(global); } + + setGlobal(global); }); addActionHandler('createHardwareAccounts', async (global, actions) => { @@ -187,6 +357,11 @@ addActionHandler('createHardwareAccounts', async (global, actions) => { if (isFirstAccount) { actions.afterSignIn(); actions.resetApiSettings(); + actions.requestConfetti(); + + if (IS_CAPACITOR) { + void vibrateOnSuccess(); + } } }); @@ -259,11 +434,24 @@ addActionHandler('afterImportMnemonic', async (global, actions, { mnemonic }) => global = updateAuth(global, { mnemonic, error: undefined, - ...(!hasAccounts && { state: AuthState.disclaimer }), + ...(!hasAccounts && { + state: getIsBiometricAuthSupported() + ? (getIsNativeBiometricAuthSupported() + ? AuthState.importWalletCreatePin + : AuthState.importWalletCreateBiometrics + ) + : AuthState.importWalletCreatePassword, + }), }); setGlobal(global); - if (hasAccounts) { + if (!hasAccounts) { + actions.requestConfetti(); + + if (IS_CAPACITOR) { + void vibrateOnSuccess(); + } + } else { actions.confirmDisclaimer(); } }); @@ -278,7 +466,21 @@ addActionHandler('confirmDisclaimer', (global, actions) => { return; } - setGlobal(updateAuth(global, { state: AuthState.importWalletCreatePassword })); + actions.afterConfirmDisclaimer(); +}); + +addActionHandler('afterConfirmDisclaimer', (global, actions) => { + const { accountId, address } = global.auth; + + global = { ...global, currentAccountId: accountId }; + global = updateAuth(global, { state: AuthState.ready }); + global = createAccount(global, accountId!, address!); + setGlobal(global); + + actions.afterSignIn(); + if (selectIsOneAccount(global)) { + actions.resetApiSettings(); + } }); addActionHandler('cleanAuthError', (global) => { @@ -296,6 +498,10 @@ export function selectMnemonicForCheck() { } addActionHandler('startChangingNetwork', (global, actions, { network }) => { + if (IS_DELEGATED_BOTTOM_SHEET) { + callActionInMain('startChangingNetwork', { network }); + } + const accountIds = Object.keys(selectNetworkAccountsMemoized(network, global.accounts!.byId)!); if (accountIds.length) { @@ -311,7 +517,12 @@ addActionHandler('startChangingNetwork', (global, actions, { network }) => { } }); -addActionHandler('switchAccount', async (global, actions, { accountId, newNetwork }) => { +addActionHandler('switchAccount', async (global, actions, payload) => { + if (IS_DELEGATED_BOTTOM_SHEET) { + callActionInMain('switchAccount', payload); + } + + const { accountId, newNetwork } = payload; const newestTxIds = selectNewestTxIds(global, accountId); await callApi('activateAccount', accountId, newestTxIds); @@ -321,6 +532,7 @@ addActionHandler('switchAccount', async (global, actions, { accountId, newNetwor }; global = clearCurrentTransfer(global); + global = clearCurrentSwap(global); setGlobal(global); if (newNetwork) { @@ -328,75 +540,86 @@ addActionHandler('switchAccount', async (global, actions, { accountId, newNetwor } }); -addActionHandler('connectHardwareWallet', async (global) => { - setGlobal( - updateHardware(global, { - hardwareState: HardwareConnectState.Connecting, - hardwareWallets: undefined, - hardwareSelectedIndices: undefined, - isLedgerConnected: undefined, - isTonAppConnected: undefined, - }), - ); +addActionHandler('connectHardwareWallet', async (global, actions) => { + global = updateHardware(global, { + hardwareState: HardwareConnectState.Connecting, + hardwareWallets: undefined, + hardwareSelectedIndices: undefined, + isLedgerConnected: undefined, + isTonAppConnected: undefined, + }); + setGlobal(global); const ledgerApi = await import('../../../util/ledger'); const isLedgerConnected = await ledgerApi.connectLedger(); + global = getGlobal(); + if (!isLedgerConnected) { - setGlobal( - updateHardware(getGlobal(), { - isLedgerConnected: false, - hardwareState: HardwareConnectState.Failed, - }), - ); + global = updateHardware(global, { + isLedgerConnected: false, + hardwareState: HardwareConnectState.Failed, + }); + setGlobal(global); return; } - setGlobal( - updateHardware(getGlobal(), { - isLedgerConnected: true, - }), - ); + global = updateHardware(global, { + isLedgerConnected: true, + }); + setGlobal(global); const isTonAppConnected = await ledgerApi.waitLedgerTonApp(); + global = getGlobal(); if (!isTonAppConnected) { - setGlobal( - updateHardware(getGlobal(), { - isTonAppConnected: false, - hardwareState: HardwareConnectState.Failed, - }), - ); + global = updateHardware(global, { + isTonAppConnected: false, + hardwareState: HardwareConnectState.Failed, + }); + setGlobal(global); return; } - setGlobal( - updateHardware(getGlobal(), { - isTonAppConnected: true, - }), - ); + global = updateHardware(global, { + isTonAppConnected: true, + }); + setGlobal(global); try { - const network = selectCurrentNetwork(getGlobal()); - const hardwareWallets = await ledgerApi.getFirstLedgerWallets(network); + global = getGlobal(); + const { isRemoteTab } = global.hardware; + const network = selectCurrentNetwork(global); + const lastIndex = selectLastLedgerAccountIndex(global, network); + const hardwareWallets = isRemoteTab ? [] : await ledgerApi.getNextLedgerWallets(network, lastIndex); - setGlobal( - updateHardware(getGlobal(), { - hardwareWallets, - hardwareState: HardwareConnectState.Connected, - }), - ); + global = getGlobal(); + + if ('error' in hardwareWallets) { + actions.showError({ error: hardwareWallets.error }); + throw Error(hardwareWallets.error); + } + + const nextHardwareState = isRemoteTab || hardwareWallets.length === 1 + ? HardwareConnectState.ConnectedWithSingleWallet + : HardwareConnectState.ConnectedWithSeveralWallets; + + global = updateHardware(global, { + hardwareWallets, + hardwareState: nextHardwareState, + }); + setGlobal(global); } catch (err) { - setGlobal( - updateHardware(getGlobal(), { - hardwareState: HardwareConnectState.Failed, - }), - ); + global = getGlobal(); + global = updateHardware(global, { + hardwareState: HardwareConnectState.Failed, + }); + setGlobal(global); } }); addActionHandler('afterSelectHardwareWallets', (global, actions, { hardwareSelectedIndices }) => { - global = updateAuth(getGlobal(), { + global = updateAuth(global, { method: 'importHardwareWallet', error: undefined, }); @@ -410,11 +633,250 @@ addActionHandler('afterSelectHardwareWallets', (global, actions, { hardwareSelec }); addActionHandler('resetHardwareWalletConnect', (global) => { - setGlobal( - updateHardware(global, { - hardwareState: HardwareConnectState.Connect, - isLedgerConnected: undefined, - isTonAppConnected: undefined, - }), - ); + global = updateHardware(global, { + hardwareState: HardwareConnectState.Connect, + isLedgerConnected: undefined, + isTonAppConnected: undefined, + }); + setGlobal(global); +}); + +addActionHandler('enableBiometrics', async (global, actions, { password }) => { + if (!(await callApi('verifyPassword', password))) { + global = getGlobal(); + global = updateBiometrics(global, { error: 'Wrong password, please try again' }); + setGlobal(global); + + return; + } + + global = getGlobal(); + global = updateBiometrics(global, { + error: undefined, + state: BiometricsState.TurnOnRegistration, + }); + setGlobal(global); + + try { + const credential = IS_ELECTRON + ? undefined + : await webAuthn.createCredential(); + + global = getGlobal(); + global = updateBiometrics(global, { state: BiometricsState.TurnOnVerification }); + setGlobal(global); + + const result = await authApi.setupBiometrics({ credential }); + + global = getGlobal(); + if (!result) { + global = updateBiometrics(global, { + error: 'Biometric setup failed', + state: BiometricsState.TurnOnPasswordConfirmation, + }); + setGlobal(global); + + return; + } + global = updateBiometrics(global, { state: BiometricsState.TurnOnComplete }); + setGlobal(global); + + await callApi('changePassword', password, result.password); + + global = getGlobal(); + global = updateSettings(global, { authConfig: result.config }); + + setGlobal(global); + } catch (err: any) { + const error = err?.message.includes('privacy-considerations-client') + ? 'Biometric setup failed' + : (err?.message || 'Biometric setup failed'); + global = getGlobal(); + global = updateBiometrics(global, { + error, + state: BiometricsState.TurnOnPasswordConfirmation, + }); + setGlobal(global); + } +}); + +addActionHandler('disableBiometrics', async (global, actions, { password, isPasswordNumeric }) => { + const { password: oldPassword } = global.biometrics; + + if (!password || !oldPassword) { + global = updateBiometrics(global, { error: 'Biometric confirmation failed' }); + setGlobal(global); + + return; + } + + try { + await callApi('changePassword', oldPassword, password); + } catch (err: any) { + global = getGlobal(); + global = updateBiometrics(global, { error: err?.message || 'Failed to disable biometrics' }); + setGlobal(global); + + return; + } + + global = getGlobal(); + global = updateBiometrics(global, { + state: BiometricsState.TurnOffComplete, + error: undefined, + }); + global = updateSettings(global, { + authConfig: { kind: 'password' }, + isPasswordNumeric, + }); + setGlobal(global); +}); + +addActionHandler('closeBiometricSettings', (global) => { + global = { ...global, biometrics: cloneDeep(INITIAL_STATE.biometrics) }; + + setGlobal(global); +}); + +addActionHandler('openBiometricsTurnOn', (global) => { + global = updateBiometrics(global, { state: BiometricsState.TurnOnPasswordConfirmation }); + + setGlobal(global); +}); + +addActionHandler('openBiometricsTurnOffWarning', (global) => { + global = updateBiometrics(global, { state: BiometricsState.TurnOffWarning }); + + setGlobal(global); +}); + +addActionHandler('openBiometricsTurnOff', async (global) => { + global = updateBiometrics(global, { state: BiometricsState.TurnOffBiometricConfirmation }); + setGlobal(global); + + const password = await authApi.getPassword(global.settings.authConfig!); + global = getGlobal(); + + if (!password) { + global = updateBiometrics(global, { error: 'Biometric confirmation failed' }); + } else { + global = updateBiometrics(global, { + state: BiometricsState.TurnOffCreatePassword, + password, + }); + } + + setGlobal(global); +}); + +addActionHandler('disableNativeBiometrics', (global) => { + global = updateSettings(global, { + authConfig: { kind: 'password' }, + isPasswordNumeric: true, + }); + setGlobal(global); +}); + +addActionHandler('enableNativeBiometrics', async (global, actions, { password }) => { + if (!(await callApi('verifyPassword', password))) { + global = getGlobal(); + global = { + ...global, + nativeBiometricsError: 'Incorrect code, please try again', + }; + global = clearIsPinPadPasswordAccepted(global); + setGlobal(global); + + return; + } + + global = getGlobal(); + + global = updateIsPinPadPasswordAccepted(global); + global = { + ...global, + nativeBiometricsError: undefined, + }; + setGlobal(global); + + try { + const isVerified = await NativeBiometric.verifyIdentity({ + title: APP_NAME, + subtitle: '', + }) + .then(() => true) + .catch(() => false); + + if (!isVerified) { + global = getGlobal(); + global = { + ...global, + nativeBiometricsError: 'Failed to enable biometrics', + }; + global = clearIsPinPadPasswordAccepted(global); + setGlobal(global); + void vibrateOnError(); + + return; + } + + const result = await authApi.setupNativeBiometrics(password); + await pause(NATIVE_BIOMETRICS_PAUSE_MS); + + global = getGlobal(); + global = updateSettings(global, { + authConfig: result.config, + }); + global = { + ...global, + nativeBiometricsError: undefined, + }; + setGlobal(global); + + void vibrateOnSuccess(); + } catch (err: any) { + global = getGlobal(); + global = { + ...global, + nativeBiometricsError: err?.message || 'Failed to enable biometrics', + }; + global = clearIsPinPadPasswordAccepted(global); + setGlobal(global); + + void vibrateOnError(); + } +}); + +addActionHandler('clearNativeBiometricsError', (global) => { + return { + ...global, + nativeBiometricsError: undefined, + }; +}); + +addActionHandler('openAuthBackupWalletModal', (global) => { + if (IS_DELEGATED_BOTTOM_SHEET) { + callActionInMain('openAuthBackupWalletModal'); + return; + } + + global = updateAuth(global, { isBackupModalOpen: true }); + setGlobal(global); +}); + +addActionHandler('closeAuthBackupWalletModal', (global, actions, props) => { + const { isBackupCreated } = props || {}; + + if (IS_DELEGATED_BOTTOM_SHEET) { + callActionInMain('closeAuthBackupWalletModal', props); + } + + global = updateAuth(global, { + isBackupModalOpen: undefined, + }); + setGlobal(global); + + if (!IS_DELEGATED_BOTTOM_SHEET && isBackupCreated) { + actions.afterCheckMnemonic(); + } }); diff --git a/src/global/actions/api/dapps.ts b/src/global/actions/api/dapps.ts index e91c329f..ddc70a25 100644 --- a/src/global/actions/api/dapps.ts +++ b/src/global/actions/api/dapps.ts @@ -1,5 +1,9 @@ import { DappConnectState, TransferState } from '../../types'; +import { IS_CAPACITOR } from '../../../config'; +import { vibrateOnSuccess } from '../../../util/capacitor'; +import { pause } from '../../../util/schedulers'; +import { IS_DELEGATED_BOTTOM_SHEET } from '../../../util/windowEnvironment'; import { callApi } from '../../../api'; import { ApiUserRejectsError } from '../../../api/errors'; import { addActionHandler, getGlobal, setGlobal } from '../../index'; @@ -7,11 +11,18 @@ import { clearConnectedDapps, clearCurrentDappTransfer, clearDappConnectRequest, + clearIsPinPadPasswordAccepted, removeConnectedDapp, updateConnectedDapps, updateCurrentDappTransfer, updateDappConnectRequest, + updateIsPinPadPasswordAccepted, } from '../../reducers'; +import { selectAccount, selectIsHardwareAccount, selectNewestTxIds } from '../../selectors'; + +import { callActionInMain } from '../../../hooks/useDelegatedBottomSheet'; + +const GET_DAPPS_PAUSE = 250; addActionHandler('submitDappConnectRequestConfirm', async (global, actions, { password, accountId }) => { const { @@ -20,7 +31,22 @@ addActionHandler('submitDappConnectRequestConfirm', async (global, actions, { pa if (permissions?.isPasswordRequired && (!password || !(await callApi('verifyPassword', password)))) { global = getGlobal(); - setGlobal(updateDappConnectRequest(global, { error: 'Wrong password, please try again' })); + global = updateDappConnectRequest(global, { error: 'Wrong password, please try again' }); + setGlobal(global); + + return; + } + + if (IS_CAPACITOR) { + global = getGlobal(); + global = updateIsPinPadPasswordAccepted(global); + setGlobal(global); + + await vibrateOnSuccess(true); + } + + if (IS_DELEGATED_BOTTOM_SHEET) { + callActionInMain('submitDappConnectRequestConfirm', { password, accountId }); return; } @@ -37,6 +63,7 @@ addActionHandler('submitDappConnectRequestConfirm', async (global, actions, { pa const { currentAccountId } = global; + await pause(GET_DAPPS_PAUSE); const result = await callApi('getDapps', currentAccountId!); if (!result) { @@ -84,6 +111,7 @@ addActionHandler( const { currentAccountId } = global; + await pause(GET_DAPPS_PAUSE); const result = await callApi('getDapps', currentAccountId!); if (!result) { @@ -99,6 +127,11 @@ addActionHandler( addActionHandler('cancelDappConnectRequestConfirm', (global) => { const { promiseId } = global.dappConnectRequest || {}; + if (IS_CAPACITOR) { + global = clearIsPinPadPasswordAccepted(global); + setGlobal(global); + } + if (!promiseId) { return; } @@ -117,11 +150,17 @@ addActionHandler('setDappConnectRequestState', (global, actions, { state }) => { addActionHandler('cancelDappTransfer', (global) => { const { promiseId } = global.currentDappTransfer; + if (IS_CAPACITOR) { + global = clearIsPinPadPasswordAccepted(global); + setGlobal(global); + } + if (promiseId) { void callApi('cancelDappRequest', promiseId, 'Canceled by the user'); } - setGlobal(clearCurrentDappTransfer(global)); + global = clearCurrentDappTransfer(getGlobal()); + setGlobal(global); }); addActionHandler('submitDappTransferPassword', async (global, actions, { password }) => { @@ -141,13 +180,26 @@ addActionHandler('submitDappTransferPassword', async (global, actions, { passwor return; } + if (IS_DELEGATED_BOTTOM_SHEET) { + callActionInMain('submitDappTransferPassword', { password }); + + return; + } + global = getGlobal(); + if (IS_CAPACITOR) { + global = updateIsPinPadPasswordAccepted(global); + } global = updateCurrentDappTransfer(global, { isLoading: true, error: undefined, }); setGlobal(global); + if (IS_CAPACITOR) { + await vibrateOnSuccess(true); + } + void callApi('confirmDappRequest', promiseId, password); global = getGlobal(); @@ -217,9 +269,8 @@ addActionHandler('deleteAllDapps', (global) => { setGlobal(global); }); -addActionHandler('deleteDapp', (global, actions, payload) => { +addActionHandler('deleteDapp', (global, actions, { origin }) => { const { currentAccountId } = global; - const { origin } = payload; void callApi('deleteDapp', currentAccountId!, origin); @@ -227,3 +278,56 @@ addActionHandler('deleteDapp', (global, actions, payload) => { global = removeConnectedDapp(global, origin); setGlobal(global); }); + +addActionHandler('apiUpdateDappConnect', (global, actions, { + accountId, dapp, permissions, promiseId, proof, +}) => { + const { isHardware } = selectAccount(global, accountId)!; + + global = updateDappConnectRequest(global, { + state: DappConnectState.Info, + promiseId, + accountId, + dapp, + permissions: { + isAddressRequired: permissions.address, + isPasswordRequired: permissions.proof && !isHardware, + }, + proof, + }); + setGlobal(global); +}); + +addActionHandler('apiUpdateDappSendTransaction', async (global, actions, { + promiseId, + transactions, + fee, + accountId, + dapp, +}) => { + const { currentAccountId } = global; + if (currentAccountId !== accountId) { + const newestTxIds = selectNewestTxIds(global, accountId); + await callApi('activateAccount', accountId, newestTxIds); + global = getGlobal(); + setGlobal({ + ...global, + currentAccountId: accountId, + }); + } + + const state = selectIsHardwareAccount(global) && transactions.length > 1 + ? TransferState.WarningHardware + : TransferState.Initial; + + global = getGlobal(); + global = clearCurrentDappTransfer(global); + global = updateCurrentDappTransfer(global, { + state, + promiseId, + transactions, + fee, + dapp, + }); + setGlobal(global); +}); diff --git a/src/global/actions/api/initial.ts b/src/global/actions/api/initial.ts index 82a4d2ea..f779c03d 100644 --- a/src/global/actions/api/initial.ts +++ b/src/global/actions/api/initial.ts @@ -1,7 +1,8 @@ import { ElectronEvent } from '../../../electron/types'; -import { IS_ELECTRON, IS_EXTENSION } from '../../../config'; +import { DEFAULT_PRICE_CURRENCY, IS_EXTENSION } from '../../../config'; import { tonConnectGetDeviceInfo } from '../../../util/tonConnectEnvironment'; +import { IS_ELECTRON } from '../../../util/windowEnvironment'; import { callApi, initApi } from '../../../api'; import { addActionHandler, getGlobal } from '../../index'; import { selectNewestTxIds } from '../../selectors'; @@ -39,4 +40,5 @@ addActionHandler('resetApiSettings', (global, actions, params) => { if (IS_EXTENSION || IS_ELECTRON) { actions.toggleDeeplinkHook({ isEnabled: isDefaultEnabled }); } + actions.changeBaseCurrency({ currency: DEFAULT_PRICE_CURRENCY }); }); diff --git a/src/global/actions/api/staking.ts b/src/global/actions/api/staking.ts index 6f937eaa..ea807d19 100644 --- a/src/global/actions/api/staking.ts +++ b/src/global/actions/api/staking.ts @@ -1,31 +1,31 @@ import { StakingState } from '../../types'; -import { DEFAULT_DECIMAL_PLACES } from '../../../config'; +import { DEFAULT_DECIMAL_PLACES, IS_CAPACITOR } from '../../../config'; +import { Big } from '../../../lib/big.js'; +import { vibrateOnSuccess } from '../../../util/capacitor'; +import { IS_DELEGATED_BOTTOM_SHEET } from '../../../util/windowEnvironment'; import { callApi } from '../../../api'; import { humanToBigStr } from '../../helpers'; import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { + clearIsPinPadPasswordAccepted, clearStaking, + updateAccountStakingStatePartial, updateAccountState, - updatePoolState, + updateIsPinPadPasswordAccepted, updateStaking, } from '../../reducers'; +import { selectAccountState } from '../../selectors'; -addActionHandler('fetchStakingState', async (global) => { - const { currentAccountId } = global; +import { callActionInMain } from '../../../hooks/useDelegatedBottomSheet'; - const stakingState = await callApi('getStakingState', currentAccountId!); - if (!stakingState) { +addActionHandler('startStaking', (global, actions, payload) => { + const isOpen = global.staking.state !== StakingState.None; + if (IS_DELEGATED_BOTTOM_SHEET && !isOpen) { + callActionInMain('startStaking', payload); return; } - setGlobal(updateAccountState(getGlobal(), currentAccountId!, { - stakingBalance: stakingState.amount + stakingState.pendingDepositAmount, - isUnstakeRequested: stakingState.isUnstakeRequested, - }, true)); -}); - -addActionHandler('startStaking', (global, actions, payload) => { const { isUnstaking } = payload || {}; setGlobal(updateStaking(global, { @@ -47,7 +47,7 @@ addActionHandler('fetchStakingFee', async (global, actions, payload) => { currentAccountId, humanToBigStr(amount!, DEFAULT_DECIMAL_PLACES), ); - if (!result) { + if (!result || 'error' in result) { return; } @@ -59,7 +59,8 @@ addActionHandler('fetchStakingFee', async (global, actions, payload) => { }); addActionHandler('submitStakingInitial', async (global, actions, payload) => { - const { amount, isUnstaking } = payload || {}; + const { isUnstaking } = payload || {}; + let { amount } = payload ?? {}; const { currentAccountId } = global; if (!currentAccountId) { @@ -69,7 +70,8 @@ addActionHandler('submitStakingInitial', async (global, actions, payload) => { setGlobal(updateStaking(global, { isLoading: true, error: undefined })); if (isUnstaking) { - const result = await callApi('checkUnstakeDraft', currentAccountId); + amount = selectAccountState(global, currentAccountId)!.staking!.balance; + const result = await callApi('checkUnstakeDraft', currentAccountId, humanToBigStr(amount)); global = getGlobal(); global = updateStaking(global, { isLoading: false }); @@ -82,6 +84,8 @@ addActionHandler('submitStakingInitial', async (global, actions, payload) => { fee: result.fee, amount, error: undefined, + type: result.type, + tokenAmount: result.tokenAmount, }); } } @@ -103,6 +107,7 @@ addActionHandler('submitStakingInitial', async (global, actions, payload) => { fee: result.fee, amount, error: undefined, + type: result.type, }); } } @@ -113,7 +118,13 @@ addActionHandler('submitStakingInitial', async (global, actions, payload) => { addActionHandler('submitStakingPassword', async (global, actions, payload) => { const { password, isUnstaking } = payload; - const { amount, fee } = global.staking; + const { + fee, + type, + amount, + tokenAmount, + } = global.staking; + const { currentAccountId } = global; if (!(await callApi('verifyPassword', password))) { setGlobal(updateStaking(getGlobal(), { error: 'Wrong password, please try again' })); @@ -121,26 +132,56 @@ addActionHandler('submitStakingPassword', async (global, actions, payload) => { return; } - setGlobal(updateStaking(getGlobal(), { + global = getGlobal(); + + if (IS_CAPACITOR) { + global = updateIsPinPadPasswordAccepted(global); + } + + global = updateStaking(global, { isLoading: true, error: undefined, - })); + }); + setGlobal(global); + + if (IS_CAPACITOR) { + await vibrateOnSuccess(true); + } + + global = getGlobal(); if (isUnstaking) { + const { instantAvailable } = global.stakingInfo.liquid ?? {}; + const stakingBalance = selectAccountState(global, currentAccountId!)!.staking!.balance; + + const unstakeAmount = type === 'nominators' ? humanToBigStr(stakingBalance) : tokenAmount!; const result = await callApi( 'submitUnstake', global.currentAccountId!, password, + type!, + unstakeAmount, fee, ); + const isInstantUnstakeRequested = Boolean( + type === 'liquid' && instantAvailable && Big(instantAvailable).gt(stakingBalance), + ); + global = getGlobal(); + global = updateAccountStakingStatePartial(global, currentAccountId!, { isInstantUnstakeRequested }); global = updateStaking(global, { isLoading: false }); + setGlobal(global); + global = getGlobal(); + if (!result) { actions.showDialog({ message: 'Unstaking was unsuccessful. Try again later', }); - global = getGlobal(); + + if (IS_CAPACITOR) { + global = clearIsPinPadPasswordAccepted(global); + } } else { global = updateStaking(global, { state: StakingState.UnstakeComplete }); } @@ -150,16 +191,23 @@ addActionHandler('submitStakingPassword', async (global, actions, payload) => { global.currentAccountId!, password, humanToBigStr(amount!, DEFAULT_DECIMAL_PLACES), + type!, fee, ); global = getGlobal(); global = updateStaking(global, { isLoading: false }); + setGlobal(global); + global = getGlobal(); + if (!result) { actions.showDialog({ message: 'Staking was unsuccessful. Try again later', }); - global = getGlobal(); + + if (IS_CAPACITOR) { + global = clearIsPinPadPasswordAccepted(global); + } } else { global = updateStaking(global, { state: StakingState.StakeComplete }); } @@ -173,7 +221,12 @@ addActionHandler('clearStakingError', (global) => { }); addActionHandler('cancelStaking', (global) => { - setGlobal(clearStaking(global)); + if (IS_CAPACITOR) { + global = clearIsPinPadPasswordAccepted(global); + } + + global = clearStaking(global); + setGlobal(global); }); addActionHandler('setStakingScreen', (global, actions, payload) => { @@ -182,15 +235,25 @@ addActionHandler('setStakingScreen', (global, actions, payload) => { setGlobal(updateStaking(global, { state })); }); -addActionHandler('fetchBackendStakingState', async (global) => { - const result = await callApi('getBackendStakingState', global.currentAccountId!); +addActionHandler('fetchStakingHistory', async (global, actions, payload) => { + const { limit, offset } = payload ?? {}; + const stakingHistory = await callApi('getStakingHistory', global.currentAccountId!, limit, offset); - if (!result) { + if (!stakingHistory) { return; } global = getGlobal(); - global = updateAccountState(global, global.currentAccountId!, { stakingHistory: result }, true); - global = updatePoolState(global, result.poolState, true); + global = updateAccountState(global, global.currentAccountId!, { stakingHistory }, true); + setGlobal(global); +}); + +addActionHandler('openStakingInfo', (global) => { + global = { ...global, isStakingInfoModalOpen: true }; + setGlobal(global); +}); + +addActionHandler('closeStakingInfo', (global) => { + global = { ...global, isStakingInfoModalOpen: undefined }; setGlobal(global); }); diff --git a/src/global/actions/api/swap.ts b/src/global/actions/api/swap.ts new file mode 100644 index 00000000..2a80f506 --- /dev/null +++ b/src/global/actions/api/swap.ts @@ -0,0 +1,814 @@ +import type { AssetPairs, GlobalState } from '../../types'; +import { + ApiCommonError, + type ApiSwapBuildRequest, + type ApiSwapHistoryItem, + type ApiSwapPairAsset, +} from '../../../api/types'; +import { + ActiveTab, + SwapErrorType, + SwapFeeSource, + SwapInputSource, + SwapState, + SwapType, +} from '../../types'; + +import { IS_CAPACITOR, JWBTC_TOKEN_SLUG, TON_TOKEN_SLUG } from '../../../config'; +import { Big } from '../../../lib/big.js'; +import { vibrateOnSuccess } from '../../../util/capacitor'; +import safeNumberToString from '../../../util/safeNumberToString'; +import { pause } from '../../../util/schedulers'; +import shiftDecimals from '../../../util/shiftDecimals'; +import { buildSwapId } from '../../../util/swap/buildSwapId'; +import { IS_DELEGATED_BOTTOM_SHEET } from '../../../util/windowEnvironment'; +import { callApi } from '../../../api'; +import { addActionHandler, getGlobal, setGlobal } from '../..'; +import { bigStrToHuman, humanToBigStr } from '../../helpers'; +import { + clearCurrentSwap, + clearIsPinPadPasswordAccepted, + updateCurrentSwap, + updateIsPinPadPasswordAccepted, +} from '../../reducers'; +import { selectAccount } from '../../selectors'; + +import { callActionInMain } from '../../../hooks/useDelegatedBottomSheet'; + +const PAIRS_CACHE: Record = {}; + +const CACHE_DURATION = 15 * 60 * 1000; // 15 minutes +const WAIT_FOR_CHANGELLY = 5 * 1000; + +function getSwapBuildOptions(global: GlobalState): ApiSwapBuildRequest { + const { + dexLabel, + amountIn, + amountOut, + amountOutMin, + tokenInSlug, + tokenOutSlug, + slippage, + networkFee, + swapFee, + } = global.currentSwap; + + const tokenIn = global.swapTokenInfo!.bySlug[tokenInSlug!]; + const tokenOut = global.swapTokenInfo!.bySlug[tokenOutSlug!]; + const from = tokenIn.slug === TON_TOKEN_SLUG ? tokenIn.symbol : tokenIn.contract!; + const to = tokenOut.slug === TON_TOKEN_SLUG ? tokenOut.symbol : tokenOut.contract!; + const fromAmount = safeNumberToString(amountIn!, tokenIn.decimals); + const toAmount = safeNumberToString(amountOut!, tokenOut.decimals); + const account = selectAccount(global, global.currentAccountId!); + + return { + from, + to, + fromAmount, + toAmount, + toMinAmount: amountOutMin!, + slippage, + fromAddress: account?.address!, + dexLabel: dexLabel!, + networkFee: networkFee!, + swapFee: swapFee!, + }; +} + +addActionHandler('startSwap', (global, actions, payload) => { + if (IS_DELEGATED_BOTTOM_SHEET) { + callActionInMain('startSwap', payload); + return; + } + + const { isPortrait, ...rest } = payload ?? {}; + const { tokenInSlug, tokenOutSlug, amountIn } = rest; + + if (tokenInSlug || tokenOutSlug || amountIn) { + const tokenIn = global.swapTokenInfo?.bySlug[tokenInSlug!]; + const tokenOut = global.swapTokenInfo?.bySlug[tokenOutSlug!]; + + const isCrosschain = tokenIn?.blockchain !== 'ton' || tokenOut?.blockchain !== 'ton'; + const isToTon = tokenOut?.blockchain === 'ton'; + + const swapType = isCrosschain + ? (isToTon ? SwapType.CrosschainToTon : SwapType.CrosschainFromTon) + : SwapType.OnChain; + + global = updateCurrentSwap(global, { + ...rest, + isEstimating: true, + shouldEstimate: true, + inputSource: SwapInputSource.In, + swapType, + }); + } + + global = updateCurrentSwap(global, { + state: isPortrait ? SwapState.Initial : SwapState.None, + }); + setGlobal(global); + + if (!isPortrait) { + actions.setLandscapeActionsActiveTabIndex({ index: ActiveTab.Swap }); + } +}); + +addActionHandler('setDefaultSwapParams', (global, actions, payload) => { + const { tokenInSlug: requiredTokenInSlug, tokenOutSlug: requiredTokenOutSlug } = payload ?? {}; + + global = updateCurrentSwap(global, { + tokenInSlug: requiredTokenInSlug ?? TON_TOKEN_SLUG, + tokenOutSlug: requiredTokenOutSlug ?? JWBTC_TOKEN_SLUG, + priceImpact: 0, + transactionFee: '0', + swapFee: '0', + networkFee: 0, + amountOutMin: '0', + inputSource: SwapInputSource.In, + }); + setGlobal(global); +}); + +addActionHandler('cancelSwap', (global, actions, { shouldReset } = {}) => { + if (shouldReset) { + const { + tokenInSlug, tokenOutSlug, pairs, swapType, + } = global.currentSwap; + + global = clearCurrentSwap(global); + global = updateCurrentSwap(global, { + tokenInSlug, + tokenOutSlug, + priceImpact: 0, + transactionFee: '0', + swapFee: '0', + networkFee: 0, + realNetworkFee: 0, + amountOutMin: '0', + inputSource: SwapInputSource.In, + swapType, + pairs, + }); + + setGlobal(global); + return; + } + + if (IS_CAPACITOR) { + global = clearIsPinPadPasswordAccepted(global); + } + global = updateCurrentSwap(global, { + state: SwapState.None, + }); + setGlobal(global); +}); + +addActionHandler('submitSwap', async (global, actions, { password }) => { + if (!(await callApi('verifyPassword', password))) { + setGlobal(updateCurrentSwap( + getGlobal(), + { error: 'Wrong password, please try again' }, + )); + + return; + } + + global = getGlobal(); + if (IS_CAPACITOR) { + global = updateIsPinPadPasswordAccepted(global); + } + global = updateCurrentSwap(global, { + isLoading: true, + error: undefined, + }); + setGlobal(global); + + if (IS_CAPACITOR) { + await vibrateOnSuccess(true); + } + + const options = getSwapBuildOptions(global); + const buildResult = await callApi('swapBuildTransfer', global.currentAccountId!, password, options); + + if (!buildResult || 'error' in buildResult) { + actions.showError({ error: buildResult?.error }); + global = getGlobal(); + if (IS_CAPACITOR) { + global = clearIsPinPadPasswordAccepted(global); + } + global = updateCurrentSwap(global, { + isLoading: false, + }); + setGlobal(global); + return; + } + + const swapHistoryItem: ApiSwapHistoryItem = { + id: buildResult.id, + timestamp: Date.now(), + status: 'pending', + from: options.from, + fromAmount: options.fromAmount, + to: options.to, + toAmount: options.toAmount, + networkFee: global.currentSwap.networkFee!, + swapFee: global.currentSwap.swapFee!, + }; + + const result = await callApi( + 'swapSubmit', + global.currentAccountId!, + password, + buildResult.fee, + buildResult.transfers, + swapHistoryItem, + ); + + global = getGlobal(); + + if (!result || 'error' in result) { + global = updateCurrentSwap(global, { + isLoading: false, + }); + setGlobal(global); + actions.showError({ error: result?.error }); + return; + } + + global = updateCurrentSwap(global, { + isLoading: false, + state: SwapState.Complete, + activityId: buildSwapId(buildResult.id), + }); + setGlobal(global); +}); + +addActionHandler('submitSwapCexFromTon', async (global, actions, { password }) => { + if (!(await callApi('verifyPassword', password))) { + setGlobal(updateCurrentSwap( + getGlobal(), + { error: 'Wrong password, please try again' }, + )); + + return; + } + + global = getGlobal(); + if (IS_CAPACITOR) { + global = updateIsPinPadPasswordAccepted(global); + } + global = updateCurrentSwap(global, { + isLoading: true, + error: undefined, + }); + setGlobal(global); + + if (IS_CAPACITOR) { + await vibrateOnSuccess(true); + } + + const swapOptions = getSwapBuildOptions(global); + const swapItem = await callApi( + 'swapCexCreateTransaction', + global.currentAccountId!, + password, + { + from: swapOptions.from, + fromAmount: swapOptions.fromAmount, + fromAddress: swapOptions.fromAddress, + to: swapOptions.to, + toAddress: global.currentSwap.toAddress!, + swapFee: swapOptions.swapFee, + networkFee: swapOptions.networkFee, + }, + ); + + if (!swapItem) { + global = updateCurrentSwap(global, { + isLoading: false, + }); + setGlobal(global); + actions.showError({ error: ApiCommonError.Unexpected }); + return; + } + + global = getGlobal(); + + const asset = global.swapTokenInfo.bySlug[global.currentSwap.tokenInSlug!]; + + const transferOptions = { + password, + accountId: global.currentAccountId!, + slug: global.currentSwap.tokenInSlug!, + toAddress: swapItem.swap.cex!.payinAddress, + amount: humanToBigStr(Number(swapItem.swap.fromAmount), asset.decimals), + fee: swapItem.swap.swapFee, + }; + + await pause(WAIT_FOR_CHANGELLY); + + const result = await callApi('submitTransfer', transferOptions, false); + + global = getGlobal(); + + if (!result || 'error' in result) { + global = updateCurrentSwap(global, { + isLoading: false, + }); + setGlobal(global); + actions.showError({ error: result?.error }); + return; + } + + global = getGlobal(); + global = updateCurrentSwap(global, { + isLoading: false, + state: SwapState.Complete, + activityId: swapItem.activity.id, + }); + setGlobal(global); +}); + +addActionHandler('submitSwapCexToTon', async (global, actions, { password }) => { + if (!(await callApi('verifyPassword', password))) { + setGlobal(updateCurrentSwap( + getGlobal(), + { error: 'Wrong password, please try again' }, + )); + + return; + } + + global = getGlobal(); + global = updateCurrentSwap(global, { + isLoading: true, + error: undefined, + }); + setGlobal(global); + + const swapOptions = getSwapBuildOptions(global); + const swapItem = await callApi( + 'swapCexCreateTransaction', + global.currentAccountId!, + password, + { + from: swapOptions.from, + fromAmount: swapOptions.fromAmount, + fromAddress: swapOptions.fromAddress, + to: swapOptions.to, + toAddress: swapOptions.fromAddress, + swapFee: swapOptions.swapFee, + networkFee: swapOptions.networkFee, + }, + ); + + global = getGlobal(); + + if (!swapItem) { + global = updateCurrentSwap(global, { + isLoading: false, + }); + setGlobal(global); + actions.showError({ error: ApiCommonError.Unexpected }); + return; + } + + global = getGlobal(); + global = updateCurrentSwap(global, { + isLoading: false, + state: SwapState.WaitTokens, + payinAddress: swapItem.swap.cex!.payinAddress, + activityId: swapItem.activity.id, + }); + setGlobal(global); +}); + +addActionHandler('switchSwapTokens', (global) => { + const { + tokenInSlug, tokenOutSlug, amountIn, amountOut, swapType, + } = global.currentSwap; + + const newSwapType = swapType === SwapType.OnChain + ? SwapType.OnChain + : swapType === SwapType.CrosschainFromTon + ? SwapType.CrosschainToTon + : SwapType.CrosschainFromTon; + + global = updateCurrentSwap(global, { + amountIn: amountOut, + amountOut: amountIn, + tokenInSlug: tokenOutSlug, + tokenOutSlug: tokenInSlug, + inputSource: SwapInputSource.In, + swapType: newSwapType, + isEstimating: true, + shouldEstimate: true, + }); + setGlobal(global); +}); + +addActionHandler('setSwapTokenIn', (global, actions, { tokenSlug }) => { + const { tokenInSlug, amountIn, amountOut } = global.currentSwap; + const isFilled = Boolean(amountIn || amountOut); + const oldTokenIn = global.swapTokenInfo!.bySlug[tokenInSlug!]; + const newTokenIn = global.swapTokenInfo!.bySlug[tokenSlug!]; + const amount = amountIn + ? shiftDecimals(amountIn ?? 0, oldTokenIn.decimals, newTokenIn.decimals) + : amountIn; + + global = updateCurrentSwap(global, { + amountIn: amount === 0 ? undefined : amount, + tokenInSlug: tokenSlug, + isEstimating: isFilled, + shouldEstimate: true, + }); + setGlobal(global); +}); + +addActionHandler('setSwapTokenOut', (global, actions, { tokenSlug }) => { + const { tokenOutSlug, amountIn, amountOut } = global.currentSwap; + const isFilled = Boolean(amountIn || amountOut); + const oldTokenOut = global.swapTokenInfo!.bySlug[tokenOutSlug!]; + const newTokenOut = global.swapTokenInfo!.bySlug[tokenSlug!]; + const amount = amountOut + ? shiftDecimals(amountOut ?? 0, oldTokenOut.decimals, newTokenOut.decimals) + : amountOut; + + global = updateCurrentSwap(global, { + amountOut: amount === 0 ? undefined : amount, + tokenOutSlug: tokenSlug, + isEstimating: isFilled, + shouldEstimate: true, + }); + setGlobal(global); +}); + +addActionHandler('setSwapAmountIn', (global, actions, { amount }) => { + const isEstimating = Boolean(amount && amount > 0); + + global = updateCurrentSwap(global, { + amountIn: amount, + inputSource: SwapInputSource.In, + isEstimating, + shouldEstimate: true, + }); + setGlobal(global); +}); + +addActionHandler('setSwapAmountOut', (global, actions, { amount }) => { + const isEstimating = Boolean(amount && amount > 0); + + global = updateCurrentSwap(global, { + amountOut: amount, + inputSource: SwapInputSource.Out, + isEstimating, + shouldEstimate: true, + }); + setGlobal(global); +}); + +addActionHandler('setSlippage', (global, actions, { slippage }) => { + global = updateCurrentSwap(global, { + slippage, + isEstimating: true, + shouldEstimate: true, + }); + setGlobal(global); +}); + +addActionHandler('estimateSwap', async (global, actions, { shouldBlock }) => { + const resetParams = { + amountOutMin: '0', + transactionFee: '0', + swapFee: '0', + networkFee: 0, + realNetworkFee: 0, + priceImpact: 0, + errorType: undefined, + isEstimating: false, + }; + + global = updateCurrentSwap(global, { + shouldEstimate: false, + }); + + // Check for empty string + if ((global.currentSwap.amountIn === undefined && global.currentSwap.inputSource === SwapInputSource.In) + || (global.currentSwap.amountOut === undefined && global.currentSwap.inputSource === SwapInputSource.Out)) { + global = updateCurrentSwap(global, { + amountIn: undefined, + amountOut: undefined, + ...resetParams, + }); + setGlobal(global); + return; + } + + const pairsBySlug = global.currentSwap.pairs?.bySlug ?? {}; + const canSwap = global.currentSwap.tokenOutSlug! in pairsBySlug; + + // Check for invalid pair + if (!canSwap) { + const amount = global.currentSwap.inputSource === SwapInputSource.In + ? { amountOut: undefined } + : { amountIn: undefined }; + + global = updateCurrentSwap(global, { + ...amount, + ...resetParams, + errorType: SwapErrorType.InvalidPair, + }); + setGlobal(global); + return; + } + + global = updateCurrentSwap(global, { + isEstimating: shouldBlock, + }); + setGlobal(global); + + const tokenIn = global.swapTokenInfo!.bySlug[global.currentSwap.tokenInSlug!]; + const tokenOut = global.swapTokenInfo!.bySlug[global.currentSwap.tokenOutSlug!]; + + const from = tokenIn.slug === TON_TOKEN_SLUG ? tokenIn.symbol : tokenIn.contract!; + const to = tokenOut.slug === TON_TOKEN_SLUG ? tokenOut.symbol : tokenOut.contract!; + const fromAmount = safeNumberToString(global.currentSwap.amountIn ?? 0, tokenIn.decimals); + const toAmount = safeNumberToString(global.currentSwap.amountOut ?? 0, tokenOut.decimals); + + const estimateAmount = global.currentSwap.inputSource === SwapInputSource.In ? { fromAmount } : { toAmount }; + + const estimate = await callApi('swapEstimate', { + ...estimateAmount, + from, + to, + slippage: global.currentSwap.slippage, + }); + + global = getGlobal(); + + if (!estimate || 'error' in estimate) { + if (estimate?.error === 'Insufficient liquidity') { + global = updateCurrentSwap(global, { + ...resetParams, + errorType: SwapErrorType.NotEnoughLiquidity, + }); + setGlobal(global); + return; + } + + global = updateCurrentSwap(global, { + ...resetParams, + errorType: SwapErrorType.InvalidPair, + }); + setGlobal(global); + return; + } + + // Check for outdated response + if ( + (global.currentSwap.inputSource === SwapInputSource.In + && global.currentSwap.amountIn !== Number(estimate.fromAmount)) + || (global.currentSwap.inputSource === SwapInputSource.Out + && global.currentSwap.amountOut !== Number(estimate.toAmount)) + ) { + global = updateCurrentSwap(global, { + ...resetParams, + }); + setGlobal(global); + return; + } + + global = updateCurrentSwap(global, { + ...( + global.currentSwap.inputSource === SwapInputSource.In + ? { amountOut: Number(estimate.toAmount) } + : { amountIn: Number(estimate.fromAmount) } + ), + amountOutMin: estimate.toMinAmount, + networkFee: estimate.networkFee, + realNetworkFee: estimate.realNetworkFee, + swapFee: estimate.swapFee, + priceImpact: estimate.impact, + dexLabel: estimate.dexLabel, + feeSource: SwapFeeSource.In, + isEstimating: false, + errorType: undefined, + }); + setGlobal(global); +}); + +addActionHandler('estimateSwapCex', async (global, actions, { shouldBlock }) => { + const amount = global.currentSwap.inputSource === SwapInputSource.In + ? { amountOut: undefined } + : { amountIn: undefined }; + + const resetParams = { + ...amount, + amountOutMin: '0', + transactionFee: '0', + swapFee: '0', + networkFee: 0, + realNetworkFee: 0, + priceImpact: 0, + errorType: undefined, + isEstimating: false, + }; + + global = updateCurrentSwap(global, { + shouldEstimate: false, + transactionFee: '0', + swapFee: '0', + networkFee: 0, + priceImpact: 0, + }); + + // Check for empty string + if ((global.currentSwap.amountIn === undefined && global.currentSwap.inputSource === SwapInputSource.In) + || (global.currentSwap.amountOut === undefined && global.currentSwap.inputSource === SwapInputSource.Out)) { + global = updateCurrentSwap(global, { + amountIn: undefined, + amountOut: undefined, + ...resetParams, + }); + setGlobal(global); + return; + } + + const pairsBySlug = global.currentSwap.pairs?.bySlug ?? {}; + const canSwap = global.currentSwap.tokenOutSlug! in pairsBySlug; + + // Check for invalid pair + if (!canSwap) { + global = updateCurrentSwap(global, { + ...resetParams, + errorType: SwapErrorType.InvalidPair, + }); + setGlobal(global); + return; + } + + global = updateCurrentSwap(global, { + isEstimating: shouldBlock, + }); + setGlobal(global); + + const tokenIn = global.swapTokenInfo!.bySlug[global.currentSwap.tokenInSlug!]; + const tokenOut = global.swapTokenInfo!.bySlug[global.currentSwap.tokenOutSlug!]; + + const from = tokenIn.slug === TON_TOKEN_SLUG ? tokenIn.symbol : tokenIn.contract!; + const to = tokenOut.slug === TON_TOKEN_SLUG ? tokenOut.symbol : tokenOut.contract!; + const fromAmount = safeNumberToString(global.currentSwap.amountIn ?? 0, tokenIn.decimals); + + const estimate = await callApi('swapCexEstimate', { + fromAmount, + from, + to, + }); + + global = getGlobal(); + + if (!estimate) { + global = updateCurrentSwap(global, { + ...resetParams, + errorType: SwapErrorType.InvalidPair, + }); + setGlobal(global); + return; + } + + const isLessThanMin = Big(fromAmount).lt(estimate.fromMin); + const isBiggerThanMax = Big(fromAmount).gt(estimate.fromMax); + + if (isLessThanMin || isBiggerThanMax) { + global = updateCurrentSwap(global, { + ...resetParams, + limits: { + fromMin: estimate.fromMin, + fromMax: estimate.fromMax, + }, + errorType: isLessThanMin ? SwapErrorType.ChangellyMinSwap : SwapErrorType.ChangellyMaxSwap, + }); + setGlobal(global); + return; + } + + // Check for outdated response + if ( + (global.currentSwap.inputSource === SwapInputSource.In + && global.currentSwap.amountIn !== Number(estimate.fromAmount)) + || (global.currentSwap.inputSource === SwapInputSource.Out + && global.currentSwap.amountOut !== Number(estimate.toAmount)) + ) { + global = updateCurrentSwap(global, { + ...resetParams, + }); + setGlobal(global); + return; + } + + let networkFee = 0; + + if (global.currentSwap.swapType === SwapType.CrosschainFromTon) { + const account = global.accounts?.byId[global.currentAccountId!]; + + const txDraft = await callApi( + 'checkTransactionDraft', + global.currentAccountId!, + global.currentSwap.tokenInSlug!, + account?.address!, + humanToBigStr(global.currentSwap.amountIn ?? 0, tokenIn.decimals), + ); + networkFee = bigStrToHuman(txDraft?.fee ?? '0'); + } + + global = getGlobal(); + + const realAmountOut = Big(estimate.toAmount); + + global = updateCurrentSwap(global, { + amountOut: realAmountOut.eq(0) ? undefined : Number(realAmountOut.toFixed(tokenOut.decimals)), + limits: { + fromMin: estimate.fromMin, + fromMax: estimate.fromMax, + }, + swapFee: estimate.swapFee, + networkFee, + realNetworkFee: networkFee, + amountOutMin: String(realAmountOut), + isEstimating: false, + errorType: undefined, + }); + setGlobal(global); +}); + +addActionHandler('setSwapScreen', (global, actions, { state }) => { + global = updateCurrentSwap(global, { state }); + setGlobal(global); +}); + +addActionHandler('clearSwapError', (global) => { + global = updateCurrentSwap(global, { error: undefined }); + setGlobal(global); +}); + +addActionHandler('setSwapType', (global, actions, { type }) => { + global = updateCurrentSwap(global, { swapType: type }); + setGlobal(global); +}); + +addActionHandler('loadSwapPairs', async (global, actions, { tokenSlug, shouldForceUpdate }) => { + const tokenIn = global.swapTokenInfo?.bySlug[tokenSlug]; + if (!tokenIn) { + return; + } + + const symbolOrMinter = tokenIn.slug === TON_TOKEN_SLUG ? tokenIn.symbol : tokenIn.contract!; + + const cache = PAIRS_CACHE[tokenSlug]; + const isCacheValid = cache && (Date.now() - cache.timestamp <= CACHE_DURATION); + if (isCacheValid && !shouldForceUpdate) { + return; + } + + const pairs = await callApi('swapGetPairs', symbolOrMinter); + global = getGlobal(); + + if (!pairs) { + global = updateCurrentSwap(global, { + pairs: { + bySlug: { + ...global.currentSwap.pairs?.bySlug, + [tokenSlug]: {}, + }, + }, + }); + setGlobal(global); + return; + } + + const bySlug = pairs.reduce((acc, pair) => { + acc[pair.slug] = pair.isReverseProhibited ? { + isReverseProhibited: pair.isReverseProhibited, + } : {}; + return acc; + }, {} as AssetPairs); + + PAIRS_CACHE[tokenSlug] = { data: pairs, timestamp: Date.now() }; + + global = updateCurrentSwap(global, { + pairs: { + bySlug: { + ...global.currentSwap.pairs?.bySlug, + [tokenSlug]: bySlug, + }, + }, + shouldEstimate: true, + }); + setGlobal(global); +}); + +addActionHandler('setSwapCexAddress', (global, actions, { toAddress }) => { + global = updateCurrentSwap(global, { toAddress }); + setGlobal(global); +}); diff --git a/src/global/actions/api/wallet.ts b/src/global/actions/api/wallet.ts index 501205fc..68c5f600 100644 --- a/src/global/actions/api/wallet.ts +++ b/src/global/actions/api/wallet.ts @@ -1,50 +1,69 @@ -import type { ApiDappTransaction, ApiToken } from '../../../api/types'; -import type { UserToken } from '../../types'; +import type { + ApiActivity, ApiBaseToken, ApiDappTransaction, ApiSwapAsset, ApiToken, +} from '../../../api/types'; +import type { UserSwapToken, UserToken } from '../../types'; import { ApiTransactionDraftError } from '../../../api/types'; -import { TransferState } from '../../types'; +import { ActiveTab, TransferState } from '../../types'; +import { IS_CAPACITOR } from '../../../config'; +import { vibrateOnError, vibrateOnSuccess } from '../../../util/capacitor'; +import { compareActivities } from '../../../util/compareActivities'; import { - buildCollectionByKey, findLast, mapValues, unique, + buildCollectionByKey, findLast, mapValues, pick, unique, } from '../../../util/iteratees'; -import { pause } from '../../../util/schedulers'; +import { onTickEnd, pause } from '../../../util/schedulers'; +import { IS_DELEGATED_BOTTOM_SHEET } from '../../../util/windowEnvironment'; import { callApi } from '../../../api'; import { ApiUserRejectsError } from '../../../api/errors'; import { - bigStrToHuman, getIsSwapId, getIsTxIdLocal, humanToBigStr, + getIsSwapId, getIsTinyTransaction, getIsTxIdLocal, humanToBigStr, } from '../../helpers'; import { addActionHandler, getActions, getGlobal, setGlobal, } from '../../index'; import { clearCurrentTransfer, + clearIsPinPadPasswordAccepted, updateAccountState, updateActivitiesIsHistoryEndReached, updateActivitiesIsLoading, updateCurrentAccountState, updateCurrentSignature, updateCurrentTransfer, + updateIsPinPadPasswordAccepted, updateSendingLoading, updateSettings, } from '../../reducers'; import { - selectAccount, selectAccountSettings, selectAccountState, selectCurrentAccountState, selectLastTxIds, + selectAccount, + selectAccountSettings, + selectAccountState, + selectCurrentAccountState, + selectLastTxIds, } from '../../selectors'; +import { callActionInMain } from '../../../hooks/useDelegatedBottomSheet'; + const IMPORT_TOKEN_PAUSE = 250; addActionHandler('startTransfer', (global, actions, payload) => { - const { - tokenSlug, toAddress, amount, comment, - } = payload || {}; + const isOpen = global.currentTransfer.state !== TransferState.None; + if (IS_DELEGATED_BOTTOM_SHEET && !isOpen) { + callActionInMain('startTransfer', payload); + return; + } + + const { isPortrait, ...rest } = payload ?? {}; setGlobal(updateCurrentTransfer(global, { - state: TransferState.Initial, + state: isPortrait ? TransferState.Initial : TransferState.None, error: undefined, - toAddress, - amount, - comment, - tokenSlug, + ...rest, })); + + if (!isPortrait) { + actions.setLandscapeActionsActiveTabIndex({ index: ActiveTab.Transfer }); + } }); addActionHandler('changeTransferToken', (global, actions, { tokenSlug }) => { @@ -134,7 +153,6 @@ addActionHandler('submitTransferInitial', async (global, actions, payload) => { error: undefined, toAddress, resolvedAddress: result.resolvedAddress, - normalizedAddress: result.normalizedAddress, amount, comment, shouldEncrypt, @@ -180,8 +198,7 @@ addActionHandler('submitTransferConfirm', (global, actions) => { setGlobal(global); }); -addActionHandler('submitTransferPassword', async (global, actions, payload) => { - const { password } = payload; +addActionHandler('submitTransferPassword', async (global, actions, { password }) => { const { resolvedAddress, comment, @@ -199,10 +216,19 @@ addActionHandler('submitTransferPassword', async (global, actions, payload) => { return; } - setGlobal(updateCurrentTransfer(getGlobal(), { + global = getGlobal(); + global = updateCurrentTransfer(getGlobal(), { isLoading: true, error: undefined, - })); + }); + if (IS_CAPACITOR) { + global = updateIsPinPadPasswordAccepted(global); + } + setGlobal(global); + + if (IS_CAPACITOR) { + await vibrateOnSuccess(true); + } if (promiseId) { void callApi('confirmDappRequest', promiseId, password); @@ -222,12 +248,21 @@ addActionHandler('submitTransferPassword', async (global, actions, payload) => { const result = await callApi('submitTransfer', options); - setGlobal(updateCurrentTransfer(getGlobal(), { + global = getGlobal(); + global = updateCurrentTransfer(global, { isLoading: false, - })); + }); + setGlobal(global); if (!result || 'error' in result) { + if (IS_CAPACITOR) { + global = clearIsPinPadPasswordAccepted(global); + setGlobal(global); + void vibrateOnError(); + } actions.showError({ error: result?.error }); + } else if (IS_CAPACITOR) { + void vibrateOnSuccess(); } }); @@ -305,47 +340,73 @@ addActionHandler('clearTransferError', (global) => { setGlobal(updateCurrentTransfer(global, { error: undefined })); }); -addActionHandler('cancelTransfer', (global) => { +addActionHandler('cancelTransfer', (global, actions, { shouldReset } = {}) => { const { promiseId, tokenSlug } = global.currentTransfer; - if (promiseId) { - void callApi('cancelDappRequest', promiseId, 'Canceled by the user'); - } + if (shouldReset) { + if (promiseId) { + void callApi('cancelDappRequest', promiseId, 'Canceled by the user'); + } - global = clearCurrentTransfer(global); - global = updateCurrentTransfer(global, { tokenSlug }); + global = clearCurrentTransfer(global); + global = updateCurrentTransfer(global, { tokenSlug }); + + setGlobal(global); + return; + } + if (IS_CAPACITOR) { + global = clearIsPinPadPasswordAccepted(global); + } + global = updateCurrentTransfer(global, { state: TransferState.None }); setGlobal(global); }); -addActionHandler('fetchTokenTransactions', async (global, actions, payload) => { - const { limit } = payload || {}; - const slug = payload.slug; - +addActionHandler('fetchTokenTransactions', async (global, actions, { limit, slug, shouldLoadWithBudget }) => { global = updateActivitiesIsLoading(global, true); setGlobal(global); let { idsBySlug } = selectCurrentAccountState(global)?.activities || {}; + let shouldFetchMore = true; + let fetchedActivities: ApiActivity[] = []; let tokenIds = (idsBySlug && idsBySlug[slug]) || []; + let offsetId = findLast(tokenIds, (id) => !getIsTxIdLocal(id) && !getIsSwapId(id)); - const offsetId = findLast(tokenIds, (id) => !getIsTxIdLocal(id) && !getIsSwapId(id)); + while (shouldFetchMore) { + const result = await callApi('fetchTokenActivitySlice', global.currentAccountId!, slug, offsetId, limit); - const result = await callApi('fetchTokenActivitySlice', global.currentAccountId!, slug, offsetId, limit); - global = getGlobal(); - global = updateActivitiesIsLoading(global, false); + global = getGlobal(); - if (!result || !result.length) { - global = updateActivitiesIsHistoryEndReached(global, true); - setGlobal(global); - return; + if (!result || 'error' in result) { + break; + } + + if (!result.length) { + global = updateActivitiesIsHistoryEndReached(global, true, slug); + break; + } + + const filteredResult = global.settings.areTinyTransfersHidden + ? result.filter((tx) => tx.kind === 'transaction' && !getIsTinyTransaction(tx)) + : result; + + fetchedActivities = fetchedActivities.concat(result); + shouldFetchMore = filteredResult.length < limit && fetchedActivities.length < limit; + + tokenIds = unique(tokenIds.concat(filteredResult.map((tx) => tx.id))); + offsetId = findLast(tokenIds, (id) => !getIsTxIdLocal(id) && !getIsSwapId(id)); } - const newById = buildCollectionByKey(result, 'id'); + fetchedActivities.sort((a, b) => compareActivities(a, b, false)); + + global = updateActivitiesIsLoading(global, false); + + const newById = buildCollectionByKey(fetchedActivities, 'id'); const newOrderedIds = Object.keys(newById); const currentActivities = selectCurrentAccountState(global)?.activities; idsBySlug = currentActivities?.idsBySlug || {}; - tokenIds = unique(idsBySlug[slug] || []).concat(newOrderedIds); + tokenIds = unique((idsBySlug[slug] || []).concat(newOrderedIds)); global = updateCurrentAccountState(global, { activities: { @@ -356,34 +417,65 @@ addActionHandler('fetchTokenTransactions', async (global, actions, payload) => { }); setGlobal(global); -}); -addActionHandler('fetchAllTransactions', async (global, actions, payload) => { - const { limit } = payload || {}; + if (shouldLoadWithBudget) { + onTickEnd(() => { + actions.fetchTokenTransactions({ limit, slug }); + }); + } +}); +addActionHandler('fetchAllTransactions', async (global, actions, { limit, shouldLoadWithBudget }) => { global = updateActivitiesIsLoading(global, true); setGlobal(global); const accountId = global.currentAccountId!; - const lastTxIds = selectLastTxIds(global, accountId); - const result = await callApi('fetchAllActivitySlice', global.currentAccountId!, lastTxIds, limit); - global = getGlobal(); - global = updateActivitiesIsLoading(global, false); + let lastTxIds = selectLastTxIds(global, accountId); + let shouldFetchMore = true; + let fetchedActivities: ApiActivity[] = []; - if (!result || !result.length) { - global = updateActivitiesIsHistoryEndReached(global, true); - setGlobal(global); - return; + while (shouldFetchMore) { + const result = await callApi('fetchAllActivitySlice', accountId, lastTxIds, limit); + + global = getGlobal(); + + if (!result || 'error' in result) { + break; + } + + if (!result.length) { + global = updateActivitiesIsHistoryEndReached(global, true); + break; + } + + const filteredResult = global.settings.areTinyTransfersHidden + ? result.filter((tx) => tx.kind === 'transaction' && !getIsTinyTransaction(tx)) + : result; + + fetchedActivities = fetchedActivities.concat(result); + shouldFetchMore = filteredResult.length < limit && fetchedActivities.length < limit; + + lastTxIds = selectLastTxIds(global, accountId); } - const newById = buildCollectionByKey(result, 'id'); + global = updateActivitiesIsLoading(global, false); + + const newById = buildCollectionByKey(fetchedActivities, 'id'); const currentActivities = selectCurrentAccountState(global)?.activities; let idsBySlug = { ...currentActivities?.idsBySlug }; - idsBySlug = result.reduce((acc, activity) => { - const { id, slug } = activity; - acc[slug] = (acc[slug] || []).concat([id]); + fetchedActivities = fetchedActivities.sort((a, b) => compareActivities(a, b, false)); + + idsBySlug = fetchedActivities.reduce((acc, activity) => { + if (activity.kind === 'swap') { + const { id, from, to } = activity; + acc[from] = (acc[from] || []).concat([id]); + acc[to] = (acc[to] || []).concat([id]); + } else { + const { id, slug } = activity; + acc[slug] = (acc[slug] || []).concat([id]); + } return acc; }, idsBySlug); @@ -398,15 +490,21 @@ addActionHandler('fetchAllTransactions', async (global, actions, payload) => { }); setGlobal(global); + + if (shouldLoadWithBudget) { + onTickEnd(() => { + actions.fetchAllTransactions({ limit }); + }); + } }); -addActionHandler('resetIsHistoryEndReached', (global) => { - global = updateActivitiesIsHistoryEndReached(global, false); +addActionHandler('resetIsHistoryEndReached', (global, actions, payload) => { + global = updateActivitiesIsHistoryEndReached(global, false, payload?.slug); setGlobal(global); }); addActionHandler('setIsBackupRequired', (global, actions, { isMnemonicChecked }) => { - const { isBackupRequired } = selectCurrentAccountState(global); + const { isBackupRequired } = selectCurrentAccountState(global) ?? {}; setGlobal(updateCurrentAccountState(global, { isBackupRequired: isMnemonicChecked ? undefined : isBackupRequired, @@ -447,14 +545,17 @@ addActionHandler('cancelSignature', (global) => { addActionHandler('addToken', (global, actions, { token }) => { const accountId = global.currentAccountId!; + const { areTokensWithNoPriceHidden, areTokensWithNoBalanceHidden } = global.settings; const { balances } = selectAccountState(global, accountId) || {}; const accountSettings = selectAccountSettings(global, accountId) ?? {}; + const { orderedSlugs = [], exceptionSlugs = [], deletedSlugs } = accountSettings; if (balances?.bySlug[token.slug]) { return; } - const apiToken: ApiToken = { + const existingToken = global.tokenInfo?.bySlug?.[token.slug]; + const apiToken: ApiToken = existingToken ?? { name: token.name, symbol: token.symbol, slug: token.slug, @@ -473,6 +574,13 @@ addActionHandler('addToken', (global, actions, { token }) => { }, }; + const exceptionSlugsCopy = exceptionSlugs.slice(); + const deletedSlugsCopy = deletedSlugs?.filter((slug) => slug !== token.slug); + + if ((areTokensWithNoBalanceHidden && token.amount === 0) || (areTokensWithNoPriceHidden && token.price === 0)) { + exceptionSlugsCopy.push(token.slug); + } + global = updateAccountState(global, accountId, { balances: { ...balances, @@ -489,9 +597,11 @@ addActionHandler('addToken', (global, actions, { token }) => { [accountId]: { ...accountSettings, orderedSlugs: [ - ...accountSettings.orderedSlugs ?? [], + ...orderedSlugs ?? [], apiToken.slug, ], + exceptionSlugs: exceptionSlugsCopy, + deletedSlugs: deletedSlugsCopy, }, }, }); @@ -506,75 +616,77 @@ addActionHandler('addToken', (global, actions, { token }) => { }, }, }); - - actions.toggleDisabledToken({ slug: apiToken.slug }); }); -addActionHandler('importToken', async (global, actions, { address }) => { - setGlobal( - updateSettings(global, { - importToken: { - isLoading: true, - token: undefined, - }, - }), - ); +addActionHandler('importToken', async (global, actions, { address, isSwap }) => { + global = updateSettings(global, { + importToken: { + isLoading: true, + token: undefined, + }, + }); + setGlobal(global); + + const slug = (await callApi('buildTokenSlug', address))!; + global = getGlobal(); - const baseToken = await callApi('importToken', global.currentAccountId!, address); - await pause(IMPORT_TOKEN_PAUSE); + let token: ApiToken | ApiBaseToken | undefined = global.tokenInfo.bySlug?.[slug!]; - if (!baseToken) { - global = getGlobal(); - setGlobal( - updateSettings(global, { + if (!token) { + token = await callApi('fetchToken', global.currentAccountId!, address); + await pause(IMPORT_TOKEN_PAUSE); + + if (!token) { + global = getGlobal(); + global = updateSettings(global, { importToken: { isLoading: false, token: undefined, }, - }), - ); - return; + }); + setGlobal(global); + return; + } } - const { - slug, symbol, name, image, decimals, keywords, - } = baseToken; - const amount = bigStrToHuman('0', decimals); - - const token: UserToken = { - symbol, - slug, - amount, - name, - image, - decimals, + const userToken: UserToken | UserSwapToken = { + ...pick(token, [ + 'symbol', + 'slug', + 'name', + 'image', + 'decimals', + 'keywords', + ]), + amount: 0, price: 0, change24h: 0, change7d: 0, change30d: 0, - keywords, + ...(isSwap && { + blockchain: 'ton', + contract: token.minterAddress, + }), }; global = getGlobal(); - setGlobal( - updateSettings(global, { - importToken: { - isLoading: false, - token, - }, - }), - ); + global = updateSettings(global, { + importToken: { + isLoading: false, + token: userToken, + }, + }); + setGlobal(global); }); addActionHandler('resetImportToken', (global) => { - setGlobal( - updateSettings(global, { - importToken: { - isLoading: false, - token: undefined, - }, - }), - ); + global = updateSettings(global, { + importToken: { + isLoading: false, + token: undefined, + }, + }); + setGlobal(global); }); addActionHandler('verifyHardwareAddress', async (global) => { @@ -594,9 +706,33 @@ addActionHandler('verifyHardwareAddress', async (global) => { } }); -addActionHandler('setActiveContentTabIndex', (global, actions, { index }) => { +addActionHandler('setActiveContentTab', (global, actions, { tab }) => { global = updateCurrentAccountState(global, { - activeContentTabIndex: index, + activeContentTab: tab, }); setGlobal(global); }); + +addActionHandler('addSwapToken', (global, actions, { token }) => { + const apiSwapAsset: ApiSwapAsset = { + name: token.name, + symbol: token.symbol, + blockchain: token.blockchain, + slug: token.slug, + decimals: token.decimals, + image: token.image, + contract: token.contract, + keywords: token.keywords, + }; + + setGlobal({ + ...global, + swapTokenInfo: { + ...global.swapTokenInfo, + bySlug: { + ...global.swapTokenInfo.bySlug, + [apiSwapAsset.slug]: apiSwapAsset, + }, + }, + }); +}); diff --git a/src/global/actions/apiUpdates/activities.ts b/src/global/actions/apiUpdates/activities.ts index 9bebd1c1..4a9f4715 100644 --- a/src/global/actions/apiUpdates/activities.ts +++ b/src/global/actions/apiUpdates/activities.ts @@ -1,16 +1,21 @@ import { TransferState } from '../../types'; +import { IS_CAPACITOR } from '../../../config'; import { playIncomingTransactionSound } from '../../../util/appSounds'; +import { compareActivities } from '../../../util/compareActivities'; import { bigStrToHuman, getIsTinyTransaction } from '../../helpers'; import { addActionHandler, setGlobal } from '../../index'; import { - removeTransaction, + addLocalTransaction, + assignRemoteTxId, + clearIsPinPadPasswordAccepted, + removeLocalTransaction, updateAccountState, updateActivitiesIsLoadingByAccount, updateActivity, updateCurrentTransfer, } from '../../reducers'; -import { selectAccountState } from '../../selectors'; +import { selectAccountState, selectLocalTransactions } from '../../selectors'; const TX_AGE_TO_PLAY_SOUND = 60000; // 1 min @@ -20,25 +25,23 @@ addActionHandler('apiUpdate', (global, actions, update) => { const { accountId, transaction, - transaction: { amount, toAddress, txId }, + transaction: { amount, txId }, } = update; const { decimals } = global.tokenInfo!.bySlug[transaction.slug!]!; global = updateActivity(global, accountId, transaction); + global = addLocalTransaction(global, accountId, transaction); - if ( - -bigStrToHuman(amount, decimals) === global.currentTransfer.amount - && toAddress === global.currentTransfer.normalizedAddress - ) { + // TODO $decimal + if ((-bigStrToHuman(amount, decimals)).toFixed(decimals) === global.currentTransfer.amount?.toFixed(decimals)) { global = updateCurrentTransfer(global, { txId, state: TransferState.Complete, isLoading: false, }); - } - - if (transaction.type === 'stake' || transaction.type === 'unstake') { - actions.fetchStakingState(); + if (IS_CAPACITOR) { + global = clearIsPinPadPasswordAccepted(global); + } } setGlobal(global); @@ -47,60 +50,67 @@ addActionHandler('apiUpdate', (global, actions, update) => { } case 'newActivities': { - const { activities, accountId } = update; + const { accountId } = update; + const activities = update.activities.sort((a, b) => compareActivities(a, b, true)); + let shouldPlaySound = false; - let wasStakingTransaction = false; global = updateActivitiesIsLoadingByAccount(global, accountId, false); + const localTransactions = selectLocalTransactions(global, accountId) ?? []; + for (const activity of activities) { + if (activity.kind === 'transaction') { + const localTransaction = localTransactions.find(({ amount, isIncoming, slug }) => { + return amount === activity.amount && !isIncoming && slug === activity.slug; + }); + + if (localTransaction) { + const { txId } = activity; + const localTxId = localTransaction.txId; + global = assignRemoteTxId(global, accountId, localTxId, txId); + + if (global.currentTransfer.txId === localTxId) { + global = updateCurrentTransfer(global, { txId }); + } + + const { currentActivityId } = selectAccountState(global, accountId) || {}; + if (currentActivityId === localTxId) { + global = updateAccountState(global, accountId, { currentActivityId: txId }); + } + + global = removeLocalTransaction(global, accountId, localTxId); + + continue; + } + } + global = updateActivity(global, accountId, activity); + if (activity.kind === 'swap') { + continue; + } + if ( activity.isIncoming && global.settings.canPlaySounds && (Date.now() - activity.timestamp < TX_AGE_TO_PLAY_SOUND) - && ( - !global.settings.areTinyTransfersHidden - || getIsTinyTransaction(activity, global.tokenInfo?.bySlug[activity.slug!]) + && !( + global.settings.areTinyTransfersHidden + && getIsTinyTransaction(activity, global.tokenInfo?.bySlug[activity.slug!]) ) ) { shouldPlaySound = true; } - - if (activity.type === 'stake' || activity.type === 'unstake') { - wasStakingTransaction = true; - } } if (shouldPlaySound) { playIncomingTransactionSound(); } - if (wasStakingTransaction) { - actions.fetchStakingState(); - } - setGlobal(global); break; } - - case 'updateTxComplete': { - const { txId, localTxId, accountId } = update; - - global = removeTransaction(global, accountId, localTxId); - - if (global.currentTransfer.txId === localTxId) { - global = updateCurrentTransfer(global, { txId }); - } - - const { currentActivityId } = selectAccountState(global, accountId) || {}; - if (currentActivityId === localTxId) { - global = updateAccountState(global, accountId, { currentActivityId: txId }); - } - - setGlobal(global); - } } }); diff --git a/src/global/actions/apiUpdates/dapp.ts b/src/global/actions/apiUpdates/dapp.ts index 6da5a257..299c2cae 100644 --- a/src/global/actions/apiUpdates/dapp.ts +++ b/src/global/actions/apiUpdates/dapp.ts @@ -1,9 +1,8 @@ import { DappConnectState, TransferState } from '../../types'; import { TON_TOKEN_SLUG } from '../../../config'; -import { callApi } from '../../../api'; import { bigStrToHuman } from '../../helpers'; -import { addActionHandler, getGlobal, setGlobal } from '../../index'; +import { addActionHandler, setGlobal } from '../../index'; import { clearCurrentDappTransfer, clearCurrentSignature, @@ -15,9 +14,11 @@ import { updateDappConnectRequest, } from '../../reducers'; import { - selectAccount, selectAccountState, selectIsHardwareAccount, selectNewestTxIds, + selectAccountState, } from '../../selectors'; +import { callActionInNative } from '../../../hooks/useDelegatedBottomSheet'; + addActionHandler('apiUpdate', (global, actions, update) => { switch (update.type) { case 'createTransaction': { @@ -71,28 +72,8 @@ addActionHandler('apiUpdate', (global, actions, update) => { } case 'dappConnect': { - const { - promiseId, - dapp, - accountId, - permissions, - proof, - } = update; - - const { isHardware } = selectAccount(global, accountId)!; - - global = updateDappConnectRequest(global, { - state: DappConnectState.Info, - promiseId, - accountId, - dapp, - permissions: { - isAddressRequired: permissions.address, - isPasswordRequired: permissions.proof && !isHardware, - }, - proof, - }); - setGlobal(global); + actions.apiUpdateDappConnect(update); + callActionInNative('apiUpdateDappConnect', update); break; } @@ -121,41 +102,25 @@ addActionHandler('apiUpdate', (global, actions, update) => { break; } - case 'dappSendTransactions': { - const { - promiseId, - transactions, - fee, - accountId, - dapp, - } = update; + case 'dappLoading': { + const { connectionType } = update; - (async () => { - const { currentAccountId } = global; - if (currentAccountId !== accountId) { - const newestTxIds = selectNewestTxIds(global, accountId); - await callApi('activateAccount', accountId, newestTxIds); - setGlobal({ - ...getGlobal(), - currentAccountId: accountId, - }); - } - - const state = selectIsHardwareAccount(global) && transactions.length > 1 - ? TransferState.WarningHardware - : TransferState.Initial; - - global = getGlobal(); - global = clearCurrentDappTransfer(global); + if (connectionType === 'connect') { + global = updateDappConnectRequest(global, { + state: DappConnectState.Info, + }); + } else if (connectionType === 'sendTransaction') { global = updateCurrentDappTransfer(global, { - state, - promiseId, - transactions, - fee, - dapp, + state: TransferState.Initial, }); - setGlobal(global); - })(); + } + setGlobal(global); + break; + } + + case 'dappSendTransactions': { + actions.apiUpdateDappSendTransaction(update); + callActionInNative('apiUpdateDappSendTransaction', update); break; } diff --git a/src/global/actions/apiUpdates/initial.ts b/src/global/actions/apiUpdates/initial.ts index 2df95205..3292bcce 100644 --- a/src/global/actions/apiUpdates/initial.ts +++ b/src/global/actions/apiUpdates/initial.ts @@ -1,14 +1,23 @@ -import { buildCollectionByKey } from '../../../util/iteratees'; +import { StakingState } from '../../types'; + +import { buildCollectionByKey, pick } from '../../../util/iteratees'; import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { addNft, removeNft, + updateAccount, + updateAccountStakingState, updateAccountState, updateBalance, + updateBalances, updateNft, - updatePoolState, + updateSettings, + updateStaking, + updateStakingInfo, + updateSwapTokens, updateTokens, } from '../../reducers'; +import { selectAccountState } from '../../selectors'; addActionHandler('apiUpdate', (global, actions, update) => { switch (update.type) { @@ -16,28 +25,108 @@ addActionHandler('apiUpdate', (global, actions, update) => { global = updateBalance(global, update.accountId, update.slug, update.balance); setGlobal(global); + actions.updateDeletionListForActiveTokens({ accountId: update.accountId }); break; } - case 'updateStakingState': { - global = updateAccountState(global, update.accountId, { - stakingBalance: update.stakingState.amount + update.stakingState.pendingDepositAmount, - isUnstakeRequested: update.stakingState.isUnstakeRequested, - }, true); + case 'updateBalances': { + global = updateBalances(global, update.accountId, update.balancesToUpdate); setGlobal(global); + + actions.updateDeletionListForActiveTokens({ accountId: update.accountId }); break; } - case 'updateBackendStakingState': { - const { poolState } = update.backendStakingState; - global = updatePoolState(global, poolState, true); + case 'updateStaking': { + const { + accountId, + stakingCommonData, + stakingState, + backendStakingState, + } = update; + + const oldBalance = selectAccountState(global, accountId)?.staking?.balance ?? 0; + let balance = 0; + + if (stakingState.type === 'nominators') { + balance = stakingState.amount + stakingState.pendingDepositAmount; + global = updateAccountStakingState(global, accountId, { + type: stakingState.type, + balance, + isUnstakeRequested: stakingState.isUnstakeRequested, + unstakeRequestedAmount: undefined, + apy: backendStakingState.nominatorsPool.apy, + start: backendStakingState.nominatorsPool.start, + end: backendStakingState.nominatorsPool.end, + totalProfit: backendStakingState.totalProfit, + }, true); + } else { + const isPrevRoundUnlocked = Date.now() > stakingCommonData.prevRound.unlock; + const state = { + start: isPrevRoundUnlocked ? stakingCommonData.round.start : stakingCommonData.prevRound.start, + end: isPrevRoundUnlocked ? stakingCommonData.round.unlock : stakingCommonData.prevRound.unlock, + apy: stakingCommonData.liquid.apy, + totalProfit: backendStakingState.totalProfit, + }; + + if (stakingState.type === 'liquid') { + balance = stakingState.amount; + global = updateAccountStakingState(global, accountId, { + type: stakingState.type, + balance, + isUnstakeRequested: !!stakingState.unstakeRequestAmount, + unstakeRequestedAmount: Number(stakingState.unstakeRequestAmount), + ...state, + }, true); + } else { + balance = 0; + global = updateAccountStakingState(global, accountId, { + type: 'liquid', + balance, + isUnstakeRequested: false, + unstakeRequestedAmount: undefined, + ...state, + }, true); + } + } + + let shouldOpenStakingInfo = false; + if (balance !== oldBalance && global.staking.state !== StakingState.None) { + if (balance === 0) { + global = updateStaking(global, { state: StakingState.StakeInitial }); + } else if (oldBalance === 0) { + shouldOpenStakingInfo = true; + } + } + + global = updateStakingInfo(global, { + liquid: { + instantAvailable: stakingCommonData.liquid.available, + }, + }); setGlobal(global); + if (shouldOpenStakingInfo) { + actions.cancelStaking(); + actions.openStakingInfo(); + } break; } case 'updateTokens': { - global = updateTokens(global, update.tokens, true); + const { tokens, baseCurrency } = update; + global = updateTokens(global, tokens, true); + global = updateSettings(global, { + baseCurrency, + }); + setGlobal(global); + + actions.updateDeletionListForActiveTokens(); + break; + } + + case 'updateSwapTokens': { + global = updateSwapTokens(global, update.tokens); setGlobal(global); break; @@ -79,7 +168,12 @@ addActionHandler('apiUpdate', (global, actions, update) => { setGlobal(global); break; } - } - actions.initTokensOrder(); + case 'updateAccount': { + const { accountId, partial } = update; + global = updateAccount(global, accountId, pick(partial, ['address'])); + setGlobal(global); + break; + } + } }); diff --git a/src/global/actions/index.ts b/src/global/actions/index.ts index 22c15c5c..0b2f97ce 100644 --- a/src/global/actions/index.ts +++ b/src/global/actions/index.ts @@ -3,9 +3,11 @@ import './api/auth'; import './api/wallet'; import './api/staking'; import './api/dapps'; +import './api/swap'; import './apiUpdates/initial'; import './apiUpdates/activities'; import './apiUpdates/dapp'; import './ui/initial'; import './ui/misc'; import './ui/dapps'; +import './ui/settings'; diff --git a/src/global/actions/ui/initial.ts b/src/global/actions/ui/initial.ts index 7996608a..cb7e1a29 100644 --- a/src/global/actions/ui/initial.ts +++ b/src/global/actions/ui/initial.ts @@ -1,17 +1,26 @@ import type { Account, AccountState, NotificationType } from '../../types'; -import { ApiTransactionDraftError, ApiTransactionError } from '../../../api/types'; +import { ApiCommonError, ApiTransactionDraftError, ApiTransactionError } from '../../../api/types'; -import { IS_ELECTRON, IS_EXTENSION } from '../../../config'; +import { IS_EXTENSION } from '../../../config'; import { requestMutation } from '../../../lib/fasterdom/fasterdom'; import { parseAccountId } from '../../../util/account'; import { initializeSoundsForSafari } from '../../../util/appSounds'; import { omit } from '../../../util/iteratees'; import { clearPreviousLangpacks, setLanguage } from '../../../util/langProvider'; import switchAnimationLevel from '../../../util/switchAnimationLevel'; -import switchTheme from '../../../util/switchTheme'; +import switchTheme, { setStatusBarStyle } from '../../../util/switchTheme'; import { - IS_ANDROID, IS_IOS, IS_LINUX, IS_MAC_OS, IS_OPERA, IS_SAFARI, - IS_WINDOWS, setPageSafeAreaProperty, setScrollbarWidthProperty, + IS_ANDROID, + IS_DELEGATED_BOTTOM_SHEET, + IS_ELECTRON, + IS_IOS, + IS_LINUX, + IS_MAC_OS, + IS_OPERA, + IS_SAFARI, + IS_WINDOWS, + setPageSafeAreaProperty, + setScrollbarWidthProperty, } from '../../../util/windowEnvironment'; import { callApi } from '../../../api'; import { @@ -25,6 +34,8 @@ import { selectNewestTxIds, } from '../../selectors'; +import { callActionInMain } from '../../../hooks/useDelegatedBottomSheet'; + addActionHandler('init', (_, actions) => { requestMutation(() => { const { documentElement } = document; @@ -52,6 +63,9 @@ addActionHandler('init', (_, actions) => { if (IS_ELECTRON) { documentElement.classList.add('is-electron'); } + if (IS_DELEGATED_BOTTOM_SHEET) { + documentElement.classList.add('is-native-bottom-sheet'); + } setScrollbarWidthProperty(); setPageSafeAreaProperty(); @@ -65,6 +79,7 @@ addActionHandler('afterInit', (global) => { switchTheme(theme); switchAnimationLevel(animationLevel); + setStatusBarStyle(); void setLanguage(langCode); clearPreviousLangpacks(); @@ -151,7 +166,11 @@ addActionHandler('showError', (global, actions, { error } = {}) => { actions.showDialog({ message: 'The hardware wallet does not support this data format' }); break; - case ApiTransactionError.Unexpected: + case ApiCommonError.ServerError: + actions.showDialog({ message: 'An error on the server side. Please try again.' }); + break; + + case ApiCommonError.Unexpected: case undefined: actions.showDialog({ message: 'Unexpected' }); break; @@ -163,6 +182,11 @@ addActionHandler('showError', (global, actions, { error } = {}) => { }); addActionHandler('showNotification', (global, actions, payload) => { + if (IS_DELEGATED_BOTTOM_SHEET) { + callActionInMain('showNotification', payload); + return undefined; + } + const { message, icon } = payload; const newNotifications: NotificationType[] = [...global.notifications]; @@ -231,6 +255,10 @@ addActionHandler('toggleDeeplinkHook', (global, actions, { isEnabled }) => { }); addActionHandler('signOut', async (global, actions, payload) => { + if (IS_DELEGATED_BOTTOM_SHEET) { + callActionInMain('signOut', payload); + } + const { isFromAllAccounts } = payload || {}; const network = selectCurrentNetwork(global); diff --git a/src/global/actions/ui/misc.ts b/src/global/actions/ui/misc.ts index 0133cf58..e449e787 100644 --- a/src/global/actions/ui/misc.ts +++ b/src/global/actions/ui/misc.ts @@ -1,35 +1,64 @@ -import type { UserToken } from '../../types'; -import { AppState, HardwareConnectState } from '../../types'; +import { + AppState, AuthState, HardwareConnectState, SettingsState, +} from '../../types'; -import { IS_EXTENSION } from '../../../config'; +import { + ANIMATION_LEVEL_MIN, APP_VERSION, DEBUG, IS_CAPACITOR, IS_EXTENSION, +} from '../../../config'; +import { + processDeeplink as processTonConnectDeeplink, + vibrateOnSuccess, +} from '../../../util/capacitor'; +import { getIsAddressValid } from '../../../util/getIsAddressValid'; +import getIsAppUpdateNeeded from '../../../util/getIsAppUpdateNeeded'; import { unique } from '../../../util/iteratees'; import { onLedgerTabClose, openLedgerTab } from '../../../util/ledger/tab'; +import { processDeeplink } from '../../../util/processDeeplink'; import { pause } from '../../../util/schedulers'; +import { isTonConnectDeeplink } from '../../../util/ton/deeplinks'; +import { IS_DELEGATED_BOTTOM_SHEET } from '../../../util/windowEnvironment'; import { callApi } from '../../../api'; import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { + clearCurrentSwap, clearCurrentTransfer, + clearIsPinPadPasswordAccepted, renameAccount, updateAccounts, updateAccountState, updateAuth, updateCurrentAccountState, updateHardware, + updateIsPinPadPasswordAccepted, updateSettings, } from '../../reducers'; import { selectAccountSettings, selectAccountState, - selectCurrentAccountState, selectCurrentAccountTokens, selectDisabledSlugs, selectFirstNonHardwareAccount, + selectCurrentAccountState, + selectCurrentAccountTokens, + selectFirstNonHardwareAccount, } from '../../selectors'; +import { callActionInMain } from '../../../hooks/useDelegatedBottomSheet'; + const OPEN_LEDGER_TAB_DELAY = 500; +const APP_VERSION_URL = 'version.txt'; + +addActionHandler('showActivityInfo', (global, actions, { id }) => { + if (IS_DELEGATED_BOTTOM_SHEET) { + callActionInMain('showActivityInfo', { id }); + return undefined; + } -addActionHandler('showActivityInfo', (global, actions, { id } = {}) => { return updateCurrentAccountState(global, { currentActivityId: id }); }); -addActionHandler('closeActivityInfo', (global) => { +addActionHandler('closeActivityInfo', (global, actions, { id }) => { + if (selectCurrentAccountState(global)?.currentActivityId !== id) { + return undefined; + } + return updateCurrentAccountState(global, { currentActivityId: undefined }); }); @@ -77,16 +106,38 @@ addActionHandler('addAccount', async (global, actions, { method, password }) => })); return; } + + if (IS_CAPACITOR) { + global = updateIsPinPadPasswordAccepted(getGlobal()); + setGlobal(global); + + await vibrateOnSuccess(true); + } } global = getGlobal(); - global = updateAuth(global, { password }); + global = { ...global, isAddAccountModalOpen: undefined }; + setGlobal(global); + + if (!IS_DELEGATED_BOTTOM_SHEET) { + actions.addAccount2({ method, password }); + } else { + callActionInMain('addAccount2', { method, password }); + } +}); + +addActionHandler('addAccount2', (global, actions, { method, password }) => { + const isMnemonicImport = method === 'importMnemonic'; + const authState = isMnemonicImport ? AuthState.importWallet : AuthState.createBackup; + + global = { ...global, appState: AppState.Auth }; + global = updateAuth(global, { password, state: authState }); global = clearCurrentTransfer(global); - global = { ...global, isAddAccountModalOpen: undefined, appState: AppState.Auth }; + global = clearCurrentSwap(global); setGlobal(global); - if (method === 'importMnemonic') { + if (isMnemonicImport) { actions.startImportingWallet(); } else { actions.startCreatingWallet(); @@ -106,6 +157,10 @@ addActionHandler('openAddAccountModal', (global) => { }); addActionHandler('closeAddAccountModal', (global) => { + if (IS_CAPACITOR) { + global = clearIsPinPadPasswordAccepted(global); + } + return { ...global, isAddAccountModalOpen: undefined }; }); @@ -140,7 +195,18 @@ addActionHandler('changeNetwork', (global, actions, { network }) => { }); addActionHandler('openSettings', (global) => { - return { ...global, areSettingsOpen: true }; + global = updateSettings(global, { state: SettingsState.Initial }); + setGlobal({ ...global, areSettingsOpen: true }); +}); + +addActionHandler('openSettingsWithState', (global, actions, { state }) => { + global = updateSettings(global, { state }); + setGlobal({ ...global, areSettingsOpen: true }); +}); + +addActionHandler('setSettingsState', (global, actions, { state }) => { + global = updateSettings(global, { state }); + setGlobal(global); }); addActionHandler('closeSettings', (global) => { @@ -152,6 +218,11 @@ addActionHandler('closeSettings', (global) => { }); addActionHandler('openBackupWalletModal', (global) => { + if (IS_DELEGATED_BOTTOM_SHEET) { + callActionInMain('openBackupWalletModal'); + return undefined; + } + return { ...global, isBackupWalletModalOpen: true }; }); @@ -159,12 +230,12 @@ addActionHandler('closeBackupWalletModal', (global) => { return { ...global, isBackupWalletModalOpen: undefined }; }); -addActionHandler('openHardwareWalletModal', async (global, actions) => { +addActionHandler('initializeHardwareWalletConnection', async (global, actions) => { const startConnection = () => { global = updateHardware(getGlobal(), { hardwareState: HardwareConnectState.Connecting, }); - setGlobal({ ...global, isHardwareModalOpen: true }); + setGlobal(global); actions.connectHardwareWallet(); }; @@ -180,7 +251,7 @@ addActionHandler('openHardwareWalletModal', async (global, actions) => { global = updateHardware(getGlobal(), { hardwareState: HardwareConnectState.WaitingForBrowser, }); - setGlobal({ ...global, isHardwareModalOpen: true }); + setGlobal(global); await pause(OPEN_LEDGER_TAB_DELAY); const id = await openLedgerTab(); @@ -196,14 +267,24 @@ addActionHandler('openHardwareWalletModal', async (global, actions) => { startConnection(); }); + } +}); - return; +addActionHandler('openHardwareWalletModal', async (global) => { + const ledgerApi = await import('../../../util/ledger'); + let newHardwareState; + + const isConnected = await ledgerApi.connectLedger(); + + if (!isConnected && IS_EXTENSION) { + newHardwareState = HardwareConnectState.WaitingForBrowser; + } else { + newHardwareState = HardwareConnectState.Connect; } global = updateHardware(getGlobal(), { - hardwareState: HardwareConnectState.Connect, + hardwareState: newHardwareState, }); - setGlobal({ ...global, isHardwareModalOpen: true }); }); @@ -261,8 +342,6 @@ addActionHandler('closeSecurityWarning', (global) => { addActionHandler('toggleTokensWithNoBalance', (global, actions, { isEnabled }) => { const accountId = global.currentAccountId!; const accountSettings = selectAccountSettings(global, accountId) ?? {}; - const { enabledSlugs = [] } = accountSettings; - const updatedEnabledSlugs = isEnabled ? [] : enabledSlugs; setGlobal(updateSettings(global, { areTokensWithNoBalanceHidden: isEnabled, @@ -270,19 +349,15 @@ addActionHandler('toggleTokensWithNoBalance', (global, actions, { isEnabled }) = ...global.settings.byAccountId, [accountId]: { ...accountSettings, - enabledSlugs: updatedEnabledSlugs, + exceptionSlugs: [], }, }, })); - - actions.updateDisabledSlugs(); }); addActionHandler('toggleTokensWithNoPrice', (global, actions, { isEnabled }) => { const accountId = global.currentAccountId!; const accountSettings = selectAccountSettings(global, accountId) ?? {}; - const { enabledSlugs = [] } = accountSettings; - const updatedEnabledSlugs = isEnabled ? [] : enabledSlugs; setGlobal(updateSettings(global, { areTokensWithNoPriceHidden: isEnabled, @@ -290,37 +365,19 @@ addActionHandler('toggleTokensWithNoPrice', (global, actions, { isEnabled }) => ...global.settings.byAccountId, [accountId]: { ...accountSettings, - enabledSlugs: updatedEnabledSlugs, + exceptionSlugs: [], }, }, })); - - actions.updateDisabledSlugs(); }); addActionHandler('toggleSortByValue', (global, actions, { isEnabled }) => { - const accountId = global.currentAccountId!; - const accountSettings = selectAccountSettings(global, accountId) ?? {}; - const accountToken = selectCurrentAccountTokens(global) ?? []; - const getTotalValue = ({ price, amount }: UserToken) => price * amount; - const updatedSlugs = accountToken - .slice() - .sort((a, b) => getTotalValue(b) - getTotalValue(a)) - .map(({ slug }) => slug); - setGlobal(updateSettings(global, { - byAccountId: { - ...global.settings.byAccountId, - [accountId]: { - ...accountSettings, - orderedSlugs: updatedSlugs, - }, - }, isSortByValueEnabled: isEnabled, })); }); -addActionHandler('initTokensOrder', (global, actions) => { +addActionHandler('initTokensOrder', (global) => { const accountId = global.currentAccountId!; const accountSettings = selectAccountSettings(global, accountId) ?? {}; const accountTokens = selectCurrentAccountTokens(global) ?? []; @@ -338,28 +395,48 @@ addActionHandler('initTokensOrder', (global, actions) => { }, }, })); - - actions.updateDisabledSlugs(); }); -addActionHandler('updateDisabledSlugs', (global) => { - const accountId = global.currentAccountId!; +addActionHandler('updateDeletionListForActiveTokens', (global, actions, payload) => { + const { accountId = global.currentAccountId } = payload ?? {}; + if (!accountId) { + return; + } + + const { balances } = selectAccountState(global, accountId) ?? {}; const accountSettings = selectAccountSettings(global, accountId) ?? {}; - const disabledSlugs = selectDisabledSlugs( - global, - global.settings.areTokensWithNoBalanceHidden, - global.settings.areTokensWithNoPriceHidden, + const accountTokens = selectCurrentAccountTokens(global) ?? []; + const deletedSlugs = accountSettings.deletedSlugs ?? []; + + const updatedDeletedSlugs = deletedSlugs.filter( + (slug) => !accountTokens.some((token) => token.slug === slug && token.amount > 0), ); - setGlobal(updateSettings(global, { + const balancesBySlug = balances?.bySlug ?? {}; + const updatedBalancesBySlug = Object.fromEntries( + Object.entries(balancesBySlug).filter(([slug]) => !updatedDeletedSlugs.includes(slug)), + ); + + global = updateAccountState(global, accountId, { + balances: { + ...balances, + bySlug: updatedBalancesBySlug, + }, + }); + + global = updateSettings(global, { byAccountId: { ...global.settings.byAccountId, [accountId]: { ...accountSettings, - disabledSlugs, + deletedSlugs: updatedDeletedSlugs, }, }, - })); + }); + + setGlobal(global); + + actions.initTokensOrder(); }); addActionHandler('sortTokens', (global, actions, { orderedSlugs }) => { @@ -377,23 +454,18 @@ addActionHandler('sortTokens', (global, actions, { orderedSlugs }) => { })); }); -addActionHandler('toggleDisabledToken', (global, actions, { slug }) => { +addActionHandler('toggleExceptionToken', (global, actions, { slug }) => { const accountId = global.currentAccountId!; const accountSettings = selectAccountSettings(global, accountId) ?? {}; - const { enabledSlugs = [], disabledSlugs = [] } = accountSettings; + const { exceptionSlugs = [] } = accountSettings; - const enabledSlugsCopy = enabledSlugs.slice(); - const disabledSlugsCopy = disabledSlugs.slice(); + const exceptionSlugsCopy = exceptionSlugs.slice(); + const slugIndexInAvailable = exceptionSlugsCopy.indexOf(slug); - const slugIndexInAvailable = enabledSlugsCopy.indexOf(slug); - const slugIndexInDisabled = disabledSlugsCopy.indexOf(slug); - - if (slugIndexInDisabled !== -1) { - disabledSlugsCopy.splice(slugIndexInDisabled, 1); - enabledSlugsCopy.push(slug); + if (slugIndexInAvailable !== -1) { + exceptionSlugsCopy.splice(slugIndexInAvailable, 1); } else { - enabledSlugsCopy.splice(slugIndexInAvailable, 1); - disabledSlugsCopy.push(slug); + exceptionSlugsCopy.push(slug); } setGlobal(updateSettings(global, { @@ -401,18 +473,15 @@ addActionHandler('toggleDisabledToken', (global, actions, { slug }) => { ...global.settings.byAccountId, [accountId]: { ...accountSettings, - enabledSlugs: enabledSlugsCopy, - disabledSlugs: disabledSlugsCopy, + exceptionSlugs: exceptionSlugsCopy, }, }, })); - - actions.updateDisabledSlugs(); }); addActionHandler('deleteToken', (global, actions, { slug }) => { const accountId = global.currentAccountId!; - const { balances } = selectAccountState(global, accountId) || {}; + const { balances } = selectAccountState(global, accountId) ?? {}; if (!balances?.bySlug[slug]) { return; @@ -431,8 +500,8 @@ addActionHandler('deleteToken', (global, actions, { slug }) => { const accountSettings = selectAccountSettings(global, accountId) ?? {}; const orderedSlugs = accountSettings.orderedSlugs?.filter((s) => s !== slug); - const enabledSlugs = accountSettings.enabledSlugs?.filter((s) => s !== slug); - const disabledSlugs = accountSettings.disabledSlugs?.filter((s) => s !== slug); + const exceptionSlugs = accountSettings.exceptionSlugs?.filter((s) => s !== slug); + const deletedSlugs = [...accountSettings.deletedSlugs ?? [], slug]; global = updateSettings(global, { byAccountId: { @@ -440,11 +509,100 @@ addActionHandler('deleteToken', (global, actions, { slug }) => { [accountId]: { ...accountSettings, orderedSlugs, - enabledSlugs, - disabledSlugs, + exceptionSlugs, + deletedSlugs, }, }, }); setGlobal(global); }); + +addActionHandler('checkAppVersion', (global) => { + fetch(`${APP_VERSION_URL}?${Date.now()}`) + .then((response) => response.text()) + .then((version) => { + version = version.trim(); + + if (getIsAppUpdateNeeded(version, APP_VERSION)) { + global = getGlobal(); + global = { + ...global, + isAppUpdateAvailable: true, + }; + setGlobal(global); + } + }) + .catch((err) => { + if (DEBUG) { + // eslint-disable-next-line no-console + console.error('[checkAppVersion failed] ', err); + } + }); +}); + +addActionHandler('requestConfetti', (global) => { + if (global.settings.animationLevel === ANIMATION_LEVEL_MIN) return global; + + return { + ...global, + confettiRequestedAt: Date.now(), + }; +}); + +addActionHandler('openQrScanner', (global) => { + if (IS_DELEGATED_BOTTOM_SHEET) { + callActionInMain('openQrScanner'); + return undefined; + } + + return { + ...global, + isQrScannerOpen: true, + }; +}); + +addActionHandler('closeQrScanner', (global) => { + return { + ...global, + isQrScannerOpen: undefined, + }; +}); + +addActionHandler('openDeeplink', async (global, actions, params) => { + let { url } = params; + if (IS_DELEGATED_BOTTOM_SHEET) { + callActionInMain('openDeeplink', { url }); + return; + } + + if (isTonConnectDeeplink(url)) { + void processTonConnectDeeplink(url); + + return; + } + + if (getIsAddressValid(url)) { + url = `ton://transfer/${url}`; + } + + const result = await processDeeplink(url); + if (!result) { + actions.showNotification({ + message: 'Unrecognized QR Code', + }); + } +}); + +addActionHandler('changeBaseCurrency', async (global, actions, { currency }) => { + await callApi('setBaseCurrency', currency); + void callApi('tryUpdateTokens'); +}); + +addActionHandler('setIsPinPadPasswordAccepted', (global) => { + return updateIsPinPadPasswordAccepted(global); +}); + +addActionHandler('clearIsPinPadPasswordAccepted', (global) => { + return clearIsPinPadPasswordAccepted(global); +}); diff --git a/src/global/actions/ui/settings.ts b/src/global/actions/ui/settings.ts new file mode 100644 index 00000000..b4645e4f --- /dev/null +++ b/src/global/actions/ui/settings.ts @@ -0,0 +1,28 @@ +import { addCallback } from '../../../lib/teact/teactn'; + +import type { GlobalState } from '../../types'; + +import { setLanguage } from '../../../util/langProvider'; +import switchTheme from '../../../util/switchTheme'; + +let prevGlobal: GlobalState | undefined; + +addCallback((global: GlobalState) => { + if (!prevGlobal) { + prevGlobal = global; + return; + } + + const { settings: prevSettings } = prevGlobal; + const { settings } = global; + + if (settings.theme !== prevSettings.theme) { + switchTheme(settings.theme); + } + + if (settings.langCode !== prevSettings.langCode) { + void setLanguage(settings.langCode); + } + + prevGlobal = global; +}); diff --git a/src/global/cache.ts b/src/global/cache.ts index 82de1f7a..520d1103 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -6,13 +6,21 @@ import { } from './types'; import { - DEBUG, DEFAULT_DECIMAL_PLACES, GLOBAL_STATE_CACHE_DISABLED, GLOBAL_STATE_CACHE_KEY, IS_ELECTRON, MAIN_ACCOUNT_ID, + DEBUG, + DEFAULT_DECIMAL_PLACES, + GLOBAL_STATE_CACHE_DISABLED, + GLOBAL_STATE_CACHE_KEY, + IS_CAPACITOR, + MAIN_ACCOUNT_ID, } from '../config'; import { buildAccountId, parseAccountId } from '../util/account'; +import authApi from '../util/authApi'; +import { getIsNativeBiometricAuthSupported } from '../util/capacitor'; import { cloneDeep, mapValues, pick } from '../util/iteratees'; import { onBeforeUnload, onIdle, throttle, } from '../util/schedulers'; +import { IS_ELECTRON } from '../util/windowEnvironment'; import { getIsTxIdLocal } from './helpers'; import { addActionHandler, getGlobal } from './index'; import { INITIAL_STATE, STATE_VERSION } from './initialState'; @@ -20,14 +28,15 @@ import { updateHardware } from './reducers'; import { isHeavyAnimating } from '../hooks/useHeavyAnimationCheck'; -const UPDATE_THROTTLE = 5000; -const ACTIVITIES_LIMIT = 20; +const UPDATE_THROTTLE = IS_CAPACITOR ? 500 : 5000; +const ACTIVITIES_LIMIT = 50; const ANIMATION_DELAY_MS = 320; const updateCacheThrottled = throttle(() => onIdle(updateCache), UPDATE_THROTTLE, false); let isCaching = false; let unsubscribeFromBeforeUnload: NoneToVoidFunction | undefined; +let preloadedData: Partial | undefined; export function initCache() { if (GLOBAL_STATE_CACHE_DISABLED) { @@ -55,6 +64,12 @@ export function initCache() { ...global, state: AppState.Auth, }); + global = getGlobal(); + if (getIsNativeBiometricAuthSupported() && global.settings.authConfig?.kind === 'native-biometrics') { + authApi.removeNativeBiometrics(); + } + + preloadedData = pick(global, ['swapTokenInfo', 'tokenInfo']); actions.resetApiSettings({ areAllDisabled: true }); @@ -138,6 +153,7 @@ function readCache(initialState: GlobalState): GlobalState { return { ...initialState, + ...preloadedData, ...cached, }; } @@ -318,6 +334,15 @@ function migrateCache(cached: GlobalState, initialState: GlobalState) { cached.stateVersion = 9; } + if (cached.stateVersion === 9) { + if (cached.byAccountId) { + for (const accountId of Object.keys(cached.byAccountId)) { + delete (cached.byAccountId[accountId] as any).activities; + } + } + cached.stateVersion = 10; + } + // When adding migration here, increase `STATE_VERSION` } @@ -357,18 +382,27 @@ function reduceByAccountId(global: GlobalState) { 'currentTokenSlug', 'currentTokenPeriod', 'savedAddresses', - 'stakingBalance', - 'isUnstakeRequested', - 'poolState', + 'staking', 'stakingHistory', ]); - const { idsBySlug, newestTransactionsBySlug } = state.activities || {}; + const { idsBySlug, newestTransactionsBySlug, byId } = state.activities || {}; + + if (byId && idsBySlug && Object.keys(idsBySlug).length) { + const reducedIdsBySlug = mapValues(idsBySlug, (ids) => { + const result: string[] = []; + let visibleIdCount = 0; + ids.filter((id) => !getIsTxIdLocal(id)).forEach((id) => { + if (visibleIdCount === ACTIVITIES_LIMIT) return; - if (idsBySlug && Object.keys(idsBySlug).length) { - const reducedIdsBySlug = mapValues(idsBySlug, (ids) => ids.filter( - (id) => !getIsTxIdLocal(id), - ).slice(0, ACTIVITIES_LIMIT)); + if (!byId[id].shouldHide) { + visibleIdCount += 1; + } + result.push(id); + }); + + return result; + }); acc[accountId].activities = { byId: pick(state.activities!.byId, Object.values(reducedIdsBySlug).flat()), diff --git a/src/global/init.ts b/src/global/init.ts index f9d761d5..22832efd 100644 --- a/src/global/init.ts +++ b/src/global/init.ts @@ -1,5 +1,5 @@ import { cloneDeep } from '../util/iteratees'; -import { DETACHED_TAB_URL } from '../util/ledger/tab'; +import { IS_LEDGER_EXTENSION_TAB } from '../util/windowEnvironment'; import { initCache, loadCache } from './cache'; import { addActionHandler } from './index'; import { INITIAL_STATE } from './initialState'; @@ -10,7 +10,7 @@ initCache(); addActionHandler('init', (_, actions) => { const initial = cloneDeep(INITIAL_STATE); - if (window.location.href.includes(DETACHED_TAB_URL)) { + if (IS_LEDGER_EXTENSION_TAB) { actions.initLedgerPage(); return initial; } diff --git a/src/global/initialState.ts b/src/global/initialState.ts index 57206466..7bb4fbe8 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -1,17 +1,24 @@ import type { GlobalState } from './types'; import { AppState, - AuthState, StakingState, TransferState, + AuthState, + BiometricsState, + SettingsState, + StakingState, + SwapState, + TransferState, } from './types'; import { ANIMATION_LEVEL_DEFAULT, + DEFAULT_SLIPPAGE_VALUE, + INIT_SWAP_ASSETS, THEME_DEFAULT, TOKEN_INFO, } from '../config'; import { USER_AGENT_LANG_CODE } from '../util/windowEnvironment'; -export const STATE_VERSION = 9; +export const STATE_VERSION = 10; export const INITIAL_STATE: GlobalState = { appState: AppState.Auth, @@ -20,12 +27,21 @@ export const INITIAL_STATE: GlobalState = { state: AuthState.none, }, + biometrics: { + state: BiometricsState.None, + }, + hardware: {}, currentTransfer: { state: TransferState.None, }, + currentSwap: { + state: SwapState.None, + slippage: DEFAULT_SLIPPAGE_VALUE, + }, + currentDappTransfer: { state: TransferState.None, }, @@ -34,11 +50,18 @@ export const INITIAL_STATE: GlobalState = { state: StakingState.None, }, + stakingInfo: {}, + tokenInfo: { bySlug: TOKEN_INFO, }, + swapTokenInfo: { + bySlug: INIT_SWAP_ASSETS, + }, + settings: { + state: SettingsState.Initial, theme: THEME_DEFAULT, animationLevel: ANIMATION_LEVEL_DEFAULT, areTinyTransfersHidden: true, diff --git a/src/global/reducers/activities.ts b/src/global/reducers/activities.ts index a2f0e50f..e0ce7837 100644 --- a/src/global/reducers/activities.ts +++ b/src/global/reducers/activities.ts @@ -1,4 +1,4 @@ -import type { ApiActivity } from '../../api/types'; +import type { ApiActivity, ApiTransactionActivity } from '../../api/types'; import type { GlobalState } from '../types'; import { getIsTxIdLocal } from '../helpers'; @@ -9,7 +9,29 @@ export function updateActivity(global: GlobalState, accountId: string, activity: const { activities } = selectAccountState(global, accountId) || {}; const idsBySlug = activities?.idsBySlug || {}; - const { id, timestamp } = activity; + const { id, timestamp, kind } = activity; + + if (kind === 'swap') { + const { from, to } = activity; + + let fromTokenIds = idsBySlug[from] || []; + let toTokenIds = idsBySlug[to] || []; + + if (!fromTokenIds.includes(id)) { + fromTokenIds = [id].concat(fromTokenIds); + } + if (!toTokenIds.includes(id)) { + toTokenIds = [id].concat(toTokenIds); + } + + return updateAccountState(global, accountId, { + activities: { + ...activities, + byId: { ...activities?.byId, [id]: activity }, + idsBySlug: { ...idsBySlug, [from]: fromTokenIds, [to]: toTokenIds }, + }, + }); + } const { slug } = activity; const isLocal = getIsTxIdLocal(id); @@ -38,21 +60,65 @@ export function updateActivity(global: GlobalState, accountId: string, activity: }); } -export function removeTransaction(global: GlobalState, accountId: string, txId: string) { +export function assignRemoteTxId(global: GlobalState, accountId: string, txId: string, newTxId: string) { const { activities } = selectAccountState(global, accountId) || {}; - let { idsBySlug } = activities || {}; - const { [txId]: removedActivity, ...byTxId } = activities?.byId || {}; - const slug = removedActivity?.kind === 'transaction' && removedActivity.slug; + const { byId, idsBySlug } = activities || { byId: {}, idsBySlug: {} }; + const replacedActivity: ApiActivity | undefined = byId[txId]; + + if (!replacedActivity || replacedActivity.kind !== 'transaction') return global; - if (slug && idsBySlug && idsBySlug[slug]) { - idsBySlug = { ...idsBySlug, [slug]: idsBySlug[slug].filter((x) => x !== txId) }; + const slug = replacedActivity.slug; + const updatedIdsBySlug = { ...idsBySlug }; + + if (slug in updatedIdsBySlug) { + const indexOfTxId = updatedIdsBySlug[slug].indexOf(txId); + if (indexOfTxId === -1) return global; + + updatedIdsBySlug[slug] = [ + ...updatedIdsBySlug[slug].slice(0, indexOfTxId), + newTxId, + ...updatedIdsBySlug[slug].slice(indexOfTxId + 1), + ]; } + const updatedByTxId = { + ...byId, + [newTxId]: { ...replacedActivity, id: newTxId, txId: newTxId }, + }; + + delete updatedByTxId[txId]; + + return updateAccountState(global, accountId, { + activities: { + ...activities, + byId: updatedByTxId, + idsBySlug: updatedIdsBySlug, + }, + }); +} + +export function addLocalTransaction(global: GlobalState, accountId: string, transaction: ApiTransactionActivity) { + const { activities } = selectAccountState(global, accountId) || {}; + const localTransactions = (activities?.localTransactions ?? []).concat(transaction); + + return updateAccountState(global, accountId, { + activities: { + ...activities, + byId: activities?.byId ?? {}, + localTransactions, + }, + }); +} + +export function removeLocalTransaction(global: GlobalState, accountId: string, txId: string) { + const { activities } = selectAccountState(global, accountId) || {}; + const localTransactions = (activities?.localTransactions ?? []).filter(({ id }) => id !== txId); + return updateAccountState(global, accountId, { activities: { ...activities, - byId: byTxId, - idsBySlug, + byId: activities?.byId ?? {}, + localTransactions, }, }); } diff --git a/src/global/reducers/index.ts b/src/global/reducers/index.ts index a1a9c32c..48e9aea5 100644 --- a/src/global/reducers/index.ts +++ b/src/global/reducers/index.ts @@ -4,3 +4,4 @@ export * from './misc'; export * from './dapp'; export * from './activities'; export * from './nfts'; +export * from './swap'; diff --git a/src/global/reducers/misc.ts b/src/global/reducers/misc.ts index 701fcb64..b30c32c2 100644 --- a/src/global/reducers/misc.ts +++ b/src/global/reducers/misc.ts @@ -1,4 +1,4 @@ -import type { ApiToken } from '../../api/types'; +import type { ApiSwapAsset, ApiToken } from '../../api/types'; import type { Account, AccountState, GlobalState } from '../types'; import { TON_TOKEN_SLUG } from '../../config'; @@ -33,6 +33,20 @@ export function updateAccounts( }; } +export function updateIsPinPadPasswordAccepted(global: GlobalState) { + return { + ...global, + isPinPadPasswordAccepted: true, + }; +} + +export function clearIsPinPadPasswordAccepted(global: GlobalState) { + return { + ...global, + isPinPadPasswordAccepted: undefined, + }; +} + export function createAccount(global: GlobalState, accountId: string, address: string, partial?: Partial) { if (!partial?.title) { const network = selectCurrentNetwork(global); @@ -87,6 +101,31 @@ export function updateBalance( }); } +export function updateBalances( + global: GlobalState, + accountId: string, + balancesToUpdate: Record, +): GlobalState { + if (Object.keys(balancesToUpdate).length === 0) { + return global; + } + + const { balances } = selectAccountState(global, accountId) || {}; + + const updatedBalancesBySlug = { ...(balances?.bySlug || {}) }; + + for (const [slug, balance] of Object.entries(balancesToUpdate)) { + updatedBalancesBySlug[slug] = balance; + } + + return updateAccountState(global, accountId, { + balances: { + ...balances, + bySlug: updatedBalancesBySlug, + }, + }); +} + export function updateSendingLoading(global: GlobalState, isLoading: boolean): GlobalState { return { ...global, @@ -131,6 +170,24 @@ export function updateTokens( }; } +export function updateSwapTokens( + global: GlobalState, + partial: Record, +): GlobalState { + const currentTokens = global.swapTokenInfo?.bySlug; + + return { + ...global, + swapTokenInfo: { + ...global.swapTokenInfo, + bySlug: { + ...currentTokens, + ...partial, + }, + }, + }; +} + export function updateCurrentAccountState(global: GlobalState, partial: Partial): GlobalState { return updateAccountState(global, global.currentAccountId!, partial); } @@ -175,3 +232,13 @@ export function updateSettings(global: GlobalState, settingsUpdate: Partial) { + return { + ...global, + biometrics: { + ...global.biometrics, + ...biometricsUpdate, + }, + }; +} diff --git a/src/global/reducers/staking.ts b/src/global/reducers/staking.ts index 12aad5a4..a9b27f7c 100644 --- a/src/global/reducers/staking.ts +++ b/src/global/reducers/staking.ts @@ -1,10 +1,9 @@ -import type { ApiPoolState } from '../../api/types'; -import type { GlobalState } from '../types'; +import type { AccountState, GlobalState } from '../types'; import { StakingState } from '../types'; import isPartialDeepEqual from '../../util/isPartialDeepEqual'; -import { selectCurrentAccountState } from '../selectors'; -import { updateCurrentAccountState } from './misc'; +import { selectAccountState } from '../selectors'; +import { updateAccountState } from './misc'; export function updateStaking(global: GlobalState, update: Partial): GlobalState { return { @@ -25,20 +24,48 @@ export function clearStaking(global: GlobalState) { }; } -export function updatePoolState(global: GlobalState, partial: ApiPoolState, withDeepCompare = false): GlobalState { - const currentPoolState = selectCurrentAccountState(global)?.poolState; +export function updateAccountStakingState( + global: GlobalState, + accountId: string, + state: NonNullable, + withDeepCompare = false, +): GlobalState { + const currentState = selectAccountState(global, accountId)?.staking; - if ( - !global.currentAccountId - || (withDeepCompare && currentPoolState && isPartialDeepEqual(currentPoolState, partial)) - ) { + if (withDeepCompare && currentState && isPartialDeepEqual(currentState, state)) { return global; } - return updateCurrentAccountState(global, { - poolState: { - ...currentPoolState, + return updateAccountState(global, accountId, { + staking: { + ...currentState, + ...state, + }, + }); +} + +export function updateAccountStakingStatePartial( + global: GlobalState, + accountId: string, + partial: Partial, +): GlobalState { + const currentState = selectAccountState(global, accountId)?.staking; + + if (!currentState) { + return global; + } + + return updateAccountState(global, accountId, { + staking: { + ...currentState, ...partial, }, }); } + +export function updateStakingInfo(global: GlobalState, stakingInfo: GlobalState['stakingInfo']) { + return { + ...global, + stakingInfo, + }; +} diff --git a/src/global/reducers/swap.ts b/src/global/reducers/swap.ts new file mode 100644 index 00000000..458d75ba --- /dev/null +++ b/src/global/reducers/swap.ts @@ -0,0 +1,24 @@ +import type { GlobalState } from '../types'; +import { SwapState } from '../types'; + +import { DEFAULT_SLIPPAGE_VALUE } from '../../config'; + +export function updateCurrentSwap(global: GlobalState, update: Partial) { + return { + ...global, + currentSwap: { + ...global.currentSwap, + ...update, + }, + }; +} + +export function clearCurrentSwap(global: GlobalState) { + return { + ...global, + currentSwap: { + state: SwapState.None, + slippage: DEFAULT_SLIPPAGE_VALUE, + }, + }; +} diff --git a/src/global/reducers/wallet.ts b/src/global/reducers/wallet.ts index ae76dc50..1019919a 100644 --- a/src/global/reducers/wallet.ts +++ b/src/global/reducers/wallet.ts @@ -51,13 +51,27 @@ export function updateActivitiesIsLoading(global: GlobalState, isLoading: boolea }); } -export function updateActivitiesIsHistoryEndReached(global: GlobalState, isReached: boolean) { +export function updateActivitiesIsHistoryEndReached(global: GlobalState, isReached: boolean, slug?: string) { const { activities } = selectCurrentAccountState(global) || {}; + if (slug) { + const bySlug = activities?.isHistoryEndReachedBySlug ?? {}; + + return updateCurrentAccountState(global, { + activities: { + ...activities || { byId: {} }, + isHistoryEndReachedBySlug: { + ...bySlug, + [slug]: isReached, + }, + }, + }); + } + return updateCurrentAccountState(global, { activities: { ...activities || { byId: {} }, - isHistoryEndReached: isReached, + isMainHistoryEndReached: isReached, }, }); } diff --git a/src/global/selectors/index.ts b/src/global/selectors/index.ts index 2aaa454e..b8d4d8d3 100644 --- a/src/global/selectors/index.ts +++ b/src/global/selectors/index.ts @@ -1,10 +1,16 @@ import type { ApiNetwork, ApiTxIdBySlug } from '../../api/types'; import type { - Account, AccountSettings, GlobalState, UserToken, + Account, + AccountSettings, + AccountState, + GlobalState, + UserSwapToken, + UserToken, } from '../types'; +import { TON_TOKEN_SLUG } from '../../config'; import { parseAccountId } from '../../util/account'; -import { findLast, mapValues, unique } from '../../util/iteratees'; +import { findLast, mapValues } from '../../util/iteratees'; import memoized from '../../util/memoized'; import { round } from '../../util/round'; import { bigStrToHuman, getIsSwapId, getIsTxIdLocal } from '../helpers'; @@ -13,30 +19,37 @@ export function selectHasSession(global: GlobalState) { return Boolean(global.currentAccountId); } -export const selectAccountTokensMemoized = memoized(( +const selectAccountTokensMemoized = memoized(( balancesBySlug: Record, tokenInfo: GlobalState['tokenInfo'], accountSettings: AccountSettings, + isSortByValueEnabled = false, + areTokensWithNoBalanceHidden = false, + areTokensWithNoPriceHidden = false, ) => { + const getTotalValue = ({ price, amount }: UserToken) => price * amount; + return Object .entries(balancesBySlug) .filter(([slug]) => (slug in tokenInfo.bySlug)) - .sort(([slugA], [slugB]) => { - if (!accountSettings.orderedSlugs) { - return 1; - } - const indexA = accountSettings.orderedSlugs.indexOf(slugA); - const indexB = accountSettings.orderedSlugs.indexOf(slugB); - return indexA - indexB; - }) .map(([slug, balance]) => { const { - symbol, name, image, decimals, quote: { + symbol, name, image, decimals, cmcSlug, quote: { price, percentChange24h, percentChange7d, percentChange30d, history7d, history24h, history30d, }, } = tokenInfo.bySlug[slug]; - const isDisabled = accountSettings.disabledSlugs?.includes(slug); const amount = bigStrToHuman(balance, decimals); + const isException = accountSettings.exceptionSlugs?.includes(slug); + let isDisabled = (areTokensWithNoPriceHidden && price === 0) + || (areTokensWithNoBalanceHidden && amount === 0); + + if (isException) { + isDisabled = !isDisabled; + } + + if (slug === TON_TOKEN_SLUG) { + isDisabled = false; + } return { symbol, @@ -53,7 +66,21 @@ export const selectAccountTokensMemoized = memoized(( history7d, history30d, isDisabled, + cmcSlug, } as UserToken; + }) + .sort((tokenA, tokenB) => { + if (isSortByValueEnabled) { + return getTotalValue(tokenB) - getTotalValue(tokenA); + } + + if (!accountSettings.orderedSlugs) { + return 1; + } + + const indexA = accountSettings.orderedSlugs.indexOf(tokenA.slug); + const indexB = accountSettings.orderedSlugs.indexOf(tokenB.slug); + return indexA - indexB; }); }); @@ -64,8 +91,16 @@ export function selectCurrentAccountTokens(global: GlobalState) { } const accountSettings = selectAccountSettings(global, global.currentAccountId!) ?? {}; - - return selectAccountTokensMemoized(balancesBySlug, global.tokenInfo, accountSettings); + const { areTokensWithNoBalanceHidden, areTokensWithNoPriceHidden, isSortByValueEnabled } = global.settings; + + return selectAccountTokensMemoized( + balancesBySlug, + global.tokenInfo, + accountSettings, + isSortByValueEnabled, + areTokensWithNoBalanceHidden, + areTokensWithNoPriceHidden, + ); } export const selectPopularTokensMemoized = memoized(( @@ -73,8 +108,7 @@ export const selectPopularTokensMemoized = memoized(( tokenInfo: GlobalState['tokenInfo'], ) => { return Object.entries(tokenInfo.bySlug) - .filter(([, token]) => token.isPopular) - .filter(([slug]) => !(slug in balancesBySlug)) + .filter(([slug, token]) => token.isPopular && !(slug in balancesBySlug)) .map(([slug]) => { const { symbol, name, image, decimals, keywords, quote: { @@ -111,6 +145,50 @@ export function selectPopularTokensWithoutAccountTokens(global: GlobalState) { return selectPopularTokensMemoized(balancesBySlug, global.tokenInfo); } +const selectSwapTokensMemoized = memoized(( + balancesBySlug: Record, + swapTokenInfo: GlobalState['swapTokenInfo'], +) => { + const tokenList: UserSwapToken[] = Object.entries(swapTokenInfo.bySlug) + .map(([slug]) => { + const { + symbol, name, image, decimals, keywords, blockchain, contract, + } = swapTokenInfo.bySlug[slug]; + const amount = bigStrToHuman(balancesBySlug[slug] ?? '0', decimals); + + return { + symbol, + slug, + amount, + name, + image, + decimals, + isDisabled: false, + canSwap: true, + keywords, + blockchain, + contract, + } satisfies UserSwapToken; + }); + + const userTokenList = tokenList.slice() + .sort((a, b) => a.name.trim().toLowerCase().localeCompare(b.name.trim().toLowerCase())); + + return userTokenList; +}); + +export function selectSwapTokens(global: GlobalState) { + const balancesBySlug = selectCurrentAccountState(global)?.balances?.bySlug; + if (!balancesBySlug || !global.swapTokenInfo) { + return undefined; + } + + return selectSwapTokensMemoized( + balancesBySlug, + global.swapTokenInfo, + ); +} + export function selectIsNewWallet(global: GlobalState) { const tokens = selectCurrentAccountTokens(global); @@ -151,7 +229,7 @@ export function selectCurrentAccountState(global: GlobalState) { return selectAccountState(global, global.currentAccountId!); } -export function selectAccountState(global: GlobalState, accountId: string) { +export function selectAccountState(global: GlobalState, accountId: string): AccountState | undefined { return global.byAccountId[accountId]; } @@ -175,40 +253,17 @@ export function selectNewestTxIds(global: GlobalState, accountId: string): ApiTx export function selectLastTxIds(global: GlobalState, accountId: string): ApiTxIdBySlug { const idsBySlug = selectAccountState(global, accountId)?.activities?.idsBySlug || {}; - return mapValues(idsBySlug, (tokenTxIds) => { - return findLast(tokenTxIds, (id) => !getIsTxIdLocal(id) && !getIsSwapId(id)); - }); + return Object.entries(idsBySlug).reduce((result, [slug, ids]) => { + const txId = findLast(ids, (id) => !getIsTxIdLocal(id) && !getIsSwapId(id)); + if (txId) result[slug] = txId; + return result; + }, {} as ApiTxIdBySlug); } export function selectAccountSettings(global: GlobalState, accountId: string): AccountSettings | undefined { return global.settings.byAccountId[accountId]; } -export function selectDisabledSlugs( - global: GlobalState, - areTokensWithNoBalanceHidden = false, - areTokensWithNoPriceHidden = false, -): string[] { - const accountId = global.currentAccountId!; - const tokens = selectCurrentAccountTokens(global) ?? []; - const { enabledSlugs = [], disabledSlugs = [] } = selectAccountSettings(global, accountId) ?? {}; - - const newDisabledSlugs = tokens - .filter(({ slug }) => !enabledSlugs.includes(slug)) - .filter(({ amount, price }) => { - if (areTokensWithNoBalanceHidden && areTokensWithNoPriceHidden) { - return amount === 0 || price === 0; - } else if (areTokensWithNoBalanceHidden) { - return amount === 0; - } else if (areTokensWithNoPriceHidden) { - return price === 0; - } - return false; - }).map(({ slug }) => slug); - - return unique([...disabledSlugs, ...newDisabledSlugs]); -} - export function selectIsHardwareAccount(global: GlobalState) { const state = selectAccount(global, global.currentAccountId!); @@ -218,3 +273,23 @@ export function selectIsHardwareAccount(global: GlobalState) { export function selectIsOneAccount(global: GlobalState) { return Object.keys(selectAccounts(global) || {}).length === 1; } + +export const selectEnabledTokensCountMemoized = memoized((tokens?: UserToken[]) => { + return (tokens ?? []).filter(({ isDisabled }) => !isDisabled).length; +}); + +export function selectLastLedgerAccountIndex(global: GlobalState, network: ApiNetwork) { + const byId = global.accounts?.byId ?? {}; + return Object.entries(byId).reduce((previousValue, [accountId, account]) => { + if (!account.ledger || parseAccountId(accountId).network !== network) { + return previousValue; + } + return Math.max(account.ledger.index, previousValue ?? 0); + }, undefined as number | undefined); +} + +export function selectLocalTransactions(global: GlobalState, accountId: string) { + const accountState = global.byAccountId?.[accountId]; + + return accountState?.activities?.localTransactions; +} diff --git a/src/global/types.ts b/src/global/types.ts index c117ad7e..467ec67e 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -2,7 +2,7 @@ import type { ApiTonConnectProof } from '../api/tonConnect/types'; import type { ApiActivity, ApiAnyDisplayError, - ApiBackendStakingState, + ApiBaseCurrency, ApiDapp, ApiDappPermissions, ApiDappTransaction, @@ -11,11 +11,17 @@ import type { ApiNetwork, ApiNft, ApiParsedPayload, - ApiPoolState, + ApiStakingHistory, + ApiStakingType, + ApiSwapAsset, ApiToken, ApiTransaction, + ApiTransactionActivity, ApiUpdate, + ApiUpdateDappConnect, + ApiUpdateDappSendTransactions, } from '../api/types'; +import type { AuthConfig } from '../util/authApi/types'; import type { LedgerWalletInfo } from '../util/ledger/types'; export type AnimationLevel = 0 | 1 | 2; @@ -53,16 +59,36 @@ export enum AppState { export enum AuthState { none, creatingWallet, + createPin, + confirmPin, + createBiometrics, + createNativeBiometrics, createPassword, createBackup, disclaimerAndBackup, importWallet, - disclaimer, + importWalletCreatePin, + importWalletConfirmPin, + importWalletCreateNativeBiometrics, + importWalletCreateBiometrics, importWalletCreatePassword, + disclaimer, ready, about, } +export enum BiometricsState { + None, + TurnOnPasswordConfirmation, + TurnOnRegistration, + TurnOnVerification, + TurnOnComplete, + TurnOffWarning, + TurnOffBiometricConfirmation, + TurnOffCreatePassword, + TurnOffComplete, +} + export enum TransferState { None, WarningHardware, @@ -74,6 +100,43 @@ export enum TransferState { Complete, } +export enum SwapState { + None, + Initial, + Blockchain, + WaitTokens, + Password, + ConnectHardware, + ConfirmHardware, + Complete, + SelectTokenFrom, + SelectTokenTo, +} + +export enum SwapFeeSource { + In, + Out, +} + +export enum SwapInputSource { + In, + Out, +} + +export enum SwapErrorType { + InvalidPair, + NotEnoughLiquidity, + + ChangellyMinSwap, + ChangellyMaxSwap, +} + +export enum SwapType { + OnChain, + CrosschainFromTon, + CrosschainToTon, +} + export enum DappConnectState { Info, Password, @@ -85,7 +148,8 @@ export enum HardwareConnectState { Connect, Connecting, Failed, - Connected, + ConnectedWithSeveralWallets, + ConnectedWithSingleWallet, WaitingForBrowser, } @@ -101,9 +165,22 @@ export enum StakingState { UnstakeComplete, } +export enum SettingsState { + Initial, + Appearance, + Assets, + Dapps, + Language, + About, + Disclaimer, + NativeBiometricsTurnOn, + SelectTokenList, +} + export enum ActiveTab { Receive, Transfer, + Swap, Stake, } @@ -128,9 +205,16 @@ export type UserToken = { history7d?: ApiHistoryList; history30d?: ApiHistoryList; isDisabled?: boolean; + canSwap?: boolean; keywords?: string[]; + cmcSlug?: string; }; +export type UserSwapToken = { + blockchain: string; + contract?: string; +} & Omit; + export type TokenPeriod = '24h' | '7d' | '30d'; export interface Account { @@ -143,6 +227,12 @@ export interface Account { }; } +export interface AssetPairs { + [slug: string]: { + isReverseProhibited?: boolean; + }; +} + export interface AccountState { balances?: { bySlug: Record; @@ -152,7 +242,9 @@ export interface AccountState { byId: Record; idsBySlug?: Record; newestTransactionsBySlug?: Record; - isHistoryEndReached?: boolean; + isMainHistoryEndReached?: boolean; + isHistoryEndReachedBySlug?: Record; + localTransactions?: ApiTransactionActivity[]; }; nfts?: { byAddress: Record; @@ -164,25 +256,40 @@ export interface AccountState { currentActivityId?: string; currentTokenPeriod?: TokenPeriod; savedAddresses?: Record; - stakingBalance?: number; - isUnstakeRequested?: boolean; - poolState?: ApiPoolState; - stakingHistory?: ApiBackendStakingState; - activeContentTabIndex?: ContentTab; + activeContentTab?: ContentTab; landscapeActionsActiveTabIndex?: ActiveTab; + + // Staking + staking?: { + type: ApiStakingType; + balance: number; + apy: number; + isUnstakeRequested: boolean; + start: number; + end: number; + totalProfit: number; + // liquid + unstakeRequestedAmount?: number; + tokenBalance?: number; + isInstantUnstakeRequested?: boolean; + }; + stakingHistory?: ApiStakingHistory; } export interface AccountSettings { orderedSlugs?: string[]; - enabledSlugs?: string[]; - disabledSlugs?: string[]; + exceptionSlugs?: string[]; + deletedSlugs?: string[]; } export type GlobalState = { + DEBUG_capturedId?: number; + appState: AppState; auth: { state: AuthState; + biometricsStep?: 1 | 2; method?: AuthMethod; isLoading?: boolean; mnemonic?: string[]; @@ -191,8 +298,17 @@ export type GlobalState = { address?: string; error?: string; password?: string; + isBackupModalOpen?: boolean; + }; + + biometrics: { + state: BiometricsState; + error?: string; + password?: string; }; + nativeBiometricsError?: string; + hardware: { hardwareState?: HardwareConnectState; hardwareWallets?: LedgerWalletInfo[]; @@ -209,7 +325,6 @@ export type GlobalState = { toAddress?: string; toAddressName?: string; resolvedAddress?: string; - normalizedAddress?: string; error?: string; amount?: number; fee?: string; @@ -223,6 +338,40 @@ export type GlobalState = { isToNewAddress?: boolean; }; + currentSwap: { + state: SwapState; + slippage: number; + tokenInSlug?: string; + tokenOutSlug?: string; + amountIn?: number; + amountOut?: number; + amountOutMin?: string; + transactionFee?: string; + networkFee?: number; + realNetworkFee?: number; + swapFee?: string; + priceImpact?: number; + dexLabel?: string; + activityId?: string; + error?: string; + errorType?: SwapErrorType; + isLoading?: boolean; + shouldEstimate?: boolean; + isEstimating?: boolean; + inputSource?: SwapInputSource; + swapType?: SwapType; + feeSource?: SwapFeeSource; + toAddress?: string; + payinAddress?: string; + pairs?: { + bySlug: Record; + }; + limits?: { + fromMin?: string; + fromMax?: string; + }; + }; + currentSignature?: { promiseId: string; dataHex: string; @@ -256,8 +405,16 @@ export type GlobalState = { isLoading?: boolean; isUnstaking?: boolean; amount?: number; + tokenAmount?: string; fee?: string; error?: string; + type?: ApiStakingType; + }; + + stakingInfo: { + liquid?: { + instantAvailable: string; + }; }; accounts?: { @@ -270,9 +427,14 @@ export type GlobalState = { bySlug: Record; }; + swapTokenInfo: { + bySlug: Record; + }; + byAccountId: Record; settings: { + state: SettingsState; theme: Theme; animationLevel: AnimationLevel; langCode: LangCode; @@ -284,6 +446,7 @@ export type GlobalState = { isTonProxyEnabled?: boolean; isTonMagicEnabled?: boolean; isDeeplinkHookEnabled?: boolean; + isPasswordNumeric?: boolean; // Backwards compatibility for non-numeric passwords from older versions isTestnet?: boolean; isSecurityWarningHidden?: boolean; areTokensWithNoBalanceHidden?: boolean; @@ -291,8 +454,10 @@ export type GlobalState = { isSortByValueEnabled?: boolean; importToken?: { isLoading?: boolean; - token?: UserToken; + token?: UserToken | UserSwapToken; }; + authConfig?: AuthConfig; + baseCurrency?: ApiBaseCurrency; }; dialogs: string[]; @@ -301,7 +466,12 @@ export type GlobalState = { isAddAccountModalOpen?: boolean; isBackupWalletModalOpen?: boolean; isHardwareModalOpen?: boolean; + isStakingInfoModalOpen?: boolean; + isQrScannerOpen?: boolean; areSettingsOpen?: boolean; + isAppUpdateAvailable?: boolean; + confettiRequestedAt?: number; + isPinPadPasswordAccepted?: boolean; stateVersion: number; }; @@ -317,19 +487,31 @@ export interface ActionPayloads { afterCheckMnemonic: undefined; skipCheckMnemonic: undefined; restartCheckMnemonicIndexes: undefined; - afterCreatePassword: { password: string }; + cancelDisclaimer: undefined; + afterCreatePassword: { password: string; isPasswordNumeric?: boolean }; + afterCreateBiometrics: undefined; + afterCreateNativeBiometrics: undefined; + skipCreateNativeBiometrics: undefined; + createPin: { pin: string; isImporting: boolean }; + confirmPin: { isImporting: boolean }; + cancelConfirmPin: { isImporting: boolean }; startImportingWallet: undefined; afterImportMnemonic: { mnemonic: string[] }; startImportingHardwareWallet: { driver: ApiLedgerDriver }; confirmDisclaimer: undefined; + afterConfirmDisclaimer: undefined; cleanAuthError: undefined; openAbout: undefined; closeAbout: undefined; + openAuthBackupWalletModal: undefined; + closeAuthBackupWalletModal: { isBackupCreated?: boolean } | undefined; + initializeHardwareWalletConnection: undefined; connectHardwareWallet: undefined; createHardwareAccounts: undefined; - createAccount: { password: string; isImporting: boolean }; + createAccount: { password: string; isImporting: boolean; isPasswordNumeric?: boolean }; afterSelectHardwareWallets: { hardwareSelectedIndices: number[] }; resetApiSettings: { areAllDisabled?: boolean } | undefined; + checkAppVersion: undefined; selectToken: { slug?: string } | undefined; openBackupWalletModal: undefined; @@ -343,7 +525,13 @@ export interface ActionPayloads { setTransferToAddress: { toAddress?: string }; setTransferComment: { comment?: string }; setTransferShouldEncrypt: { shouldEncrypt?: boolean }; - startTransfer: { tokenSlug?: string; amount?: number; toAddress?: string; comment?: string } | undefined; + startTransfer: { + isPortrait?: boolean; + tokenSlug?: string; + amount?: number; + toAddress?: string; + comment?: string; + } | undefined; changeTransferToken: { tokenSlug: string }; fetchFee: { tokenSlug: string; @@ -363,7 +551,7 @@ export interface ActionPayloads { submitTransferPassword: { password: string }; submitTransferHardware: undefined; clearTransferError: undefined; - cancelTransfer: undefined; + cancelTransfer: { shouldReset?: boolean } | undefined; showDialog: { message: string }; dismissDialog: undefined; showError: { error?: ApiAnyDisplayError | string }; @@ -375,18 +563,19 @@ export interface ActionPayloads { cancelCaching: undefined; afterSignOut: { isFromAllAccounts?: boolean } | undefined; addAccount: { method: AuthMethod; password: string }; + addAccount2: { method: AuthMethod; password: string }; switchAccount: { accountId: string; newNetwork?: ApiNetwork }; renameAccount: { accountId: string; title: string }; clearAccountError: undefined; validatePassword: { password: string }; verifyHardwareAddress: undefined; - fetchTokenTransactions: { limit: number; slug: string; offsetId?: string }; - fetchAllTransactions: { limit: number }; - resetIsHistoryEndReached: undefined; + fetchTokenTransactions: { limit: number; slug: string; shouldLoadWithBudget?: boolean }; + fetchAllTransactions: { limit: number; shouldLoadWithBudget?: boolean }; + resetIsHistoryEndReached: { slug: string } | undefined; fetchNfts: undefined; - showActivityInfo: { id?: string } | undefined; - closeActivityInfo: undefined; + showActivityInfo: { id: string }; + closeActivityInfo: { id: string }; submitSignature: { password: string }; clearSignatureError: undefined; @@ -400,7 +589,15 @@ export interface ActionPayloads { closeAddAccountModal: undefined; setLandscapeActionsActiveTabIndex: { index: ActiveTab }; - setActiveContentTabIndex: { index: ContentTab }; + setActiveContentTab: { tab: ContentTab }; + + requestConfetti: undefined; + setIsPinPadPasswordAccepted: undefined; + clearIsPinPadPasswordAccepted: undefined; + + openQrScanner: undefined; + closeQrScanner: undefined; + openDeeplink: { url: string }; // Staking startStaking: { isUnstaking?: boolean } | undefined; @@ -409,12 +606,15 @@ export interface ActionPayloads { submitStakingPassword: { password: string; isUnstaking?: boolean }; clearStakingError: undefined; cancelStaking: undefined; - fetchStakingState: undefined; - fetchBackendStakingState: undefined; + fetchStakingHistory: { limit?: number; offset?: number } | undefined; fetchStakingFee: { amount: number }; + openStakingInfo: undefined; + closeStakingInfo: undefined; // Settings openSettings: undefined; + openSettingsWithState: { state: SettingsState }; + setSettingsState: { state?: SettingsState }; closeSettings: undefined; setTheme: { theme: Theme }; setAnimationLevel: { level: AnimationLevel }; @@ -432,13 +632,23 @@ export interface ActionPayloads { toggleTokensWithNoPrice: { isEnabled: boolean }; toggleSortByValue: { isEnabled: boolean }; initTokensOrder: undefined; + updateDeletionListForActiveTokens: { accountId: string } | undefined; sortTokens: { orderedSlugs: string[] }; - updateDisabledSlugs: undefined; - toggleDisabledToken: { slug: string }; + toggleExceptionToken: { slug: string }; addToken: { token: UserToken }; deleteToken: { slug: string }; - importToken: { address: string }; + importToken: { address: string; isSwap?: boolean }; resetImportToken: undefined; + closeBiometricSettings: undefined; + openBiometricsTurnOn: undefined; + openBiometricsTurnOffWarning: undefined; + openBiometricsTurnOff: undefined; + enableBiometrics: { password: string }; + disableBiometrics: { password: string; isPasswordNumeric?: boolean }; + enableNativeBiometrics: { password: string }; + disableNativeBiometrics: undefined; + changeBaseCurrency: { currency: ApiBaseCurrency }; + clearNativeBiometricsError: undefined; // TON Connect submitDappConnectRequestConfirm: { accountId: string; password?: string }; @@ -457,4 +667,34 @@ export interface ActionPayloads { getDapps: undefined; deleteAllDapps: undefined; deleteDapp: { origin: string }; + + apiUpdateDappConnect: ApiUpdateDappConnect; + apiUpdateDappSendTransaction: ApiUpdateDappSendTransactions; + + // Swap + submitSwap: { password: string }; + startSwap: { tokenInSlug?: string; tokenOutSlug?: string; amountIn?: number; isPortrait?: boolean } | undefined; + cancelSwap: { shouldReset?: boolean } | undefined; + setDefaultSwapParams: { tokenInSlug?: string; tokenOutSlug?: string } | undefined; + switchSwapTokens: undefined; + setSwapTokenIn: { tokenSlug: string }; + setSwapTokenOut: { tokenSlug: string }; + setSwapAmountIn: { amount?: number }; + setSwapAmountOut: { amount?: number }; + setSlippage: { slippage: number }; + loadSwapPairs: { tokenSlug: string; shouldForceUpdate?: boolean }; + estimateSwap: { shouldBlock: boolean }; + setSwapScreen: { state: SwapState }; + clearSwapError: undefined; + estimateSwapCex: { shouldBlock: boolean }; + submitSwapCexFromTon: { password: string }; + submitSwapCexToTon: { password: string }; + setSwapType: { type: SwapType }; + setSwapCexAddress: { toAddress: string }; + addSwapToken: { token: UserSwapToken }; +} + +export enum LoadMoreDirection { + Forwards, + Backwards, } diff --git a/src/hooks/freezeWhenClosed.ts b/src/hooks/freezeWhenClosed.ts new file mode 100644 index 00000000..5968eb6f --- /dev/null +++ b/src/hooks/freezeWhenClosed.ts @@ -0,0 +1,20 @@ +import { type FC, type Props, useRef } from '../lib/teact/teact'; + +export default function freezeWhenClosed(Component: T) { + function ComponentWrapper(props: Props) { + const newProps = useRef(props); + + if (props.isOpen) { + newProps.current = props; + } else { + newProps.current = { + ...newProps.current, + isOpen: false, + }; + } + + return Component(newProps.current); + } + + return ComponentWrapper as T; +} diff --git a/src/hooks/useDelegatedBottomSheet.ts b/src/hooks/useDelegatedBottomSheet.ts new file mode 100644 index 00000000..1d72db7c --- /dev/null +++ b/src/hooks/useDelegatedBottomSheet.ts @@ -0,0 +1,207 @@ +import type { HTMLInputTypeAttribute } from 'react'; +import type { SafeAreaInsets } from 'capacitor-plugin-safe-area'; +import { SafeArea } from 'capacitor-plugin-safe-area'; +import type { BottomSheetKeys } from 'native-bottom-sheet'; +import { BottomSheet } from 'native-bottom-sheet'; +import { useEffect, useLayoutEffect, useState } from '../lib/teact/teact'; +import { forceOnHeavyAnimationOnce } from '../lib/teact/teactn'; +import { getActions, setGlobal } from '../global'; + +import type { ActionPayloads, GlobalState } from '../global/types'; + +import cssColorToHex from '../util/cssColorToHex'; +import { setStatusBarStyle } from '../util/switchTheme'; +import { IS_DELEGATED_BOTTOM_SHEET } from '../util/windowEnvironment'; +import { useDeviceScreen } from './useDeviceScreen'; +import useEffectWithPrevDeps from './useEffectWithPrevDeps'; + +const BLUR_TIMEOUT = 50; + +const controlledByMain = new Map(); + +const textInputTypes: Set = new Set([ + 'color', 'date', 'datetime-local', 'email', 'month', 'number', + 'password', 'search', 'tel', 'text', 'time', 'url', 'week', +]); + +let safeAreaCache: SafeAreaInsets['insets'] | undefined; +const safeArePromise = SafeArea.getSafeAreaInsets().then(({ insets }) => { + safeAreaCache = insets; + return safeAreaCache; +}); + +let currentKey: BottomSheetKeys | undefined; + +if (IS_DELEGATED_BOTTOM_SHEET) { + BottomSheet.addListener('delegate', ({ key, globalJson }: { key: BottomSheetKeys; globalJson: string }) => { + currentKey = key; + controlledByMain.get(key)?.(); + + setGlobal( + JSON.parse(globalJson) as GlobalState, + { forceOutdated: true, forceSyncOnIOs: true }, + ); + }); + + BottomSheet.addListener('move', () => { + window.dispatchEvent(new Event('viewportmove')); + }); + + BottomSheet.addListener( + 'callActionInNative', + ({ name, optionsJson }: { name: string; optionsJson: string }) => { + const action = getActions()[name as K]; + action((JSON.parse(optionsJson) || undefined) as ActionPayloads[K]); + }, + ); +} + +export function useDelegatedBottomSheet( + key: BottomSheetKeys | undefined, + isOpen: boolean | undefined, + onClose: AnyToVoidFunction, + dialogRef: React.RefObject, + forceFullNative = false, + noResetHeightOnBlur = false, +) { + useEffectWithPrevDeps(([prevIsOpen]) => { + if (!IS_DELEGATED_BOTTOM_SHEET || !key || key !== currentKey) return; + + if (isOpen) { + const dialogEl = dialogRef.current!; + + BottomSheet.openSelf({ + key, + height: String(dialogEl.offsetHeight), + backgroundColor: cssColorToHex(getComputedStyle(dialogEl).backgroundColor), + }).then(() => { + forceOnHeavyAnimationOnce(); + onClose(); + }); + } else if (prevIsOpen) { + BottomSheet.closeSelf({ key }); + setStatusBarStyle(); + } + }, [dialogRef, isOpen, key, onClose]); + + const { screenHeight } = useDeviceScreen(); + const [safeArea, setSafeArea] = useState(safeAreaCache); + safeArePromise.then(setSafeArea); + // We use Safe Area plugin instead of CSS `env()` function as it does not depend on modal position + const maxHeight = screenHeight - (safeArea?.top || 0); + + useLayoutEffect(() => { + if (!IS_DELEGATED_BOTTOM_SHEET || !isOpen) return; + + dialogRef.current!.style[forceFullNative ? 'height' : 'maxHeight'] = `${maxHeight}px`; + }, [dialogRef, forceFullNative, isOpen, maxHeight]); + + useEffectWithPrevDeps(([prevForceFullNative]) => { + if (!IS_DELEGATED_BOTTOM_SHEET || !isOpen) return; + + // Skip initial opening + if (prevForceFullNative === undefined) return; + + BottomSheet.setSelfSize({ size: forceFullNative ? 'full' : 'half' }); + }, [forceFullNative, isOpen]); + + useLayoutEffect(() => { + if (!IS_DELEGATED_BOTTOM_SHEET || !isOpen) return undefined; + + const dialogEl = dialogRef.current!; + let blurTimeout: number | undefined; + + function onFocus(e: FocusEvent) { + if (!isInput(e.target)) { + return; + } + + if (blurTimeout) { + clearTimeout(blurTimeout); + blurTimeout = undefined; + return; + } + + preventScrollOnFocus(dialogEl); + + BottomSheet.setSelfSize({ size: 'full' }); + } + + function onBlur(e: FocusEvent) { + if (!isInput(e.target) || noResetHeightOnBlur) { + return; + } + + blurTimeout = window.setTimeout(() => { + blurTimeout = undefined; + BottomSheet.setSelfSize({ size: 'half' }); + }, BLUR_TIMEOUT); + } + + document.addEventListener('focusin', onFocus); + document.addEventListener('focusout', onBlur); + + return () => { + document.removeEventListener('focusout', onBlur); + document.removeEventListener('focusin', onFocus); + }; + }, [dialogRef, isOpen, key, noResetHeightOnBlur]); +} + +export function useOpenFromMainBottomSheet( + key: BottomSheetKeys, + open: NoneToVoidFunction, +) { + useEffect(() => { + if (!IS_DELEGATED_BOTTOM_SHEET) return undefined; + + controlledByMain.set(key, open); + + if (currentKey === key) { + open(); + } + + return () => { + if (controlledByMain.get(key) === open) { + controlledByMain.delete(key); + } + }; + }, [key, open]); +} + +export function callActionInMain(name: K, options?: ActionPayloads[K]) { + BottomSheet.callActionInMain({ + name, + // eslint-disable-next-line no-null/no-null + optionsJson: JSON.stringify(options ?? null), + }); +} + +export function callActionInNative(name: K, options?: ActionPayloads[K]) { + BottomSheet.callActionInNative({ + name, + // eslint-disable-next-line no-null/no-null + optionsJson: JSON.stringify(options ?? null), + }); +} + +export function openInMain(key: BottomSheetKeys) { + BottomSheet.openInMain({ key }); +} + +function isInput(el?: EventTarget | null) { + if (!el || !(el instanceof HTMLElement)) return false; + + return (el.tagName === 'INPUT' && textInputTypes.has((el as HTMLInputElement).type)) + || el.tagName === 'TEXTAREA' + || (el.tagName === 'DIV' && el.isContentEditable); +} + +function preventScrollOnFocus(el: HTMLDivElement) { + el.style.opacity = '0'; + setTimeout(() => { + el.style.opacity = '1'; + }); + + document.documentElement.scrollTop = 0; +} diff --git a/src/hooks/useDelegatingBottomSheet.ts b/src/hooks/useDelegatingBottomSheet.ts new file mode 100644 index 00000000..4974fb9f --- /dev/null +++ b/src/hooks/useDelegatingBottomSheet.ts @@ -0,0 +1,103 @@ +import type { BottomSheetKeys } from 'native-bottom-sheet'; +import { BottomSheet } from 'native-bottom-sheet'; +import { useEffect } from '../lib/teact/teact'; +import { forceOnHeavyAnimationOnce } from '../lib/teact/teactn'; +import { getActions, getGlobal } from '../global'; + +import type { ActionPayloads } from '../global/types'; + +import { pause } from '../util/schedulers'; +import { CAN_DELEGATE_BOTTOM_SHEET } from '../util/windowEnvironment'; +import useEffectWithPrevDeps from './useEffectWithPrevDeps'; + +const RACE_TIMEOUT = 1000; +const CLOSING_DURATION = 100; + +const controlledByNative = new Map(); + +if (CAN_DELEGATE_BOTTOM_SHEET) { + BottomSheet.prepare(); + + BottomSheet.addListener( + 'callActionInMain', + ({ name, optionsJson }: { name: string; optionsJson: string }) => { + const action = getActions()[name as K]; + action((JSON.parse(optionsJson) || undefined) as ActionPayloads[K]); + }, + ); + + BottomSheet.addListener( + 'openInMain', + ({ key }: { key: BottomSheetKeys }) => { + controlledByNative.get(key)?.(); + }, + ); +} + +let lastOpenCall = Promise.resolve(); +let closeCurrent: NoneToVoidFunction | undefined; + +export function useDelegatingBottomSheet( + key: BottomSheetKeys | undefined, + isPortrait: boolean | undefined, + isOpen: boolean | undefined, + onClose: AnyToVoidFunction, +) { + const isDelegating = CAN_DELEGATE_BOTTOM_SHEET && key; + const shouldOpen = isOpen && isPortrait; + + useEffectWithPrevDeps(([prevShouldOpen]) => { + if (!isDelegating) return; + + if (shouldOpen) { + closeCurrent?.(); + + const closeNext = () => { + forceOnHeavyAnimationOnce(); + onClose(); + }; + + closeCurrent = closeNext; + + // Wait until previous call resolves to get an up-to-date global + lastOpenCall = Promise.race([ + lastOpenCall, + pause(RACE_TIMEOUT), // Sometimes the last open call is stuck for some unknown reason + ]) + .then(() => { + return BottomSheet.delegate({ + key, + globalJson: JSON.stringify(getGlobal()), + }); + }) + .then(() => { + if (closeCurrent === closeNext) { + closeCurrent(); + closeCurrent = undefined; + } + }) + .then(() => pause(CLOSING_DURATION)); + } else if (prevShouldOpen) { + BottomSheet.release({ key }); + } + }, [shouldOpen, isDelegating, key, onClose]); + + return isDelegating && isPortrait; +} + +export function useOpenFromNativeBottomSheet( + key: BottomSheetKeys, + open: NoneToVoidFunction, +) { + useEffect(() => { + if (!CAN_DELEGATE_BOTTOM_SHEET) return undefined; + + controlledByNative.set(key, open); + + return () => { + if (controlledByNative.get(key) === open) { + controlledByNative.delete(key); + } + }; + }, [key, open]); +} diff --git a/src/hooks/useDeviceScreen.ts b/src/hooks/useDeviceScreen.ts index 01ba58da..2fe8b313 100644 --- a/src/hooks/useDeviceScreen.ts +++ b/src/hooks/useDeviceScreen.ts @@ -3,9 +3,12 @@ import { useMediaQuery } from './useMediaQuery'; export function useDeviceScreen() { const isPortrait = useMediaQuery(`(max-width: ${MOBILE_SCREEN_MAX_WIDTH - 0.02}px)`); + const isSmallHeight = useMediaQuery('(max-height: 43.5rem)'); return { isPortrait, + isSmallHeight, isLandscape: !isPortrait, + screenHeight: window.screen.height, }; } diff --git a/src/hooks/useElectronDrag.ts b/src/hooks/useElectronDrag.ts index 782c1daa..8749428e 100644 --- a/src/hooks/useElectronDrag.ts +++ b/src/hooks/useElectronDrag.ts @@ -1,8 +1,7 @@ import type { RefObject } from 'react'; import { useEffect, useRef } from '../lib/teact/teact'; -import { IS_ELECTRON } from '../config'; -import { IS_MAC_OS } from '../util/windowEnvironment'; +import { IS_ELECTRON, IS_MAC_OS } from '../util/windowEnvironment'; const DRAG_DISTANCE_THRESHOLD = 5; diff --git a/src/hooks/useHistoryBack.ts b/src/hooks/useHistoryBack.ts new file mode 100644 index 00000000..c210018b --- /dev/null +++ b/src/hooks/useHistoryBack.ts @@ -0,0 +1,266 @@ +import { useCallback, useRef } from '../lib/teact/teact'; + +import { IS_TEST } from '../config'; +import { requestMeasure } from '../lib/fasterdom/fasterdom'; +import { IS_IOS, IS_LEDGER_EXTENSION_TAB } from '../util/windowEnvironment'; +import useEffectOnce from './useEffectOnce'; +import useLastCallback from './useLastCallback'; +import useSyncEffect from './useSyncEffect'; + +const PATH_BASE = `${window.location.pathname}${window.location.search}`; + +type HistoryRecord = { + index: number; + // Should this record be replaced by the next record (for example Menu) + shouldBeReplaced?: boolean; + // Mark this record as replaced by the next record. Only used to check if needed to perform effectBack + markReplaced?: VoidFunction; + onBack?: VoidFunction; + // Set if the element is closed in the UI, but not in the real history + isClosed?: boolean; +}; + +type HistoryOperationGo = { + type: 'go'; + delta: number; +}; + +type HistoryOperationState = { + type: 'pushState' | 'replaceState'; + data: any; +}; + +type HistoryOperation = HistoryOperationGo | HistoryOperationState; + +// Needed to dismiss any 'trashed' history records from the previous page reloads. +const historyUniqueSessionId = Number(new Date()); +// Reflects real history state, but also contains information on which records should be replaced by the next record and +// which records are deferred to close on the next operation +let historyState: HistoryRecord[]; +// Reflects current real history index +let historyCursor: number; +// If we alter real history programmatically, the popstate event will be fired, which we don't need +let isAlteringHistory = false; +// Unfortunately Safari doesn't really like when there's 2+ consequent history operations in one frame, so we need +// to delay them to the next raf +let deferredHistoryOperations: HistoryOperation[] = []; +let deferredPopstateOperations: HistoryOperationState[] = []; + +// Do not remove: used for history unit tests +if (IS_TEST) { + (window as any).TEST_getHistoryState = () => historyState; + (window as any).TEST_getHistoryCursor = () => historyCursor; +} + +function applyDeferredHistoryOperations() { + const goOperations = deferredHistoryOperations.filter((op) => op.type === 'go') as HistoryOperationGo[]; + const stateOperations = deferredHistoryOperations.filter((op) => op.type !== 'go') as HistoryOperationState[]; + const goCount = goOperations.reduce((acc, op) => acc + op.delta, 0); + + deferredHistoryOperations = []; + + if (goCount) { + window.history.go(goCount); + + // If we have some `state` operations after the `go` operations, we need to wait until the popstate event + // so the order of operations is correctly preserved + if (stateOperations.length) { + deferredPopstateOperations.push(...stateOperations); + return; + } + } + + processStateOperations(stateOperations); +} + +function processStateOperations(stateOperations: HistoryOperationState[]) { + stateOperations.forEach((op) => window.history[op.type](op.data, '')); +} + +function deferHistoryOperation(historyOperation: HistoryOperation) { + if (!deferredHistoryOperations.length) { + requestMeasure(applyDeferredHistoryOperations); + } + + deferredHistoryOperations.push(historyOperation); +} + +// Resets history to the `root` state +function resetHistory() { + historyCursor = 0; + historyState = [{ + index: 0, + onBack: () => window.history.back(), + }]; + + if (!IS_LEDGER_EXTENSION_TAB) { + window.history.replaceState({ index: 0, historyUniqueSessionId }, '', PATH_BASE); + } +} + +resetHistory(); + +function cleanupClosed(alreadyClosedCount = 1) { + let countClosed = alreadyClosedCount; + for (let i = historyCursor - 1; i > 0; i--) { + if (!historyState[i].isClosed) break; + countClosed++; + } + if (countClosed) { + isAlteringHistory = true; + deferHistoryOperation({ + type: 'go', + delta: -countClosed, + }); + } + return countClosed; +} + +function cleanupTrashedState() { + for (let i = historyState.length - 1; i > 0; i--) { + if (historyState[i].isClosed) { + continue; + } + historyState[i].onBack?.(); + } + + resetHistory(); +} + +window.addEventListener('popstate', ({ state }: PopStateEvent) => { + if (isAlteringHistory) { + isAlteringHistory = false; + if (deferredPopstateOperations.length) { + processStateOperations(deferredPopstateOperations); + deferredPopstateOperations = []; + } + return; + } + + if (!state) { + cleanupTrashedState(); + return; + } + + const { index, historyUniqueSessionId: previousUniqueSessionId } = state; + if (previousUniqueSessionId !== historyUniqueSessionId) { + cleanupTrashedState(); + return; + } + + // New real history state matches the old virtual one. Not possible in theory, but in practice we have Safari + if (index === historyCursor) { + return; + } + + if (index < historyCursor) { + // Navigating back + let alreadyClosedCount = 0; + for (let i = historyCursor; i > index - alreadyClosedCount; i--) { + if (historyState[i].isClosed) { + alreadyClosedCount++; + continue; + } + historyState[i].onBack?.(); + } + + const countClosed = cleanupClosed(alreadyClosedCount); + historyCursor += index - historyCursor - countClosed; + + // Can happen when we have deferred a real back for some element (for example Menu), closed via UI, + // pressed back button and caused a pushState. + if (historyCursor < 0) { + historyCursor = 0; + } + } else if (index > historyCursor) { + // Forward navigation is not yet supported + isAlteringHistory = true; + deferHistoryOperation({ + type: 'go', + delta: -(index - historyCursor), + }); + } +}); + +export default function useHistoryBack({ + isActive, + shouldBeReplaced, + onBack, +}: { + isActive?: boolean; + shouldBeReplaced?: boolean; + onBack: VoidFunction; +}) { + const lastOnBack = useLastCallback(onBack); + + // Active index of the record + const indexRef = useRef(); + const wasReplaced = useRef(false); + + const isFirstRender = useRef(true); + + const pushState = useCallback((forceReplace = false) => { + // Check if the old state should be replaced + const shouldReplace = forceReplace || historyState[historyCursor].shouldBeReplaced; + indexRef.current = shouldReplace ? historyCursor : ++historyCursor; + + historyCursor = indexRef.current; + + // Mark the previous record as replaced so effectBack doesn't perform back operation on the new record + const previousRecord = historyState[indexRef.current]; + if (previousRecord && !previousRecord.isClosed) { + previousRecord.markReplaced?.(); + } + + historyState[indexRef.current] = { + index: indexRef.current, + onBack: lastOnBack, + shouldBeReplaced, + markReplaced: () => { + wasReplaced.current = true; + }, + }; + + deferHistoryOperation({ + type: shouldReplace ? 'replaceState' : 'pushState', + data: { + index: indexRef.current, + historyUniqueSessionId, + }, + }); + }, [lastOnBack, shouldBeReplaced]); + + const processBack = useCallback(() => { + // Only process back on open records + if (indexRef.current && historyState[indexRef.current] && !wasReplaced.current) { + historyState[indexRef.current].isClosed = true; + wasReplaced.current = true; + if (indexRef.current === historyCursor && !shouldBeReplaced) { + historyCursor -= cleanupClosed(); + } + } + }, [shouldBeReplaced]); + + // Process back navigation when element is unmounted + useEffectOnce(() => { + if (IS_IOS) return undefined; + + isFirstRender.current = false; + return () => { + if (!isActive || wasReplaced.current) return; + processBack(); + }; + }); + + useSyncEffect(([prevIsActive]) => { + if (IS_IOS) return; + if (prevIsActive === isActive) return; + if (isFirstRender.current && !isActive) return; + + if (isActive) { + pushState(); + } else { + processBack(); + } + }, [isActive, processBack, pushState]); +} diff --git a/src/hooks/useInfiniteScroll.ts b/src/hooks/useInfiniteScroll.ts new file mode 100644 index 00000000..38ccbf4a --- /dev/null +++ b/src/hooks/useInfiniteScroll.ts @@ -0,0 +1,135 @@ +import { useRef } from '../lib/teact/teact'; + +import { LoadMoreDirection } from '../global/types'; + +import { areSortedArraysEqual } from '../util/iteratees'; +import useForceUpdate from './useForceUpdate'; +import useLastCallback from './useLastCallback'; +import usePrevious from './usePrevious'; + +type GetMore = (args: { direction: LoadMoreDirection }) => void; +type LoadMoreBackwards = (args: { offsetId?: string | number }) => void; + +const DEFAULT_LIST_SLICE = 30; + +const useInfiniteScroll = ( + loadMoreBackwards?: LoadMoreBackwards, + listIds?: ListId[], + isDisabled = false, + listSlice = DEFAULT_LIST_SLICE, +): [ListId[]?, GetMore?] => { + const requestParamsRef = useRef<{ + direction?: LoadMoreDirection; + offsetId?: ListId; + }>(); + + const currentStateRef = useRef<{ viewportIds: ListId[]; isOnTop: boolean } | undefined>(); + if (!currentStateRef.current && listIds && !isDisabled) { + const { + newViewportIds, + newIsOnTop, + } = getViewportSlice(listIds, LoadMoreDirection.Forwards, listSlice, listIds[0]); + currentStateRef.current = { viewportIds: newViewportIds, isOnTop: newIsOnTop }; + } + + const forceUpdate = useForceUpdate(); + + if (isDisabled) { + requestParamsRef.current = {}; + } + + const prevListIds = usePrevious(listIds); + const prevIsDisabled = usePrevious(isDisabled); + const areListsEqual = listIds && prevListIds + && listIds.length > 0 && prevListIds.length > 0 + && listIds[0] === prevListIds[0] + && listIds[listIds.length - 1] === prevListIds[prevListIds.length - 1]; + + if (listIds && !isDisabled && (!areListsEqual || isDisabled !== prevIsDisabled)) { + const { viewportIds, isOnTop } = currentStateRef.current || {}; + const currentMiddleId = viewportIds && !isOnTop ? viewportIds[Math.round(viewportIds.length / 2)] : undefined; + const defaultOffsetId = currentMiddleId && listIds.includes(currentMiddleId) ? currentMiddleId : listIds[0]; + const { offsetId = defaultOffsetId, direction = LoadMoreDirection.Forwards } = requestParamsRef.current || {}; + const { newViewportIds, newIsOnTop } = getViewportSlice(listIds, direction, listSlice, offsetId); + + requestParamsRef.current = {}; + + if (!viewportIds || !areSortedArraysEqual(viewportIds, newViewportIds)) { + currentStateRef.current = { viewportIds: newViewportIds, isOnTop: newIsOnTop }; + } + } else if (!listIds) { + currentStateRef.current = undefined; + } + + const getMore: GetMore = useLastCallback(({ + direction, + }: { direction: LoadMoreDirection; noScroll?: boolean }) => { + const { viewportIds } = currentStateRef.current || {}; + + const offsetId = viewportIds + ? direction === LoadMoreDirection.Backwards ? viewportIds[viewportIds.length - 1] : viewportIds[0] + : undefined; + + requestParamsRef.current = { direction, offsetId }; + + if (!listIds) { + if (loadMoreBackwards) { + loadMoreBackwards({ offsetId }); + } + + return; + } + + const { + newViewportIds, areSomeLocal, areAllLocal, newIsOnTop, + } = getViewportSlice(listIds, direction, listSlice, offsetId); + + if (areSomeLocal && !(viewportIds && areSortedArraysEqual(viewportIds, newViewportIds))) { + currentStateRef.current = { viewportIds: newViewportIds, isOnTop: newIsOnTop }; + forceUpdate(); + } + + if (!areAllLocal && loadMoreBackwards) { + loadMoreBackwards({ offsetId }); + } + }); + + return isDisabled ? [listIds] : [currentStateRef.current?.viewportIds, getMore]; +}; + +function getViewportSlice( + sourceIds: ListId[], + direction: LoadMoreDirection, + listSlice: number, + offsetId?: ListId, +) { + const { length } = sourceIds; + const index = offsetId ? sourceIds.indexOf(offsetId) : 0; + const isForwards = direction === LoadMoreDirection.Forwards; + const indexForDirection = isForwards ? index : (index + 1) || length; + const from = Math.max(0, indexForDirection - listSlice); + const to = indexForDirection + listSlice - 1; + const newViewportIds = sourceIds.slice(Math.max(0, from), to + 1); + + let areSomeLocal; + let areAllLocal; + switch (direction) { + case LoadMoreDirection.Forwards: + areSomeLocal = indexForDirection >= 0; + areAllLocal = from >= 0; + break; + case LoadMoreDirection.Backwards: + areSomeLocal = indexForDirection < length; + areAllLocal = to <= length - 1; + break; + } + + return { + newViewportIds, + areSomeLocal, + areAllLocal, + newIsOnTop: newViewportIds[0] === sourceIds[0], + }; +} + +export default useInfiniteScroll; diff --git a/src/hooks/useInterval.ts b/src/hooks/useInterval.ts index e85f41ba..bc490e31 100644 --- a/src/hooks/useInterval.ts +++ b/src/hooks/useInterval.ts @@ -1,22 +1,20 @@ -import { useEffect, useLayoutEffect, useRef } from '../lib/teact/teact'; +import { useEffect } from '../lib/teact/teact'; -function useInterval(callback: NoneToVoidFunction, delay?: number, noFirst = false) { - const savedCallback = useRef(callback); +import useLastCallback from './useLastCallback'; - useLayoutEffect(() => { - savedCallback.current = callback; - }, [callback]); +function useInterval(callback: NoneToVoidFunction, delay?: number, noFirst = false) { + const lastCallback = useLastCallback(callback); useEffect(() => { if (delay === undefined) { return undefined; } - const id = setInterval(() => savedCallback.current(), delay); - if (!noFirst) savedCallback.current(); + const id = setInterval(lastCallback, delay); + if (!noFirst) lastCallback(); return () => clearInterval(id); - }, [delay, noFirst]); + }, [delay, lastCallback, noFirst]); } export default useInterval; diff --git a/src/hooks/usePasswordValidation.ts b/src/hooks/usePasswordValidation.ts index c4936db0..0a5e1daa 100644 --- a/src/hooks/usePasswordValidation.ts +++ b/src/hooks/usePasswordValidation.ts @@ -2,11 +2,21 @@ import { useEffect, useState } from '../lib/teact/teact'; const SPECIAL_CHARS_REGEX = /[`!@#$%^&*()_+\-=\]{};':"\\|,.<>?~]/; +interface OwnProps { + firstPassword?: string; + secondPassword?: string; + requiredMinLength?: number; + requiredLength?: number; + isOnlyNumbers?: boolean; +} + export const usePasswordValidation = ({ firstPassword = '', secondPassword = '', - requiredLength = 8, -}) => { + requiredMinLength = 8, + requiredLength, + isOnlyNumbers, +}: OwnProps) => { const [invalidLength, setInvalidLength] = useState(false); const [noNumber, setNoNumber] = useState(false); const [noUpperCase, setNoUpperCase] = useState(false); @@ -15,13 +25,17 @@ export const usePasswordValidation = ({ const [noEqual, setNoEqual] = useState(false); useEffect(() => { - setInvalidLength(firstPassword.length < requiredLength); - setNoUpperCase(firstPassword.toLowerCase() === firstPassword); - setNoLowerCase(firstPassword.toUpperCase() === firstPassword); + const isInvalidLength = Boolean( + (!requiredLength && firstPassword.length < requiredMinLength) + || (requiredLength && firstPassword.length !== requiredLength), + ); + setInvalidLength(isInvalidLength); + setNoUpperCase(!isOnlyNumbers && firstPassword.toLowerCase() === firstPassword); + setNoLowerCase(!isOnlyNumbers && firstPassword.toUpperCase() === firstPassword); setNoNumber(!/\d/.test(firstPassword)); setNoEqual(Boolean(firstPassword && firstPassword !== secondPassword)); - setNoSpecialChar(!SPECIAL_CHARS_REGEX.test(firstPassword)); - }, [firstPassword, secondPassword, requiredLength]); + setNoSpecialChar(!isOnlyNumbers && !SPECIAL_CHARS_REGEX.test(firstPassword)); + }, [firstPassword, secondPassword, requiredMinLength, isOnlyNumbers, requiredLength]); return { invalidLength, noNumber, noUpperCase, noLowerCase, noEqual, noSpecialChar, diff --git a/src/hooks/usePrevious.ts b/src/hooks/usePrevious.ts index 9e7c2139..6da28f0b 100644 --- a/src/hooks/usePrevious.ts +++ b/src/hooks/usePrevious.ts @@ -1,5 +1,6 @@ import { useRef } from '../lib/teact/teact'; +// Deprecated. Use `usePrevious2` instead function usePrevious(next: T): T | undefined; function usePrevious(next: T, shouldSkipUndefined: true): Exclude | undefined; function usePrevious(next: T, shouldSkipUndefined?: boolean): Exclude | undefined; diff --git a/src/hooks/usePrevious2.ts b/src/hooks/usePrevious2.ts new file mode 100644 index 00000000..996e25ed --- /dev/null +++ b/src/hooks/usePrevious2.ts @@ -0,0 +1,15 @@ +import { useRef } from '../lib/teact/teact'; + +// This is not render-dependent and will never allow previous to match current +export default function usePrevious2(current: T) { + const prevRef = useRef(); + const lastRef = useRef(); + + if (lastRef.current !== current) { + prevRef.current = lastRef.current; + } + + lastRef.current = current; + + return prevRef.current; +} diff --git a/src/hooks/useScrolledState.ts b/src/hooks/useScrolledState.ts index cd206572..ea7d650a 100644 --- a/src/hooks/useScrolledState.ts +++ b/src/hooks/useScrolledState.ts @@ -15,5 +15,10 @@ export default function useScrolledState(threshold = THRESHOLD) { setIsAtEnd(scrollHeight - scrollTop - clientHeight < threshold); }); - return { isAtBeginning, isAtEnd, handleScroll }; + return { + isAtBeginning, + isAtEnd, + isScrolled: !isAtBeginning, + handleScroll, + }; } diff --git a/src/hooks/useTimeout.ts b/src/hooks/useTimeout.ts index 5cc3b18b..4053bf22 100644 --- a/src/hooks/useTimeout.ts +++ b/src/hooks/useTimeout.ts @@ -1,19 +1,19 @@ -import { useEffect, useLayoutEffect, useRef } from '../lib/teact/teact'; +import { useEffect } from '../lib/teact/teact'; -function useTimeout(callback: () => void, delay?: number) { - const savedCallback = useRef(callback); +import useLastCallback from './useLastCallback'; - useLayoutEffect(() => { - savedCallback.current = callback; - }, [callback]); +function useTimeout(callback: () => void, delay?: number, dependencies: readonly any[] = []) { + const savedCallback = useLastCallback(callback); useEffect(() => { if (typeof delay !== 'number') { return undefined; } - const id = setTimeout(() => savedCallback.current(), delay); + + const id = setTimeout(() => savedCallback(), delay); return () => clearTimeout(id); - }, [delay]); + // eslint-disable-next-line react-hooks-static-deps/exhaustive-deps + }, [delay, savedCallback, ...dependencies]); } export default useTimeout; diff --git a/src/hooks/useTraceUpdatedProps.ts b/src/hooks/useTraceUpdatedProps.ts new file mode 100644 index 00000000..0cd242f5 --- /dev/null +++ b/src/hooks/useTraceUpdatedProps.ts @@ -0,0 +1,33 @@ +import { useEffect, useRef } from '../lib/teact/teact'; + +/** + * Custom React hook for tracing updates to props. + * This hook logs the changed properties of a component every time it re-renders. + * It is useful for debugging purposes to see which props have changed between renders. + * + * @param {Record} props - The current props of the component. + * @param {boolean} [shouldTrace=false] - Flag to enable or disable tracing of prop changes. + * - When true, the hook logs the changes to the console. + * - Default is false, meaning tracing is off by default. + */ +export default function useTraceUpdatedProps(props: Record, shouldTrace = false) { + const prevProps = useRef>(); + + useEffect(() => { + const changedProps = Object.entries(props).reduce((acc: Record, [key, value]) => { + if (prevProps.current) { + if (prevProps.current[key] !== value) { + acc[key] = { old: prevProps.current[key], new: value }; + } + } + return acc; + }, {}); + + if (Object.keys(changedProps).length > 0 && shouldTrace) { + // eslint-disable-next-line no-console + console.log('Changed props:', changedProps); + } + + prevProps.current = props; + }); +} diff --git a/src/hooks/useTransitionFixes.ts b/src/hooks/useTransitionFixes.ts deleted file mode 100644 index be471c11..00000000 --- a/src/hooks/useTransitionFixes.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { requestMeasure, requestMutation } from '../lib/fasterdom/fasterdom'; -import useLastCallback from './useLastCallback'; - -// Set `min-height` for transition container to prevent jumping when switching tabs -export default function useTransitionFixes( - containerRef: { current: HTMLDivElement | null }, - transitionElSelector: string, - tabsElSelector: string, -) { - const applyTransitionFix = useLastCallback(() => { - // This callback is called from `Transition.onStart` which is "mutate" phase - requestMeasure(() => { - const container = containerRef.current; - if (!container) return; - - const transitionEl = container.querySelector(transitionElSelector); - const tabsEl = container.querySelector(tabsElSelector); - if (transitionEl && tabsEl) { - const newHeight = container.offsetHeight - tabsEl.offsetHeight; - - requestMutation(() => { - transitionEl.style.minHeight = `${newHeight}px`; - }); - } - }); - }); - - const releaseTransitionFix = useLastCallback(() => { - const container = containerRef.current!; - if (!container) return; - - const transitionEl = container.querySelector(transitionElSelector); - if (transitionEl) { - transitionEl.style.minHeight = ''; - } - }); - - return { applyTransitionFix, releaseTransitionFix }; -} diff --git a/src/hooks/useWindowSize.ts b/src/hooks/useWindowSize.ts index a26253c8..514eae11 100644 --- a/src/hooks/useWindowSize.ts +++ b/src/hooks/useWindowSize.ts @@ -7,9 +7,14 @@ import useDebouncedCallback from './useDebouncedCallback'; const THROTTLE = 250; export default function useWindowSize() { - const { width: initialWidth, height: initialHeight } = windowSize.get(); + const { + width: initialWidth, + height: initialHeight, + screenHeight: initialScreenHeight, + } = windowSize.get(); const [width, setWidth] = useState(initialWidth); const [height, setHeight] = useState(initialHeight); + const [screenHeight, setScreenHeight] = useState(initialScreenHeight); const [isResizing, setIsResizing] = useState(false); const setIsResizingDebounced = useDebouncedCallback(setIsResizing, [setIsResizing], THROTTLE, true); @@ -19,9 +24,10 @@ export default function useWindowSize() { }, THROTTLE, true); const throttledSetSize = throttle(() => { - const { width: newWidth, height: newHeight } = windowSize.get(); + const { width: newWidth, height: newHeight, screenHeight: newScreenHeight } = windowSize.get(); setWidth(newWidth); setHeight(newHeight); + setScreenHeight(newScreenHeight); setIsResizingDebounced(false); }, THROTTLE, false); @@ -37,5 +43,7 @@ export default function useWindowSize() { }; }, [setIsResizingDebounced]); - return useMemo(() => ({ width, height, isResizing }), [height, isResizing, width]); + return useMemo(() => ({ + width, height, screenHeight, isResizing, + }), [height, isResizing, screenHeight, width]); } diff --git a/src/i18n/en.yaml b/src/i18n/en.yaml index 710755ba..74a5d090 100644 --- a/src/i18n/en.yaml +++ b/src/i18n/en.yaml @@ -101,21 +101,20 @@ $logout_accounts_without_backup_warning: You have not backed up %links%. If you $logout_warning: This will disconnect the wallet from this app. You will be able to restore your wallet using **%1$d secret words** - or import another wallet. Receive TON: Receive TON Your address was copied!: Your address was copied! -Show QR-Code: Show QR-Code -Create Invoice: Create Invoice -QR-code: QR-code +Show QR Code: Show QR Code +Create Deposit Link: Create Deposit Link +QR Code: QR Code $receive_invoice_description: | You can specify the amount and purpose of - the payment to save the sender some time + the payment to save the sender some time. Amount: Amount Comment: Comment Share this URL to receive TON: Share this URL to receive TON Invoice link was copied!: Invoice link was copied! -US Dollar: US Dollar Theme: Theme Language: Language Enable Animations: Enable Animations -Fiat Currency: Fiat Currency +Base Currency: Base Currency Investor View: Investor View Toggle Investor View: Toggle Investor View Hide Tiny Transfers: Hide Tiny Transfers @@ -167,12 +166,12 @@ Loading...: Loading... Recipient Address: Recipient Address Wallet address or domain: Wallet address or domain Incorrect address: Incorrect address +Incorrect address.: Incorrect address. Paste: Paste Insufficient balance: Insufficient balance InsufficientBalance: Insufficient balance Optional: Optional $send_token_symbol: Send %1$s -$balance_is: "Balance: %balance%" Is it all ok?: Is it all ok? Receiving Address: Receiving Address Fee: Fee @@ -200,8 +199,8 @@ Today: Today Yesterday: Yesterday Now: Now $receive_ton_description: | - You can share this address, show QR-code - or create invoice to receive TON + You can share your address, scan QR code + or create a deposit link to receive crypto. Your address: Your address Wrong password, please try again: Wrong password, please try again Appearance: Appearance @@ -213,7 +212,7 @@ Earn from your tokens while holding them: Earn from your tokens while holding th $est_apy_val: Est. APY %1$d% Why this is safe: Why this is safe Why staking is safe?: Why staking is safe? -$safe_staking_description1: Staking is **fully decentralized** and operated by the **official TON Nominator** smart contract. No one can gain access to your staked tokens. +$safe_staking_description1: Staking is **fully decentralized** and operated by the **official TON Liquid Staking** smart contracts. $safe_staking_description2: The deposited stake will be used for the TON network validation as part of its **proof-of-stake** essence. $safe_staking_description3: You can withdraw your stake at **any time** and it will be deposited back to your account within **two days**. $min_value: Min %value% @@ -338,6 +337,28 @@ $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 +Swap: Swap +You sell: You sell +You buy: You buy +Swap Details: Swap Details +Blockchain fee: Blockchain fee +Price Impact: Price Impact +Minimum Received: Minimum Received +Slippage: Slippage +$swap_from_to: Swap %from% to %to% +The exchange rate is below market value!: The exchange rate is %value% below market value! +Invalid Pair: Invalid Pair +We do not recommend to perform an exchange, try to specify a lower amount.: We do not recommend to perform an exchange, try to specify a lower amount. +$swap_minimum_received_tooltip1: This is the least amount of new tokens you'll get from this swap, considering current market conditions, slippage tolerance, and potential price impact. +$swap_minimum_received_tooltip2: It's an expected minimum, but if the conditions change a lot while your swap is being processed, the final amount could be less. +$swap_price_impact_tooltip1: This shows how much your trade might change the token's price. +$swap_price_impact_tooltip2: Big trades can make the price go up or down more. Lower is usually better. +$swap_slippage_tooltip1: This sets how much the price is allowed to change before your swap is processed. +$swap_slippage_tooltip2: If during the processing of your swap, the price changes more than this value, your order will be canceled. +Coins have been swapped!: Coins have been swapped! +Slippage not specified: Slippage not specified +Slippage too high: Slippage too high +Not enough TON: Not enough TON 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. @@ -363,9 +384,128 @@ $auth_backup_description1: | $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. +Swap Placed: Swap Placed +Swapping: Swapping +Swapped: Swapped +Swap Failed: Swap Failed +Exchange rate: Exchange rate +Swap Again: Swap Again Terms of Use: Terms of Use Privacy Policy: Privacy Policy +Insufficient liquidity: Insufficient liquidity This address is new and never received transfers before.: This address is new and never received transfers before. Create Backup: Create Backup Sending: Sending -failed: failed +Failed: Failed +Refunded: Refunded +On hold: On hold +Expired: Expired +In progress: In progress +Waiting Payment: Waiting Payment +Waiting for payment: Waiting for payment +Blockchain: Blockchain +Receiving address in blockchain: Receiving address in %blockchain% blockchain +Please provide an address of your wallet in another blockchain to receive bought tokens.: Please provide an address of your wallet in another blockchain to receive bought tokens. +The time for sending coins is over: The time for sending coins is over +Please wait a few moments...: Please wait a few moments... +You have not sent the coins to the specified address.: You have not sent the coins to the specified address. +$swap_changelly_to_ton_description1: "You must send %value% to this address in %blockchain% blockchain within %time%" +Please contact support and provide a transaction ID.: Please contact support and provide a transaction ID. +Exchange failed and coins were refunded to your wallet.: Exchange failed and coins were refunded to your wallet. +Please contact security team to pass the KYC procedure.: Please contact security team to pass the KYC procedure. +Swap Expired: Swap Expired +Swap On Hold: Swap On Hold +Swap Refunded: Swap Refunded +Changelly Payment Address: Changelly Payment Address +Cross-chain exchange provided by Changelly: Cross-chain exchange provided by Changelly +$swap_changelly_agreement_message: By continuing, you agree to the %terms% and %policy% and understand that the transaction may trigger verification according to %kyc%. +$swap_changelly_terms_of_use: terms of use +$swap_changelly_privacy_policy: privacy policy +Password must contain %length% digits.: Password must contain %length% digits. +Deposit Link: Deposit Link +$swap_changelly_from_ton_description: "Tokens will be sent to your address in %blockchain% blockchain in a few moments:" +Minimum amount: Minimum %value% +Maximum amount: Maximum %value% +Display Tray Icon: Display Tray Icon +Enable Auto-Updates: Enable Auto-Updates +Biometric Authentication: Biometric Authentication +Turn off biometrics?: Turn off biometrics? +If you turn off biometric protection, you will need to create a password.: If you turn off biometric protection, you will need to create a password. +Enter your current password: Enter your current password +Turn On Biometrics: Turn On Biometrics +Please confirm transaction using biometrics: Please confirm transaction using biometrics +Enabling biometric confirmation will reset the password.: Enabling biometric confirmation will reset the password. +Biometric Registration: Biometric Registration +Step 1 of 2. Registration: Step 1 of 2. Registration +Step 2 of 2. Verification: Step 2 of 2. Verification +Biometric setup failed: Biometric setup failed +Biometric confirmation failed: Biometric confirmation failed +Failed to disable biometrics: Failed to disable biometrics +Biometric Confirmation: Biometric Confirmation +Please verify your identity.: Please verify your identity. +Create Password: Create Password +Complete: Complete +Verification: Verification +Create a password or use biometric authentication to protect it.: Create a password or use biometric authentication to protect it. +Connect Biometrics: Connect Biometrics +Use Password: Use Password +Biometrics Disabled: Biometrics Disabled +Biometrics Enabled: Biometrics Enabled +A request is already pending: A request is already pending +Please confirm operation using biometrics: Please confirm operation using biometrics +Scan QR Code: Scan QR Code +Permission denied. Please grant camera permission to use the QR code scanner.: Permission denied. Please grant camera permission to use the QR code scanner. +Unsupported barcode format: Unsupported barcode format +An error on the server side. Please try again.: An error on the server side. Please try again. +Unrecognized QR Code: Unrecognized QR Code +$fee_value_almost_equal: Fee ≈ %fee% +Address was saved!: Address was saved! +US Dollar: US Dollar +Euro: Euro +Ruble: Ruble +Yuan: Yuan +Bitcoin: Bitcoin +Toncoin: Toncoin +$max_balance: "Max: %balance%" +$unstake_information_instantly: You will get your deposit instantly. +Select Token: Select Token +MY: MY +POPULAR: POPULAR +A-Z: A-Z +Not Found: Not Found +Wallet is ready!: Wallet is ready! +Wallet is imported!: Wallet is imported! +Create a code to protect it: Create a code to protect it +Enter your code again: Enter your code again +Codes don't match: Codes don’t match +Code set successfully: Code set successfully +Use Biometrics: Use Biometrics +Use Touch ID: Use Touch ID +Use Face ID: Use Face ID +You can connect your biometric data for more convenience: You can connect your biometric data for more convenience +Connect Touch ID: Connect Touch ID +Connect Face ID: Connect Face ID +Not Now: Not Now +Biometric Authentification: Biometric Authentification +Touch ID: Touch ID +Face ID: Face ID +Turn Off Touch ID?: Turn Off Touch ID? +Turn Off Face ID?: Turn Off Face ID? +Turn Off Biometrics?: Turn Off Biometrics? +Enter code: Enter code +Enter code or use Face ID: Enter code or use Face ID +Enter code or use Touch ID: Enter code or use Touch ID +Enter code or user biometrics: Enter code or user biometrics +Wrong code, please try again: Wrong code, please try again +Scan your fingerprint: Scan your fingerprint +Incorrect code, please try again: Incorrect code, please try again +Correct: Correct +Confirm Operation: Confirm Operation +Confirm Swap: Confirm Swap +"%amount% to %address%": "%amount% to %address%" +"%amount_from% to %amount_to%": "%amount_from% to %amount_to%" +Failed to enable biometrics: Failed to enable biometrics +Are you sure you want to disable Face ID?: Are you sure you want to disable Face ID? +Are you sure you want to disable Touch ID?: Are you sure you want to disable Touch ID? +Are you sure you want to disable biometrics?: Are you sure you want to disable biometrics? +Yes: Yes diff --git a/src/i18n/es.yaml b/src/i18n/es.yaml index b10a53ef..2023ba99 100644 --- a/src/i18n/es.yaml +++ b/src/i18n/es.yaml @@ -7,7 +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... -Back Up: Hacer copia de seguridad +Back Up: Siguiente 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 @@ -40,7 +40,7 @@ Let's Check: Revisemos Let's Check!: ¡Revisemos! Safety Rules: Normas de seguridad $safety_rules_one: | - En la siguiente pantalla verás las %1$s palabras de la **frase semilla**. Escríbalas en el mismo orden y **guárdelas en un lugar seguro**. + En la siguiente pantalla verás las palabras de la **frase semilla**. Escríbalas en el mismo orden y **guárdelas en un lugar seguro**. $safety_rules_two: Permiten **acceder a su mondero** si pierde su contraseña o el acceso a este dispositivo. $safety_rules_three: Si alguien más las ve, sus activos **pueden ser robados**. ¡Mire a su alrededor! Understood: Entiendo @@ -101,21 +101,20 @@ $logout_accounts_without_backup_warning: No ha realizado copia de seguridad de % $logout_warning: Esto desconectará el monedero de esta aplicación. Podrá restaurar su monedero usando la frase semilla... Receive TON: Recibir TON Your address was copied!: ¡Su dirección fue copiada! -Show QR-Code: Mostrar código QR -Create Invoice: Crear factura -QR-code: Código QR +Show QR Code: Mostrar código QR +Create Deposit Link: Crear enlace de depósito +QR Code: Código QR $receive_invoice_description: | Puede especificar la cantidad y - el propósito del pago para ahorrar tiempo al remitente + el propósito del pago para ahorrar tiempo al remitente. Amount: Cantidad Comment: Comentario Share this URL to receive TON: Comparte esta URL para recibir TON Invoice link was copied!: ¡Se copió el enlace de la factura! -US Dollar: Dólar estadounidense Theme: Tema Language: Idioma Enable Animations: Habilitar animaciones -Fiat Currency: Moneda fíat +Base Currency: Moneda base Investor View: Modo Inversor Toggle Investor View: Alternar Modo Inversor Hide Tiny Transfers: Ocultar transacciones pequeñas @@ -167,12 +166,12 @@ Loading...: Cargando... Recipient Address: Dirección del destinatario Wallet address or domain: Dirección o dominio del monedero Incorrect address: Dirección incorrecta +Incorrect address.: Dirección incorrecta. Paste: Pegar Insufficient balance: Saldo insuficiente InsufficientBalance: Saldo insuficiente Optional: Opcional $send_token_symbol: Enviar %1$s -$balance_is: "Saldo: %balance%" Is it all ok?: ¿Está todo bien? Receiving Address: Dirección del receptor Fee: Comisión @@ -199,7 +198,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 su dirección, escanear un código QR o crear un enlace de depósito para recibir criptomonedas. Your address: Tu dirección Wrong password, please try again: Contraseña incorrecta, inténtalo de nuevo Appearance: Apariencia @@ -212,7 +211,7 @@ Earn from your tokens while holding them: Gana con tus tokens mientras los manti $est_apy_val: Est. APY %1$d% Why this is safe: Por qué esto es seguro Why staking is safe?: ¿Por qué el staking es seguro? -$safe_staking_description1: El staking es **totalmente descentralizado** y operado por los **nominadores oficiales de TON**. Nadie puede obtener acceso a sus tokens en staking. +$safe_staking_description1: El staking es **totalmente descentralizado** y operado por los **TON Liquid Staking smart contracts**. $safe_staking_description2: La participación depositada se utilizará para la validación de la red TON como parte de su esencia de **prueba de participación**. $safe_staking_description3: Puede retirar su apuesta en **cualquier momento** y se depositará de nuevo en su cuenta dentro de **dos días**. $min_value: Min %value% @@ -338,6 +337,28 @@ $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 +Swap: Intercambiar +You sell: Usted vende +You buy: Usted compra +Swap Details: Detalles del intercambio +Blockchain fee: Tarifa de blockchain +Price Impact: Impacto en el precio +Minimum Received: Cantidad mínima recibida +Slippage: Deslizamiento +$swap_from_to: Intercambiar de %from% a %to% +The exchange rate is below market value!: La tasa de cambio está un %value% por debajo del valor de mercado. +Invalid Pair: Par Inválido +We do not recommend to perform an exchange, try to specify a lower amount.: No recomendamos realizar un intercambio, intente especificar una cantidad menor. +$swap_minimum_received_tooltip1: Esta es la cantidad mínima de nuevos tokens que recibirá en este intercambio, considerando las condiciones actuales del mercado, la tolerancia al deslizamiento y el posible impacto en el precio. +$swap_minimum_received_tooltip2: Es un mínimo esperado, pero si las condiciones cambian mucho mientras su intercambio está siendo procesado, la cantidad final podría ser menor. +$swap_price_impact_tooltip1: Esto muestra cuánto puede cambiar el precio de su operación el precio del token. +$swap_price_impact_tooltip2: Las operaciones grandes pueden hacer que el precio suba o baje más. En general, es mejor un valor más bajo. +$swap_slippage_tooltip1: Esto establece cuánto se permite que el precio cambie antes de que se procese su intercambio. +$swap_slippage_tooltip2: Si durante el procesamiento de su intercambio, el precio cambia más que este valor, su orden será cancelada. +Coins have been swapped!: ¡Las monedas han sido intercambiadas! +Slippage not specified: Deslizamiento no especificado +Slippage too high: Deslizamiento demasiado alto +Not enough TON: TON insuficiente 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. @@ -363,9 +384,125 @@ $auth_backup_description1: | $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. +Swap Placed: Intercambio Colocado +Swapping: Intercambiando +Swapped: Intercambiado +Swap Failed: Intercambio Fallido +Exchange rate: Tasa de Cambio +Swap Again: Intercambiar de Nuevo Terms of Use: Términos de Uso Privacy Policy: Política de Privacidad +Insufficient liquidity: Liquidez insuficiente This address is new and never received transfers before.: Esta dirección es nueva y nunca ha recibido transferencias antes. Create Backup: Crear copia de seguridad Sending: Enviando -failed: fallido +Failed: Fallido +Refunded: Reembolsado +On hold: En espera +Expired: Expirado +In progress: En progreso +Waiting Payment: Esperando Pago +Waiting for payment: Esperando el pago +Blockchain: Blockchain +Receiving address in blockchain: Dirección en %blockchain% +Please provide an address of your wallet in another blockchain to receive bought tokens.: Por favor, proporcione una dirección de su billetera en otra cadena de bloques para recibir tokens comprados. +The time for sending coins is over: El tiempo para enviar monedas ha terminado +Please wait a few moments...: Por favor, espere unos momentos... +You have not sent the coins to the specified address.: Usted no ha enviado las monedas a la dirección especificada. +$swap_changelly_to_ton_description1: "Debe enviar %value% a esta dirección en la cadena de bloques %blockchain% dentro de %time%" +Please contact support and provide a transaction ID.: Por favor, póngase en contacto con el soporte y proporcione un ID de transacción. +Exchange failed and coins were refunded to your wallet.: El intercambio falló y las monedas fueron reembolsadas a su billetera. +Please contact security team to pass the KYC procedure.: Por favor, póngase en contacto con el equipo de seguridad para completar el procedimiento de KYC. +Swap Expired: Intercambio vencido +Swap On Hold: Intercambio en Espera +Swap Refunded: Intercambio reembolsado +Changelly Payment Address: Dirección de Pago de Changelly +Cross-chain exchange provided by Changelly: Proporcionado por Changelly +$swap_changelly_agreement_message: Al continuar, usted acepta los %terms% y %policy% y comprende que la transacción puede desencadenar una verificación según el %kyc%. +$swap_changelly_terms_of_use: términos de uso +$swap_changelly_privacy_policy: política de privacidad +Password must contain %length% digits.: La contraseña debe contener %length% dígitos. +Deposit Link: Enlace de depósito +$swap_changelly_from_ton_description: "Los tokens serán enviados a tu dirección en la blockchain de %blockchain% en unos momentos:" +Minimum amount: Mínimo %value% +Maximum amount: Máximo %value% +Display Tray Icon: Icono de bandeja +Enable Auto-Updates: Habilitar actualizaciones automáticas +Biometric Authentication: Autenticación biométrica +Turn off biometrics?: ¿Desactivar la biometría? +If you turn off biometric protection, you will need to create a password.: Si desactiva la protección biométrica, deberá crear una contraseña. +Enter your current password: Introduce tu contraseña actual +Turn On Biometrics: Activar la biometría +Please confirm transaction using biometrics: Confirme la transacción utilizando datos biométricos +Enabling biometric confirmation will reset the password.: Al habilitar la confirmación biométrica se restablecerá la contraseña. +Biometric Registration: Registro Biométrico +Step 1 of 2. Registration: Paso 1 de 2. Registro +Step 2 of 2. Verification: Paso 2 de 2. Verificación +Biometric setup failed: Error en la configuración biométrica +Biometric confirmation failed: Error de confirmación biométrica +Failed to disable biometrics: No se pudo desactivar la biometría +Biometric Confirmation: Confirmación biométrica +Please verify your identity.: Verifique por favor su Identidad. +Create Password: Crear contraseña +Complete: Completo +Verification: Verificación +Create a password or use biometric authentication to protect it.: Cree una contraseña o utilice autenticación biométrica para protegerla. +Connect Biometrics: Conectar biométrico +Use Password: Usar contraseña +Biometrics Disabled: Biometría deshabilitada +Biometrics Enabled: Biometría habilitada +A request is already pending: Ya hay una solicitud pendiente +Please confirm operation using biometrics: Confirme la operación mediante datos biométricos. +Scan QR Code: Escanear código QR +Permission denied. Please grant camera permission to use the QR code scanner.: Permiso denegado. Otorgue permiso a la cámara para usar el escáner QR. +Unsupported barcode format: Formato de código de barras no admitido +An error on the server side. Please try again.: Un error en el lado del servidor. Inténtalo de nuevo. +Unrecognized QR Code: Código QR no reconocido +$fee_value_almost_equal: Comisión ≈ %fee% +Address was saved!: ¡Dirección guardada! +US Dollar: Dólar estadounidense +Euro: Euro +Ruble: Rublo +Yuan: Yuan +$max_balance: "Máximo: %balance%" +$unstake_information_instantly: Obtendrá su depósito al instante. +Select Token: Seleccionar Token +MY: MI +POPULAR: POPULAR +A-Z: A-Z +Not Found: No Encontrado +Wallet is ready!: ¡La billetera está lista! +Wallet is imported!: ¡La billetera es importada! +Create a code to protect it: Crea un código para protegerlo. +Enter your code again: Ingresa tu código nuevamente +Codes don't match: Los códigos no coinciden +Code set successfully: Código configurado correctamente +Use Biometrics: Utilice la biometría +Use Touch ID: Usar Touch ID +Use Face ID: Usar Face ID +You can connect your biometric data for more convenience: Puede conectar sus datos biométricos para mayor comodidad +Connect Touch ID: Conectar Touch ID +Connect Face ID: Conectar Face ID +Not Now: Ahora no +Biometric Authentification: Autenticación biométrica +Touch ID: Touch ID +Face ID: Face ID +Turn Off Touch ID?: ¿Desactivar Touch ID? +Turn Off Face ID?: ¿Desactivar Face ID? +Turn Off Biometrics?: ¿Desactivar la biometría? +Enter code: Introduzca el código +Enter code or use Face ID: Ingresa el código o usa Face ID +Enter code or use Touch ID: Ingresa el código o usa Touch ID +Enter code or user biometrics: Ingrese el código o la biometría del usuario +Wrong code, please try again: Código incorrecto, inténtalo de nuevo. +Scan your fingerprint: Escanea tu huella digital +Incorrect code, please try again: Codigo Incorrecto, por favor intenta de nuevo +Correct: Correcto +Confirm Operation: Confirmar operación +Confirm Swap: Confirmar intercambio +"%amount% to %address%": "%amount% a %address%" +"%amount_from% to %amount_to%": "%amount_from% a %amount_to%" +Are you sure you want to disable Face ID?: ¿Estás seguro de que quieres deshabilitar Face ID? +Are you sure you want to disable Touch ID?: ¿Estás seguro de que quieres deshabilitar Touch ID? +Are you sure you want to disable biometrics?: ¿Estás seguro de que quieres deshabilitar la biometría? +Yes: Sí diff --git a/src/i18n/ru.yaml b/src/i18n/ru.yaml index 9aa0504e..6694d76b 100644 --- a/src/i18n/ru.yaml +++ b/src/i18n/ru.yaml @@ -93,7 +93,7 @@ Delete: Удалить Delete Saved Address: Удалить сохранённый адрес Are you sure you want to remove this address from your saved ones?: Уверены, что хотите удалить сохранённый адрес кошелька? You will be able to save it again via Transaction Info with this address.: Вы сможете сохранить его снова, открыв любой перевод с этим адресом. -Address removed from saved: Адрес кошелька удален из сохранённых +Address removed from saved: Адрес кошелька удалён из сохранённых Warning!: Важно! Log Out: Выход есть $logout_without_backup_warning: Резервная копия кошелька не создана. Доступ к токенам и NFT будет ограничен, если вы выйдете из системы. @@ -102,9 +102,9 @@ $logout_warning: Кошелёк будет удалён только **на эт $logout_confirm: Выйти из **всех** кошельков Receive TON: Получить TON Your address was copied!: Адрес скопирован! -Show QR-Code: Показать QR-код -Create Invoice: Создать инвойс -QR-code: QR-код +Show QR Code: Показать QR-код +Create Deposit Link: Ссылка на депозит +QR Code: QR-код $receive_invoice_description: | Укажите сумму и назначение платежа, если это необходимо. @@ -112,11 +112,10 @@ Amount: Сумма Comment: Комментарий Share this URL to receive TON: Поделиться ссылкой Invoice link was copied!: Ссылка скопирована! -US Dollar: Доллар США Theme: Тема Language: Язык Enable Animations: Включить анимацию -Fiat Currency: Фиатная валюта +Base Currency: Базовая валюта Investor View: Режим инвестора Toggle Investor View: Переключить в режим инвестора Hide Tiny Transfers: Скрывать мелкие переводы @@ -168,12 +167,12 @@ Loading...: Загрузка... Recipient Address: Адрес получателя Wallet address or domain: Адрес кошелька или домен Incorrect address: Неверный адрес +Incorrect address.: Неверный адрес. Paste: Вставить Insufficient balance: Недостаточный баланс InsufficientBalance: Недостаточный баланс Optional: Необязательно $send_token_symbol: Отправить %1$s -$balance_is: "Баланс: %balance%" Is it all ok?: Всё верно? Receiving Address: Адрес получателя Fee: Комиссия @@ -198,19 +197,19 @@ $tiny_transfers_help: Выключите этот параметр, чтобы Today: Сегодня Yesterday: Вчера Now: Сейчас -$receive_ton_description: Вы можете поделиться этим адресом, отсканировать QR-код или создать инвойс для получения TON +$receive_ton_description: Вы можете поделиться своим адресом, отсканировать QR-код или создать ссылку на депозит для получения криптовалюты. Your address: Ваш адрес Wrong password, please try again: Неправильный пароль, попробуйте ещё раз Appearance: Внешний вид Light: Светлая Dark: Тёмная System: Системная -Stake TON: Продолжить +Stake TON: Стейкинг Earn from your tokens while holding them: Получайте пассивный доход от хранения TON на надёжном официальном смарт-контракте $est_apy_val: Доходность ~%1$d% Why this is safe: Почему это безопасно Why staking is safe?: Это точно безопасно? -$safe_staking_description1: Стейкинг **полностью децентрализован** и управляется **официальным смарт-контрактом** TON Nominator. Ни разработчики, ни кто-либо ещё **не имеет доступа** к средствам на вашем депозите. +$safe_staking_description1: Стейкинг **полностью децентрализован** и управляется **официальными смарт-контрактами** TON Liquid Staking. $safe_staking_description2: Ваш депозит будет использован для валидации транзакций в блокчейне в рамках механизма **proof-of-stake**. $safe_staking_description3: Вы можете вывести депозит с заработанными процентами **в любой момент** — средства переведутся на ваш основной счёт **в течение двух дней**. $min_value: Мин. %value% @@ -229,7 +228,7 @@ Unstake TON: Вывести депозит $unstake_information_with_time: Текущий депозит будет полностью отправлен обратно на ваш кошелёк через %time%. Amount to unstake: Сумма к выводу Confirm Unstaking: Подтвердить вывод -Request for unstaking is sent!: Запрос на вывод депозита отправлен! +Request for unstaking is sent!: Запрос на вывод отправлен! $unstaking_when_receive: Депозит будет выведен через %time% $unstake_insufficient_balance: Вам необходимо иметь %balance% на вашем основном балансе, чтобы отправить заявку на вывод депозита. at APY %1$s%: при APY %1$s% @@ -250,7 +249,7 @@ InvalidAmount: Некорректная сумма InvalidToAddress: Некорректный адрес получателя DomainNotResolved: Ошибка резолвинга домена You can save this address for quick access while sending.: Вы можете сохранить этот адрес для быстрого доступа при отправке. -Stake Again: Stake Again +Stake Again: Повторить депозит Earned: Earned Connect Dapp: Подключить приложение Select wallets to use on this dapp: Выберите кошельки для использования с этим приложением @@ -317,7 +316,7 @@ Switch to the newly opened tab to connect Ledger.: Переключитесь н Once connected, switch back to this window to proceed.: После подключения вернитесь в это окно, чтобы продолжить. Not all transactions were sent successfully: Не все транзакции были успешно отправлены The time on your device is incorrect, sync it and try again: Время на вашем устройстве некорректно, синхронизируйте его и попробуйте снова -Name or Address...: Имя или Адрес... +Name or Address...: Имя или адрес... Such error, many tabs: Неактивная вкладка $many_tabs_error_description: | MyTonWallet поддерживает только одну активную вкладку с приложением. @@ -333,6 +332,27 @@ $dapp_ledger_warning1: Вы собираетесь отправить много $dapp_ledger_warning2: Пожалуйста, не торопитесь и не прерывайте процесс. Agree: Согласен The hardware wallet does not support this data format: Аппаратный кошелёк не поддерживает данный формат данных +Swap: Обмен +You sell: Вы продаёте +You buy: Вы покупаете +Swap Details: Детали обмена +Blockchain fee: Комиссия блокчейна +Price Impact: Влияние на цену +Minimum Received: Минимально получите +Slippage: Проскальзывание +$swap_from_to: Обменять %from% на %to% +The exchange rate is below market value!: Курс обмена на %value% ниже рыночной стоимости! +Invalid Pair: Недопустимая пара +We do not recommend to perform an exchange, try to specify a lower amount.: Не рекомендуем выполнять обмен, попробуйте указать меньшую сумму. +$swap_minimum_received_tooltip1: Это наименьшее количество новых токенов, которое вы получите от этого обмена, учитывая текущие условия рынка, допустимое проскальзывание и потенциальное влияние на цену. +$swap_minimum_received_tooltip2: Это ожидаемое минимальное значение, но, если условия сильно изменятся во время обработки вашего обмена, итоговая сумма может быть меньше. +$swap_price_impact_tooltip1: Это показывает, насколько ваша сделка может изменить цену токена. +$swap_price_impact_tooltip2: Большие сделки могут сильнее повлиять на цену. В целом, меньшее значение лучше. +$swap_slippage_tooltip1: Здесь устанавливается, насколько цена может измениться, прежде чем ваш обмен будет обработан. +$swap_slippage_tooltip2: Если во время обработки вашего обмена цена изменится больше, чем на это значение, ваш заказ будет отменён. +Coins have been swapped!: Токены были обменяны! +Slippage not specified: Проскальзывание не указано +Slippage too high: Проскальзывание слишком большое Not enough TON: Недостаточно TON Use Responsibly: Используйте ответственно $auth_responsibly_description1: | @@ -349,7 +369,7 @@ $auth_backup_warning_notice: | Later: Позже Back Up Now: Показать слова сейчас I have read and accept this information: Я прочитал и принимаю эту информацию. -$ledger_verify_address: Всегда проверяйте вставленный адрес используя Ledger. +$ledger_verify_address: Всегда проверяйте вставленный адрес, используя Ledger. $ledger_not_ready: Ledger не подключён или приложение TON не открыто. Verify now: Проверить сейчас Invalid address format. Only URL Safe Base64 format is allowed.: Некорректный формат адреса. Разрешен только URL Safe Base64 формат. @@ -365,9 +385,126 @@ $auth_backup_description3: | **резервную копию** секретных слов. Это поможет, если вы потеряете доступ к этому устройству или забудете пароль. +Swap Placed: Обмен размещён +Swapping: Идёт обмен +Swapped: Обмен +Swap Failed: Обмен не выполнен +Exchange rate: Обменный курс +Swap Again: Повторить обмен Terms of Use: Условия использования Privacy Policy: Политика конфиденциальности +Insufficient liquidity: Недостаточно ликвидности This address is new and never received transfers before.: Этот адрес новый и ранее не получал переводов. Create Backup: Создание резервной копии Sending: Отправка -failed: не выполнен +Failed: Ошибка +Refunded: Возвращено +On hold: Удержан +Expired: Истёк +In progress: В процессе +Waiting Payment: Ожидание оплаты +Waiting for payment: Ожидание оплаты +Blockchain: Блокчейн +Receiving address in blockchain: Адрес для получения в блокчейне %blockchain% +Please provide an address of your wallet in another blockchain to receive bought tokens.: Пожалуйста, укажите адрес вашего кошелька в другом блокчейне для получения купленных токенов. +The time for sending coins is over: Время отправки монет истекло +Please wait a few moments...: Пожалуйста, подождите некоторое время... +You have not sent the coins to the specified address.: Вы не отправили монеты на указанный адрес. +$swap_changelly_to_ton_description1: "Вы должны отправить %value% на этот адрес в блокчейне %blockchain% в течение %time%" +Please contact support and provide a transaction ID.: Пожалуйста, свяжитесь с поддержкой и предоставьте идентификатор транзакции. +Exchange failed and coins were refunded to your wallet.: Обмен не удался и монеты были возвращены на ваш кошелёк. +Please contact security team to pass the KYC procedure.: Пожалуйста, свяжитесь с командой безопасности, чтобы пройти процедуру KYC. +Swap Expired: Обмен истёк +Swap On Hold: Обмен удержан +Swap Refunded: Обмен возвращен +Changelly Payment Address: Адрес оплаты Changelly +Cross-chain exchange provided by Changelly: Сервис предоставлен Changelly +$swap_changelly_agreement_message: Продолжая, вы соглашаетесь с %terms% и %policy% и понимаете, что транзакция может вызвать проверку в соответствии с %kyc%. +$swap_changelly_terms_of_use: условиями использования +$swap_changelly_privacy_policy: политикой конфиденциальности +Password must contain %length% digits.: Пароль должен содержать %length% цифры. +Deposit Link: Ссылка для пополнения +$swap_changelly_from_ton_description: "Токены будут отправлены на ваш адрес в блокчейне %blockchain% в ближайшее время:" +Minimum amount: Минимум %value% +Maximum amount: Максимум %value% +Display Tray Icon: Иконка на панели задач +Enable Auto-Updates: Включить автообновления +Biometric Authentication: Биометрическая аутентификация +Turn off biometrics?: Отключить биометрию? +If you turn off biometric protection, you will need to create a password.: Если вы отключите биометрическую защиту, вам потребуется создать пароль. +Enter your current password: Введите ваш текущий пароль +Turn On Biometrics: Включение биометрии +Please confirm transaction using biometrics: Пожалуйста, подтвердите транзакцию с помощью биометрии +Enabling biometric confirmation will reset the password.: Включение биометрического подтверждения приведет к сбросу пароля. +Biometric Registration: Подключение биометрии +Step 1 of 2. Registration: Шаг 1 из 2. Регистрация +Step 2 of 2. Verification: Шаг 2 из 2. Проверка +Biometric setup failed: Настроить биометрию не удалось +Biometric confirmation failed: Подтвердить биометрию не удалось +Failed to disable biometrics: Не удалось отключить биометрию +Biometric Confirmation: Подтверждение биометрии +Please verify your identity.: Пожалуйста, подтвердите вашу личность. +Create Password: Придумайте пароль +Complete: Завершить +Verification: Проверка +Create a password or use biometric authentication to protect it.: Создайте пароль или используйте биометрическую аутентификацию для его защиты. +Connect Biometrics: Подключить биометрию +Use Password: Использовать пароль +Biometrics Disabled: Биометрия отключена +Biometrics Enabled: Биометрия включена +A request is already pending: Подключение биометрии уже выполняется +Please confirm operation using biometrics: Пожалуйста, подтвердите операцию с помощью биометрии +Scan QR Code: Сканировать QR-код +Permission denied. Please grant camera permission to use the QR code scanner.: Доступ запрещён. Пожалуйста, дайте камере разрешение на использование сканера QR-кода. +Unsupported barcode format: Неподдерживаемый формат штрих-кода +An error on the server side. Please try again.: Ошибка на стороне сервера. Пожалуйста, попробуйте ещё раз. +Unrecognized QR Code: Нераспознанный QR-код +$fee_value_almost_equal: Комиссия ≈ %fee% +Address was saved!: Адрес сохранён! +US Dollar: Доллар США +Euro: Евро +Ruble: Рубль +Yuan: Юань +$max_balance: "Максимум: %balance%" +$unstake_information_instantly: Вы получите свой депозит мгновенно. +Select Token: Выбрать токен +MY: МОИ +POPULAR: ПОПУЛЯРНЫЕ +A-Z: А-Я +Not Found: Не найдено +Wallet is ready!: Кошелёк готов! +Wallet is imported!: Кошелёк импортирован! +Create a code to protect it: Создайте код для его защиты +Enter your code again: Введите свой код ещё раз +Codes don't match: Коды не совпадают +Code set successfully: Код успешно установлен +Use Biometrics: Используйте биометрию +Use Touch ID: Используйте Touch ID +Use Face ID: Используйте Face ID +You can connect your biometric data for more convenience: Для большего удобства вы можете подключить свои биометрические данные +Connect Touch ID: Подключить Touch ID +Connect Face ID: Подключить Face ID +Not Now: Не сейчас +Biometric Authentification: Биометрическая аутентификация +Touch ID: Touch ID +Face ID: Face ID +Turn Off Touch ID?: Отключить Touch ID? +Turn Off Face ID?: Отключить Face ID? +Turn Off Biometrics?: Отключить биометрию? +Enter code: Введите код +Enter code or use Face ID: Введите код или используйте Face ID +Enter code or use Touch ID: Введите код или используйте Touch ID +Enter code or user biometrics: Введите код или используйте биометрические данные +Wrong code, please try again: Неверный код, попробуйте ещё раз +Scan your fingerprint: Отсканируйте свой отпечаток пальца +Incorrect code, please try again: Неверный код, пожалуйста, попробуйте снова +Correct: Правильно +Confirm Operation: Подтвердите операцию +Confirm Swap: Подтвердить обмен +"%amount% to %address%": "%amount% на %address%" +"%amount_from% to %amount_to%": "%amount_from% на %amount_to%" +Failed to enable biometrics: Не удалось включить биометрию +Are you sure you want to disable Face ID?: Вы уверены, что хотите отключить Face ID? +Are you sure you want to disable Touch ID?: Вы уверены, что хотите отключить Touch ID? +Are you sure you want to disable biometrics?: Вы уверены, что хотите отключить биометрию? +Yes: Да diff --git a/src/i18n/zh-Hans.yaml b/src/i18n/zh-Hans.yaml index 42be9aac..07116482 100644 --- a/src/i18n/zh-Hans.yaml +++ b/src/i18n/zh-Hans.yaml @@ -96,19 +96,18 @@ $logout_accounts_without_backup_warning: 您还没有备份此账户 %links%。 $logout_warning: 这将断开钱包与此应用程序的连接,您将能使用 **%1$d 个密钥** 恢复您的钱包 - 或导入另一个钱包。 Receive TON: 接收 TON Your address was copied!: 您的地址已复制! -Show QR-Code: 显示二维码 -Create Invoice: 创建发票 -QR-code: 二维码 -$receive_invoice_description: 您可以指定付款金额和收款地址来节省付款方的一些时间 +Show QR Code: 显示二维码 +Create Deposit Link: 创建存款链接 +QR Code: 二维码 +$receive_invoice_description: 您可以指定付款金额和收款地址来节省付款方的一些时间。 Amount: 数量 Comment: 留言 Share this URL to receive TON: 分享此链接来接收 TON Invoice link was copied!: 发票链接已复制! -US Dollar: 美元 Theme: 主题 Language: 语言 Enable Animations: 启用动画 -Fiat Currency: 法定货币 +Base Currency: 基本货币 Investor View: 投资者见解 Toggle Investor View: 切换投资者见解 Hide Tiny Transfers: 隐藏小额转账 @@ -158,12 +157,12 @@ Loading...: 读取中... Recipient Address: 收款人地址 Wallet address or domain: 钱包地址或域名 Incorrect address: 地址错误 +Incorrect address.: 地址错误。 Paste: 粘贴 Insufficient balance: 余额不足 InsufficientBalance: 余额不足 Optional: 选项 $send_token_symbol: 发送 %1$s -$balance_is: "余额:%balance%" Is it all ok?: 确认全部正确? Receiving Address: 接收地址 Fee: 手续费 @@ -191,9 +190,7 @@ Understood: 明白 Today: 今天 Yesterday: 明天 Now: 现在 -$receive_ton_description: | - 您可以分享这个地址,显示二维码 - 或创建支票来收取 TON +$receive_ton_description: 您可以分享您的地址、扫描二维码或创建存款链接来接收加密货币。 Your address: 您的地址 Wrong password, please try again: 密码错误,请再试一次。 Appearance: 外观 @@ -205,7 +202,7 @@ Earn from your tokens while holding them: 从你持有的代币身上赚一笔 $est_apy_val: 期望年回报率 %1$d% Why this is safe: 为什么这是安全的 Why staking is safe?: 为什么质押是安全的? -$safe_staking_description1: TON 质押是**完全去中心化的**,由官方 TON Nominator 智能合约运营。 没有人可以访问您抵押的代币。 +$safe_staking_description1: TON 质押是**完全去中心化的**,由官方 TON Liquid Staking 智能合约运营。 $safe_staking_description2: 作为其 **权益证明** 的一部分,您存入的 TON 将用于 TON 区块网络验证。 $safe_staking_description3: 您可以在 **任何时候** 解除您 TON 的质押,它会在 **36小时** 内存入您的账户。 $min_value: 最小 %1$s @@ -324,6 +321,28 @@ $dapp_ledger_warning1: 您即将使用您的**Ledger**钱包发送多方交易 $dapp_ledger_warning2: 请慢慢来,不要中断过程。 Agree: 同意 The hardware wallet does not support this data format: 硬件钱包不支持该数据格式 +Swap: 交换 +You sell: 您卖出 +You buy: 您购买 +Swap Details: 交换详情 +Blockchain fee: 区块链费用 +Price Impact: 价格影响 +Minimum Received: 最低接收数量 +Slippage: 下单误差 +$swap_from_to: 从 %from% 交换至 %to% +The exchange rate is below market value!: 汇率低于市场价值 %value%! +Invalid Pair: 无效交易对 +We do not recommend to perform an exchange, try to specify a lower amount.: 我们不建议进行交换,请尝试指定较小的金额。 +$swap_minimum_received_tooltip1: 这是您从此次交换中可能获得的最少新代币数量,考虑了目前市场条件、滑点容忍度和潜在价格影响。 +$swap_minimum_received_tooltip2: 这是预期的最低值,但如果在处理您的交换时条件发生很大变化,最终金额可能会较少。 +$swap_price_impact_tooltip1: 这显示了您的交易可能对代币价格造成的影响。 +$swap_price_impact_tooltip2: 大型交易可能会使价格上涨或下跌得更多。较低的值通常较好。 +$swap_slippage_tooltip1: 设置此值可以允许价格在进行交换前发生变化的程度。 +$swap_slippage_tooltip2: 如果在处理您的交换时,价格变化超过此值,您的订单将被取消。 +Coins have been swapped!: 币已交换完成! +Slippage not specified: 未指定下单误差 +Slippage too high: 下单误差过高 +Not enough TON: TON 不足 Use Responsibly: 负责任地使用 $auth_responsibly_description1: | MyTonWallet 是一个**自我托管**钱包,这意味着**只有您**拥有完全控制权,最重要的是,对您的资金**承担全部责任**。 @@ -348,10 +367,129 @@ $auth_backup_description1: | $auth_backup_description2: “能力越大,责任越大。” $auth_backup_description3: | 您需要手动**备份助记词**,以免忘记助记词以永远失去您的钱包。 -$transaction_at: 于价格 %price% +Swap Placed: 已提交交换 +Swapping: 进行交换 +Swapped: 已交换 +Swap Failed: 交换失败 +Exchange rate: 汇率 +Swap Again: 再次进行交换 Terms of Use: 使用条款 Privacy Policy: 隐私政策 +Insufficient liquidity: 流动性不足 This address is new and never received transfers before.: 这个地址是新的,以前从未收到过转帐。 Create Backup: 创建备份 Sending: 发送 -failed: 失敗 +Failed: 失败 +Refunded: 退款 +On hold: 持有 +Expired: 过期 +In progress: 进行中 +Waiting Payment: 等待付款 +Waiting for payment: 等待付款 +Blockchain: 区块链 +Receiving address in blockchain: 在 %blockchain% 区块链中的接收地址 +Please provide an address of your wallet in another blockchain to receive bought tokens.: 请提供您在另一个区块链上的钱包地址以接收已购买的代币。 +The time for sending coins is over: 发送硬币的时间已结束 +Please wait a few moments...: 请稍等片刻... +You have not sent the coins to the specified address.: 您尚未将硬币发送到指定地址。 +$swap_changelly_to_ton_description1: "您必须在 %time% 内将 %value% 发送到此地址 在 %blockchain% 区块链中" +Please contact support and provide a transaction ID.: 请联系支持并提供交易ID。 +Exchange failed and coins were refunded to your wallet.: 交换失败,硬币已退还到您的钱包。 +Please contact security team to pass the KYC procedure.: 请联系安全团队以完成KYC程序。 +Swap Expired: 交换已过期 +Swap On Hold: 交换暂停 +Swap Refunded: 交换已退款 +Changelly Payment Address: Changelly 付款地址 +Cross-chain exchange provided by Changelly: 由Changelly提供的跨链交换 +$swap_changelly_agreement_message: 继续操作,即表示您同意 %terms% 和 %policy%,并理解该交易可能会触发根据 %kyc% 进行的验证。 +$swap_changelly_terms_of_use: 使用条款 +$swap_changelly_privacy_policy: 隐私政策 +Password must contain %length% digits.: 密码必须包含%length%位数字。 +Deposit Link: 充值链接 +$swap_changelly_from_ton_description: "代币将在几分钟内发送到您在%blockchain%区块链上的地址:" +Minimum amount: 最低 %value% +Maximum amount: 最高 %value% +Display Tray Icon: 显示托盘图标 +Enable Auto-Updates: 启用自动更新 +Biometric Authentication: 生物识别认证 +Turn off biometrics?: 关闭生物识别? +If you turn off biometric protection, you will need to create a password.: 如果您关闭生物识别保护,则需要创建密码。 +Enter your current password: 输入当前密码 +Turn On Biometrics: 打开生物识别 +Please confirm transaction using biometrics: 请使用生物识别技术确认交易 +Enabling biometric confirmation will reset the password.: 启用生物识别确认将重置密码。 +Biometric Registration: 生物识别注册 +Step 1 of 2. Registration: 第 1 步(共 2 步):注册 +Step 2 of 2. Verification: 第 2 步(共 2 步):验证 +Biometric setup failed: 生物识别设置失败 +Biometric confirmation failed: 生物识别确认失败 +Failed to disable biometrics: 无法禁用生物识别 +Biometric Confirmation: 生物识别确认 +Please verify your identity.: 请证明你的身份。 +Create Password: 创建密码 +Complete: 完成 +Verification: 确认 +Create a password or use biometric authentication to protect it.: 创建密码或使用生物识别身份验证来保护它。 +Connect Biometrics: 连接生物识别 +Use Password: 使用密码 +Biometrics Disabled: 生物识别已禁用 +Biometrics Enabled: 启用生物识别 +A request is already pending: 请求已待处理 +Please confirm operation using biometrics: 请使用生物识别确认操作 +Scan QR Code: 扫描二维码 +Permission denied. Please grant camera permission to use the QR code scanner.: 没有权限。 请授予相机使用 QR 扫描仪的权限。 +Unsupported barcode format: 不支持的条形码格式 +An error on the server side. Please try again.: 服务器端的错误。请再试一次。 +Unrecognized QR Code: 无法识别的二维码 +$fee_value_almost_equal: 手续费 ≈ %fee% +Address was saved!: 地址已保存! +US Dollar: 美元 +Euro: 欧元 +Ruble: 卢布 +Yuan: 元 +$max_balance: "最大: %balance%" +$unstake_information_instantly: 您将立即获得存款。 +Select Token: 选择代币 +MY: 我的 +POPULAR: 流行 +A-Z: A-Z +Not Found: 未找到 +Wallet is ready!: 钱包准备好了! +Wallet is imported!: 钱包是进口的! +Create a code to protect it: 创建代码来保护它 +Enter your code again: 再次输入您的代码 +Codes don't match: 代码不匹配 +Code set successfully: 代码设置成功 +Use Biometrics: 使用生物识别技术 +Use Touch ID: 使用 Touch ID +Use Face ID: 使用 Face ID +You can connect your biometric data for more convenience: 您可以连接您的生物识别数据以获得更多便利 +Connect Touch ID: 连接 Touch ID +Connect Face ID: 连接 Face ID +Not Now: 现在不要 +Biometric Authentification: 生物识别认证 +Touch ID: Touch ID +Face ID: Face ID +Turn Off Touch ID?: 关闭 Touch ID? +Turn Off Face ID?: 关闭 Face ID? +Turn Off Biometrics?: 关闭生物识别技术? +Enter code: 输入代码 +Enter code or use Face ID: 输入代码或使用 Face ID +Enter code or use Touch ID: 输入代码或使用 Touch ID +Enter code or user biometrics: 输入代码或用户生物识别信息 +Wrong code, please try again: 代码错误,请重试 +Scan your fingerprint: 扫描您的指纹 +Incorrect code, please try again: 不正确的代码,请重试 +Correct: 正确 +Confirm Operation: 确认操作 +Confirm Swap: 确认兑换 +"%amount% to %address%": "%amount% 至 %address%" +"%amount_from% to %amount_to%": "%amount_from% 至 %amount_to%" +Failed to enable biometrics: 无法启用生物识别 +Disable Face ID: Disable Face ID +Disable Touch ID: Disable Touch ID +Disable Biometrics: Disable Biometrics +Are you sure you want to disable Face ID?: 您确定要禁用Face ID吗? +Are you sure you want to disable Touch ID?: 您确定要禁用Touch ID吗? +Are you sure you want to disable biometrics?: 您确定要禁用生物识别技术吗? +Yes: 是的 diff --git a/src/i18n/zh-Hant.yaml b/src/i18n/zh-Hant.yaml index 2e4ffb81..346e05f8 100644 --- a/src/i18n/zh-Hant.yaml +++ b/src/i18n/zh-Hant.yaml @@ -96,20 +96,18 @@ $logout_accounts_without_backup_warning: 您還沒有備份 %links%。 如果您 $logout_warning: 這將斷開錢包與此應用程序的連接。 您將能夠使用 **%1$d 個密語** 恢復您的錢包 - 或者導入另一個錢包。 Receive TON: 接收 TON Your address was copied!: 您的地址已被複製! -Show QR-Code: 顯示 QR-Code -Create Invoice: 建立發票 -QR-code: QR-code -$receive_invoice_description: | - 您可以指定付款金額和地址來節省發送人的一些時間 +Show QR Code: 顯示 QR Code +Create Deposit Link: 建立存款連結 +QR Code: QR Code +$receive_invoice_description: 您可以指定付款金額和地址來節省發送人的一些時間。 Amount: 數量 Comment: 註記欄 Share this URL to receive TON: 分享此連結來接收 TON Invoice link was copied!: 發票連結已複製! -US Dollar: 美金 Theme: 主題 Language: 語言 Enable Animations: 啟用動畫 -Fiat Currency: 法定貨幣 +Base Currency: 基本貨幣 Investor View: 投資者觀點 Toggle Investor View: 切換投資者觀點 Hide Tiny Transfers: 隱藏小額轉帳 @@ -154,17 +152,17 @@ $transaction_to: 到 %address% Wallet is not backed up: 錢包未備份 Testnet Version: 測試網版本 Error reading clipboard: 讀取剪貼板時出現錯誤 -$fee_value: "Fee: %fee%" +$fee_value: "手續費: %fee%" Loading...: 讀取中... Recipient Address: 接收人地址 Wallet address or domain: 錢包地址或是域名 Incorrect address: 錯誤的地址 +Incorrect address.: 錯誤的地址。 Paste: 貼上 Insufficient balance: 餘額不足 InsufficientBalance: 餘額不足 Optional: 選項 $send_token_symbol: 發送 %1$s -$balance_is: "餘額:%balance%" Is it all ok?: 全部確認都 OK? Receiving Address: 接收地址 Fee: 手續費 @@ -192,8 +190,7 @@ Understood: 了解 Today: 今天 Yesterday: 昨天 Now: 現在 -$receive_ton_description: | - 您可以分享這個地址,出示 QR-Code 或創建發票來接收 TON +$receive_ton_description: 您可以分享您的地址、掃描二維碼或建立存款連結來接收加密貨幣。 Your address: 您的地址 Wrong password, please try again: 密碼錯誤,請再嘗試一次 Appearance: 外觀 @@ -205,7 +202,7 @@ Earn from your tokens while holding them: 從你持有中的代幣獲利 $est_apy_val: Est. APY %1$d% Why this is safe: 為什麼這是安全的 Why staking is safe?: 為什麼質押是安全的? -$safe_staking_description1: Staking 是**完全去中心化的**,由官方 TON Nominator 智能合約運作。 沒有人可以使用您質押中的代幣. +$safe_staking_description1: Staking 是**完全去中心化的**,由官方 TON Liquid Staking 智能合約運作。 $safe_staking_description2: 作為其 **權益證明** 本質的一部分,存入的代幣將用於 TON 網路驗證。 $safe_staking_description3: 您可以在**任何時候**提取您的質押資產,它會在**2 天內**存入您的帳戶。 $min_value: 最少 %value% @@ -324,6 +321,28 @@ $dapp_ledger_warning1: 您即將使用您的**Ledger**錢包發送多方交易 $dapp_ledger_warning2: 請慢慢來,不要中斷過程。 Agree: 同意 The hardware wallet does not support this data format: 硬件錢包不支持該數據格式 +Swap: 交換 +You sell: 您賣出 +You buy: 您購買 +Swap Details: 交換詳情 +Blockchain fee: 區塊鏈費用 +Price Impact: 價格影響 +Minimum Received: 最低接收數量 +Slippage: 下單誤差 +$swap_from_to: 從 %from% 交換至 %to% +The exchange rate is below market value!: 匯率低於市場價值 %value%! +Invalid Pair: 無效交易對 +We do not recommend to perform an exchange, try to specify a lower amount.: 我們不建議進行交換,請嘗試指定較小的金額。 +$swap_minimum_received_tooltip1: 這是您從此次交換中可能獲得的最少新代幣數量,考慮了目前市場條件、滑點容忍度和潛在價格影響。 +$swap_minimum_received_tooltip2: 這是預期的最低值,但如果在處理您的交換時條件發生很大變化,最終金額可能會較少。 +$swap_price_impact_tooltip1: 這顯示了您的交易可能對代幣價格造成的影響。 +$swap_price_impact_tooltip2: 大型交易可能會使價格上漲或下跌得更多。較低的值通常較好。 +$swap_slippage_tooltip1: 設置此值可以允許價格在進行交換前發生變化的程度。 +$swap_slippage_tooltip2: 如果在處理您的交換時,價格變化超過此值,您的訂單將被取消。 +Coins have been swapped!: 币已交换完成! +Slippage not specified: 未指定下單誤差 +Slippage too high: 下單誤差過高 +Not enough TON: TON 不足 Use Responsibly: 負責任地使用 $auth_responsibly_description1: | MyTonWallet 是一個**自我託管**錢包,這意味著**只有您**擁有完全控制權,最重要的是,對您的資金**承擔全部責任**。 @@ -348,9 +367,129 @@ $auth_backup_description1: | $auth_backup_description2: 「能力越強,責任越大。」 $auth_backup_description3: | 你需要手動**備份註記詞**,免得你忘記助記詞而無法登入此裝置。 +Swap Placed: 已提交交換 +Swapping: 進行交換 +Swapped: 已交換 +Swap Failed: 交換失敗 +Exchange rate: 兌換率 +Swap Again: 再次進行交換 Terms of Use: 使用條款 Privacy Policy: 隱私政策 +Insufficient liquidity: 流動性不足 This address is new and never received transfers before.: 這個地址是新的,以前從未收到過轉帳。 Create Backup: 建立備份 Sending: 傳送 -failed: 失败 +Failed: 失敗 +Refunded: 退款 +On hold: 持有 +Expired: 過期 +In progress: 進行中 +Waiting Payment: 等待付款 +Waiting for payment: 等待付款 +Blockchain: 區塊鏈 +Receiving address in blockchain: 在 %blockchain% 區塊鏈中的接收地址 +Please provide an address of your wallet in another blockchain to receive bought tokens.: 請提供您在另一個區塊鏈上的錢包地址以接收已購買的代幣。 +The time for sending coins is over: 發送硬幣的時間已結束 +Please wait a few moments...: 請稍等片刻... +You have not sent the coins to the specified address.: 您尚未將硬幣發送到指定地址。 +$swap_changelly_to_ton_description1: "您必須在 %time% 內將 %value% 發送到此地址 在 %blockchain% 區塊鏈中" +Please contact support and provide a transaction ID.: 請聯繫支援並提供交易ID。 +Exchange failed and coins were refunded to your wallet.: 交換失敗,硬幣已退還到您的錢包。 +Please contact security team to pass the KYC procedure.: 請聯繫安全團隊以通過KYC程序。 +Swap Expired: 交換過期 +Swap On Hold: 交換暫停 +Swap Refunded: 交換退款 +Changelly Payment Address: Changelly 付款地址 +Cross-chain exchange provided by Changelly: Changelly提供的跨鏈交換 +$swap_changelly_agreement_message: 繼續操作即表示您同意 %terms% 和 %policy%,並理解該交易可能會觸發根據 %kyc% 的驗證。 +$swap_changelly_terms_of_use: 使用條款 +$swap_changelly_privacy_policy: 隱私政策 +Password must contain %length% digits.: 密碼必須包含%length%位數字。 +Deposit Link: 儲值連結 +$swap_changelly_from_ton_description: "代幣將在幾分鐘內發送到您在%blockchain%區塊鏈上的地址:" +Minimum amount: 最低 %value% +Maximum amount: 最高 %value% +Display Tray Icon: 顯示托盤圖標 +Enable Auto-Updates: 啟用自動更新 +Biometric Authentication: 生物辨識認證 +Turn off biometrics?: 關閉生物識別? +If you turn off biometric protection, you will need to create a password.: 如果您關閉生物辨識保護,則需要建立密碼。 +Enter your current password: 輸入目前密碼 +Turn On Biometrics: 打開生物識別 +Please confirm transaction using biometrics: 請使用生物辨識技術確認交易 +Enabling biometric confirmation will reset the password.: 啟用生物辨識確認將重設密碼。 +Biometric Registration: 生物辨識註冊 +Step 1 of 2. Registration: 第 1 步(共 2 步):註冊 +Step 2 of 2. Verification: 第 2 步(共 2 步):驗證 +Biometric setup failed: 生物辨識設定失敗 +Biometric confirmation failed: 生物辨識確認失敗 +Failed to disable biometrics: 無法禁用生物識別 +Biometric Confirmation: 生物辨識確認 +Please verify your identity.: 請證明你的身分。 +Create Password: 建立密碼 +Complete: 罷 +Verification: 確認 +Create a password or use biometric authentication to protect it.: 創建密碼或使用生物識別身份驗證來保護它。 +Connect Biometrics: 連接生物識別 +Use Password: 使用密碼 +Biometrics Disabled: 生物辨識已停用 +Biometrics Enabled: 啟用生物識別 +A request is already pending: 請求已待處理 +Please confirm operation using biometrics: 請使用生物辨識確認操作 +Scan QR Code: 掃描二維碼 +Permission denied. Please grant camera permission to use the QR code scanner.: 沒有權限。 請授予相機使用 QR 掃描器的權限。 +Unsupported barcode format: 不支援的條碼格式 +An error on the server side. Please try again.: 服務器端的錯誤。請再試一次。 +Unrecognized QR Code: 無法辨識的二維碼 +$fee_value_almost_equal: 手續費 ≈ %fee% +Address was saved!: 地址已儲存! +US Dollar: 美金 +Euro: 歐元 +Ruble: 盧布 +Yuan: 元 +$max_balance: "最大: %balance%" +$unstake_information_instantly: 您將立即收到您的存款。 +Select Token: 選擇代幣 +MY: 我的 +POPULAR: 受歡迎 +A-Z: A-Z +Not Found: 未找到 +Wallet is ready!: 錢包準備好了! +Wallet is imported!: 錢包是進口的! +Create a code to protect it: 創建程式碼來保護它 +Enter your code again: 再次輸入您的代碼 +Codes don't match: 代碼不匹配 +Code set successfully: 代碼設定成功 +Use Biometrics: 使用生物辨識技術 +Use Touch ID: 使用 Touch ID +Use Face ID: 使用 Face ID +You can connect your biometric data for more convenience: 您可以連接您的生物識別數據以獲得更多便利 +Connect Touch ID: 連接 Touch ID +Connect Face ID: 連接 Face ID +Not Now: 現在不要 +Biometric Authentification: 生物辨識認證 +Touch ID: Touch ID +Face ID: Face ID +Turn Off Touch ID?: 關閉 Touch ID? +Turn Off Face ID?: 關閉 Off Face ID? +Turn Off Biometrics?: 關閉生物辨識技術? +Enter code: 輸入代碼 +Enter code or use Face ID: 輸入代碼或使用 Face ID +Enter code or use Touch ID: 輸入代碼或使用 Touch ID +Enter code or user biometrics: 輸入代碼或使用者生物辨識訊息 +Wrong code, please try again: 程式碼錯誤,請重試 +Scan your fingerprint: 掃描您的指紋 +Incorrect code, please try again: Incorrect code, please try again +Correct: 不錯 +Confirm Operation: 確認操作 +Confirm Swap: 確認兌換 +"%amount% to %address%": "%amount% 至 %address%" +"%amount_from% to %amount_to%": "%amount_from% 至 %amount_to%" +Failed to enable biometrics: 無法啟用生物識別 +Disable Face ID: Disable Face ID +Disable Touch ID: Disable Touch ID +Disable Biometrics: Disable Biometrics +Are you sure you want to disable Face ID?: 您確定要禁用Face ID嗎? +Are you sure you want to disable Touch ID?: 您確定要禁用Touch ID嗎? +Are you sure you want to disable biometrics?: 您確定要禁用生物識別技術嗎? +Yes: 是的 diff --git a/src/index.tsx b/src/index.tsx index c0b2992f..1d8f0887 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,9 +6,13 @@ import React from './lib/teact/teact'; import TeactDOM from './lib/teact/teact-dom'; import { getActions, getGlobal } from './global'; -import { DEBUG, STRICTERDOM_ENABLED } from './config'; +import { DEBUG, IS_CAPACITOR, STRICTERDOM_ENABLED } from './config'; import { requestMutation } from './lib/fasterdom/fasterdom'; import { enableStrict } from './lib/fasterdom/stricterdom'; +import { betterView } from './util/betterView'; +import { initCapacitor } from './util/capacitor'; +import { initMultitab } from './util/multitab'; +import { CAN_DELEGATE_BOTTOM_SHEET, IS_DELEGATED_BOTTOM_SHEET } from './util/windowEnvironment'; import App from './components/App'; @@ -23,46 +27,62 @@ if (STRICTERDOM_ENABLED) { enableStrict(); } -getActions().init(); -getActions().initApi(); +if (IS_CAPACITOR) { + void initCapacitor(); +} -if (DEBUG) { - // eslint-disable-next-line no-console - console.log('>>> START INITIAL RENDER'); +if (CAN_DELEGATE_BOTTOM_SHEET) { + initMultitab({ noPub: true }); +} else if (IS_DELEGATED_BOTTOM_SHEET) { + initMultitab({ noSub: true }); } -requestMutation(() => { - TeactDOM.render( - , - document.getElementById('root')!, - ); -}); +(async () => { + await window.electron?.restoreStorage(); -if (DEBUG) { - // eslint-disable-next-line no-console - console.log('>>> FINISH INITIAL RENDER'); -} + getActions().init(); + getActions().initApi(); -document.addEventListener('dblclick', () => { - // eslint-disable-next-line no-console - console.warn('GLOBAL STATE', getGlobal()); -}); - -if (window.top === window) { - const selfXssWarnings: AnyLiteral = { - en: 'WARNING! This console can be a way for bad people to take over your crypto wallet through something called ' - + 'a Self-XSS attack. So, don\'t put in or paste code you don\'t understand. Stay safe!', - ru: 'ВНИМАНИЕ! Через эту консоль злоумышленники могут захватить ваш криптовалютный кошелёк с помощью так ' - + 'называемой атаки Self-XSS. Поэтому не вводите и не вставляйте код, который вы не понимаете. Берегите себя!', - es: '¡ADVERTENCIA! Esta consola puede ser una forma en que las personas malintencionadas se apoderen de su ' - + 'billetera de criptomonedas mediante un ataque llamado Self-XSS. Por lo tanto, ' - + 'no introduzca ni pegue código que no comprenda. ¡Cuídese!', - zh: '警告!这个控制台可能成为坏人通过所谓的Self-XSS攻击来接管你的加密货币钱包的方式。因此,请不要输入或粘贴您不理解的代码。请保护自己!', - }; - - const langCode = navigator.language.split('-')[0]; - const text = selfXssWarnings[langCode] || selfXssWarnings.en; + if (DEBUG) { + // eslint-disable-next-line no-console + console.log('>>> START INITIAL RENDER'); + } - // eslint-disable-next-line no-console - console.log('%c%s', 'color: red; background: yellow; font-size: 18px;', text); -} + requestMutation(() => { + TeactDOM.render( + , + document.getElementById('root')!, + ); + + betterView(); + }); + + if (DEBUG) { + // eslint-disable-next-line no-console + console.log('>>> FINISH INITIAL RENDER'); + } + + document.addEventListener('dblclick', () => { + // eslint-disable-next-line no-console + console.warn('GLOBAL STATE', getGlobal()); + }); + + if (window.top === window) { + const selfXssWarnings: AnyLiteral = { + en: 'WARNING! This console can be a way for bad people to take over your crypto wallet through something called ' + + 'a Self-XSS attack. So, don\'t put in or paste code you don\'t understand. Stay safe!', + ru: 'ВНИМАНИЕ! Через эту консоль злоумышленники могут захватить ваш криптовалютный кошелёк с помощью так ' + + 'называемой атаки Self-XSS. Поэтому не вводите и не вставляйте код, который вы не понимаете. Берегите себя!', + es: '¡ADVERTENCIA! Esta consola puede ser una forma en que las personas malintencionadas se apoderen de su ' + + 'billetera de criptomonedas mediante un ataque llamado Self-XSS. Por lo tanto, ' + + 'no introduzca ni pegue código que no comprenda. ¡Cuídese!', + zh: '警告!这个控制台可能成为坏人通过所谓的Self-XSS攻击来接管你的加密货币钱包的方式。因此,请不要输入或粘贴您不理解的代码。请保护自己!', + }; + + const langCode = navigator.language.split('-')[0]; + const text = selfXssWarnings[langCode] || selfXssWarnings.en; + + // eslint-disable-next-line no-console + console.log('%c%s', 'color: red; background: yellow; font-size: 18px;', text); + } +})(); diff --git a/src/lib/dexie/dexie.d.ts b/src/lib/dexie/dexie.d.ts new file mode 100644 index 00000000..336e1391 --- /dev/null +++ b/src/lib/dexie/dexie.d.ts @@ -0,0 +1,1028 @@ +/* + * Dexie.js - a minimalistic wrapper for IndexedDB + * =============================================== + * + * By David Fahlander, david.fahlander@gmail.com + * + * Version 3.2.4, Tue May 30 2023 + * + * https://dexie.org + * + * Apache License Version 2.0, January 2004, http://www.apache.org/licenses/ + */ + // Generated by dts-bundle-generator v5.9.0 + +export interface IndexSpec { + name: string; + keyPath: string | Array | undefined; + unique: boolean | undefined; + multi: boolean | undefined; + auto: boolean | undefined; + compound: boolean | undefined; + src: string; +} +export interface TableSchema { + name: string; + primKey: IndexSpec; + indexes: IndexSpec[]; + mappedClass: Function; + idxByName: { + [name: string]: IndexSpec; + }; + readHook?: (x: any) => any; +} +export type IndexableTypePart = string | number | Date | ArrayBuffer | ArrayBufferView | DataView | Array>; +export type IndexableTypeArray = Array; +export type IndexableTypeArrayReadonly = ReadonlyArray; +export type IndexableType = IndexableTypePart | IndexableTypeArrayReadonly; +export interface DexieEvent { + subscribers: Function[]; + fire(...args: any[]): any; + subscribe(fn: (...args: any[]) => any): void; + unsubscribe(fn: (...args: any[]) => any): void; +} +export interface DexieEventSet { + (eventName: string): DexieEvent; // To be able to unsubscribe. + addEventType(eventName: string, chainFunction?: (f1: Function, f2: Function) => Function, defaultFunction?: Function): DexieEvent; + addEventType(events: { + [eventName: string]: ("asap" | [ + (f1: Function, f2: Function) => Function, + Function + ]); + }): DexieEvent; +} +export type TransactionMode = "readonly" | "readwrite" | "r" | "r!" | "r?" | "rw" | "rw!" | "rw?"; +export interface PromiseExtendedConstructor extends PromiseConstructor { + readonly prototype: PromiseExtended; + new (executor: (resolve: (value?: T | PromiseLike) => void, reject: (reason?: any) => void) => void): PromiseExtended; + all(values: [ + T1 | PromiseLike, + T2 | PromiseLike, + T3 | PromiseLike, + T4 | PromiseLike, + T5 | PromiseLike, + T6 | PromiseLike, + T7 | PromiseLike, + T8 | PromiseLike, + T9 | PromiseLike, + T10 | PromiseLike + ]): PromiseExtended<[ + T1, + T2, + T3, + T4, + T5, + T6, + T7, + T8, + T9, + T10 + ]>; + all(values: [ + T1 | PromiseLike, + T2 | PromiseLike, + T3 | PromiseLike, + T4 | PromiseLike, + T5 | PromiseLike, + T6 | PromiseLike, + T7 | PromiseLike, + T8 | PromiseLike, + T9 | PromiseLike + ]): PromiseExtended<[ + T1, + T2, + T3, + T4, + T5, + T6, + T7, + T8, + T9 + ]>; + all(values: [ + T1 | PromiseLike, + T2 | PromiseLike, + T3 | PromiseLike, + T4 | PromiseLike, + T5 | PromiseLike, + T6 | PromiseLike, + T7 | PromiseLike, + T8 | PromiseLike + ]): PromiseExtended<[ + T1, + T2, + T3, + T4, + T5, + T6, + T7, + T8 + ]>; + all(values: [ + T1 | PromiseLike, + T2 | PromiseLike, + T3 | PromiseLike, + T4 | PromiseLike, + T5 | PromiseLike, + T6 | PromiseLike, + T7 | PromiseLike + ]): PromiseExtended<[ + T1, + T2, + T3, + T4, + T5, + T6, + T7 + ]>; + all(values: [ + T1 | PromiseLike, + T2 | PromiseLike, + T3 | PromiseLike, + T4 | PromiseLike, + T5 | PromiseLike, + T6 | PromiseLike + ]): PromiseExtended<[ + T1, + T2, + T3, + T4, + T5, + T6 + ]>; + all(values: [ + T1 | PromiseLike, + T2 | PromiseLike, + T3 | PromiseLike, + T4 | PromiseLike, + T5 | PromiseLike + ]): PromiseExtended<[ + T1, + T2, + T3, + T4, + T5 + ]>; + all(values: [ + T1 | PromiseLike, + T2 | PromiseLike, + T3 | PromiseLike, + T4 | PromiseLike + ]): PromiseExtended<[ + T1, + T2, + T3, + T4 + ]>; + all(values: [ + T1 | PromiseLike, + T2 | PromiseLike, + T3 | PromiseLike + ]): PromiseExtended<[ + T1, + T2, + T3 + ]>; + all(values: [ + T1 | PromiseLike, + T2 | PromiseLike + ]): PromiseExtended<[ + T1, + T2 + ]>; + all(values: (T | PromiseLike)[]): PromiseExtended; + race(values: [ + T1 | PromiseLike, + T2 | PromiseLike, + T3 | PromiseLike, + T4 | PromiseLike, + T5 | PromiseLike, + T6 | PromiseLike, + T7 | PromiseLike, + T8 | PromiseLike, + T9 | PromiseLike, + T10 | PromiseLike + ]): PromiseExtended; + race(values: [ + T1 | PromiseLike, + T2 | PromiseLike, + T3 | PromiseLike, + T4 | PromiseLike, + T5 | PromiseLike, + T6 | PromiseLike, + T7 | PromiseLike, + T8 | PromiseLike, + T9 | PromiseLike + ]): PromiseExtended; + race(values: [ + T1 | PromiseLike, + T2 | PromiseLike, + T3 | PromiseLike, + T4 | PromiseLike, + T5 | PromiseLike, + T6 | PromiseLike, + T7 | PromiseLike, + T8 | PromiseLike + ]): PromiseExtended; + race(values: [ + T1 | PromiseLike, + T2 | PromiseLike, + T3 | PromiseLike, + T4 | PromiseLike, + T5 | PromiseLike, + T6 | PromiseLike, + T7 | PromiseLike + ]): PromiseExtended; + race(values: [ + T1 | PromiseLike, + T2 | PromiseLike, + T3 | PromiseLike, + T4 | PromiseLike, + T5 | PromiseLike, + T6 | PromiseLike + ]): PromiseExtended; + race(values: [ + T1 | PromiseLike, + T2 | PromiseLike, + T3 | PromiseLike, + T4 | PromiseLike, + T5 | PromiseLike + ]): PromiseExtended; + race(values: [ + T1 | PromiseLike, + T2 | PromiseLike, + T3 | PromiseLike, + T4 | PromiseLike + ]): PromiseExtended; + race(values: [ + T1 | PromiseLike, + T2 | PromiseLike, + T3 | PromiseLike + ]): PromiseExtended; + race(values: [ + T1 | PromiseLike, + T2 | PromiseLike + ]): PromiseExtended; + race(values: (T | PromiseLike)[]): PromiseExtended; + reject(reason: any): PromiseExtended; + reject(reason: any): PromiseExtended; + resolve(value: T | PromiseLike): PromiseExtended; + resolve(): PromiseExtended; +} +/** The interface of Dexie.Promise, which basically extends standard Promise with methods: + * + * finally() - also subject for standardization + * timeout() - set a completion timeout + * catch(ErrorClass, handler) - java style error catching + * catch(errorName, handler) - cross-domain safe type error catching (checking error.name instead of instanceof) + * + */ +export interface PromiseExtended extends Promise { + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): PromiseExtended; + catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): PromiseExtended; + catch(ErrorConstructor: Function, onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): PromiseExtended; + catch(errorName: string, onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): PromiseExtended; + finally(onFinally?: () => U | PromiseLike): PromiseExtended; + timeout(ms: number, msg?: string): PromiseExtended; +} +export type ThenShortcut = (value: T) => TResult | PromiseLike; +export interface Collection { + //db: Database; + and(filter: (x: T) => boolean): Collection; + clone(props?: Object): Collection; + count(): PromiseExtended; + count(thenShortcut: ThenShortcut): PromiseExtended; + distinct(): Collection; + each(callback: (obj: T, cursor: { + key: IndexableType; + primaryKey: TKey; + }) => any): PromiseExtended; + eachKey(callback: (key: IndexableType, cursor: { + key: IndexableType; + primaryKey: TKey; + }) => any): PromiseExtended; + eachPrimaryKey(callback: (key: TKey, cursor: { + key: IndexableType; + primaryKey: TKey; + }) => any): PromiseExtended; + eachUniqueKey(callback: (key: IndexableType, cursor: { + key: IndexableType; + primaryKey: TKey; + }) => any): PromiseExtended; + filter(filter: (x: T) => boolean): Collection; + first(): PromiseExtended; + first(thenShortcut: ThenShortcut): PromiseExtended; + keys(): PromiseExtended; + keys(thenShortcut: ThenShortcut): PromiseExtended; + primaryKeys(): PromiseExtended; + primaryKeys(thenShortcut: ThenShortcut): PromiseExtended; + last(): PromiseExtended; + last(thenShortcut: ThenShortcut): PromiseExtended; + limit(n: number): Collection; + offset(n: number): Collection; + or(indexOrPrimayKey: string): WhereClause; + raw(): Collection; + reverse(): Collection; + sortBy(keyPath: string): PromiseExtended; + sortBy(keyPath: string, thenShortcut: ThenShortcut): PromiseExtended; + toArray(): PromiseExtended>; + toArray(thenShortcut: ThenShortcut): PromiseExtended; + uniqueKeys(): PromiseExtended; + uniqueKeys(thenShortcut: ThenShortcut): PromiseExtended; + until(filter: (value: T) => boolean, includeStopEntry?: boolean): Collection; + // Mutating methods + delete(): PromiseExtended; + modify(changeCallback: (obj: T, ctx: { + value: T; + }) => void | boolean): PromiseExtended; + modify(changes: { + [keyPath: string]: any; + }): PromiseExtended; +} +export interface WhereClause { + above(key: any): Collection; + aboveOrEqual(key: any): Collection; + anyOf(keys: ReadonlyArray): Collection; + anyOf(...keys: Array): Collection; + anyOfIgnoreCase(keys: string[]): Collection; + anyOfIgnoreCase(...keys: string[]): Collection; + below(key: any): Collection; + belowOrEqual(key: any): Collection; + between(lower: any, upper: any, includeLower?: boolean, includeUpper?: boolean): Collection; + equals(key: IndexableType): Collection; + equalsIgnoreCase(key: string): Collection; + inAnyRange(ranges: ReadonlyArray<{ + 0: any; + 1: any; + }>, options?: { + includeLowers?: boolean; + includeUppers?: boolean; + }): Collection; + startsWith(key: string): Collection; + startsWithAnyOf(prefixes: string[]): Collection; + startsWithAnyOf(...prefixes: string[]): Collection; + startsWithIgnoreCase(key: string): Collection; + startsWithAnyOfIgnoreCase(prefixes: string[]): Collection; + startsWithAnyOfIgnoreCase(...prefixes: string[]): Collection; + noneOf(keys: ReadonlyArray): Collection; + notEqual(key: IndexableType): Collection; +} +export interface Database { + readonly name: string; + readonly tables: Table[]; + table(tableName: string): Table; + transaction(mode: TransactionMode, table: Table, scope: () => PromiseLike | U): PromiseExtended; + transaction(mode: TransactionMode, table: Table, table2: Table, scope: () => PromiseLike | U): PromiseExtended; + transaction(mode: TransactionMode, table: Table, table2: Table, table3: Table, scope: () => PromiseLike | U): PromiseExtended; + transaction(mode: TransactionMode, table: Table, table2: Table, table3: Table, table4: Table, scope: () => PromiseLike | U): PromiseExtended; + transaction(mode: TransactionMode, table: Table, table2: Table, table3: Table, table4: Table, table5: Table, scope: () => PromiseLike | U): PromiseExtended; + transaction(mode: TransactionMode, tables: Table[], scope: () => PromiseLike | U): PromiseExtended; +} +export interface TransactionEvents extends DexieEventSet { + (eventName: "complete", subscriber: () => any): void; + (eventName: "abort", subscriber: () => any): void; + (eventName: "error", subscriber: (error: any) => any): void; + complete: DexieEvent; + abort: DexieEvent; + error: DexieEvent; +} +export interface Transaction { + db: Database; + active: boolean; + mode: IDBTransactionMode; + //tables: { [type: string]: Table }; Deprecated since 2.0. Obsolete from v3.0. + storeNames: Array; + parent?: Transaction; + on: TransactionEvents; + abort(): void; + table(tableName: string): Table; + table(tableName: string): Table; + table(tableName: string): Table; +} +export interface CreatingHookContext { + onsuccess?: (primKey: Key) => void; + onerror?: (err: any) => void; +} +export interface UpdatingHookContext { + onsuccess?: (updatedObj: T) => void; + onerror?: (err: any) => void; +} +export interface DeletingHookContext { + onsuccess?: () => void; + onerror?: (err: any) => void; +} +export interface TableHooks extends DexieEventSet { + (eventName: "creating", subscriber: (this: CreatingHookContext, primKey: TKey, obj: T, transaction: Transaction) => any): void; + (eventName: "reading", subscriber: (obj: T) => T | any): void; + (eventName: "updating", subscriber: (this: UpdatingHookContext, modifications: Object, primKey: TKey, obj: T, transaction: Transaction) => any): void; + (eventName: "deleting", subscriber: (this: DeletingHookContext, primKey: TKey, obj: T, transaction: Transaction) => any): void; + creating: DexieEvent; + reading: DexieEvent; + updating: DexieEvent; + deleting: DexieEvent; +} +export const enum DBCoreRangeType { + Equal = 1, + Range = 2, + Any = 3, + Never = 4 +} +export interface DBCoreKeyRange { + readonly type: DBCoreRangeType; + readonly lower: any; + readonly lowerOpen?: boolean; + readonly upper: any; + readonly upperOpen?: boolean; +} +export interface DBCoreTransaction { + abort(): void; +} +export interface DbCoreTransactionOptions { + durability: ChromeTransactionDurability; +} +export type DBCoreMutateRequest = DBCoreAddRequest | DBCorePutRequest | DBCoreDeleteRequest | DBCoreDeleteRangeRequest; +export interface DBCoreMutateResponse { + numFailures: number; + failures: { + [operationNumber: number]: Error; + }; + lastResult: any; + results?: any[]; // Present on AddRequest and PutRequest. +} +export interface DBCoreAddRequest { + type: "add"; + trans: DBCoreTransaction; + values: any[]; + keys?: any[]; + /** @deprecated Will always get results since 3.1.0-alpha.5 */ + wantResults?: boolean; +} +export interface DBCorePutRequest { + type: "put"; + trans: DBCoreTransaction; + values: any[]; + keys?: any[]; + criteria?: { + index: string | null; + range: DBCoreKeyRange; + }; + changeSpec?: { + [keyPath: string]: any; + }; // Common changeSpec for each key + changeSpecs?: { + [keyPath: string]: any; + }[]; // changeSpec per key. + /** @deprecated Will always get results since 3.1.0-alpha.5 */ + wantResults?: boolean; +} +export interface DBCoreDeleteRequest { + type: "delete"; + trans: DBCoreTransaction; + keys: any[]; + criteria?: { + index: string | null; + range: DBCoreKeyRange; + }; +} +export interface DBCoreDeleteRangeRequest { + type: "deleteRange"; + trans: DBCoreTransaction; + range: DBCoreKeyRange; +} +export interface DBCoreGetManyRequest { + trans: DBCoreTransaction; + keys: any[]; + cache?: "immutable" | "clone"; +} +export interface DBCoreGetRequest { + trans: DBCoreTransaction; + key: any; +} +export interface DBCoreQuery { + index: DBCoreIndex; //keyPath: null | string | string[]; // null represents primary key. string a property, string[] several properties. + range: DBCoreKeyRange; +} +export interface DBCoreQueryRequest { + trans: DBCoreTransaction; + values?: boolean; + limit?: number; + query: DBCoreQuery; +} +export interface DBCoreQueryResponse { + result: any[]; +} +export interface DBCoreOpenCursorRequest { + trans: DBCoreTransaction; + values?: boolean; + unique?: boolean; + reverse?: boolean; + query: DBCoreQuery; +} +export interface DBCoreCountRequest { + trans: DBCoreTransaction; + query: DBCoreQuery; +} +export interface DBCoreCursor { + readonly trans: DBCoreTransaction; + readonly key: any; + readonly primaryKey: any; + readonly value?: any; + readonly done?: boolean; + continue(key?: any): void; + continuePrimaryKey(key: any, primaryKey: any): void; + advance(count: number): void; + start(onNext: () => void): Promise; + stop(value?: any | Promise): void; + next(): Promise; + fail(error: Error): void; +} +export interface DBCoreSchema { + name: string; + tables: DBCoreTableSchema[]; +} +export interface DBCoreTableSchema { + readonly name: string; + readonly primaryKey: DBCoreIndex; + readonly indexes: DBCoreIndex[]; + readonly getIndexByKeyPath: (keyPath: null | string | string[]) => DBCoreIndex | undefined; +} +export interface DBCoreIndex { + /** Name of the index, or null for primary key */ + readonly name: string | null; + /** True if this index represents the primary key */ + readonly isPrimaryKey?: boolean; + /** True if this index represents the primary key and is not inbound (https://dexie.org/docs/inbound) */ + readonly outbound?: boolean; + /** True if and only if keyPath is an array (https://dexie.org/docs/Compound-Index) */ + readonly compound?: boolean; + /** keyPath, null for primary key, string for single-property indexes, Array for compound indexes */ + readonly keyPath: null | string | string[]; + /** Auto-generated primary key (does not apply to secondary indexes) */ + readonly autoIncrement?: boolean; + /** Whether index is unique. Also true if index is primary key. */ + readonly unique?: boolean; + /** Whether index is multiEntry. */ + readonly multiEntry?: boolean; + /** Extract (using keyPath) a key from given value (object). Null for outbound primary keys */ + readonly extractKey: ((value: any) => any) | null; +} +export interface DBCore { + stack: "dbcore"; + // Transaction and Object Store + transaction(stores: string[], mode: "readonly" | "readwrite", options?: DbCoreTransactionOptions): DBCoreTransaction; + // Utility methods + readonly MIN_KEY: any; + readonly MAX_KEY: any; + readonly schema: DBCoreSchema; + table(name: string): DBCoreTable; +} +export interface DBCoreTable { + readonly name: string; + readonly schema: DBCoreTableSchema; + mutate(req: DBCoreMutateRequest): Promise; + get(req: DBCoreGetRequest): Promise; + getMany(req: DBCoreGetManyRequest): Promise; + query(req: DBCoreQueryRequest): Promise; + openCursor(req: DBCoreOpenCursorRequest): Promise; + count(req: DBCoreCountRequest): Promise; +} +export interface Table { + db: Database; + name: string; + schema: TableSchema; + hook: TableHooks; + core: DBCoreTable; + get(key: TKey): PromiseExtended; + get(key: TKey, thenShortcut: ThenShortcut): PromiseExtended; + get(equalityCriterias: { + [key: string]: any; + }): PromiseExtended; + get(equalityCriterias: { + [key: string]: any; + }, thenShortcut: ThenShortcut): PromiseExtended; + where(index: string | string[]): WhereClause; + where(equalityCriterias: { + [key: string]: any; + }): Collection; + filter(fn: (obj: T) => boolean): Collection; + count(): PromiseExtended; + count(thenShortcut: ThenShortcut): PromiseExtended; + offset(n: number): Collection; + limit(n: number): Collection; + each(callback: (obj: T, cursor: { + key: any; + primaryKey: TKey; + }) => any): PromiseExtended; + toArray(): PromiseExtended>; + toArray(thenShortcut: ThenShortcut): PromiseExtended; + toCollection(): Collection; + orderBy(index: string | string[]): Collection; + reverse(): Collection; + mapToClass(constructor: Function): Function; + add(item: T, key?: TKey): PromiseExtended; + update(key: TKey | T, changes: { + [keyPath: string]: any; + }): PromiseExtended; + put(item: T, key?: TKey): PromiseExtended; + delete(key: TKey): PromiseExtended; + clear(): PromiseExtended; + bulkGet(keys: TKey[]): PromiseExtended<(T | undefined)[]>; + bulkAdd(items: readonly T[], keys: IndexableTypeArrayReadonly, options: { + allKeys: B; + }): PromiseExtended; + bulkAdd(items: readonly T[], options: { + allKeys: B; + }): PromiseExtended; + bulkAdd(items: readonly T[], keys?: IndexableTypeArrayReadonly, options?: { + allKeys: boolean; + }): PromiseExtended; + bulkPut(items: readonly T[], keys: IndexableTypeArrayReadonly, options: { + allKeys: B; + }): PromiseExtended; + bulkPut(items: readonly T[], options: { + allKeys: B; + }): PromiseExtended; + bulkPut(items: readonly T[], keys?: IndexableTypeArrayReadonly, options?: { + allKeys: boolean; + }): PromiseExtended; + bulkDelete(keys: TKey[]): PromiseExtended; +} +export interface Version { + stores(schema: { + [tableName: string]: string | null; + }): Version; + upgrade(fn: (trans: Transaction) => PromiseLike | void): Version; +} +export type IntervalTree = IntervalTreeNode | EmptyRange; +export interface IntervalTreeNode { + from: IndexableType; // lower bound + to: IndexableType; // upper bound + l: IntervalTreeNode | null; // left + r: IntervalTreeNode | null; // right + d: number; // depth +} +export interface EmptyRange { + d: 0; +} +export interface DexieOnReadyEvent { + subscribe(fn: (vipDb: Dexie) => any, bSticky: boolean): void; + unsubscribe(fn: (vipDb: Dexie) => any): void; + fire(vipDb: Dexie): any; +} +export interface DexieVersionChangeEvent { + subscribe(fn: (event: IDBVersionChangeEvent) => any): void; + unsubscribe(fn: (event: IDBVersionChangeEvent) => any): void; + fire(event: IDBVersionChangeEvent): any; +} +export interface DexiePopulateEvent { + subscribe(fn: (trans: Transaction) => any): void; + unsubscribe(fn: (trans: Transaction) => any): void; + fire(trans: Transaction): any; +} +export interface DexieCloseEvent { + subscribe(fn: (event: Event) => any): void; + unsubscribe(fn: (event: Event) => any): void; + fire(event: Event): any; +} +export interface DbEvents extends DexieEventSet { + (eventName: "ready", subscriber: (vipDb: Dexie) => any, bSticky?: boolean): void; + (eventName: "populate", subscriber: (trans: Transaction) => any): void; + (eventName: "blocked", subscriber: (event: IDBVersionChangeEvent) => any): void; + (eventName: "versionchange", subscriber: (event: IDBVersionChangeEvent) => any): void; + (eventName: "close", subscriber: (event: Event) => any): void; + ready: DexieOnReadyEvent; + populate: DexiePopulateEvent; + blocked: DexieEvent; + versionchange: DexieVersionChangeEvent; + close: DexieCloseEvent; +} +export type ObservabilitySet = { + // `idb:${dbName}/${tableName}/changedRowContents` - keys. + // `idb:${dbName}/${tableName}/changedIndexes/${indexName}` - indexes + [part: string]: IntervalTree; +}; +export interface DexieOnStorageMutatedEvent { + subscribe(fn: (parts: ObservabilitySet) => any): void; + unsubscribe(fn: (parts: ObservabilitySet) => any): void; + fire(parts: ObservabilitySet): any; +} +export interface GlobalDexieEvents extends DexieEventSet { + (eventName: "storagemutated", subscriber: (parts: ObservabilitySet) => any): void; + storagemutated: DexieOnStorageMutatedEvent; +} +export type DbSchema = { + [tableName: string]: TableSchema; +}; +export interface Middleware { + stack: TStack["stack"]; + create: (down: TStack) => Partial; + level?: number; + name?: string; +} +export interface DexieStacks { + dbcore: DBCore; +} +export interface Dexie extends Database { + readonly name: string; + readonly tables: Table[]; + readonly verno: number; + readonly vip: Dexie; + readonly _allTables: { + [name: string]: Table; + }; + readonly core: DBCore; + _createTransaction: (this: Dexie, mode: IDBTransactionMode, storeNames: ArrayLike, dbschema: DbSchema, parentTransaction?: Transaction | null) => Transaction; + _dbSchema: DbSchema; + version(versionNumber: number): Version; + on: DbEvents; + open(): PromiseExtended; + table(tableName: string): Table; + transaction(mode: TransactionMode, table: Table, scope: (trans: Transaction) => PromiseLike | U): PromiseExtended; + transaction(mode: TransactionMode, table: string, scope: (trans: Transaction) => PromiseLike | U): PromiseExtended; + transaction(mode: TransactionMode, table: Table, table2: Table, scope: (trans: Transaction) => PromiseLike | U): PromiseExtended; + transaction(mode: TransactionMode, table: string, table2: string, scope: (trans: Transaction) => PromiseLike | U): PromiseExtended; + transaction(mode: TransactionMode, table: Table, table2: Table, table3: Table, scope: (trans: Transaction) => PromiseLike | U): PromiseExtended; + transaction(mode: TransactionMode, table: string, table2: string, table3: string, scope: (trans: Transaction) => PromiseLike | U): PromiseExtended; + transaction(mode: TransactionMode, table: Table, table2: Table, table3: Table, table4: Table, scope: (trans: Transaction) => PromiseLike | U): PromiseExtended; + transaction(mode: TransactionMode, table: string, table2: string, table3: string, table4: string, scope: (trans: Transaction) => PromiseLike | U): PromiseExtended; + transaction(mode: TransactionMode, table: Table, table2: Table, table3: Table, table4: Table, table5: Table, scope: (trans: Transaction) => PromiseLike | U): PromiseExtended; + transaction(mode: TransactionMode, table: string, table2: string, table3: string, table4: string, table5: string, scope: (trans: Transaction) => PromiseLike | U): PromiseExtended; + transaction(mode: TransactionMode, tables: Table[], scope: (trans: Transaction) => PromiseLike | U): PromiseExtended; + transaction(mode: TransactionMode, tables: string[], scope: (trans: Transaction) => PromiseLike | U): PromiseExtended; + close(): void; + delete(): PromiseExtended; + isOpen(): boolean; + hasBeenClosed(): boolean; + hasFailed(): boolean; + dynamicallyOpened(): boolean; + backendDB(): IDBDatabase; + use(middleware: Middleware): this; + // Add more supported stacks here... : use(middleware: Middleware): this; + unuse({ stack, create }: Middleware<{ + stack: keyof DexieStacks; + }>): this; + unuse({ stack, name }: { + stack: keyof DexieStacks; + name: string; + }): this; + // Make it possible to touch physical class constructors where they reside - as properties on db instance. + // For example, checking if (x instanceof db.Table). Can't do (x instanceof Dexie.Table because it's just a virtual interface) + Table: { + prototype: Table; + }; + WhereClause: { + prototype: WhereClause; + }; + Version: { + prototype: Version; + }; + Transaction: { + prototype: Transaction; + }; + Collection: { + prototype: Collection; + }; +} +/** DexieError + * + * Common base class for all errors originating from Dexie.js except TypeError, + * SyntaxError and RangeError. + * + * https://dexie.org/docs/DexieErrors/DexieError + * + */ +export interface DexieError extends Error { + name: string; + message: string; + stack: string; + inner: any; + toString(): string; +} +/** + * List of the names of auto-generated error classes that extends DexieError + * and shares the interface of DexieError. + * + * Each error should be documented at https://dexie.org/docs/DexieErrors/Dexie. + * + * The generic type DexieExceptionClasses is a map of full error name to + * error constructor. The DexieExceptionClasses is mixed in into Dexie, + * so that it is always possible to throw or catch certain errors via + * Dexie.ErrorName. Example: + * + * try { + * throw new Dexie.InvalidTableError("Invalid table foo", innerError?); + * } catch (err) { + * if (err instanceof Dexie.InvalidTableError) { + * // Could also have check for err.name === "InvalidTableError", or + * // err.name === Dexie.errnames.InvalidTableError. + * console.log("Seems to be an invalid table here..."); + * } else { + * throw err; + * } + * } + */ +export type DexieErrors = { + // https://dexie.org/docs/DexieErrors/Dexie.OpenFailedError + OpenFailed: "OpenFailedError"; + // https://dexie.org/docs/DexieErrors/Dexie.VersionChangeError + VersionChange: "VersionChangeError"; + // https://dexie.org/docs/DexieErrors/Dexie.SchemaError + Schema: "SchemaError"; + // https://dexie.org/docs/DexieErrors/Dexie.UpgradeError + Upgrade: "UpgradeError"; + // https://dexie.org/docs/DexieErrors/Dexie.InvalidTableError + InvalidTable: "InvalidTableError"; + // https://dexie.org/docs/DexieErrors/Dexie.MissingAPIError + MissingAPI: "MissingAPIError"; + // https://dexie.org/docs/DexieErrors/Dexie.NoSuchDatabaseError + NoSuchDatabase: "NoSuchDatabaseError"; + // https://dexie.org/docs/DexieErrors/Dexie.InvalidArgumentError + InvalidArgument: "InvalidArgumentError"; + // https://dexie.org/docs/DexieErrors/Dexie.SubTransactionError + SubTransaction: "SubTransactionError"; + // https://dexie.org/docs/DexieErrors/Dexie.UnsupportedError + Unsupported: "UnsupportedError"; + // https://dexie.org/docs/DexieErrors/Dexie.InternalError + Internal: "InternalError"; + // https://dexie.org/docs/DexieErrors/Dexie.DatabaseClosedError + DatabaseClosed: "DatabaseClosedError"; + // https://dexie.org/docs/DexieErrors/Dexie.PrematureCommitError + PrematureCommit: "PrematureCommitError"; + // https://dexie.org/docs/DexieErrors/Dexie.ForeignAwaitError + ForeignAwait: "ForeignAwaitError"; + // https://dexie.org/docs/DexieErrors/Dexie.UnknownError + Unknown: "UnknownError"; + // https://dexie.org/docs/DexieErrors/Dexie.ConstraintError + Constraint: "ConstraintError"; + // https://dexie.org/docs/DexieErrors/Dexie.DataError + Data: "DataError"; + // https://dexie.org/docs/DexieErrors/Dexie.TransactionInactiveError + TransactionInactive: "TransactionInactiveError"; + // https://dexie.org/docs/DexieErrors/Dexie.ReadOnlyError + ReadOnly: "ReadOnlyError"; + // https://dexie.org/docs/DexieErrors/Dexie.VersionError + Version: "VersionError"; + // https://dexie.org/docs/DexieErrors/Dexie.NotFoundError + NotFound: "NotFoundError"; + // https://dexie.org/docs/DexieErrors/Dexie.InvalidStateError + InvalidState: "InvalidStateError"; + // https://dexie.org/docs/DexieErrors/Dexie.InvalidAccessError + InvalidAccess: "InvalidAccessError"; + // https://dexie.org/docs/DexieErrors/Dexie.AbortError + Abort: "AbortError"; + // https://dexie.org/docs/DexieErrors/Dexie.TimeoutError + Timeout: "TimeoutError"; + // https://dexie.org/docs/DexieErrors/Dexie.QuotaExceededError + QuotaExceeded: "QuotaExceededError"; + // https://dexie.org/docs/DexieErrors/Dexie.DataCloneError + DataClone: "DataCloneError"; +}; +/** ModifyError + * + * https://dexie.org/docs/DexieErrors/Dexie.ModifyError + */ +export interface ModifyError extends DexieError { + failures: Array; + failedKeys: IndexableTypeArrayReadonly; + successCount: number; +} +/** BulkError + * + * https://dexie.org/docs/DexieErrors/Dexie.BulkError + */ +export interface BulkError extends DexieError { + failures: Error[]; + failuresByPos: { + [operationNumber: number]: Error; + }; +} +export interface DexieErrorConstructor { + new (msg?: string, inner?: Object): DexieError; + new (inner: Object): DexieError; + prototype: DexieError; +} +export interface ModifyErrorConstructor { + new (msg?: string, failures?: any[], successCount?: number, failedKeys?: IndexableTypeArrayReadonly): ModifyError; + prototype: ModifyError; +} +export interface BulkErrorConstructor { + new (msg?: string, failures?: { + [operationNumber: number]: Error; + }): BulkError; + prototype: BulkError; +} +export type ExceptionSet = { + [P in DexieErrors[keyof DexieErrors]]: DexieErrorConstructor; +}; +export type DexieExceptionClasses = ExceptionSet & { + DexieError: DexieErrorConstructor; + ModifyError: ModifyErrorConstructor; + BulkError: BulkErrorConstructor; +}; +export interface DexieDOMDependencies { + indexedDB: IDBFactory; + IDBKeyRange: typeof IDBKeyRange; +} +declare global { + interface SymbolConstructor { + readonly observable: symbol; + } +} +export interface Subscribable { + subscribe(observer: Partial>): Unsubscribable; +} +export interface Unsubscribable { + unsubscribe(): void; +} +export interface Observable { + subscribe(observerOrNext?: Observer | ((value: T) => void)): Subscription; + subscribe(next?: ((value: T) => void) | null, error?: ((error: any) => void) | null, complete?: (() => void) | null): Subscription; + getValue?(): T; + hasValue?(): boolean; + [Symbol.observable]: () => Subscribable; +} +export interface Subscription { + unsubscribe(): void; + readonly closed: boolean; +} +export interface Observer { + start?: (subscription: Subscription) => void; + next?: (value: T) => void; + error?: (error: any) => void; + complete?: () => void; +} +export type ChromeTransactionDurability = "default" | "strict" | "relaxed"; +export interface DexieOptions { + addons?: Array<(db: Dexie) => void>; + autoOpen?: boolean; + indexedDB?: { + open: Function; + }; + IDBKeyRange?: { + bound: Function; + lowerBound: Function; + upperBound: Function; + }; + allowEmptyDB?: boolean; + modifyChunkSize?: number; + chromeTransactionDurability?: ChromeTransactionDurability; +} +export interface DexieConstructor extends DexieExceptionClasses { + new (databaseName: string, options?: DexieOptions): Dexie; + prototype: Dexie; + addons: Array<(db: Dexie) => void>; + version: number; + semVer: string; + currentTransaction: Transaction; + waitFor(promise: PromiseLike | T, timeoutMilliseconds?: number): Promise; + getDatabaseNames(): Promise; + getDatabaseNames(thenShortcut: ThenShortcut): Promise; + vip(scopeFunction: () => U): U; + ignoreTransaction(fn: () => U): U; + liveQuery(fn: () => T | Promise): Observable; + extendObservabilitySet(target: ObservabilitySet, newSet: ObservabilitySet): ObservabilitySet; + override(origFunc: F, overridedFactory: (fn: any) => any): F; // ? + getByKeyPath(obj: Object, keyPath: string): any; + setByKeyPath(obj: Object, keyPath: string, value: any): void; + delByKeyPath(obj: Object, keyPath: string): void; + shallowClone(obj: T): T; + deepClone(obj: T): T; + asap(fn: Function): void; //? + maxKey: Array> | string; + minKey: number; + exists(dbName: string): Promise; + delete(dbName: string): Promise; + dependencies: DexieDOMDependencies; + default: Dexie; // Work-around for different build tools handling default imports differently. + Promise: PromiseExtendedConstructor; + //TableSchema: {}; // Deprecate! + //IndexSpec: {new():IndexSpec}; //? Deprecate + Events: (ctx?: any) => DexieEventSet; + on: GlobalDexieEvents; + errnames: DexieErrors; +} +export declare var Dexie: DexieConstructor; +export interface _Table extends Table { +} +export interface _Collection extends Collection { +} +export declare module Dexie { + // The "Dexie.Promise" type. + type Promise = PromiseExtended; // Because many samples have been Dexie.Promise. + // The "Dexie.Table" interface. Same as named exported interface Table. + interface Table extends _Table { + } // Because all samples have been Dexie.Table<...> + // The "Dexie.Collection" interface. Same as named exported interface Collection. + interface Collection extends _Collection { + } // Because app-code may declare it. +} +export function liveQuery(querier: () => T | Promise): Observable; +export function mergeRanges(target: IntervalTree, newSet: IntervalTree): void; +export function rangesOverlap(rangeSet1: IntervalTree, rangeSet2: IntervalTree): boolean; +/** Exporting 'Dexie' as the default export. + **/ +export default Dexie; + +export {}; diff --git a/src/lib/dexie/dexie.js b/src/lib/dexie/dexie.js new file mode 100644 index 00000000..08fce7b9 --- /dev/null +++ b/src/lib/dexie/dexie.js @@ -0,0 +1,5200 @@ +/* + * Dexie.js - a minimalistic wrapper for IndexedDB + * =============================================== + * + * By David Fahlander, david.fahlander@gmail.com + * + * Version 3.2.4, Tue May 30 2023 + * + * https://dexie.org + * + * Apache License Version 2.0, January 2004, http://www.apache.org/licenses/ + */ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Dexie = factory()); +})(this, (function () { 'use strict'; + + /*! ***************************************************************************** + Copyright (c) Microsoft Corporation. + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted. + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + ***************************************************************************** */ + var __assign = function() { + __assign = Object.assign || function __assign(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); + }; + function __spreadArray(to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); + } + + var _global = typeof globalThis !== 'undefined' ? globalThis : + typeof self !== 'undefined' ? self : + typeof window !== 'undefined' ? window : + global; + + var keys = Object.keys; + var isArray = Array.isArray; + if (typeof Promise !== 'undefined' && !_global.Promise) { + _global.Promise = Promise; + } + function extend(obj, extension) { + if (typeof extension !== 'object') + return obj; + keys(extension).forEach(function (key) { + obj[key] = extension[key]; + }); + return obj; + } + var getProto = Object.getPrototypeOf; + var _hasOwn = {}.hasOwnProperty; + function hasOwn(obj, prop) { + return _hasOwn.call(obj, prop); + } + function props(proto, extension) { + if (typeof extension === 'function') + extension = extension(getProto(proto)); + (typeof Reflect === "undefined" ? keys : Reflect.ownKeys)(extension).forEach(function (key) { + setProp(proto, key, extension[key]); + }); + } + var defineProperty = Object.defineProperty; + function setProp(obj, prop, functionOrGetSet, options) { + defineProperty(obj, prop, extend(functionOrGetSet && hasOwn(functionOrGetSet, "get") && typeof functionOrGetSet.get === 'function' ? + { get: functionOrGetSet.get, set: functionOrGetSet.set, configurable: true } : + { value: functionOrGetSet, configurable: true, writable: true }, options)); + } + function derive(Child) { + return { + from: function (Parent) { + Child.prototype = Object.create(Parent.prototype); + setProp(Child.prototype, "constructor", Child); + return { + extend: props.bind(null, Child.prototype) + }; + } + }; + } + var getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; + function getPropertyDescriptor(obj, prop) { + var pd = getOwnPropertyDescriptor(obj, prop); + var proto; + return pd || (proto = getProto(obj)) && getPropertyDescriptor(proto, prop); + } + var _slice = [].slice; + function slice(args, start, end) { + return _slice.call(args, start, end); + } + function override(origFunc, overridedFactory) { + return overridedFactory(origFunc); + } + function assert(b) { + if (!b) + throw new Error("Assertion Failed"); + } + function asap$1(fn) { + if (_global.setImmediate) + setImmediate(fn); + else + setTimeout(fn, 0); + } + function arrayToObject(array, extractor) { + return array.reduce(function (result, item, i) { + var nameAndValue = extractor(item, i); + if (nameAndValue) + result[nameAndValue[0]] = nameAndValue[1]; + return result; + }, {}); + } + function tryCatch(fn, onerror, args) { + try { + fn.apply(null, args); + } + catch (ex) { + onerror && onerror(ex); + } + } + function getByKeyPath(obj, keyPath) { + if (hasOwn(obj, keyPath)) + return obj[keyPath]; + if (!keyPath) + return obj; + if (typeof keyPath !== 'string') { + var rv = []; + for (var i = 0, l = keyPath.length; i < l; ++i) { + var val = getByKeyPath(obj, keyPath[i]); + rv.push(val); + } + return rv; + } + var period = keyPath.indexOf('.'); + if (period !== -1) { + var innerObj = obj[keyPath.substr(0, period)]; + return innerObj === undefined ? undefined : getByKeyPath(innerObj, keyPath.substr(period + 1)); + } + return undefined; + } + function setByKeyPath(obj, keyPath, value) { + if (!obj || keyPath === undefined) + return; + if ('isFrozen' in Object && Object.isFrozen(obj)) + return; + if (typeof keyPath !== 'string' && 'length' in keyPath) { + assert(typeof value !== 'string' && 'length' in value); + for (var i = 0, l = keyPath.length; i < l; ++i) { + setByKeyPath(obj, keyPath[i], value[i]); + } + } + else { + var period = keyPath.indexOf('.'); + if (period !== -1) { + var currentKeyPath = keyPath.substr(0, period); + var remainingKeyPath = keyPath.substr(period + 1); + if (remainingKeyPath === "") + if (value === undefined) { + if (isArray(obj) && !isNaN(parseInt(currentKeyPath))) + obj.splice(currentKeyPath, 1); + else + delete obj[currentKeyPath]; + } + else + obj[currentKeyPath] = value; + else { + var innerObj = obj[currentKeyPath]; + if (!innerObj || !hasOwn(obj, currentKeyPath)) + innerObj = (obj[currentKeyPath] = {}); + setByKeyPath(innerObj, remainingKeyPath, value); + } + } + else { + if (value === undefined) { + if (isArray(obj) && !isNaN(parseInt(keyPath))) + obj.splice(keyPath, 1); + else + delete obj[keyPath]; + } + else + obj[keyPath] = value; + } + } + } + function delByKeyPath(obj, keyPath) { + if (typeof keyPath === 'string') + setByKeyPath(obj, keyPath, undefined); + else if ('length' in keyPath) + [].map.call(keyPath, function (kp) { + setByKeyPath(obj, kp, undefined); + }); + } + function shallowClone(obj) { + var rv = {}; + for (var m in obj) { + if (hasOwn(obj, m)) + rv[m] = obj[m]; + } + return rv; + } + var concat = [].concat; + function flatten(a) { + return concat.apply([], a); + } + var intrinsicTypeNames = "Boolean,String,Date,RegExp,Blob,File,FileList,FileSystemFileHandle,ArrayBuffer,DataView,Uint8ClampedArray,ImageBitmap,ImageData,Map,Set,CryptoKey" + .split(',').concat(flatten([8, 16, 32, 64].map(function (num) { return ["Int", "Uint", "Float"].map(function (t) { return t + num + "Array"; }); }))).filter(function (t) { return _global[t]; }); + var intrinsicTypes = intrinsicTypeNames.map(function (t) { return _global[t]; }); + arrayToObject(intrinsicTypeNames, function (x) { return [x, true]; }); + var circularRefs = null; + function deepClone(any) { + circularRefs = typeof WeakMap !== 'undefined' && new WeakMap(); + var rv = innerDeepClone(any); + circularRefs = null; + return rv; + } + function innerDeepClone(any) { + if (!any || typeof any !== 'object') + return any; + var rv = circularRefs && circularRefs.get(any); + if (rv) + return rv; + if (isArray(any)) { + rv = []; + circularRefs && circularRefs.set(any, rv); + for (var i = 0, l = any.length; i < l; ++i) { + rv.push(innerDeepClone(any[i])); + } + } + else if (intrinsicTypes.indexOf(any.constructor) >= 0) { + rv = any; + } + else { + var proto = getProto(any); + rv = proto === Object.prototype ? {} : Object.create(proto); + circularRefs && circularRefs.set(any, rv); + for (var prop in any) { + if (hasOwn(any, prop)) { + rv[prop] = innerDeepClone(any[prop]); + } + } + } + return rv; + } + var toString = {}.toString; + function toStringTag(o) { + return toString.call(o).slice(8, -1); + } + var iteratorSymbol = typeof Symbol !== 'undefined' ? + Symbol.iterator : + '@@iterator'; + var getIteratorOf = typeof iteratorSymbol === "symbol" ? function (x) { + var i; + return x != null && (i = x[iteratorSymbol]) && i.apply(x); + } : function () { return null; }; + var NO_CHAR_ARRAY = {}; + function getArrayOf(arrayLike) { + var i, a, x, it; + if (arguments.length === 1) { + if (isArray(arrayLike)) + return arrayLike.slice(); + if (this === NO_CHAR_ARRAY && typeof arrayLike === 'string') + return [arrayLike]; + if ((it = getIteratorOf(arrayLike))) { + a = []; + while ((x = it.next()), !x.done) + a.push(x.value); + return a; + } + if (arrayLike == null) + return [arrayLike]; + i = arrayLike.length; + if (typeof i === 'number') { + a = new Array(i); + while (i--) + a[i] = arrayLike[i]; + return a; + } + return [arrayLike]; + } + i = arguments.length; + a = new Array(i); + while (i--) + a[i] = arguments[i]; + return a; + } + var isAsyncFunction = typeof Symbol !== 'undefined' + ? function (fn) { return fn[Symbol.toStringTag] === 'AsyncFunction'; } + : function () { return false; }; + + var debug = typeof location !== 'undefined' && + /^(http|https):\/\/(localhost|127\.0\.0\.1)/.test(location.href); + function setDebug(value, filter) { + debug = value; + libraryFilter = filter; + } + var libraryFilter = function () { return true; }; + var NEEDS_THROW_FOR_STACK = !new Error("").stack; + function getErrorWithStack() { + if (NEEDS_THROW_FOR_STACK) + try { + getErrorWithStack.arguments; + throw new Error(); + } + catch (e) { + return e; + } + return new Error(); + } + function prettyStack(exception, numIgnoredFrames) { + var stack = exception.stack; + if (!stack) + return ""; + numIgnoredFrames = (numIgnoredFrames || 0); + if (stack.indexOf(exception.name) === 0) + numIgnoredFrames += (exception.name + exception.message).split('\n').length; + return stack.split('\n') + .slice(numIgnoredFrames) + .filter(libraryFilter) + .map(function (frame) { return "\n" + frame; }) + .join(''); + } + + var dexieErrorNames = [ + 'Modify', + 'Bulk', + 'OpenFailed', + 'VersionChange', + 'Schema', + 'Upgrade', + 'InvalidTable', + 'MissingAPI', + 'NoSuchDatabase', + 'InvalidArgument', + 'SubTransaction', + 'Unsupported', + 'Internal', + 'DatabaseClosed', + 'PrematureCommit', + 'ForeignAwait' + ]; + var idbDomErrorNames = [ + 'Unknown', + 'Constraint', + 'Data', + 'TransactionInactive', + 'ReadOnly', + 'Version', + 'NotFound', + 'InvalidState', + 'InvalidAccess', + 'Abort', + 'Timeout', + 'QuotaExceeded', + 'Syntax', + 'DataClone' + ]; + var errorList = dexieErrorNames.concat(idbDomErrorNames); + var defaultTexts = { + VersionChanged: "Database version changed by other database connection", + DatabaseClosed: "Database has been closed", + Abort: "Transaction aborted", + TransactionInactive: "Transaction has already completed or failed", + MissingAPI: "IndexedDB API missing. Please visit https://tinyurl.com/y2uuvskb" + }; + function DexieError(name, msg) { + this._e = getErrorWithStack(); + this.name = name; + this.message = msg; + } + derive(DexieError).from(Error).extend({ + stack: { + get: function () { + return this._stack || + (this._stack = this.name + ": " + this.message + prettyStack(this._e, 2)); + } + }, + toString: function () { return this.name + ": " + this.message; } + }); + function getMultiErrorMessage(msg, failures) { + return msg + ". Errors: " + Object.keys(failures) + .map(function (key) { return failures[key].toString(); }) + .filter(function (v, i, s) { return s.indexOf(v) === i; }) + .join('\n'); + } + function ModifyError(msg, failures, successCount, failedKeys) { + this._e = getErrorWithStack(); + this.failures = failures; + this.failedKeys = failedKeys; + this.successCount = successCount; + this.message = getMultiErrorMessage(msg, failures); + } + derive(ModifyError).from(DexieError); + function BulkError(msg, failures) { + this._e = getErrorWithStack(); + this.name = "BulkError"; + this.failures = Object.keys(failures).map(function (pos) { return failures[pos]; }); + this.failuresByPos = failures; + this.message = getMultiErrorMessage(msg, failures); + } + derive(BulkError).from(DexieError); + var errnames = errorList.reduce(function (obj, name) { return (obj[name] = name + "Error", obj); }, {}); + var BaseException = DexieError; + var exceptions = errorList.reduce(function (obj, name) { + var fullName = name + "Error"; + function DexieError(msgOrInner, inner) { + this._e = getErrorWithStack(); + this.name = fullName; + if (!msgOrInner) { + this.message = defaultTexts[name] || fullName; + this.inner = null; + } + else if (typeof msgOrInner === 'string') { + this.message = "" + msgOrInner + (!inner ? '' : '\n ' + inner); + this.inner = inner || null; + } + else if (typeof msgOrInner === 'object') { + this.message = msgOrInner.name + " " + msgOrInner.message; + this.inner = msgOrInner; + } + } + derive(DexieError).from(BaseException); + obj[name] = DexieError; + return obj; + }, {}); + exceptions.Syntax = SyntaxError; + exceptions.Type = TypeError; + exceptions.Range = RangeError; + var exceptionMap = idbDomErrorNames.reduce(function (obj, name) { + obj[name + "Error"] = exceptions[name]; + return obj; + }, {}); + function mapError(domError, message) { + if (!domError || domError instanceof DexieError || domError instanceof TypeError || domError instanceof SyntaxError || !domError.name || !exceptionMap[domError.name]) + return domError; + var rv = new exceptionMap[domError.name](message || domError.message, domError); + if ("stack" in domError) { + setProp(rv, "stack", { get: function () { + return this.inner.stack; + } }); + } + return rv; + } + var fullNameExceptions = errorList.reduce(function (obj, name) { + if (["Syntax", "Type", "Range"].indexOf(name) === -1) + obj[name + "Error"] = exceptions[name]; + return obj; + }, {}); + fullNameExceptions.ModifyError = ModifyError; + fullNameExceptions.DexieError = DexieError; + fullNameExceptions.BulkError = BulkError; + + function nop() { } + function mirror(val) { return val; } + function pureFunctionChain(f1, f2) { + if (f1 == null || f1 === mirror) + return f2; + return function (val) { + return f2(f1(val)); + }; + } + function callBoth(on1, on2) { + return function () { + on1.apply(this, arguments); + on2.apply(this, arguments); + }; + } + function hookCreatingChain(f1, f2) { + if (f1 === nop) + return f2; + return function () { + var res = f1.apply(this, arguments); + if (res !== undefined) + arguments[0] = res; + var onsuccess = this.onsuccess, + onerror = this.onerror; + this.onsuccess = null; + this.onerror = null; + var res2 = f2.apply(this, arguments); + if (onsuccess) + this.onsuccess = this.onsuccess ? callBoth(onsuccess, this.onsuccess) : onsuccess; + if (onerror) + this.onerror = this.onerror ? callBoth(onerror, this.onerror) : onerror; + return res2 !== undefined ? res2 : res; + }; + } + function hookDeletingChain(f1, f2) { + if (f1 === nop) + return f2; + return function () { + f1.apply(this, arguments); + var onsuccess = this.onsuccess, + onerror = this.onerror; + this.onsuccess = this.onerror = null; + f2.apply(this, arguments); + if (onsuccess) + this.onsuccess = this.onsuccess ? callBoth(onsuccess, this.onsuccess) : onsuccess; + if (onerror) + this.onerror = this.onerror ? callBoth(onerror, this.onerror) : onerror; + }; + } + function hookUpdatingChain(f1, f2) { + if (f1 === nop) + return f2; + return function (modifications) { + var res = f1.apply(this, arguments); + extend(modifications, res); + var onsuccess = this.onsuccess, + onerror = this.onerror; + this.onsuccess = null; + this.onerror = null; + var res2 = f2.apply(this, arguments); + if (onsuccess) + this.onsuccess = this.onsuccess ? callBoth(onsuccess, this.onsuccess) : onsuccess; + if (onerror) + this.onerror = this.onerror ? callBoth(onerror, this.onerror) : onerror; + return res === undefined ? + (res2 === undefined ? undefined : res2) : + (extend(res, res2)); + }; + } + function reverseStoppableEventChain(f1, f2) { + if (f1 === nop) + return f2; + return function () { + if (f2.apply(this, arguments) === false) + return false; + return f1.apply(this, arguments); + }; + } + function promisableChain(f1, f2) { + if (f1 === nop) + return f2; + return function () { + var res = f1.apply(this, arguments); + if (res && typeof res.then === 'function') { + var thiz = this, i = arguments.length, args = new Array(i); + while (i--) + args[i] = arguments[i]; + return res.then(function () { + return f2.apply(thiz, args); + }); + } + return f2.apply(this, arguments); + }; + } + + var INTERNAL = {}; + var LONG_STACKS_CLIP_LIMIT = 100, + MAX_LONG_STACKS = 20, ZONE_ECHO_LIMIT = 100, _a$1 = typeof Promise === 'undefined' ? + [] : + (function () { + var globalP = Promise.resolve(); + if (typeof crypto === 'undefined' || !crypto.subtle) + return [globalP, getProto(globalP), globalP]; + var nativeP = crypto.subtle.digest("SHA-512", new Uint8Array([0])); + return [ + nativeP, + getProto(nativeP), + globalP + ]; + })(), resolvedNativePromise = _a$1[0], nativePromiseProto = _a$1[1], resolvedGlobalPromise = _a$1[2], nativePromiseThen = nativePromiseProto && nativePromiseProto.then; + var NativePromise = resolvedNativePromise && resolvedNativePromise.constructor; + var patchGlobalPromise = !!resolvedGlobalPromise; + var stack_being_generated = false; + var schedulePhysicalTick = resolvedGlobalPromise ? + function () { resolvedGlobalPromise.then(physicalTick); } + : + _global.setImmediate ? + setImmediate.bind(null, physicalTick) : + _global.MutationObserver ? + function () { + var hiddenDiv = document.createElement("div"); + (new MutationObserver(function () { + physicalTick(); + hiddenDiv = null; + })).observe(hiddenDiv, { attributes: true }); + hiddenDiv.setAttribute('i', '1'); + } : + function () { setTimeout(physicalTick, 0); }; + var asap = function (callback, args) { + microtickQueue.push([callback, args]); + if (needsNewPhysicalTick) { + schedulePhysicalTick(); + needsNewPhysicalTick = false; + } + }; + var isOutsideMicroTick = true, + needsNewPhysicalTick = true, + unhandledErrors = [], + rejectingErrors = [], + currentFulfiller = null, rejectionMapper = mirror; + var globalPSD = { + id: 'global', + global: true, + ref: 0, + unhandleds: [], + onunhandled: globalError, + pgp: false, + env: {}, + finalize: function () { + this.unhandleds.forEach(function (uh) { + try { + globalError(uh[0], uh[1]); + } + catch (e) { } + }); + } + }; + var PSD = globalPSD; + var microtickQueue = []; + var numScheduledCalls = 0; + var tickFinalizers = []; + function DexiePromise(fn) { + if (typeof this !== 'object') + throw new TypeError('Promises must be constructed via new'); + this._listeners = []; + this.onuncatched = nop; + this._lib = false; + var psd = (this._PSD = PSD); + if (debug) { + this._stackHolder = getErrorWithStack(); + this._prev = null; + this._numPrev = 0; + } + if (typeof fn !== 'function') { + if (fn !== INTERNAL) + throw new TypeError('Not a function'); + this._state = arguments[1]; + this._value = arguments[2]; + if (this._state === false) + handleRejection(this, this._value); + return; + } + this._state = null; + this._value = null; + ++psd.ref; + executePromiseTask(this, fn); + } + var thenProp = { + get: function () { + var psd = PSD, microTaskId = totalEchoes; + function then(onFulfilled, onRejected) { + var _this = this; + var possibleAwait = !psd.global && (psd !== PSD || microTaskId !== totalEchoes); + var cleanup = possibleAwait && !decrementExpectedAwaits(); + var rv = new DexiePromise(function (resolve, reject) { + propagateToListener(_this, new Listener(nativeAwaitCompatibleWrap(onFulfilled, psd, possibleAwait, cleanup), nativeAwaitCompatibleWrap(onRejected, psd, possibleAwait, cleanup), resolve, reject, psd)); + }); + debug && linkToPreviousPromise(rv, this); + return rv; + } + then.prototype = INTERNAL; + return then; + }, + set: function (value) { + setProp(this, 'then', value && value.prototype === INTERNAL ? + thenProp : + { + get: function () { + return value; + }, + set: thenProp.set + }); + } + }; + props(DexiePromise.prototype, { + then: thenProp, + _then: function (onFulfilled, onRejected) { + propagateToListener(this, new Listener(null, null, onFulfilled, onRejected, PSD)); + }, + catch: function (onRejected) { + if (arguments.length === 1) + return this.then(null, onRejected); + var type = arguments[0], handler = arguments[1]; + return typeof type === 'function' ? this.then(null, function (err) { + return err instanceof type ? handler(err) : PromiseReject(err); + }) + : this.then(null, function (err) { + return err && err.name === type ? handler(err) : PromiseReject(err); + }); + }, + finally: function (onFinally) { + return this.then(function (value) { + onFinally(); + return value; + }, function (err) { + onFinally(); + return PromiseReject(err); + }); + }, + stack: { + get: function () { + if (this._stack) + return this._stack; + try { + stack_being_generated = true; + var stacks = getStack(this, [], MAX_LONG_STACKS); + var stack = stacks.join("\nFrom previous: "); + if (this._state !== null) + this._stack = stack; + return stack; + } + finally { + stack_being_generated = false; + } + } + }, + timeout: function (ms, msg) { + var _this = this; + return ms < Infinity ? + new DexiePromise(function (resolve, reject) { + var handle = setTimeout(function () { return reject(new exceptions.Timeout(msg)); }, ms); + _this.then(resolve, reject).finally(clearTimeout.bind(null, handle)); + }) : this; + } + }); + if (typeof Symbol !== 'undefined' && Symbol.toStringTag) + setProp(DexiePromise.prototype, Symbol.toStringTag, 'Dexie.Promise'); + globalPSD.env = snapShot(); + function Listener(onFulfilled, onRejected, resolve, reject, zone) { + this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null; + this.onRejected = typeof onRejected === 'function' ? onRejected : null; + this.resolve = resolve; + this.reject = reject; + this.psd = zone; + } + props(DexiePromise, { + all: function () { + var values = getArrayOf.apply(null, arguments) + .map(onPossibleParallellAsync); + return new DexiePromise(function (resolve, reject) { + if (values.length === 0) + resolve([]); + var remaining = values.length; + values.forEach(function (a, i) { return DexiePromise.resolve(a).then(function (x) { + values[i] = x; + if (!--remaining) + resolve(values); + }, reject); }); + }); + }, + resolve: function (value) { + if (value instanceof DexiePromise) + return value; + if (value && typeof value.then === 'function') + return new DexiePromise(function (resolve, reject) { + value.then(resolve, reject); + }); + var rv = new DexiePromise(INTERNAL, true, value); + linkToPreviousPromise(rv, currentFulfiller); + return rv; + }, + reject: PromiseReject, + race: function () { + var values = getArrayOf.apply(null, arguments).map(onPossibleParallellAsync); + return new DexiePromise(function (resolve, reject) { + values.map(function (value) { return DexiePromise.resolve(value).then(resolve, reject); }); + }); + }, + PSD: { + get: function () { return PSD; }, + set: function (value) { return PSD = value; } + }, + totalEchoes: { get: function () { return totalEchoes; } }, + newPSD: newScope, + usePSD: usePSD, + scheduler: { + get: function () { return asap; }, + set: function (value) { asap = value; } + }, + rejectionMapper: { + get: function () { return rejectionMapper; }, + set: function (value) { rejectionMapper = value; } + }, + follow: function (fn, zoneProps) { + return new DexiePromise(function (resolve, reject) { + return newScope(function (resolve, reject) { + var psd = PSD; + psd.unhandleds = []; + psd.onunhandled = reject; + psd.finalize = callBoth(function () { + var _this = this; + run_at_end_of_this_or_next_physical_tick(function () { + _this.unhandleds.length === 0 ? resolve() : reject(_this.unhandleds[0]); + }); + }, psd.finalize); + fn(); + }, zoneProps, resolve, reject); + }); + } + }); + if (NativePromise) { + if (NativePromise.allSettled) + setProp(DexiePromise, "allSettled", function () { + var possiblePromises = getArrayOf.apply(null, arguments).map(onPossibleParallellAsync); + return new DexiePromise(function (resolve) { + if (possiblePromises.length === 0) + resolve([]); + var remaining = possiblePromises.length; + var results = new Array(remaining); + possiblePromises.forEach(function (p, i) { return DexiePromise.resolve(p).then(function (value) { return results[i] = { status: "fulfilled", value: value }; }, function (reason) { return results[i] = { status: "rejected", reason: reason }; }) + .then(function () { return --remaining || resolve(results); }); }); + }); + }); + if (NativePromise.any && typeof AggregateError !== 'undefined') + setProp(DexiePromise, "any", function () { + var possiblePromises = getArrayOf.apply(null, arguments).map(onPossibleParallellAsync); + return new DexiePromise(function (resolve, reject) { + if (possiblePromises.length === 0) + reject(new AggregateError([])); + var remaining = possiblePromises.length; + var failures = new Array(remaining); + possiblePromises.forEach(function (p, i) { return DexiePromise.resolve(p).then(function (value) { return resolve(value); }, function (failure) { + failures[i] = failure; + if (!--remaining) + reject(new AggregateError(failures)); + }); }); + }); + }); + } + function executePromiseTask(promise, fn) { + try { + fn(function (value) { + if (promise._state !== null) + return; + if (value === promise) + throw new TypeError('A promise cannot be resolved with itself.'); + var shouldExecuteTick = promise._lib && beginMicroTickScope(); + if (value && typeof value.then === 'function') { + executePromiseTask(promise, function (resolve, reject) { + value instanceof DexiePromise ? + value._then(resolve, reject) : + value.then(resolve, reject); + }); + } + else { + promise._state = true; + promise._value = value; + propagateAllListeners(promise); + } + if (shouldExecuteTick) + endMicroTickScope(); + }, handleRejection.bind(null, promise)); + } + catch (ex) { + handleRejection(promise, ex); + } + } + function handleRejection(promise, reason) { + rejectingErrors.push(reason); + if (promise._state !== null) + return; + var shouldExecuteTick = promise._lib && beginMicroTickScope(); + reason = rejectionMapper(reason); + promise._state = false; + promise._value = reason; + debug && reason !== null && typeof reason === 'object' && !reason._promise && tryCatch(function () { + var origProp = getPropertyDescriptor(reason, "stack"); + reason._promise = promise; + setProp(reason, "stack", { + get: function () { + return stack_being_generated ? + origProp && (origProp.get ? + origProp.get.apply(reason) : + origProp.value) : + promise.stack; + } + }); + }); + addPossiblyUnhandledError(promise); + propagateAllListeners(promise); + if (shouldExecuteTick) + endMicroTickScope(); + } + function propagateAllListeners(promise) { + var listeners = promise._listeners; + promise._listeners = []; + for (var i = 0, len = listeners.length; i < len; ++i) { + propagateToListener(promise, listeners[i]); + } + var psd = promise._PSD; + --psd.ref || psd.finalize(); + if (numScheduledCalls === 0) { + ++numScheduledCalls; + asap(function () { + if (--numScheduledCalls === 0) + finalizePhysicalTick(); + }, []); + } + } + function propagateToListener(promise, listener) { + if (promise._state === null) { + promise._listeners.push(listener); + return; + } + var cb = promise._state ? listener.onFulfilled : listener.onRejected; + if (cb === null) { + return (promise._state ? listener.resolve : listener.reject)(promise._value); + } + ++listener.psd.ref; + ++numScheduledCalls; + asap(callListener, [cb, promise, listener]); + } + function callListener(cb, promise, listener) { + try { + currentFulfiller = promise; + var ret, value = promise._value; + if (promise._state) { + ret = cb(value); + } + else { + if (rejectingErrors.length) + rejectingErrors = []; + ret = cb(value); + if (rejectingErrors.indexOf(value) === -1) + markErrorAsHandled(promise); + } + listener.resolve(ret); + } + catch (e) { + listener.reject(e); + } + finally { + currentFulfiller = null; + if (--numScheduledCalls === 0) + finalizePhysicalTick(); + --listener.psd.ref || listener.psd.finalize(); + } + } + function getStack(promise, stacks, limit) { + if (stacks.length === limit) + return stacks; + var stack = ""; + if (promise._state === false) { + var failure = promise._value, errorName, message; + if (failure != null) { + errorName = failure.name || "Error"; + message = failure.message || failure; + stack = prettyStack(failure, 0); + } + else { + errorName = failure; + message = ""; + } + stacks.push(errorName + (message ? ": " + message : "") + stack); + } + if (debug) { + stack = prettyStack(promise._stackHolder, 2); + if (stack && stacks.indexOf(stack) === -1) + stacks.push(stack); + if (promise._prev) + getStack(promise._prev, stacks, limit); + } + return stacks; + } + function linkToPreviousPromise(promise, prev) { + var numPrev = prev ? prev._numPrev + 1 : 0; + if (numPrev < LONG_STACKS_CLIP_LIMIT) { + promise._prev = prev; + promise._numPrev = numPrev; + } + } + function physicalTick() { + beginMicroTickScope() && endMicroTickScope(); + } + function beginMicroTickScope() { + var wasRootExec = isOutsideMicroTick; + isOutsideMicroTick = false; + needsNewPhysicalTick = false; + return wasRootExec; + } + function endMicroTickScope() { + var callbacks, i, l; + do { + while (microtickQueue.length > 0) { + callbacks = microtickQueue; + microtickQueue = []; + l = callbacks.length; + for (i = 0; i < l; ++i) { + var item = callbacks[i]; + item[0].apply(null, item[1]); + } + } + } while (microtickQueue.length > 0); + isOutsideMicroTick = true; + needsNewPhysicalTick = true; + } + function finalizePhysicalTick() { + var unhandledErrs = unhandledErrors; + unhandledErrors = []; + unhandledErrs.forEach(function (p) { + p._PSD.onunhandled.call(null, p._value, p); + }); + var finalizers = tickFinalizers.slice(0); + var i = finalizers.length; + while (i) + finalizers[--i](); + } + function run_at_end_of_this_or_next_physical_tick(fn) { + function finalizer() { + fn(); + tickFinalizers.splice(tickFinalizers.indexOf(finalizer), 1); + } + tickFinalizers.push(finalizer); + ++numScheduledCalls; + asap(function () { + if (--numScheduledCalls === 0) + finalizePhysicalTick(); + }, []); + } + function addPossiblyUnhandledError(promise) { + if (!unhandledErrors.some(function (p) { return p._value === promise._value; })) + unhandledErrors.push(promise); + } + function markErrorAsHandled(promise) { + var i = unhandledErrors.length; + while (i) + if (unhandledErrors[--i]._value === promise._value) { + unhandledErrors.splice(i, 1); + return; + } + } + function PromiseReject(reason) { + return new DexiePromise(INTERNAL, false, reason); + } + function wrap(fn, errorCatcher) { + var psd = PSD; + return function () { + var wasRootExec = beginMicroTickScope(), outerScope = PSD; + try { + switchToZone(psd, true); + return fn.apply(this, arguments); + } + catch (e) { + errorCatcher && errorCatcher(e); + } + finally { + switchToZone(outerScope, false); + if (wasRootExec) + endMicroTickScope(); + } + }; + } + var task = { awaits: 0, echoes: 0, id: 0 }; + var taskCounter = 0; + var zoneStack = []; + var zoneEchoes = 0; + var totalEchoes = 0; + var zone_id_counter = 0; + function newScope(fn, props, a1, a2) { + var parent = PSD, psd = Object.create(parent); + psd.parent = parent; + psd.ref = 0; + psd.global = false; + psd.id = ++zone_id_counter; + var globalEnv = globalPSD.env; + psd.env = patchGlobalPromise ? { + Promise: DexiePromise, + PromiseProp: { value: DexiePromise, configurable: true, writable: true }, + all: DexiePromise.all, + race: DexiePromise.race, + allSettled: DexiePromise.allSettled, + any: DexiePromise.any, + resolve: DexiePromise.resolve, + reject: DexiePromise.reject, + nthen: getPatchedPromiseThen(globalEnv.nthen, psd), + gthen: getPatchedPromiseThen(globalEnv.gthen, psd) + } : {}; + if (props) + extend(psd, props); + ++parent.ref; + psd.finalize = function () { + --this.parent.ref || this.parent.finalize(); + }; + var rv = usePSD(psd, fn, a1, a2); + if (psd.ref === 0) + psd.finalize(); + return rv; + } + function incrementExpectedAwaits() { + if (!task.id) + task.id = ++taskCounter; + ++task.awaits; + task.echoes += ZONE_ECHO_LIMIT; + return task.id; + } + function decrementExpectedAwaits() { + if (!task.awaits) + return false; + if (--task.awaits === 0) + task.id = 0; + task.echoes = task.awaits * ZONE_ECHO_LIMIT; + return true; + } + if (('' + nativePromiseThen).indexOf('[native code]') === -1) { + incrementExpectedAwaits = decrementExpectedAwaits = nop; + } + function onPossibleParallellAsync(possiblePromise) { + if (task.echoes && possiblePromise && possiblePromise.constructor === NativePromise) { + incrementExpectedAwaits(); + return possiblePromise.then(function (x) { + decrementExpectedAwaits(); + return x; + }, function (e) { + decrementExpectedAwaits(); + return rejection(e); + }); + } + return possiblePromise; + } + function zoneEnterEcho(targetZone) { + ++totalEchoes; + if (!task.echoes || --task.echoes === 0) { + task.echoes = task.id = 0; + } + zoneStack.push(PSD); + switchToZone(targetZone, true); + } + function zoneLeaveEcho() { + var zone = zoneStack[zoneStack.length - 1]; + zoneStack.pop(); + switchToZone(zone, false); + } + function switchToZone(targetZone, bEnteringZone) { + var currentZone = PSD; + if (bEnteringZone ? task.echoes && (!zoneEchoes++ || targetZone !== PSD) : zoneEchoes && (!--zoneEchoes || targetZone !== PSD)) { + enqueueNativeMicroTask(bEnteringZone ? zoneEnterEcho.bind(null, targetZone) : zoneLeaveEcho); + } + if (targetZone === PSD) + return; + PSD = targetZone; + if (currentZone === globalPSD) + globalPSD.env = snapShot(); + if (patchGlobalPromise) { + var GlobalPromise_1 = globalPSD.env.Promise; + var targetEnv = targetZone.env; + nativePromiseProto.then = targetEnv.nthen; + GlobalPromise_1.prototype.then = targetEnv.gthen; + if (currentZone.global || targetZone.global) { + Object.defineProperty(_global, 'Promise', targetEnv.PromiseProp); + GlobalPromise_1.all = targetEnv.all; + GlobalPromise_1.race = targetEnv.race; + GlobalPromise_1.resolve = targetEnv.resolve; + GlobalPromise_1.reject = targetEnv.reject; + if (targetEnv.allSettled) + GlobalPromise_1.allSettled = targetEnv.allSettled; + if (targetEnv.any) + GlobalPromise_1.any = targetEnv.any; + } + } + } + function snapShot() { + var GlobalPromise = _global.Promise; + return patchGlobalPromise ? { + Promise: GlobalPromise, + PromiseProp: Object.getOwnPropertyDescriptor(_global, "Promise"), + all: GlobalPromise.all, + race: GlobalPromise.race, + allSettled: GlobalPromise.allSettled, + any: GlobalPromise.any, + resolve: GlobalPromise.resolve, + reject: GlobalPromise.reject, + nthen: nativePromiseProto.then, + gthen: GlobalPromise.prototype.then + } : {}; + } + function usePSD(psd, fn, a1, a2, a3) { + var outerScope = PSD; + try { + switchToZone(psd, true); + return fn(a1, a2, a3); + } + finally { + switchToZone(outerScope, false); + } + } + function enqueueNativeMicroTask(job) { + nativePromiseThen.call(resolvedNativePromise, job); + } + function nativeAwaitCompatibleWrap(fn, zone, possibleAwait, cleanup) { + return typeof fn !== 'function' ? fn : function () { + var outerZone = PSD; + if (possibleAwait) + incrementExpectedAwaits(); + switchToZone(zone, true); + try { + return fn.apply(this, arguments); + } + finally { + switchToZone(outerZone, false); + if (cleanup) + enqueueNativeMicroTask(decrementExpectedAwaits); + } + }; + } + function getPatchedPromiseThen(origThen, zone) { + return function (onResolved, onRejected) { + return origThen.call(this, nativeAwaitCompatibleWrap(onResolved, zone), nativeAwaitCompatibleWrap(onRejected, zone)); + }; + } + var UNHANDLEDREJECTION = "unhandledrejection"; + function globalError(err, promise) { + var rv; + try { + rv = promise.onuncatched(err); + } + catch (e) { } + if (rv !== false) + try { + var event, eventData = { promise: promise, reason: err }; + if (_global.document && document.createEvent) { + event = document.createEvent('Event'); + event.initEvent(UNHANDLEDREJECTION, true, true); + extend(event, eventData); + } + else if (_global.CustomEvent) { + event = new CustomEvent(UNHANDLEDREJECTION, { detail: eventData }); + extend(event, eventData); + } + if (event && _global.dispatchEvent) { + dispatchEvent(event); + if (!_global.PromiseRejectionEvent && _global.onunhandledrejection) + try { + _global.onunhandledrejection(event); + } + catch (_) { } + } + if (debug && event && !event.defaultPrevented) { + console.warn("Unhandled rejection: " + (err.stack || err)); + } + } + catch (e) { } + } + var rejection = DexiePromise.reject; + + function tempTransaction(db, mode, storeNames, fn) { + if (!db.idbdb || (!db._state.openComplete && (!PSD.letThrough && !db._vip))) { + if (db._state.openComplete) { + return rejection(new exceptions.DatabaseClosed(db._state.dbOpenError)); + } + if (!db._state.isBeingOpened) { + if (!db._options.autoOpen) + return rejection(new exceptions.DatabaseClosed()); + db.open().catch(nop); + } + return db._state.dbReadyPromise.then(function () { return tempTransaction(db, mode, storeNames, fn); }); + } + else { + var trans = db._createTransaction(mode, storeNames, db._dbSchema); + try { + trans.create(); + db._state.PR1398_maxLoop = 3; + } + catch (ex) { + if (ex.name === errnames.InvalidState && db.isOpen() && --db._state.PR1398_maxLoop > 0) { + console.warn('Dexie: Need to reopen db'); + db._close(); + return db.open().then(function () { return tempTransaction(db, mode, storeNames, fn); }); + } + return rejection(ex); + } + return trans._promise(mode, function (resolve, reject) { + return newScope(function () { + PSD.trans = trans; + return fn(resolve, reject, trans); + }); + }).then(function (result) { + return trans._completion.then(function () { return result; }); + }); + } + } + + var DEXIE_VERSION = '3.2.4'; + var maxString = String.fromCharCode(65535); + var minKey = -Infinity; + var INVALID_KEY_ARGUMENT = "Invalid key provided. Keys must be of type string, number, Date or Array."; + var STRING_EXPECTED = "String expected."; + var connections = []; + var isIEOrEdge = typeof navigator !== 'undefined' && /(MSIE|Trident|Edge)/.test(navigator.userAgent); + var hasIEDeleteObjectStoreBug = isIEOrEdge; + var hangsOnDeleteLargeKeyRange = isIEOrEdge; + var dexieStackFrameFilter = function (frame) { return !/(dexie\.js|dexie\.min\.js)/.test(frame); }; + var DBNAMES_DB = '__dbnames'; + var READONLY = 'readonly'; + var READWRITE = 'readwrite'; + + function combine(filter1, filter2) { + return filter1 ? + filter2 ? + function () { return filter1.apply(this, arguments) && filter2.apply(this, arguments); } : + filter1 : + filter2; + } + + var AnyRange = { + type: 3 , + lower: -Infinity, + lowerOpen: false, + upper: [[]], + upperOpen: false + }; + + function workaroundForUndefinedPrimKey(keyPath) { + return typeof keyPath === "string" && !/\./.test(keyPath) + ? function (obj) { + if (obj[keyPath] === undefined && (keyPath in obj)) { + obj = deepClone(obj); + delete obj[keyPath]; + } + return obj; + } + : function (obj) { return obj; }; + } + + var Table = (function () { + function Table() { + } + Table.prototype._trans = function (mode, fn, writeLocked) { + var trans = this._tx || PSD.trans; + var tableName = this.name; + function checkTableInTransaction(resolve, reject, trans) { + if (!trans.schema[tableName]) + throw new exceptions.NotFound("Table " + tableName + " not part of transaction"); + return fn(trans.idbtrans, trans); + } + var wasRootExec = beginMicroTickScope(); + try { + return trans && trans.db === this.db ? + trans === PSD.trans ? + trans._promise(mode, checkTableInTransaction, writeLocked) : + newScope(function () { return trans._promise(mode, checkTableInTransaction, writeLocked); }, { trans: trans, transless: PSD.transless || PSD }) : + tempTransaction(this.db, mode, [this.name], checkTableInTransaction); + } + finally { + if (wasRootExec) + endMicroTickScope(); + } + }; + Table.prototype.get = function (keyOrCrit, cb) { + var _this = this; + if (keyOrCrit && keyOrCrit.constructor === Object) + return this.where(keyOrCrit).first(cb); + return this._trans('readonly', function (trans) { + return _this.core.get({ trans: trans, key: keyOrCrit }) + .then(function (res) { return _this.hook.reading.fire(res); }); + }).then(cb); + }; + Table.prototype.where = function (indexOrCrit) { + if (typeof indexOrCrit === 'string') + return new this.db.WhereClause(this, indexOrCrit); + if (isArray(indexOrCrit)) + return new this.db.WhereClause(this, "[" + indexOrCrit.join('+') + "]"); + var keyPaths = keys(indexOrCrit); + if (keyPaths.length === 1) + return this + .where(keyPaths[0]) + .equals(indexOrCrit[keyPaths[0]]); + var compoundIndex = this.schema.indexes.concat(this.schema.primKey).filter(function (ix) { + return ix.compound && + keyPaths.every(function (keyPath) { return ix.keyPath.indexOf(keyPath) >= 0; }) && + ix.keyPath.every(function (keyPath) { return keyPaths.indexOf(keyPath) >= 0; }); + })[0]; + if (compoundIndex && this.db._maxKey !== maxString) + return this + .where(compoundIndex.name) + .equals(compoundIndex.keyPath.map(function (kp) { return indexOrCrit[kp]; })); + if (!compoundIndex && debug) + console.warn("The query " + JSON.stringify(indexOrCrit) + " on " + this.name + " would benefit of a " + + ("compound index [" + keyPaths.join('+') + "]")); + var idxByName = this.schema.idxByName; + var idb = this.db._deps.indexedDB; + function equals(a, b) { + try { + return idb.cmp(a, b) === 0; + } + catch (e) { + return false; + } + } + var _a = keyPaths.reduce(function (_a, keyPath) { + var prevIndex = _a[0], prevFilterFn = _a[1]; + var index = idxByName[keyPath]; + var value = indexOrCrit[keyPath]; + return [ + prevIndex || index, + prevIndex || !index ? + combine(prevFilterFn, index && index.multi ? + function (x) { + var prop = getByKeyPath(x, keyPath); + return isArray(prop) && prop.some(function (item) { return equals(value, item); }); + } : function (x) { return equals(value, getByKeyPath(x, keyPath)); }) + : prevFilterFn + ]; + }, [null, null]), idx = _a[0], filterFunction = _a[1]; + return idx ? + this.where(idx.name).equals(indexOrCrit[idx.keyPath]) + .filter(filterFunction) : + compoundIndex ? + this.filter(filterFunction) : + this.where(keyPaths).equals(''); + }; + Table.prototype.filter = function (filterFunction) { + return this.toCollection().and(filterFunction); + }; + Table.prototype.count = function (thenShortcut) { + return this.toCollection().count(thenShortcut); + }; + Table.prototype.offset = function (offset) { + return this.toCollection().offset(offset); + }; + Table.prototype.limit = function (numRows) { + return this.toCollection().limit(numRows); + }; + Table.prototype.each = function (callback) { + return this.toCollection().each(callback); + }; + Table.prototype.toArray = function (thenShortcut) { + return this.toCollection().toArray(thenShortcut); + }; + Table.prototype.toCollection = function () { + return new this.db.Collection(new this.db.WhereClause(this)); + }; + Table.prototype.orderBy = function (index) { + return new this.db.Collection(new this.db.WhereClause(this, isArray(index) ? + "[" + index.join('+') + "]" : + index)); + }; + Table.prototype.reverse = function () { + return this.toCollection().reverse(); + }; + Table.prototype.mapToClass = function (constructor) { + this.schema.mappedClass = constructor; + var readHook = function (obj) { + if (!obj) + return obj; + var res = Object.create(constructor.prototype); + for (var m in obj) + if (hasOwn(obj, m)) + try { + res[m] = obj[m]; + } + catch (_) { } + return res; + }; + if (this.schema.readHook) { + this.hook.reading.unsubscribe(this.schema.readHook); + } + this.schema.readHook = readHook; + this.hook("reading", readHook); + return constructor; + }; + Table.prototype.defineClass = function () { + function Class(content) { + extend(this, content); + } + return this.mapToClass(Class); + }; + Table.prototype.add = function (obj, key) { + var _this = this; + var _a = this.schema.primKey, auto = _a.auto, keyPath = _a.keyPath; + var objToAdd = obj; + if (keyPath && auto) { + objToAdd = workaroundForUndefinedPrimKey(keyPath)(obj); + } + return this._trans('readwrite', function (trans) { + return _this.core.mutate({ trans: trans, type: 'add', keys: key != null ? [key] : null, values: [objToAdd] }); + }).then(function (res) { return res.numFailures ? DexiePromise.reject(res.failures[0]) : res.lastResult; }) + .then(function (lastResult) { + if (keyPath) { + try { + setByKeyPath(obj, keyPath, lastResult); + } + catch (_) { } + } + return lastResult; + }); + }; + Table.prototype.update = function (keyOrObject, modifications) { + if (typeof keyOrObject === 'object' && !isArray(keyOrObject)) { + var key = getByKeyPath(keyOrObject, this.schema.primKey.keyPath); + if (key === undefined) + return rejection(new exceptions.InvalidArgument("Given object does not contain its primary key")); + try { + if (typeof modifications !== "function") { + keys(modifications).forEach(function (keyPath) { + setByKeyPath(keyOrObject, keyPath, modifications[keyPath]); + }); + } + else { + modifications(keyOrObject, { value: keyOrObject, primKey: key }); + } + } + catch (_a) { + } + return this.where(":id").equals(key).modify(modifications); + } + else { + return this.where(":id").equals(keyOrObject).modify(modifications); + } + }; + Table.prototype.put = function (obj, key) { + var _this = this; + var _a = this.schema.primKey, auto = _a.auto, keyPath = _a.keyPath; + var objToAdd = obj; + if (keyPath && auto) { + objToAdd = workaroundForUndefinedPrimKey(keyPath)(obj); + } + return this._trans('readwrite', function (trans) { return _this.core.mutate({ trans: trans, type: 'put', values: [objToAdd], keys: key != null ? [key] : null }); }) + .then(function (res) { return res.numFailures ? DexiePromise.reject(res.failures[0]) : res.lastResult; }) + .then(function (lastResult) { + if (keyPath) { + try { + setByKeyPath(obj, keyPath, lastResult); + } + catch (_) { } + } + return lastResult; + }); + }; + Table.prototype.delete = function (key) { + var _this = this; + return this._trans('readwrite', function (trans) { return _this.core.mutate({ trans: trans, type: 'delete', keys: [key] }); }) + .then(function (res) { return res.numFailures ? DexiePromise.reject(res.failures[0]) : undefined; }); + }; + Table.prototype.clear = function () { + var _this = this; + return this._trans('readwrite', function (trans) { return _this.core.mutate({ trans: trans, type: 'deleteRange', range: AnyRange }); }) + .then(function (res) { return res.numFailures ? DexiePromise.reject(res.failures[0]) : undefined; }); + }; + Table.prototype.bulkGet = function (keys) { + var _this = this; + return this._trans('readonly', function (trans) { + return _this.core.getMany({ + keys: keys, + trans: trans + }).then(function (result) { return result.map(function (res) { return _this.hook.reading.fire(res); }); }); + }); + }; + Table.prototype.bulkAdd = function (objects, keysOrOptions, options) { + var _this = this; + var keys = Array.isArray(keysOrOptions) ? keysOrOptions : undefined; + options = options || (keys ? undefined : keysOrOptions); + var wantResults = options ? options.allKeys : undefined; + return this._trans('readwrite', function (trans) { + var _a = _this.schema.primKey, auto = _a.auto, keyPath = _a.keyPath; + if (keyPath && keys) + throw new exceptions.InvalidArgument("bulkAdd(): keys argument invalid on tables with inbound keys"); + if (keys && keys.length !== objects.length) + throw new exceptions.InvalidArgument("Arguments objects and keys must have the same length"); + var numObjects = objects.length; + var objectsToAdd = keyPath && auto ? + objects.map(workaroundForUndefinedPrimKey(keyPath)) : + objects; + return _this.core.mutate({ trans: trans, type: 'add', keys: keys, values: objectsToAdd, wantResults: wantResults }) + .then(function (_a) { + var numFailures = _a.numFailures, results = _a.results, lastResult = _a.lastResult, failures = _a.failures; + var result = wantResults ? results : lastResult; + if (numFailures === 0) + return result; + throw new BulkError(_this.name + ".bulkAdd(): " + numFailures + " of " + numObjects + " operations failed", failures); + }); + }); + }; + Table.prototype.bulkPut = function (objects, keysOrOptions, options) { + var _this = this; + var keys = Array.isArray(keysOrOptions) ? keysOrOptions : undefined; + options = options || (keys ? undefined : keysOrOptions); + var wantResults = options ? options.allKeys : undefined; + return this._trans('readwrite', function (trans) { + var _a = _this.schema.primKey, auto = _a.auto, keyPath = _a.keyPath; + if (keyPath && keys) + throw new exceptions.InvalidArgument("bulkPut(): keys argument invalid on tables with inbound keys"); + if (keys && keys.length !== objects.length) + throw new exceptions.InvalidArgument("Arguments objects and keys must have the same length"); + var numObjects = objects.length; + var objectsToPut = keyPath && auto ? + objects.map(workaroundForUndefinedPrimKey(keyPath)) : + objects; + return _this.core.mutate({ trans: trans, type: 'put', keys: keys, values: objectsToPut, wantResults: wantResults }) + .then(function (_a) { + var numFailures = _a.numFailures, results = _a.results, lastResult = _a.lastResult, failures = _a.failures; + var result = wantResults ? results : lastResult; + if (numFailures === 0) + return result; + throw new BulkError(_this.name + ".bulkPut(): " + numFailures + " of " + numObjects + " operations failed", failures); + }); + }); + }; + Table.prototype.bulkDelete = function (keys) { + var _this = this; + var numKeys = keys.length; + return this._trans('readwrite', function (trans) { + return _this.core.mutate({ trans: trans, type: 'delete', keys: keys }); + }).then(function (_a) { + var numFailures = _a.numFailures, lastResult = _a.lastResult, failures = _a.failures; + if (numFailures === 0) + return lastResult; + throw new BulkError(_this.name + ".bulkDelete(): " + numFailures + " of " + numKeys + " operations failed", failures); + }); + }; + return Table; + }()); + + function Events(ctx) { + var evs = {}; + var rv = function (eventName, subscriber) { + if (subscriber) { + var i = arguments.length, args = new Array(i - 1); + while (--i) + args[i - 1] = arguments[i]; + evs[eventName].subscribe.apply(null, args); + return ctx; + } + else if (typeof (eventName) === 'string') { + return evs[eventName]; + } + }; + rv.addEventType = add; + for (var i = 1, l = arguments.length; i < l; ++i) { + add(arguments[i]); + } + return rv; + function add(eventName, chainFunction, defaultFunction) { + if (typeof eventName === 'object') + return addConfiguredEvents(eventName); + if (!chainFunction) + chainFunction = reverseStoppableEventChain; + if (!defaultFunction) + defaultFunction = nop; + var context = { + subscribers: [], + fire: defaultFunction, + subscribe: function (cb) { + if (context.subscribers.indexOf(cb) === -1) { + context.subscribers.push(cb); + context.fire = chainFunction(context.fire, cb); + } + }, + unsubscribe: function (cb) { + context.subscribers = context.subscribers.filter(function (fn) { return fn !== cb; }); + context.fire = context.subscribers.reduce(chainFunction, defaultFunction); + } + }; + evs[eventName] = rv[eventName] = context; + return context; + } + function addConfiguredEvents(cfg) { + keys(cfg).forEach(function (eventName) { + var args = cfg[eventName]; + if (isArray(args)) { + add(eventName, cfg[eventName][0], cfg[eventName][1]); + } + else if (args === 'asap') { + var context = add(eventName, mirror, function fire() { + var i = arguments.length, args = new Array(i); + while (i--) + args[i] = arguments[i]; + context.subscribers.forEach(function (fn) { + asap$1(function fireEvent() { + fn.apply(null, args); + }); + }); + }); + } + else + throw new exceptions.InvalidArgument("Invalid event config"); + }); + } + } + + function makeClassConstructor(prototype, constructor) { + derive(constructor).from({ prototype: prototype }); + return constructor; + } + + function createTableConstructor(db) { + return makeClassConstructor(Table.prototype, function Table(name, tableSchema, trans) { + this.db = db; + this._tx = trans; + this.name = name; + this.schema = tableSchema; + this.hook = db._allTables[name] ? db._allTables[name].hook : Events(null, { + "creating": [hookCreatingChain, nop], + "reading": [pureFunctionChain, mirror], + "updating": [hookUpdatingChain, nop], + "deleting": [hookDeletingChain, nop] + }); + }); + } + + function isPlainKeyRange(ctx, ignoreLimitFilter) { + return !(ctx.filter || ctx.algorithm || ctx.or) && + (ignoreLimitFilter ? ctx.justLimit : !ctx.replayFilter); + } + function addFilter(ctx, fn) { + ctx.filter = combine(ctx.filter, fn); + } + function addReplayFilter(ctx, factory, isLimitFilter) { + var curr = ctx.replayFilter; + ctx.replayFilter = curr ? function () { return combine(curr(), factory()); } : factory; + ctx.justLimit = isLimitFilter && !curr; + } + function addMatchFilter(ctx, fn) { + ctx.isMatch = combine(ctx.isMatch, fn); + } + function getIndexOrStore(ctx, coreSchema) { + if (ctx.isPrimKey) + return coreSchema.primaryKey; + var index = coreSchema.getIndexByKeyPath(ctx.index); + if (!index) + throw new exceptions.Schema("KeyPath " + ctx.index + " on object store " + coreSchema.name + " is not indexed"); + return index; + } + function openCursor(ctx, coreTable, trans) { + var index = getIndexOrStore(ctx, coreTable.schema); + return coreTable.openCursor({ + trans: trans, + values: !ctx.keysOnly, + reverse: ctx.dir === 'prev', + unique: !!ctx.unique, + query: { + index: index, + range: ctx.range + } + }); + } + function iter(ctx, fn, coreTrans, coreTable) { + var filter = ctx.replayFilter ? combine(ctx.filter, ctx.replayFilter()) : ctx.filter; + if (!ctx.or) { + return iterate(openCursor(ctx, coreTable, coreTrans), combine(ctx.algorithm, filter), fn, !ctx.keysOnly && ctx.valueMapper); + } + else { + var set_1 = {}; + var union = function (item, cursor, advance) { + if (!filter || filter(cursor, advance, function (result) { return cursor.stop(result); }, function (err) { return cursor.fail(err); })) { + var primaryKey = cursor.primaryKey; + var key = '' + primaryKey; + if (key === '[object ArrayBuffer]') + key = '' + new Uint8Array(primaryKey); + if (!hasOwn(set_1, key)) { + set_1[key] = true; + fn(item, cursor, advance); + } + } + }; + return Promise.all([ + ctx.or._iterate(union, coreTrans), + iterate(openCursor(ctx, coreTable, coreTrans), ctx.algorithm, union, !ctx.keysOnly && ctx.valueMapper) + ]); + } + } + function iterate(cursorPromise, filter, fn, valueMapper) { + var mappedFn = valueMapper ? function (x, c, a) { return fn(valueMapper(x), c, a); } : fn; + var wrappedFn = wrap(mappedFn); + return cursorPromise.then(function (cursor) { + if (cursor) { + return cursor.start(function () { + var c = function () { return cursor.continue(); }; + if (!filter || filter(cursor, function (advancer) { return c = advancer; }, function (val) { cursor.stop(val); c = nop; }, function (e) { cursor.fail(e); c = nop; })) + wrappedFn(cursor.value, cursor, function (advancer) { return c = advancer; }); + c(); + }); + } + }); + } + + function cmp(a, b) { + try { + var ta = type(a); + var tb = type(b); + if (ta !== tb) { + if (ta === 'Array') + return 1; + if (tb === 'Array') + return -1; + if (ta === 'binary') + return 1; + if (tb === 'binary') + return -1; + if (ta === 'string') + return 1; + if (tb === 'string') + return -1; + if (ta === 'Date') + return 1; + if (tb !== 'Date') + return NaN; + return -1; + } + switch (ta) { + case 'number': + case 'Date': + case 'string': + return a > b ? 1 : a < b ? -1 : 0; + case 'binary': { + return compareUint8Arrays(getUint8Array(a), getUint8Array(b)); + } + case 'Array': + return compareArrays(a, b); + } + } + catch (_a) { } + return NaN; + } + function compareArrays(a, b) { + var al = a.length; + var bl = b.length; + var l = al < bl ? al : bl; + for (var i = 0; i < l; ++i) { + var res = cmp(a[i], b[i]); + if (res !== 0) + return res; + } + return al === bl ? 0 : al < bl ? -1 : 1; + } + function compareUint8Arrays(a, b) { + var al = a.length; + var bl = b.length; + var l = al < bl ? al : bl; + for (var i = 0; i < l; ++i) { + if (a[i] !== b[i]) + return a[i] < b[i] ? -1 : 1; + } + return al === bl ? 0 : al < bl ? -1 : 1; + } + function type(x) { + var t = typeof x; + if (t !== 'object') + return t; + if (ArrayBuffer.isView(x)) + return 'binary'; + var tsTag = toStringTag(x); + return tsTag === 'ArrayBuffer' ? 'binary' : tsTag; + } + function getUint8Array(a) { + if (a instanceof Uint8Array) + return a; + if (ArrayBuffer.isView(a)) + return new Uint8Array(a.buffer, a.byteOffset, a.byteLength); + return new Uint8Array(a); + } + + var Collection = (function () { + function Collection() { + } + Collection.prototype._read = function (fn, cb) { + var ctx = this._ctx; + return ctx.error ? + ctx.table._trans(null, rejection.bind(null, ctx.error)) : + ctx.table._trans('readonly', fn).then(cb); + }; + Collection.prototype._write = function (fn) { + var ctx = this._ctx; + return ctx.error ? + ctx.table._trans(null, rejection.bind(null, ctx.error)) : + ctx.table._trans('readwrite', fn, "locked"); + }; + Collection.prototype._addAlgorithm = function (fn) { + var ctx = this._ctx; + ctx.algorithm = combine(ctx.algorithm, fn); + }; + Collection.prototype._iterate = function (fn, coreTrans) { + return iter(this._ctx, fn, coreTrans, this._ctx.table.core); + }; + Collection.prototype.clone = function (props) { + var rv = Object.create(this.constructor.prototype), ctx = Object.create(this._ctx); + if (props) + extend(ctx, props); + rv._ctx = ctx; + return rv; + }; + Collection.prototype.raw = function () { + this._ctx.valueMapper = null; + return this; + }; + Collection.prototype.each = function (fn) { + var ctx = this._ctx; + return this._read(function (trans) { return iter(ctx, fn, trans, ctx.table.core); }); + }; + Collection.prototype.count = function (cb) { + var _this = this; + return this._read(function (trans) { + var ctx = _this._ctx; + var coreTable = ctx.table.core; + if (isPlainKeyRange(ctx, true)) { + return coreTable.count({ + trans: trans, + query: { + index: getIndexOrStore(ctx, coreTable.schema), + range: ctx.range + } + }).then(function (count) { return Math.min(count, ctx.limit); }); + } + else { + var count = 0; + return iter(ctx, function () { ++count; return false; }, trans, coreTable) + .then(function () { return count; }); + } + }).then(cb); + }; + Collection.prototype.sortBy = function (keyPath, cb) { + var parts = keyPath.split('.').reverse(), lastPart = parts[0], lastIndex = parts.length - 1; + function getval(obj, i) { + if (i) + return getval(obj[parts[i]], i - 1); + return obj[lastPart]; + } + var order = this._ctx.dir === "next" ? 1 : -1; + function sorter(a, b) { + var aVal = getval(a, lastIndex), bVal = getval(b, lastIndex); + return aVal < bVal ? -order : aVal > bVal ? order : 0; + } + return this.toArray(function (a) { + return a.sort(sorter); + }).then(cb); + }; + Collection.prototype.toArray = function (cb) { + var _this = this; + return this._read(function (trans) { + var ctx = _this._ctx; + if (ctx.dir === 'next' && isPlainKeyRange(ctx, true) && ctx.limit > 0) { + var valueMapper_1 = ctx.valueMapper; + var index = getIndexOrStore(ctx, ctx.table.core.schema); + return ctx.table.core.query({ + trans: trans, + limit: ctx.limit, + values: true, + query: { + index: index, + range: ctx.range + } + }).then(function (_a) { + var result = _a.result; + return valueMapper_1 ? result.map(valueMapper_1) : result; + }); + } + else { + var a_1 = []; + return iter(ctx, function (item) { return a_1.push(item); }, trans, ctx.table.core).then(function () { return a_1; }); + } + }, cb); + }; + Collection.prototype.offset = function (offset) { + var ctx = this._ctx; + if (offset <= 0) + return this; + ctx.offset += offset; + if (isPlainKeyRange(ctx)) { + addReplayFilter(ctx, function () { + var offsetLeft = offset; + return function (cursor, advance) { + if (offsetLeft === 0) + return true; + if (offsetLeft === 1) { + --offsetLeft; + return false; + } + advance(function () { + cursor.advance(offsetLeft); + offsetLeft = 0; + }); + return false; + }; + }); + } + else { + addReplayFilter(ctx, function () { + var offsetLeft = offset; + return function () { return (--offsetLeft < 0); }; + }); + } + return this; + }; + Collection.prototype.limit = function (numRows) { + this._ctx.limit = Math.min(this._ctx.limit, numRows); + addReplayFilter(this._ctx, function () { + var rowsLeft = numRows; + return function (cursor, advance, resolve) { + if (--rowsLeft <= 0) + advance(resolve); + return rowsLeft >= 0; + }; + }, true); + return this; + }; + Collection.prototype.until = function (filterFunction, bIncludeStopEntry) { + addFilter(this._ctx, function (cursor, advance, resolve) { + if (filterFunction(cursor.value)) { + advance(resolve); + return bIncludeStopEntry; + } + else { + return true; + } + }); + return this; + }; + Collection.prototype.first = function (cb) { + return this.limit(1).toArray(function (a) { return a[0]; }).then(cb); + }; + Collection.prototype.last = function (cb) { + return this.reverse().first(cb); + }; + Collection.prototype.filter = function (filterFunction) { + addFilter(this._ctx, function (cursor) { + return filterFunction(cursor.value); + }); + addMatchFilter(this._ctx, filterFunction); + return this; + }; + Collection.prototype.and = function (filter) { + return this.filter(filter); + }; + Collection.prototype.or = function (indexName) { + return new this.db.WhereClause(this._ctx.table, indexName, this); + }; + Collection.prototype.reverse = function () { + this._ctx.dir = (this._ctx.dir === "prev" ? "next" : "prev"); + if (this._ondirectionchange) + this._ondirectionchange(this._ctx.dir); + return this; + }; + Collection.prototype.desc = function () { + return this.reverse(); + }; + Collection.prototype.eachKey = function (cb) { + var ctx = this._ctx; + ctx.keysOnly = !ctx.isMatch; + return this.each(function (val, cursor) { cb(cursor.key, cursor); }); + }; + Collection.prototype.eachUniqueKey = function (cb) { + this._ctx.unique = "unique"; + return this.eachKey(cb); + }; + Collection.prototype.eachPrimaryKey = function (cb) { + var ctx = this._ctx; + ctx.keysOnly = !ctx.isMatch; + return this.each(function (val, cursor) { cb(cursor.primaryKey, cursor); }); + }; + Collection.prototype.keys = function (cb) { + var ctx = this._ctx; + ctx.keysOnly = !ctx.isMatch; + var a = []; + return this.each(function (item, cursor) { + a.push(cursor.key); + }).then(function () { + return a; + }).then(cb); + }; + Collection.prototype.primaryKeys = function (cb) { + var ctx = this._ctx; + if (ctx.dir === 'next' && isPlainKeyRange(ctx, true) && ctx.limit > 0) { + return this._read(function (trans) { + var index = getIndexOrStore(ctx, ctx.table.core.schema); + return ctx.table.core.query({ + trans: trans, + values: false, + limit: ctx.limit, + query: { + index: index, + range: ctx.range + } + }); + }).then(function (_a) { + var result = _a.result; + return result; + }).then(cb); + } + ctx.keysOnly = !ctx.isMatch; + var a = []; + return this.each(function (item, cursor) { + a.push(cursor.primaryKey); + }).then(function () { + return a; + }).then(cb); + }; + Collection.prototype.uniqueKeys = function (cb) { + this._ctx.unique = "unique"; + return this.keys(cb); + }; + Collection.prototype.firstKey = function (cb) { + return this.limit(1).keys(function (a) { return a[0]; }).then(cb); + }; + Collection.prototype.lastKey = function (cb) { + return this.reverse().firstKey(cb); + }; + Collection.prototype.distinct = function () { + var ctx = this._ctx, idx = ctx.index && ctx.table.schema.idxByName[ctx.index]; + if (!idx || !idx.multi) + return this; + var set = {}; + addFilter(this._ctx, function (cursor) { + var strKey = cursor.primaryKey.toString(); + var found = hasOwn(set, strKey); + set[strKey] = true; + return !found; + }); + return this; + }; + Collection.prototype.modify = function (changes) { + var _this = this; + var ctx = this._ctx; + return this._write(function (trans) { + var modifyer; + if (typeof changes === 'function') { + modifyer = changes; + } + else { + var keyPaths = keys(changes); + var numKeys = keyPaths.length; + modifyer = function (item) { + var anythingModified = false; + for (var i = 0; i < numKeys; ++i) { + var keyPath = keyPaths[i], val = changes[keyPath]; + if (getByKeyPath(item, keyPath) !== val) { + setByKeyPath(item, keyPath, val); + anythingModified = true; + } + } + return anythingModified; + }; + } + var coreTable = ctx.table.core; + var _a = coreTable.schema.primaryKey, outbound = _a.outbound, extractKey = _a.extractKey; + var limit = _this.db._options.modifyChunkSize || 200; + var totalFailures = []; + var successCount = 0; + var failedKeys = []; + var applyMutateResult = function (expectedCount, res) { + var failures = res.failures, numFailures = res.numFailures; + successCount += expectedCount - numFailures; + for (var _i = 0, _a = keys(failures); _i < _a.length; _i++) { + var pos = _a[_i]; + totalFailures.push(failures[pos]); + } + }; + return _this.clone().primaryKeys().then(function (keys) { + var nextChunk = function (offset) { + var count = Math.min(limit, keys.length - offset); + return coreTable.getMany({ + trans: trans, + keys: keys.slice(offset, offset + count), + cache: "immutable" + }).then(function (values) { + var addValues = []; + var putValues = []; + var putKeys = outbound ? [] : null; + var deleteKeys = []; + for (var i = 0; i < count; ++i) { + var origValue = values[i]; + var ctx_1 = { + value: deepClone(origValue), + primKey: keys[offset + i] + }; + if (modifyer.call(ctx_1, ctx_1.value, ctx_1) !== false) { + if (ctx_1.value == null) { + deleteKeys.push(keys[offset + i]); + } + else if (!outbound && cmp(extractKey(origValue), extractKey(ctx_1.value)) !== 0) { + deleteKeys.push(keys[offset + i]); + addValues.push(ctx_1.value); + } + else { + putValues.push(ctx_1.value); + if (outbound) + putKeys.push(keys[offset + i]); + } + } + } + var criteria = isPlainKeyRange(ctx) && + ctx.limit === Infinity && + (typeof changes !== 'function' || changes === deleteCallback) && { + index: ctx.index, + range: ctx.range + }; + return Promise.resolve(addValues.length > 0 && + coreTable.mutate({ trans: trans, type: 'add', values: addValues }) + .then(function (res) { + for (var pos in res.failures) { + deleteKeys.splice(parseInt(pos), 1); + } + applyMutateResult(addValues.length, res); + })).then(function () { return (putValues.length > 0 || (criteria && typeof changes === 'object')) && + coreTable.mutate({ + trans: trans, + type: 'put', + keys: putKeys, + values: putValues, + criteria: criteria, + changeSpec: typeof changes !== 'function' + && changes + }).then(function (res) { return applyMutateResult(putValues.length, res); }); }).then(function () { return (deleteKeys.length > 0 || (criteria && changes === deleteCallback)) && + coreTable.mutate({ + trans: trans, + type: 'delete', + keys: deleteKeys, + criteria: criteria + }).then(function (res) { return applyMutateResult(deleteKeys.length, res); }); }).then(function () { + return keys.length > offset + count && nextChunk(offset + limit); + }); + }); + }; + return nextChunk(0).then(function () { + if (totalFailures.length > 0) + throw new ModifyError("Error modifying one or more objects", totalFailures, successCount, failedKeys); + return keys.length; + }); + }); + }); + }; + Collection.prototype.delete = function () { + var ctx = this._ctx, range = ctx.range; + if (isPlainKeyRange(ctx) && + ((ctx.isPrimKey && !hangsOnDeleteLargeKeyRange) || range.type === 3 )) + { + return this._write(function (trans) { + var primaryKey = ctx.table.core.schema.primaryKey; + var coreRange = range; + return ctx.table.core.count({ trans: trans, query: { index: primaryKey, range: coreRange } }).then(function (count) { + return ctx.table.core.mutate({ trans: trans, type: 'deleteRange', range: coreRange }) + .then(function (_a) { + var failures = _a.failures; _a.lastResult; _a.results; var numFailures = _a.numFailures; + if (numFailures) + throw new ModifyError("Could not delete some values", Object.keys(failures).map(function (pos) { return failures[pos]; }), count - numFailures); + return count - numFailures; + }); + }); + }); + } + return this.modify(deleteCallback); + }; + return Collection; + }()); + var deleteCallback = function (value, ctx) { return ctx.value = null; }; + + function createCollectionConstructor(db) { + return makeClassConstructor(Collection.prototype, function Collection(whereClause, keyRangeGenerator) { + this.db = db; + var keyRange = AnyRange, error = null; + if (keyRangeGenerator) + try { + keyRange = keyRangeGenerator(); + } + catch (ex) { + error = ex; + } + var whereCtx = whereClause._ctx; + var table = whereCtx.table; + var readingHook = table.hook.reading.fire; + this._ctx = { + table: table, + index: whereCtx.index, + isPrimKey: (!whereCtx.index || (table.schema.primKey.keyPath && whereCtx.index === table.schema.primKey.name)), + range: keyRange, + keysOnly: false, + dir: "next", + unique: "", + algorithm: null, + filter: null, + replayFilter: null, + justLimit: true, + isMatch: null, + offset: 0, + limit: Infinity, + error: error, + or: whereCtx.or, + valueMapper: readingHook !== mirror ? readingHook : null + }; + }); + } + + function simpleCompare(a, b) { + return a < b ? -1 : a === b ? 0 : 1; + } + function simpleCompareReverse(a, b) { + return a > b ? -1 : a === b ? 0 : 1; + } + + function fail(collectionOrWhereClause, err, T) { + var collection = collectionOrWhereClause instanceof WhereClause ? + new collectionOrWhereClause.Collection(collectionOrWhereClause) : + collectionOrWhereClause; + collection._ctx.error = T ? new T(err) : new TypeError(err); + return collection; + } + function emptyCollection(whereClause) { + return new whereClause.Collection(whereClause, function () { return rangeEqual(""); }).limit(0); + } + function upperFactory(dir) { + return dir === "next" ? + function (s) { return s.toUpperCase(); } : + function (s) { return s.toLowerCase(); }; + } + function lowerFactory(dir) { + return dir === "next" ? + function (s) { return s.toLowerCase(); } : + function (s) { return s.toUpperCase(); }; + } + function nextCasing(key, lowerKey, upperNeedle, lowerNeedle, cmp, dir) { + var length = Math.min(key.length, lowerNeedle.length); + var llp = -1; + for (var i = 0; i < length; ++i) { + var lwrKeyChar = lowerKey[i]; + if (lwrKeyChar !== lowerNeedle[i]) { + if (cmp(key[i], upperNeedle[i]) < 0) + return key.substr(0, i) + upperNeedle[i] + upperNeedle.substr(i + 1); + if (cmp(key[i], lowerNeedle[i]) < 0) + return key.substr(0, i) + lowerNeedle[i] + upperNeedle.substr(i + 1); + if (llp >= 0) + return key.substr(0, llp) + lowerKey[llp] + upperNeedle.substr(llp + 1); + return null; + } + if (cmp(key[i], lwrKeyChar) < 0) + llp = i; + } + if (length < lowerNeedle.length && dir === "next") + return key + upperNeedle.substr(key.length); + if (length < key.length && dir === "prev") + return key.substr(0, upperNeedle.length); + return (llp < 0 ? null : key.substr(0, llp) + lowerNeedle[llp] + upperNeedle.substr(llp + 1)); + } + function addIgnoreCaseAlgorithm(whereClause, match, needles, suffix) { + var upper, lower, compare, upperNeedles, lowerNeedles, direction, nextKeySuffix, needlesLen = needles.length; + if (!needles.every(function (s) { return typeof s === 'string'; })) { + return fail(whereClause, STRING_EXPECTED); + } + function initDirection(dir) { + upper = upperFactory(dir); + lower = lowerFactory(dir); + compare = (dir === "next" ? simpleCompare : simpleCompareReverse); + var needleBounds = needles.map(function (needle) { + return { lower: lower(needle), upper: upper(needle) }; + }).sort(function (a, b) { + return compare(a.lower, b.lower); + }); + upperNeedles = needleBounds.map(function (nb) { return nb.upper; }); + lowerNeedles = needleBounds.map(function (nb) { return nb.lower; }); + direction = dir; + nextKeySuffix = (dir === "next" ? "" : suffix); + } + initDirection("next"); + var c = new whereClause.Collection(whereClause, function () { return createRange(upperNeedles[0], lowerNeedles[needlesLen - 1] + suffix); }); + c._ondirectionchange = function (direction) { + initDirection(direction); + }; + var firstPossibleNeedle = 0; + c._addAlgorithm(function (cursor, advance, resolve) { + var key = cursor.key; + if (typeof key !== 'string') + return false; + var lowerKey = lower(key); + if (match(lowerKey, lowerNeedles, firstPossibleNeedle)) { + return true; + } + else { + var lowestPossibleCasing = null; + for (var i = firstPossibleNeedle; i < needlesLen; ++i) { + var casing = nextCasing(key, lowerKey, upperNeedles[i], lowerNeedles[i], compare, direction); + if (casing === null && lowestPossibleCasing === null) + firstPossibleNeedle = i + 1; + else if (lowestPossibleCasing === null || compare(lowestPossibleCasing, casing) > 0) { + lowestPossibleCasing = casing; + } + } + if (lowestPossibleCasing !== null) { + advance(function () { cursor.continue(lowestPossibleCasing + nextKeySuffix); }); + } + else { + advance(resolve); + } + return false; + } + }); + return c; + } + function createRange(lower, upper, lowerOpen, upperOpen) { + return { + type: 2 , + lower: lower, + upper: upper, + lowerOpen: lowerOpen, + upperOpen: upperOpen + }; + } + function rangeEqual(value) { + return { + type: 1 , + lower: value, + upper: value + }; + } + + var WhereClause = (function () { + function WhereClause() { + } + Object.defineProperty(WhereClause.prototype, "Collection", { + get: function () { + return this._ctx.table.db.Collection; + }, + enumerable: false, + configurable: true + }); + WhereClause.prototype.between = function (lower, upper, includeLower, includeUpper) { + includeLower = includeLower !== false; + includeUpper = includeUpper === true; + try { + if ((this._cmp(lower, upper) > 0) || + (this._cmp(lower, upper) === 0 && (includeLower || includeUpper) && !(includeLower && includeUpper))) + return emptyCollection(this); + return new this.Collection(this, function () { return createRange(lower, upper, !includeLower, !includeUpper); }); + } + catch (e) { + return fail(this, INVALID_KEY_ARGUMENT); + } + }; + WhereClause.prototype.equals = function (value) { + if (value == null) + return fail(this, INVALID_KEY_ARGUMENT); + return new this.Collection(this, function () { return rangeEqual(value); }); + }; + WhereClause.prototype.above = function (value) { + if (value == null) + return fail(this, INVALID_KEY_ARGUMENT); + return new this.Collection(this, function () { return createRange(value, undefined, true); }); + }; + WhereClause.prototype.aboveOrEqual = function (value) { + if (value == null) + return fail(this, INVALID_KEY_ARGUMENT); + return new this.Collection(this, function () { return createRange(value, undefined, false); }); + }; + WhereClause.prototype.below = function (value) { + if (value == null) + return fail(this, INVALID_KEY_ARGUMENT); + return new this.Collection(this, function () { return createRange(undefined, value, false, true); }); + }; + WhereClause.prototype.belowOrEqual = function (value) { + if (value == null) + return fail(this, INVALID_KEY_ARGUMENT); + return new this.Collection(this, function () { return createRange(undefined, value); }); + }; + WhereClause.prototype.startsWith = function (str) { + if (typeof str !== 'string') + return fail(this, STRING_EXPECTED); + return this.between(str, str + maxString, true, true); + }; + WhereClause.prototype.startsWithIgnoreCase = function (str) { + if (str === "") + return this.startsWith(str); + return addIgnoreCaseAlgorithm(this, function (x, a) { return x.indexOf(a[0]) === 0; }, [str], maxString); + }; + WhereClause.prototype.equalsIgnoreCase = function (str) { + return addIgnoreCaseAlgorithm(this, function (x, a) { return x === a[0]; }, [str], ""); + }; + WhereClause.prototype.anyOfIgnoreCase = function () { + var set = getArrayOf.apply(NO_CHAR_ARRAY, arguments); + if (set.length === 0) + return emptyCollection(this); + return addIgnoreCaseAlgorithm(this, function (x, a) { return a.indexOf(x) !== -1; }, set, ""); + }; + WhereClause.prototype.startsWithAnyOfIgnoreCase = function () { + var set = getArrayOf.apply(NO_CHAR_ARRAY, arguments); + if (set.length === 0) + return emptyCollection(this); + return addIgnoreCaseAlgorithm(this, function (x, a) { return a.some(function (n) { return x.indexOf(n) === 0; }); }, set, maxString); + }; + WhereClause.prototype.anyOf = function () { + var _this = this; + var set = getArrayOf.apply(NO_CHAR_ARRAY, arguments); + var compare = this._cmp; + try { + set.sort(compare); + } + catch (e) { + return fail(this, INVALID_KEY_ARGUMENT); + } + if (set.length === 0) + return emptyCollection(this); + var c = new this.Collection(this, function () { return createRange(set[0], set[set.length - 1]); }); + c._ondirectionchange = function (direction) { + compare = (direction === "next" ? + _this._ascending : + _this._descending); + set.sort(compare); + }; + var i = 0; + c._addAlgorithm(function (cursor, advance, resolve) { + var key = cursor.key; + while (compare(key, set[i]) > 0) { + ++i; + if (i === set.length) { + advance(resolve); + return false; + } + } + if (compare(key, set[i]) === 0) { + return true; + } + else { + advance(function () { cursor.continue(set[i]); }); + return false; + } + }); + return c; + }; + WhereClause.prototype.notEqual = function (value) { + return this.inAnyRange([[minKey, value], [value, this.db._maxKey]], { includeLowers: false, includeUppers: false }); + }; + WhereClause.prototype.noneOf = function () { + var set = getArrayOf.apply(NO_CHAR_ARRAY, arguments); + if (set.length === 0) + return new this.Collection(this); + try { + set.sort(this._ascending); + } + catch (e) { + return fail(this, INVALID_KEY_ARGUMENT); + } + var ranges = set.reduce(function (res, val) { return res ? + res.concat([[res[res.length - 1][1], val]]) : + [[minKey, val]]; }, null); + ranges.push([set[set.length - 1], this.db._maxKey]); + return this.inAnyRange(ranges, { includeLowers: false, includeUppers: false }); + }; + WhereClause.prototype.inAnyRange = function (ranges, options) { + var _this = this; + var cmp = this._cmp, ascending = this._ascending, descending = this._descending, min = this._min, max = this._max; + if (ranges.length === 0) + return emptyCollection(this); + if (!ranges.every(function (range) { + return range[0] !== undefined && + range[1] !== undefined && + ascending(range[0], range[1]) <= 0; + })) { + return fail(this, "First argument to inAnyRange() must be an Array of two-value Arrays [lower,upper] where upper must not be lower than lower", exceptions.InvalidArgument); + } + var includeLowers = !options || options.includeLowers !== false; + var includeUppers = options && options.includeUppers === true; + function addRange(ranges, newRange) { + var i = 0, l = ranges.length; + for (; i < l; ++i) { + var range = ranges[i]; + if (cmp(newRange[0], range[1]) < 0 && cmp(newRange[1], range[0]) > 0) { + range[0] = min(range[0], newRange[0]); + range[1] = max(range[1], newRange[1]); + break; + } + } + if (i === l) + ranges.push(newRange); + return ranges; + } + var sortDirection = ascending; + function rangeSorter(a, b) { return sortDirection(a[0], b[0]); } + var set; + try { + set = ranges.reduce(addRange, []); + set.sort(rangeSorter); + } + catch (ex) { + return fail(this, INVALID_KEY_ARGUMENT); + } + var rangePos = 0; + var keyIsBeyondCurrentEntry = includeUppers ? + function (key) { return ascending(key, set[rangePos][1]) > 0; } : + function (key) { return ascending(key, set[rangePos][1]) >= 0; }; + var keyIsBeforeCurrentEntry = includeLowers ? + function (key) { return descending(key, set[rangePos][0]) > 0; } : + function (key) { return descending(key, set[rangePos][0]) >= 0; }; + function keyWithinCurrentRange(key) { + return !keyIsBeyondCurrentEntry(key) && !keyIsBeforeCurrentEntry(key); + } + var checkKey = keyIsBeyondCurrentEntry; + var c = new this.Collection(this, function () { return createRange(set[0][0], set[set.length - 1][1], !includeLowers, !includeUppers); }); + c._ondirectionchange = function (direction) { + if (direction === "next") { + checkKey = keyIsBeyondCurrentEntry; + sortDirection = ascending; + } + else { + checkKey = keyIsBeforeCurrentEntry; + sortDirection = descending; + } + set.sort(rangeSorter); + }; + c._addAlgorithm(function (cursor, advance, resolve) { + var key = cursor.key; + while (checkKey(key)) { + ++rangePos; + if (rangePos === set.length) { + advance(resolve); + return false; + } + } + if (keyWithinCurrentRange(key)) { + return true; + } + else if (_this._cmp(key, set[rangePos][1]) === 0 || _this._cmp(key, set[rangePos][0]) === 0) { + return false; + } + else { + advance(function () { + if (sortDirection === ascending) + cursor.continue(set[rangePos][0]); + else + cursor.continue(set[rangePos][1]); + }); + return false; + } + }); + return c; + }; + WhereClause.prototype.startsWithAnyOf = function () { + var set = getArrayOf.apply(NO_CHAR_ARRAY, arguments); + if (!set.every(function (s) { return typeof s === 'string'; })) { + return fail(this, "startsWithAnyOf() only works with strings"); + } + if (set.length === 0) + return emptyCollection(this); + return this.inAnyRange(set.map(function (str) { return [str, str + maxString]; })); + }; + return WhereClause; + }()); + + function createWhereClauseConstructor(db) { + return makeClassConstructor(WhereClause.prototype, function WhereClause(table, index, orCollection) { + this.db = db; + this._ctx = { + table: table, + index: index === ":id" ? null : index, + or: orCollection + }; + var indexedDB = db._deps.indexedDB; + if (!indexedDB) + throw new exceptions.MissingAPI(); + this._cmp = this._ascending = indexedDB.cmp.bind(indexedDB); + this._descending = function (a, b) { return indexedDB.cmp(b, a); }; + this._max = function (a, b) { return indexedDB.cmp(a, b) > 0 ? a : b; }; + this._min = function (a, b) { return indexedDB.cmp(a, b) < 0 ? a : b; }; + this._IDBKeyRange = db._deps.IDBKeyRange; + }); + } + + function eventRejectHandler(reject) { + return wrap(function (event) { + preventDefault(event); + reject(event.target.error); + return false; + }); + } + function preventDefault(event) { + if (event.stopPropagation) + event.stopPropagation(); + if (event.preventDefault) + event.preventDefault(); + } + + var DEXIE_STORAGE_MUTATED_EVENT_NAME = 'storagemutated'; + var STORAGE_MUTATED_DOM_EVENT_NAME = 'x-storagemutated-1'; + var globalEvents = Events(null, DEXIE_STORAGE_MUTATED_EVENT_NAME); + + var Transaction = (function () { + function Transaction() { + } + Transaction.prototype._lock = function () { + assert(!PSD.global); + ++this._reculock; + if (this._reculock === 1 && !PSD.global) + PSD.lockOwnerFor = this; + return this; + }; + Transaction.prototype._unlock = function () { + assert(!PSD.global); + if (--this._reculock === 0) { + if (!PSD.global) + PSD.lockOwnerFor = null; + while (this._blockedFuncs.length > 0 && !this._locked()) { + var fnAndPSD = this._blockedFuncs.shift(); + try { + usePSD(fnAndPSD[1], fnAndPSD[0]); + } + catch (e) { } + } + } + return this; + }; + Transaction.prototype._locked = function () { + return this._reculock && PSD.lockOwnerFor !== this; + }; + Transaction.prototype.create = function (idbtrans) { + var _this = this; + if (!this.mode) + return this; + var idbdb = this.db.idbdb; + var dbOpenError = this.db._state.dbOpenError; + assert(!this.idbtrans); + if (!idbtrans && !idbdb) { + switch (dbOpenError && dbOpenError.name) { + case "DatabaseClosedError": + throw new exceptions.DatabaseClosed(dbOpenError); + case "MissingAPIError": + throw new exceptions.MissingAPI(dbOpenError.message, dbOpenError); + default: + throw new exceptions.OpenFailed(dbOpenError); + } + } + if (!this.active) + throw new exceptions.TransactionInactive(); + assert(this._completion._state === null); + idbtrans = this.idbtrans = idbtrans || + (this.db.core + ? this.db.core.transaction(this.storeNames, this.mode, { durability: this.chromeTransactionDurability }) + : idbdb.transaction(this.storeNames, this.mode, { durability: this.chromeTransactionDurability })); + idbtrans.onerror = wrap(function (ev) { + preventDefault(ev); + _this._reject(idbtrans.error); + }); + idbtrans.onabort = wrap(function (ev) { + preventDefault(ev); + _this.active && _this._reject(new exceptions.Abort(idbtrans.error)); + _this.active = false; + _this.on("abort").fire(ev); + }); + idbtrans.oncomplete = wrap(function () { + _this.active = false; + _this._resolve(); + if ('mutatedParts' in idbtrans) { + globalEvents.storagemutated.fire(idbtrans["mutatedParts"]); + } + }); + return this; + }; + Transaction.prototype._promise = function (mode, fn, bWriteLock) { + var _this = this; + if (mode === 'readwrite' && this.mode !== 'readwrite') + return rejection(new exceptions.ReadOnly("Transaction is readonly")); + if (!this.active) + return rejection(new exceptions.TransactionInactive()); + if (this._locked()) { + return new DexiePromise(function (resolve, reject) { + _this._blockedFuncs.push([function () { + _this._promise(mode, fn, bWriteLock).then(resolve, reject); + }, PSD]); + }); + } + else if (bWriteLock) { + return newScope(function () { + var p = new DexiePromise(function (resolve, reject) { + _this._lock(); + var rv = fn(resolve, reject, _this); + if (rv && rv.then) + rv.then(resolve, reject); + }); + p.finally(function () { return _this._unlock(); }); + p._lib = true; + return p; + }); + } + else { + var p = new DexiePromise(function (resolve, reject) { + var rv = fn(resolve, reject, _this); + if (rv && rv.then) + rv.then(resolve, reject); + }); + p._lib = true; + return p; + } + }; + Transaction.prototype._root = function () { + return this.parent ? this.parent._root() : this; + }; + Transaction.prototype.waitFor = function (promiseLike) { + var root = this._root(); + var promise = DexiePromise.resolve(promiseLike); + if (root._waitingFor) { + root._waitingFor = root._waitingFor.then(function () { return promise; }); + } + else { + root._waitingFor = promise; + root._waitingQueue = []; + var store = root.idbtrans.objectStore(root.storeNames[0]); + (function spin() { + ++root._spinCount; + while (root._waitingQueue.length) + (root._waitingQueue.shift())(); + if (root._waitingFor) + store.get(-Infinity).onsuccess = spin; + }()); + } + var currentWaitPromise = root._waitingFor; + return new DexiePromise(function (resolve, reject) { + promise.then(function (res) { return root._waitingQueue.push(wrap(resolve.bind(null, res))); }, function (err) { return root._waitingQueue.push(wrap(reject.bind(null, err))); }).finally(function () { + if (root._waitingFor === currentWaitPromise) { + root._waitingFor = null; + } + }); + }); + }; + Transaction.prototype.abort = function () { + if (this.active) { + this.active = false; + if (this.idbtrans) + this.idbtrans.abort(); + this._reject(new exceptions.Abort()); + } + }; + Transaction.prototype.table = function (tableName) { + var memoizedTables = (this._memoizedTables || (this._memoizedTables = {})); + if (hasOwn(memoizedTables, tableName)) + return memoizedTables[tableName]; + var tableSchema = this.schema[tableName]; + if (!tableSchema) { + throw new exceptions.NotFound("Table " + tableName + " not part of transaction"); + } + var transactionBoundTable = new this.db.Table(tableName, tableSchema, this); + transactionBoundTable.core = this.db.core.table(tableName); + memoizedTables[tableName] = transactionBoundTable; + return transactionBoundTable; + }; + return Transaction; + }()); + + function createTransactionConstructor(db) { + return makeClassConstructor(Transaction.prototype, function Transaction(mode, storeNames, dbschema, chromeTransactionDurability, parent) { + var _this = this; + this.db = db; + this.mode = mode; + this.storeNames = storeNames; + this.schema = dbschema; + this.chromeTransactionDurability = chromeTransactionDurability; + this.idbtrans = null; + this.on = Events(this, "complete", "error", "abort"); + this.parent = parent || null; + this.active = true; + this._reculock = 0; + this._blockedFuncs = []; + this._resolve = null; + this._reject = null; + this._waitingFor = null; + this._waitingQueue = null; + this._spinCount = 0; + this._completion = new DexiePromise(function (resolve, reject) { + _this._resolve = resolve; + _this._reject = reject; + }); + this._completion.then(function () { + _this.active = false; + _this.on.complete.fire(); + }, function (e) { + var wasActive = _this.active; + _this.active = false; + _this.on.error.fire(e); + _this.parent ? + _this.parent._reject(e) : + wasActive && _this.idbtrans && _this.idbtrans.abort(); + return rejection(e); + }); + }); + } + + function createIndexSpec(name, keyPath, unique, multi, auto, compound, isPrimKey) { + return { + name: name, + keyPath: keyPath, + unique: unique, + multi: multi, + auto: auto, + compound: compound, + src: (unique && !isPrimKey ? '&' : '') + (multi ? '*' : '') + (auto ? "++" : "") + nameFromKeyPath(keyPath) + }; + } + function nameFromKeyPath(keyPath) { + return typeof keyPath === 'string' ? + keyPath : + keyPath ? ('[' + [].join.call(keyPath, '+') + ']') : ""; + } + + function createTableSchema(name, primKey, indexes) { + return { + name: name, + primKey: primKey, + indexes: indexes, + mappedClass: null, + idxByName: arrayToObject(indexes, function (index) { return [index.name, index]; }) + }; + } + + function safariMultiStoreFix(storeNames) { + return storeNames.length === 1 ? storeNames[0] : storeNames; + } + var getMaxKey = function (IdbKeyRange) { + try { + IdbKeyRange.only([[]]); + getMaxKey = function () { return [[]]; }; + return [[]]; + } + catch (e) { + getMaxKey = function () { return maxString; }; + return maxString; + } + }; + + function getKeyExtractor(keyPath) { + if (keyPath == null) { + return function () { return undefined; }; + } + else if (typeof keyPath === 'string') { + return getSinglePathKeyExtractor(keyPath); + } + else { + return function (obj) { return getByKeyPath(obj, keyPath); }; + } + } + function getSinglePathKeyExtractor(keyPath) { + var split = keyPath.split('.'); + if (split.length === 1) { + return function (obj) { return obj[keyPath]; }; + } + else { + return function (obj) { return getByKeyPath(obj, keyPath); }; + } + } + + function arrayify(arrayLike) { + return [].slice.call(arrayLike); + } + var _id_counter = 0; + function getKeyPathAlias(keyPath) { + return keyPath == null ? + ":id" : + typeof keyPath === 'string' ? + keyPath : + "[" + keyPath.join('+') + "]"; + } + function createDBCore(db, IdbKeyRange, tmpTrans) { + function extractSchema(db, trans) { + var tables = arrayify(db.objectStoreNames); + return { + schema: { + name: db.name, + tables: tables.map(function (table) { return trans.objectStore(table); }).map(function (store) { + var keyPath = store.keyPath, autoIncrement = store.autoIncrement; + var compound = isArray(keyPath); + var outbound = keyPath == null; + var indexByKeyPath = {}; + var result = { + name: store.name, + primaryKey: { + name: null, + isPrimaryKey: true, + outbound: outbound, + compound: compound, + keyPath: keyPath, + autoIncrement: autoIncrement, + unique: true, + extractKey: getKeyExtractor(keyPath) + }, + indexes: arrayify(store.indexNames).map(function (indexName) { return store.index(indexName); }) + .map(function (index) { + var name = index.name, unique = index.unique, multiEntry = index.multiEntry, keyPath = index.keyPath; + var compound = isArray(keyPath); + var result = { + name: name, + compound: compound, + keyPath: keyPath, + unique: unique, + multiEntry: multiEntry, + extractKey: getKeyExtractor(keyPath) + }; + indexByKeyPath[getKeyPathAlias(keyPath)] = result; + return result; + }), + getIndexByKeyPath: function (keyPath) { return indexByKeyPath[getKeyPathAlias(keyPath)]; } + }; + indexByKeyPath[":id"] = result.primaryKey; + if (keyPath != null) { + indexByKeyPath[getKeyPathAlias(keyPath)] = result.primaryKey; + } + return result; + }) + }, + hasGetAll: tables.length > 0 && ('getAll' in trans.objectStore(tables[0])) && + !(typeof navigator !== 'undefined' && /Safari/.test(navigator.userAgent) && + !/(Chrome\/|Edge\/)/.test(navigator.userAgent) && + [].concat(navigator.userAgent.match(/Safari\/(\d*)/))[1] < 604) + }; + } + function makeIDBKeyRange(range) { + if (range.type === 3 ) + return null; + if (range.type === 4 ) + throw new Error("Cannot convert never type to IDBKeyRange"); + var lower = range.lower, upper = range.upper, lowerOpen = range.lowerOpen, upperOpen = range.upperOpen; + var idbRange = lower === undefined ? + upper === undefined ? + null : + IdbKeyRange.upperBound(upper, !!upperOpen) : + upper === undefined ? + IdbKeyRange.lowerBound(lower, !!lowerOpen) : + IdbKeyRange.bound(lower, upper, !!lowerOpen, !!upperOpen); + return idbRange; + } + function createDbCoreTable(tableSchema) { + var tableName = tableSchema.name; + function mutate(_a) { + var trans = _a.trans, type = _a.type, keys = _a.keys, values = _a.values, range = _a.range; + return new Promise(function (resolve, reject) { + resolve = wrap(resolve); + var store = trans.objectStore(tableName); + var outbound = store.keyPath == null; + var isAddOrPut = type === "put" || type === "add"; + if (!isAddOrPut && type !== 'delete' && type !== 'deleteRange') + throw new Error("Invalid operation type: " + type); + var length = (keys || values || { length: 1 }).length; + if (keys && values && keys.length !== values.length) { + throw new Error("Given keys array must have same length as given values array."); + } + if (length === 0) + return resolve({ numFailures: 0, failures: {}, results: [], lastResult: undefined }); + var req; + var reqs = []; + var failures = []; + var numFailures = 0; + var errorHandler = function (event) { + ++numFailures; + preventDefault(event); + }; + if (type === 'deleteRange') { + if (range.type === 4 ) + return resolve({ numFailures: numFailures, failures: failures, results: [], lastResult: undefined }); + if (range.type === 3 ) + reqs.push(req = store.clear()); + else + reqs.push(req = store.delete(makeIDBKeyRange(range))); + } + else { + var _a = isAddOrPut ? + outbound ? + [values, keys] : + [values, null] : + [keys, null], args1 = _a[0], args2 = _a[1]; + if (isAddOrPut) { + for (var i = 0; i < length; ++i) { + reqs.push(req = (args2 && args2[i] !== undefined ? + store[type](args1[i], args2[i]) : + store[type](args1[i]))); + req.onerror = errorHandler; + } + } + else { + for (var i = 0; i < length; ++i) { + reqs.push(req = store[type](args1[i])); + req.onerror = errorHandler; + } + } + } + var done = function (event) { + var lastResult = event.target.result; + reqs.forEach(function (req, i) { return req.error != null && (failures[i] = req.error); }); + resolve({ + numFailures: numFailures, + failures: failures, + results: type === "delete" ? keys : reqs.map(function (req) { return req.result; }), + lastResult: lastResult + }); + }; + req.onerror = function (event) { + errorHandler(event); + done(event); + }; + req.onsuccess = done; + }); + } + function openCursor(_a) { + var trans = _a.trans, values = _a.values, query = _a.query, reverse = _a.reverse, unique = _a.unique; + return new Promise(function (resolve, reject) { + resolve = wrap(resolve); + var index = query.index, range = query.range; + var store = trans.objectStore(tableName); + var source = index.isPrimaryKey ? + store : + store.index(index.name); + var direction = reverse ? + unique ? + "prevunique" : + "prev" : + unique ? + "nextunique" : + "next"; + var req = values || !('openKeyCursor' in source) ? + source.openCursor(makeIDBKeyRange(range), direction) : + source.openKeyCursor(makeIDBKeyRange(range), direction); + req.onerror = eventRejectHandler(reject); + req.onsuccess = wrap(function (ev) { + var cursor = req.result; + if (!cursor) { + resolve(null); + return; + } + cursor.___id = ++_id_counter; + cursor.done = false; + var _cursorContinue = cursor.continue.bind(cursor); + var _cursorContinuePrimaryKey = cursor.continuePrimaryKey; + if (_cursorContinuePrimaryKey) + _cursorContinuePrimaryKey = _cursorContinuePrimaryKey.bind(cursor); + var _cursorAdvance = cursor.advance.bind(cursor); + var doThrowCursorIsNotStarted = function () { throw new Error("Cursor not started"); }; + var doThrowCursorIsStopped = function () { throw new Error("Cursor not stopped"); }; + cursor.trans = trans; + cursor.stop = cursor.continue = cursor.continuePrimaryKey = cursor.advance = doThrowCursorIsNotStarted; + cursor.fail = wrap(reject); + cursor.next = function () { + var _this = this; + var gotOne = 1; + return this.start(function () { return gotOne-- ? _this.continue() : _this.stop(); }).then(function () { return _this; }); + }; + cursor.start = function (callback) { + var iterationPromise = new Promise(function (resolveIteration, rejectIteration) { + resolveIteration = wrap(resolveIteration); + req.onerror = eventRejectHandler(rejectIteration); + cursor.fail = rejectIteration; + cursor.stop = function (value) { + cursor.stop = cursor.continue = cursor.continuePrimaryKey = cursor.advance = doThrowCursorIsStopped; + resolveIteration(value); + }; + }); + var guardedCallback = function () { + if (req.result) { + try { + callback(); + } + catch (err) { + cursor.fail(err); + } + } + else { + cursor.done = true; + cursor.start = function () { throw new Error("Cursor behind last entry"); }; + cursor.stop(); + } + }; + req.onsuccess = wrap(function (ev) { + req.onsuccess = guardedCallback; + guardedCallback(); + }); + cursor.continue = _cursorContinue; + cursor.continuePrimaryKey = _cursorContinuePrimaryKey; + cursor.advance = _cursorAdvance; + guardedCallback(); + return iterationPromise; + }; + resolve(cursor); + }, reject); + }); + } + function query(hasGetAll) { + return function (request) { + return new Promise(function (resolve, reject) { + resolve = wrap(resolve); + var trans = request.trans, values = request.values, limit = request.limit, query = request.query; + var nonInfinitLimit = limit === Infinity ? undefined : limit; + var index = query.index, range = query.range; + var store = trans.objectStore(tableName); + var source = index.isPrimaryKey ? store : store.index(index.name); + var idbKeyRange = makeIDBKeyRange(range); + if (limit === 0) + return resolve({ result: [] }); + if (hasGetAll) { + var req = values ? + source.getAll(idbKeyRange, nonInfinitLimit) : + source.getAllKeys(idbKeyRange, nonInfinitLimit); + req.onsuccess = function (event) { return resolve({ result: event.target.result }); }; + req.onerror = eventRejectHandler(reject); + } + else { + var count_1 = 0; + var req_1 = values || !('openKeyCursor' in source) ? + source.openCursor(idbKeyRange) : + source.openKeyCursor(idbKeyRange); + var result_1 = []; + req_1.onsuccess = function (event) { + var cursor = req_1.result; + if (!cursor) + return resolve({ result: result_1 }); + result_1.push(values ? cursor.value : cursor.primaryKey); + if (++count_1 === limit) + return resolve({ result: result_1 }); + cursor.continue(); + }; + req_1.onerror = eventRejectHandler(reject); + } + }); + }; + } + return { + name: tableName, + schema: tableSchema, + mutate: mutate, + getMany: function (_a) { + var trans = _a.trans, keys = _a.keys; + return new Promise(function (resolve, reject) { + resolve = wrap(resolve); + var store = trans.objectStore(tableName); + var length = keys.length; + var result = new Array(length); + var keyCount = 0; + var callbackCount = 0; + var req; + var successHandler = function (event) { + var req = event.target; + if ((result[req._pos] = req.result) != null) + ; + if (++callbackCount === keyCount) + resolve(result); + }; + var errorHandler = eventRejectHandler(reject); + for (var i = 0; i < length; ++i) { + var key = keys[i]; + if (key != null) { + req = store.get(keys[i]); + req._pos = i; + req.onsuccess = successHandler; + req.onerror = errorHandler; + ++keyCount; + } + } + if (keyCount === 0) + resolve(result); + }); + }, + get: function (_a) { + var trans = _a.trans, key = _a.key; + return new Promise(function (resolve, reject) { + resolve = wrap(resolve); + var store = trans.objectStore(tableName); + var req = store.get(key); + req.onsuccess = function (event) { return resolve(event.target.result); }; + req.onerror = eventRejectHandler(reject); + }); + }, + query: query(hasGetAll), + openCursor: openCursor, + count: function (_a) { + var query = _a.query, trans = _a.trans; + var index = query.index, range = query.range; + return new Promise(function (resolve, reject) { + var store = trans.objectStore(tableName); + var source = index.isPrimaryKey ? store : store.index(index.name); + var idbKeyRange = makeIDBKeyRange(range); + var req = idbKeyRange ? source.count(idbKeyRange) : source.count(); + req.onsuccess = wrap(function (ev) { return resolve(ev.target.result); }); + req.onerror = eventRejectHandler(reject); + }); + } + }; + } + var _a = extractSchema(db, tmpTrans), schema = _a.schema, hasGetAll = _a.hasGetAll; + var tables = schema.tables.map(function (tableSchema) { return createDbCoreTable(tableSchema); }); + var tableMap = {}; + tables.forEach(function (table) { return tableMap[table.name] = table; }); + return { + stack: "dbcore", + transaction: db.transaction.bind(db), + table: function (name) { + var result = tableMap[name]; + if (!result) + throw new Error("Table '" + name + "' not found"); + return tableMap[name]; + }, + MIN_KEY: -Infinity, + MAX_KEY: getMaxKey(IdbKeyRange), + schema: schema + }; + } + + function createMiddlewareStack(stackImpl, middlewares) { + return middlewares.reduce(function (down, _a) { + var create = _a.create; + return (__assign(__assign({}, down), create(down))); + }, stackImpl); + } + function createMiddlewareStacks(middlewares, idbdb, _a, tmpTrans) { + var IDBKeyRange = _a.IDBKeyRange; _a.indexedDB; + var dbcore = createMiddlewareStack(createDBCore(idbdb, IDBKeyRange, tmpTrans), middlewares.dbcore); + return { + dbcore: dbcore + }; + } + function generateMiddlewareStacks(_a, tmpTrans) { + var db = _a._novip; + var idbdb = tmpTrans.db; + var stacks = createMiddlewareStacks(db._middlewares, idbdb, db._deps, tmpTrans); + db.core = stacks.dbcore; + db.tables.forEach(function (table) { + var tableName = table.name; + if (db.core.schema.tables.some(function (tbl) { return tbl.name === tableName; })) { + table.core = db.core.table(tableName); + if (db[tableName] instanceof db.Table) { + db[tableName].core = table.core; + } + } + }); + } + + function setApiOnPlace(_a, objs, tableNames, dbschema) { + var db = _a._novip; + tableNames.forEach(function (tableName) { + var schema = dbschema[tableName]; + objs.forEach(function (obj) { + var propDesc = getPropertyDescriptor(obj, tableName); + if (!propDesc || ("value" in propDesc && propDesc.value === undefined)) { + if (obj === db.Transaction.prototype || obj instanceof db.Transaction) { + setProp(obj, tableName, { + get: function () { return this.table(tableName); }, + set: function (value) { + defineProperty(this, tableName, { value: value, writable: true, configurable: true, enumerable: true }); + } + }); + } + else { + obj[tableName] = new db.Table(tableName, schema); + } + } + }); + }); + } + function removeTablesApi(_a, objs) { + var db = _a._novip; + objs.forEach(function (obj) { + for (var key in obj) { + if (obj[key] instanceof db.Table) + delete obj[key]; + } + }); + } + function lowerVersionFirst(a, b) { + return a._cfg.version - b._cfg.version; + } + function runUpgraders(db, oldVersion, idbUpgradeTrans, reject) { + var globalSchema = db._dbSchema; + var trans = db._createTransaction('readwrite', db._storeNames, globalSchema); + trans.create(idbUpgradeTrans); + trans._completion.catch(reject); + var rejectTransaction = trans._reject.bind(trans); + var transless = PSD.transless || PSD; + newScope(function () { + PSD.trans = trans; + PSD.transless = transless; + if (oldVersion === 0) { + keys(globalSchema).forEach(function (tableName) { + createTable(idbUpgradeTrans, tableName, globalSchema[tableName].primKey, globalSchema[tableName].indexes); + }); + generateMiddlewareStacks(db, idbUpgradeTrans); + DexiePromise.follow(function () { return db.on.populate.fire(trans); }).catch(rejectTransaction); + } + else + updateTablesAndIndexes(db, oldVersion, trans, idbUpgradeTrans).catch(rejectTransaction); + }); + } + function updateTablesAndIndexes(_a, oldVersion, trans, idbUpgradeTrans) { + var db = _a._novip; + var queue = []; + var versions = db._versions; + var globalSchema = db._dbSchema = buildGlobalSchema(db, db.idbdb, idbUpgradeTrans); + var anyContentUpgraderHasRun = false; + var versToRun = versions.filter(function (v) { return v._cfg.version >= oldVersion; }); + versToRun.forEach(function (version) { + queue.push(function () { + var oldSchema = globalSchema; + var newSchema = version._cfg.dbschema; + adjustToExistingIndexNames(db, oldSchema, idbUpgradeTrans); + adjustToExistingIndexNames(db, newSchema, idbUpgradeTrans); + globalSchema = db._dbSchema = newSchema; + var diff = getSchemaDiff(oldSchema, newSchema); + diff.add.forEach(function (tuple) { + createTable(idbUpgradeTrans, tuple[0], tuple[1].primKey, tuple[1].indexes); + }); + diff.change.forEach(function (change) { + if (change.recreate) { + throw new exceptions.Upgrade("Not yet support for changing primary key"); + } + else { + var store_1 = idbUpgradeTrans.objectStore(change.name); + change.add.forEach(function (idx) { return addIndex(store_1, idx); }); + change.change.forEach(function (idx) { + store_1.deleteIndex(idx.name); + addIndex(store_1, idx); + }); + change.del.forEach(function (idxName) { return store_1.deleteIndex(idxName); }); + } + }); + var contentUpgrade = version._cfg.contentUpgrade; + if (contentUpgrade && version._cfg.version > oldVersion) { + generateMiddlewareStacks(db, idbUpgradeTrans); + trans._memoizedTables = {}; + anyContentUpgraderHasRun = true; + var upgradeSchema_1 = shallowClone(newSchema); + diff.del.forEach(function (table) { + upgradeSchema_1[table] = oldSchema[table]; + }); + removeTablesApi(db, [db.Transaction.prototype]); + setApiOnPlace(db, [db.Transaction.prototype], keys(upgradeSchema_1), upgradeSchema_1); + trans.schema = upgradeSchema_1; + var contentUpgradeIsAsync_1 = isAsyncFunction(contentUpgrade); + if (contentUpgradeIsAsync_1) { + incrementExpectedAwaits(); + } + var returnValue_1; + var promiseFollowed = DexiePromise.follow(function () { + returnValue_1 = contentUpgrade(trans); + if (returnValue_1) { + if (contentUpgradeIsAsync_1) { + var decrementor = decrementExpectedAwaits.bind(null, null); + returnValue_1.then(decrementor, decrementor); + } + } + }); + return (returnValue_1 && typeof returnValue_1.then === 'function' ? + DexiePromise.resolve(returnValue_1) : promiseFollowed.then(function () { return returnValue_1; })); + } + }); + queue.push(function (idbtrans) { + if (!anyContentUpgraderHasRun || !hasIEDeleteObjectStoreBug) { + var newSchema = version._cfg.dbschema; + deleteRemovedTables(newSchema, idbtrans); + } + removeTablesApi(db, [db.Transaction.prototype]); + setApiOnPlace(db, [db.Transaction.prototype], db._storeNames, db._dbSchema); + trans.schema = db._dbSchema; + }); + }); + function runQueue() { + return queue.length ? DexiePromise.resolve(queue.shift()(trans.idbtrans)).then(runQueue) : + DexiePromise.resolve(); + } + return runQueue().then(function () { + createMissingTables(globalSchema, idbUpgradeTrans); + }); + } + function getSchemaDiff(oldSchema, newSchema) { + var diff = { + del: [], + add: [], + change: [] + }; + var table; + for (table in oldSchema) { + if (!newSchema[table]) + diff.del.push(table); + } + for (table in newSchema) { + var oldDef = oldSchema[table], newDef = newSchema[table]; + if (!oldDef) { + diff.add.push([table, newDef]); + } + else { + var change = { + name: table, + def: newDef, + recreate: false, + del: [], + add: [], + change: [] + }; + if (( + '' + (oldDef.primKey.keyPath || '')) !== ('' + (newDef.primKey.keyPath || '')) || + (oldDef.primKey.auto !== newDef.primKey.auto && !isIEOrEdge)) + { + change.recreate = true; + diff.change.push(change); + } + else { + var oldIndexes = oldDef.idxByName; + var newIndexes = newDef.idxByName; + var idxName = void 0; + for (idxName in oldIndexes) { + if (!newIndexes[idxName]) + change.del.push(idxName); + } + for (idxName in newIndexes) { + var oldIdx = oldIndexes[idxName], newIdx = newIndexes[idxName]; + if (!oldIdx) + change.add.push(newIdx); + else if (oldIdx.src !== newIdx.src) + change.change.push(newIdx); + } + if (change.del.length > 0 || change.add.length > 0 || change.change.length > 0) { + diff.change.push(change); + } + } + } + } + return diff; + } + function createTable(idbtrans, tableName, primKey, indexes) { + var store = idbtrans.db.createObjectStore(tableName, primKey.keyPath ? + { keyPath: primKey.keyPath, autoIncrement: primKey.auto } : + { autoIncrement: primKey.auto }); + indexes.forEach(function (idx) { return addIndex(store, idx); }); + return store; + } + function createMissingTables(newSchema, idbtrans) { + keys(newSchema).forEach(function (tableName) { + if (!idbtrans.db.objectStoreNames.contains(tableName)) { + createTable(idbtrans, tableName, newSchema[tableName].primKey, newSchema[tableName].indexes); + } + }); + } + function deleteRemovedTables(newSchema, idbtrans) { + [].slice.call(idbtrans.db.objectStoreNames).forEach(function (storeName) { + return newSchema[storeName] == null && idbtrans.db.deleteObjectStore(storeName); + }); + } + function addIndex(store, idx) { + store.createIndex(idx.name, idx.keyPath, { unique: idx.unique, multiEntry: idx.multi }); + } + function buildGlobalSchema(db, idbdb, tmpTrans) { + var globalSchema = {}; + var dbStoreNames = slice(idbdb.objectStoreNames, 0); + dbStoreNames.forEach(function (storeName) { + var store = tmpTrans.objectStore(storeName); + var keyPath = store.keyPath; + var primKey = createIndexSpec(nameFromKeyPath(keyPath), keyPath || "", false, false, !!store.autoIncrement, keyPath && typeof keyPath !== "string", true); + var indexes = []; + for (var j = 0; j < store.indexNames.length; ++j) { + var idbindex = store.index(store.indexNames[j]); + keyPath = idbindex.keyPath; + var index = createIndexSpec(idbindex.name, keyPath, !!idbindex.unique, !!idbindex.multiEntry, false, keyPath && typeof keyPath !== "string", false); + indexes.push(index); + } + globalSchema[storeName] = createTableSchema(storeName, primKey, indexes); + }); + return globalSchema; + } + function readGlobalSchema(_a, idbdb, tmpTrans) { + var db = _a._novip; + db.verno = idbdb.version / 10; + var globalSchema = db._dbSchema = buildGlobalSchema(db, idbdb, tmpTrans); + db._storeNames = slice(idbdb.objectStoreNames, 0); + setApiOnPlace(db, [db._allTables], keys(globalSchema), globalSchema); + } + function verifyInstalledSchema(db, tmpTrans) { + var installedSchema = buildGlobalSchema(db, db.idbdb, tmpTrans); + var diff = getSchemaDiff(installedSchema, db._dbSchema); + return !(diff.add.length || diff.change.some(function (ch) { return ch.add.length || ch.change.length; })); + } + function adjustToExistingIndexNames(_a, schema, idbtrans) { + var db = _a._novip; + var storeNames = idbtrans.db.objectStoreNames; + for (var i = 0; i < storeNames.length; ++i) { + var storeName = storeNames[i]; + var store = idbtrans.objectStore(storeName); + db._hasGetAll = 'getAll' in store; + for (var j = 0; j < store.indexNames.length; ++j) { + var indexName = store.indexNames[j]; + var keyPath = store.index(indexName).keyPath; + var dexieName = typeof keyPath === 'string' ? keyPath : "[" + slice(keyPath).join('+') + "]"; + if (schema[storeName]) { + var indexSpec = schema[storeName].idxByName[dexieName]; + if (indexSpec) { + indexSpec.name = indexName; + delete schema[storeName].idxByName[dexieName]; + schema[storeName].idxByName[indexName] = indexSpec; + } + } + } + } + if (typeof navigator !== 'undefined' && /Safari/.test(navigator.userAgent) && + !/(Chrome\/|Edge\/)/.test(navigator.userAgent) && + _global.WorkerGlobalScope && _global instanceof _global.WorkerGlobalScope && + [].concat(navigator.userAgent.match(/Safari\/(\d*)/))[1] < 604) { + db._hasGetAll = false; + } + } + function parseIndexSyntax(primKeyAndIndexes) { + return primKeyAndIndexes.split(',').map(function (index, indexNum) { + index = index.trim(); + var name = index.replace(/([&*]|\+\+)/g, ""); + var keyPath = /^\[/.test(name) ? name.match(/^\[(.*)\]$/)[1].split('+') : name; + return createIndexSpec(name, keyPath || null, /\&/.test(index), /\*/.test(index), /\+\+/.test(index), isArray(keyPath), indexNum === 0); + }); + } + + var Version = (function () { + function Version() { + } + Version.prototype._parseStoresSpec = function (stores, outSchema) { + keys(stores).forEach(function (tableName) { + if (stores[tableName] !== null) { + var indexes = parseIndexSyntax(stores[tableName]); + var primKey = indexes.shift(); + if (primKey.multi) + throw new exceptions.Schema("Primary key cannot be multi-valued"); + indexes.forEach(function (idx) { + if (idx.auto) + throw new exceptions.Schema("Only primary key can be marked as autoIncrement (++)"); + if (!idx.keyPath) + throw new exceptions.Schema("Index must have a name and cannot be an empty string"); + }); + outSchema[tableName] = createTableSchema(tableName, primKey, indexes); + } + }); + }; + Version.prototype.stores = function (stores) { + var db = this.db; + this._cfg.storesSource = this._cfg.storesSource ? + extend(this._cfg.storesSource, stores) : + stores; + var versions = db._versions; + var storesSpec = {}; + var dbschema = {}; + versions.forEach(function (version) { + extend(storesSpec, version._cfg.storesSource); + dbschema = (version._cfg.dbschema = {}); + version._parseStoresSpec(storesSpec, dbschema); + }); + db._dbSchema = dbschema; + removeTablesApi(db, [db._allTables, db, db.Transaction.prototype]); + setApiOnPlace(db, [db._allTables, db, db.Transaction.prototype, this._cfg.tables], keys(dbschema), dbschema); + db._storeNames = keys(dbschema); + return this; + }; + Version.prototype.upgrade = function (upgradeFunction) { + this._cfg.contentUpgrade = promisableChain(this._cfg.contentUpgrade || nop, upgradeFunction); + return this; + }; + return Version; + }()); + + function createVersionConstructor(db) { + return makeClassConstructor(Version.prototype, function Version(versionNumber) { + this.db = db; + this._cfg = { + version: versionNumber, + storesSource: null, + dbschema: {}, + tables: {}, + contentUpgrade: null + }; + }); + } + + function getDbNamesTable(indexedDB, IDBKeyRange) { + var dbNamesDB = indexedDB["_dbNamesDB"]; + if (!dbNamesDB) { + dbNamesDB = indexedDB["_dbNamesDB"] = new Dexie$1(DBNAMES_DB, { + addons: [], + indexedDB: indexedDB, + IDBKeyRange: IDBKeyRange, + }); + dbNamesDB.version(1).stores({ dbnames: "name" }); + } + return dbNamesDB.table("dbnames"); + } + function hasDatabasesNative(indexedDB) { + return indexedDB && typeof indexedDB.databases === "function"; + } + function getDatabaseNames(_a) { + var indexedDB = _a.indexedDB, IDBKeyRange = _a.IDBKeyRange; + return hasDatabasesNative(indexedDB) + ? Promise.resolve(indexedDB.databases()).then(function (infos) { + return infos + .map(function (info) { return info.name; }) + .filter(function (name) { return name !== DBNAMES_DB; }); + }) + : getDbNamesTable(indexedDB, IDBKeyRange).toCollection().primaryKeys(); + } + function _onDatabaseCreated(_a, name) { + var indexedDB = _a.indexedDB, IDBKeyRange = _a.IDBKeyRange; + !hasDatabasesNative(indexedDB) && + name !== DBNAMES_DB && + getDbNamesTable(indexedDB, IDBKeyRange).put({ name: name }).catch(nop); + } + function _onDatabaseDeleted(_a, name) { + var indexedDB = _a.indexedDB, IDBKeyRange = _a.IDBKeyRange; + !hasDatabasesNative(indexedDB) && + name !== DBNAMES_DB && + getDbNamesTable(indexedDB, IDBKeyRange).delete(name).catch(nop); + } + + function vip(fn) { + return newScope(function () { + PSD.letThrough = true; + return fn(); + }); + } + + function idbReady() { + var isSafari = !navigator.userAgentData && + /Safari\//.test(navigator.userAgent) && + !/Chrom(e|ium)\//.test(navigator.userAgent); + if (!isSafari || !indexedDB.databases) + return Promise.resolve(); + var intervalId; + return new Promise(function (resolve) { + var tryIdb = function () { return indexedDB.databases().finally(resolve); }; + intervalId = setInterval(tryIdb, 100); + tryIdb(); + }).finally(function () { return clearInterval(intervalId); }); + } + + function dexieOpen(db) { + var state = db._state; + var indexedDB = db._deps.indexedDB; + if (state.isBeingOpened || db.idbdb) + return state.dbReadyPromise.then(function () { return state.dbOpenError ? + rejection(state.dbOpenError) : + db; }); + debug && (state.openCanceller._stackHolder = getErrorWithStack()); + state.isBeingOpened = true; + state.dbOpenError = null; + state.openComplete = false; + var openCanceller = state.openCanceller; + function throwIfCancelled() { + if (state.openCanceller !== openCanceller) + throw new exceptions.DatabaseClosed('db.open() was cancelled'); + } + var resolveDbReady = state.dbReadyResolve, + upgradeTransaction = null, wasCreated = false; + return DexiePromise.race([openCanceller, (typeof navigator === 'undefined' ? DexiePromise.resolve() : idbReady()).then(function () { return new DexiePromise(function (resolve, reject) { + throwIfCancelled(); + if (!indexedDB) + throw new exceptions.MissingAPI(); + var dbName = db.name; + var req = state.autoSchema ? + indexedDB.open(dbName) : + indexedDB.open(dbName, Math.round(db.verno * 10)); + if (!req) + throw new exceptions.MissingAPI(); + req.onerror = eventRejectHandler(reject); + req.onblocked = wrap(db._fireOnBlocked); + req.onupgradeneeded = wrap(function (e) { + upgradeTransaction = req.transaction; + if (state.autoSchema && !db._options.allowEmptyDB) { + req.onerror = preventDefault; + upgradeTransaction.abort(); + req.result.close(); + var delreq = indexedDB.deleteDatabase(dbName); + delreq.onsuccess = delreq.onerror = wrap(function () { + reject(new exceptions.NoSuchDatabase("Database " + dbName + " doesnt exist")); + }); + } + else { + upgradeTransaction.onerror = eventRejectHandler(reject); + var oldVer = e.oldVersion > Math.pow(2, 62) ? 0 : e.oldVersion; + wasCreated = oldVer < 1; + db._novip.idbdb = req.result; + runUpgraders(db, oldVer / 10, upgradeTransaction, reject); + } + }, reject); + req.onsuccess = wrap(function () { + upgradeTransaction = null; + var idbdb = db._novip.idbdb = req.result; + var objectStoreNames = slice(idbdb.objectStoreNames); + if (objectStoreNames.length > 0) + try { + var tmpTrans = idbdb.transaction(safariMultiStoreFix(objectStoreNames), 'readonly'); + if (state.autoSchema) + readGlobalSchema(db, idbdb, tmpTrans); + else { + adjustToExistingIndexNames(db, db._dbSchema, tmpTrans); + if (!verifyInstalledSchema(db, tmpTrans)) { + console.warn("Dexie SchemaDiff: Schema was extended without increasing the number passed to db.version(). Some queries may fail."); + } + } + generateMiddlewareStacks(db, tmpTrans); + } + catch (e) { + } + connections.push(db); + idbdb.onversionchange = wrap(function (ev) { + state.vcFired = true; + db.on("versionchange").fire(ev); + }); + idbdb.onclose = wrap(function (ev) { + db.on("close").fire(ev); + }); + if (wasCreated) + _onDatabaseCreated(db._deps, dbName); + resolve(); + }, reject); + }); })]).then(function () { + throwIfCancelled(); + state.onReadyBeingFired = []; + return DexiePromise.resolve(vip(function () { return db.on.ready.fire(db.vip); })).then(function fireRemainders() { + if (state.onReadyBeingFired.length > 0) { + var remainders_1 = state.onReadyBeingFired.reduce(promisableChain, nop); + state.onReadyBeingFired = []; + return DexiePromise.resolve(vip(function () { return remainders_1(db.vip); })).then(fireRemainders); + } + }); + }).finally(function () { + state.onReadyBeingFired = null; + state.isBeingOpened = false; + }).then(function () { + return db; + }).catch(function (err) { + state.dbOpenError = err; + try { + upgradeTransaction && upgradeTransaction.abort(); + } + catch (_a) { } + if (openCanceller === state.openCanceller) { + db._close(); + } + return rejection(err); + }).finally(function () { + state.openComplete = true; + resolveDbReady(); + }); + } + + function awaitIterator(iterator) { + var callNext = function (result) { return iterator.next(result); }, doThrow = function (error) { return iterator.throw(error); }, onSuccess = step(callNext), onError = step(doThrow); + function step(getNext) { + return function (val) { + var next = getNext(val), value = next.value; + return next.done ? value : + (!value || typeof value.then !== 'function' ? + isArray(value) ? Promise.all(value).then(onSuccess, onError) : onSuccess(value) : + value.then(onSuccess, onError)); + }; + } + return step(callNext)(); + } + + function extractTransactionArgs(mode, _tableArgs_, scopeFunc) { + var i = arguments.length; + if (i < 2) + throw new exceptions.InvalidArgument("Too few arguments"); + var args = new Array(i - 1); + while (--i) + args[i - 1] = arguments[i]; + scopeFunc = args.pop(); + var tables = flatten(args); + return [mode, tables, scopeFunc]; + } + function enterTransactionScope(db, mode, storeNames, parentTransaction, scopeFunc) { + return DexiePromise.resolve().then(function () { + var transless = PSD.transless || PSD; + var trans = db._createTransaction(mode, storeNames, db._dbSchema, parentTransaction); + var zoneProps = { + trans: trans, + transless: transless + }; + if (parentTransaction) { + trans.idbtrans = parentTransaction.idbtrans; + } + else { + try { + trans.create(); + db._state.PR1398_maxLoop = 3; + } + catch (ex) { + if (ex.name === errnames.InvalidState && db.isOpen() && --db._state.PR1398_maxLoop > 0) { + console.warn('Dexie: Need to reopen db'); + db._close(); + return db.open().then(function () { return enterTransactionScope(db, mode, storeNames, null, scopeFunc); }); + } + return rejection(ex); + } + } + var scopeFuncIsAsync = isAsyncFunction(scopeFunc); + if (scopeFuncIsAsync) { + incrementExpectedAwaits(); + } + var returnValue; + var promiseFollowed = DexiePromise.follow(function () { + returnValue = scopeFunc.call(trans, trans); + if (returnValue) { + if (scopeFuncIsAsync) { + var decrementor = decrementExpectedAwaits.bind(null, null); + returnValue.then(decrementor, decrementor); + } + else if (typeof returnValue.next === 'function' && typeof returnValue.throw === 'function') { + returnValue = awaitIterator(returnValue); + } + } + }, zoneProps); + return (returnValue && typeof returnValue.then === 'function' ? + DexiePromise.resolve(returnValue).then(function (x) { return trans.active ? + x + : rejection(new exceptions.PrematureCommit("Transaction committed too early. See http://bit.ly/2kdckMn")); }) + : promiseFollowed.then(function () { return returnValue; })).then(function (x) { + if (parentTransaction) + trans._resolve(); + return trans._completion.then(function () { return x; }); + }).catch(function (e) { + trans._reject(e); + return rejection(e); + }); + }); + } + + function pad(a, value, count) { + var result = isArray(a) ? a.slice() : [a]; + for (var i = 0; i < count; ++i) + result.push(value); + return result; + } + function createVirtualIndexMiddleware(down) { + return __assign(__assign({}, down), { table: function (tableName) { + var table = down.table(tableName); + var schema = table.schema; + var indexLookup = {}; + var allVirtualIndexes = []; + function addVirtualIndexes(keyPath, keyTail, lowLevelIndex) { + var keyPathAlias = getKeyPathAlias(keyPath); + var indexList = (indexLookup[keyPathAlias] = indexLookup[keyPathAlias] || []); + var keyLength = keyPath == null ? 0 : typeof keyPath === 'string' ? 1 : keyPath.length; + var isVirtual = keyTail > 0; + var virtualIndex = __assign(__assign({}, lowLevelIndex), { isVirtual: isVirtual, keyTail: keyTail, keyLength: keyLength, extractKey: getKeyExtractor(keyPath), unique: !isVirtual && lowLevelIndex.unique }); + indexList.push(virtualIndex); + if (!virtualIndex.isPrimaryKey) { + allVirtualIndexes.push(virtualIndex); + } + if (keyLength > 1) { + var virtualKeyPath = keyLength === 2 ? + keyPath[0] : + keyPath.slice(0, keyLength - 1); + addVirtualIndexes(virtualKeyPath, keyTail + 1, lowLevelIndex); + } + indexList.sort(function (a, b) { return a.keyTail - b.keyTail; }); + return virtualIndex; + } + var primaryKey = addVirtualIndexes(schema.primaryKey.keyPath, 0, schema.primaryKey); + indexLookup[":id"] = [primaryKey]; + for (var _i = 0, _a = schema.indexes; _i < _a.length; _i++) { + var index = _a[_i]; + addVirtualIndexes(index.keyPath, 0, index); + } + function findBestIndex(keyPath) { + var result = indexLookup[getKeyPathAlias(keyPath)]; + return result && result[0]; + } + function translateRange(range, keyTail) { + return { + type: range.type === 1 ? + 2 : + range.type, + lower: pad(range.lower, range.lowerOpen ? down.MAX_KEY : down.MIN_KEY, keyTail), + lowerOpen: true, + upper: pad(range.upper, range.upperOpen ? down.MIN_KEY : down.MAX_KEY, keyTail), + upperOpen: true + }; + } + function translateRequest(req) { + var index = req.query.index; + return index.isVirtual ? __assign(__assign({}, req), { query: { + index: index, + range: translateRange(req.query.range, index.keyTail) + } }) : req; + } + var result = __assign(__assign({}, table), { schema: __assign(__assign({}, schema), { primaryKey: primaryKey, indexes: allVirtualIndexes, getIndexByKeyPath: findBestIndex }), count: function (req) { + return table.count(translateRequest(req)); + }, query: function (req) { + return table.query(translateRequest(req)); + }, openCursor: function (req) { + var _a = req.query.index, keyTail = _a.keyTail, isVirtual = _a.isVirtual, keyLength = _a.keyLength; + if (!isVirtual) + return table.openCursor(req); + function createVirtualCursor(cursor) { + function _continue(key) { + key != null ? + cursor.continue(pad(key, req.reverse ? down.MAX_KEY : down.MIN_KEY, keyTail)) : + req.unique ? + cursor.continue(cursor.key.slice(0, keyLength) + .concat(req.reverse + ? down.MIN_KEY + : down.MAX_KEY, keyTail)) : + cursor.continue(); + } + var virtualCursor = Object.create(cursor, { + continue: { value: _continue }, + continuePrimaryKey: { + value: function (key, primaryKey) { + cursor.continuePrimaryKey(pad(key, down.MAX_KEY, keyTail), primaryKey); + } + }, + primaryKey: { + get: function () { + return cursor.primaryKey; + } + }, + key: { + get: function () { + var key = cursor.key; + return keyLength === 1 ? + key[0] : + key.slice(0, keyLength); + } + }, + value: { + get: function () { + return cursor.value; + } + } + }); + return virtualCursor; + } + return table.openCursor(translateRequest(req)) + .then(function (cursor) { return cursor && createVirtualCursor(cursor); }); + } }); + return result; + } }); + } + var virtualIndexMiddleware = { + stack: "dbcore", + name: "VirtualIndexMiddleware", + level: 1, + create: createVirtualIndexMiddleware + }; + + function getObjectDiff(a, b, rv, prfx) { + rv = rv || {}; + prfx = prfx || ''; + keys(a).forEach(function (prop) { + if (!hasOwn(b, prop)) { + rv[prfx + prop] = undefined; + } + else { + var ap = a[prop], bp = b[prop]; + if (typeof ap === 'object' && typeof bp === 'object' && ap && bp) { + var apTypeName = toStringTag(ap); + var bpTypeName = toStringTag(bp); + if (apTypeName !== bpTypeName) { + rv[prfx + prop] = b[prop]; + } + else if (apTypeName === 'Object') { + getObjectDiff(ap, bp, rv, prfx + prop + '.'); + } + else if (ap !== bp) { + rv[prfx + prop] = b[prop]; + } + } + else if (ap !== bp) + rv[prfx + prop] = b[prop]; + } + }); + keys(b).forEach(function (prop) { + if (!hasOwn(a, prop)) { + rv[prfx + prop] = b[prop]; + } + }); + return rv; + } + + function getEffectiveKeys(primaryKey, req) { + if (req.type === 'delete') + return req.keys; + return req.keys || req.values.map(primaryKey.extractKey); + } + + var hooksMiddleware = { + stack: "dbcore", + name: "HooksMiddleware", + level: 2, + create: function (downCore) { return (__assign(__assign({}, downCore), { table: function (tableName) { + var downTable = downCore.table(tableName); + var primaryKey = downTable.schema.primaryKey; + var tableMiddleware = __assign(__assign({}, downTable), { mutate: function (req) { + var dxTrans = PSD.trans; + var _a = dxTrans.table(tableName).hook, deleting = _a.deleting, creating = _a.creating, updating = _a.updating; + switch (req.type) { + case 'add': + if (creating.fire === nop) + break; + return dxTrans._promise('readwrite', function () { return addPutOrDelete(req); }, true); + case 'put': + if (creating.fire === nop && updating.fire === nop) + break; + return dxTrans._promise('readwrite', function () { return addPutOrDelete(req); }, true); + case 'delete': + if (deleting.fire === nop) + break; + return dxTrans._promise('readwrite', function () { return addPutOrDelete(req); }, true); + case 'deleteRange': + if (deleting.fire === nop) + break; + return dxTrans._promise('readwrite', function () { return deleteRange(req); }, true); + } + return downTable.mutate(req); + function addPutOrDelete(req) { + var dxTrans = PSD.trans; + var keys = req.keys || getEffectiveKeys(primaryKey, req); + if (!keys) + throw new Error("Keys missing"); + req = req.type === 'add' || req.type === 'put' ? __assign(__assign({}, req), { keys: keys }) : __assign({}, req); + if (req.type !== 'delete') + req.values = __spreadArray([], req.values, true); + if (req.keys) + req.keys = __spreadArray([], req.keys, true); + return getExistingValues(downTable, req, keys).then(function (existingValues) { + var contexts = keys.map(function (key, i) { + var existingValue = existingValues[i]; + var ctx = { onerror: null, onsuccess: null }; + if (req.type === 'delete') { + deleting.fire.call(ctx, key, existingValue, dxTrans); + } + else if (req.type === 'add' || existingValue === undefined) { + var generatedPrimaryKey = creating.fire.call(ctx, key, req.values[i], dxTrans); + if (key == null && generatedPrimaryKey != null) { + key = generatedPrimaryKey; + req.keys[i] = key; + if (!primaryKey.outbound) { + setByKeyPath(req.values[i], primaryKey.keyPath, key); + } + } + } + else { + var objectDiff = getObjectDiff(existingValue, req.values[i]); + var additionalChanges_1 = updating.fire.call(ctx, objectDiff, key, existingValue, dxTrans); + if (additionalChanges_1) { + var requestedValue_1 = req.values[i]; + Object.keys(additionalChanges_1).forEach(function (keyPath) { + if (hasOwn(requestedValue_1, keyPath)) { + requestedValue_1[keyPath] = additionalChanges_1[keyPath]; + } + else { + setByKeyPath(requestedValue_1, keyPath, additionalChanges_1[keyPath]); + } + }); + } + } + return ctx; + }); + return downTable.mutate(req).then(function (_a) { + var failures = _a.failures, results = _a.results, numFailures = _a.numFailures, lastResult = _a.lastResult; + for (var i = 0; i < keys.length; ++i) { + var primKey = results ? results[i] : keys[i]; + var ctx = contexts[i]; + if (primKey == null) { + ctx.onerror && ctx.onerror(failures[i]); + } + else { + ctx.onsuccess && ctx.onsuccess(req.type === 'put' && existingValues[i] ? + req.values[i] : + primKey + ); + } + } + return { failures: failures, results: results, numFailures: numFailures, lastResult: lastResult }; + }).catch(function (error) { + contexts.forEach(function (ctx) { return ctx.onerror && ctx.onerror(error); }); + return Promise.reject(error); + }); + }); + } + function deleteRange(req) { + return deleteNextChunk(req.trans, req.range, 10000); + } + function deleteNextChunk(trans, range, limit) { + return downTable.query({ trans: trans, values: false, query: { index: primaryKey, range: range }, limit: limit }) + .then(function (_a) { + var result = _a.result; + return addPutOrDelete({ type: 'delete', keys: result, trans: trans }).then(function (res) { + if (res.numFailures > 0) + return Promise.reject(res.failures[0]); + if (result.length < limit) { + return { failures: [], numFailures: 0, lastResult: undefined }; + } + else { + return deleteNextChunk(trans, __assign(__assign({}, range), { lower: result[result.length - 1], lowerOpen: true }), limit); + } + }); + }); + } + } }); + return tableMiddleware; + } })); } + }; + function getExistingValues(table, req, effectiveKeys) { + return req.type === "add" + ? Promise.resolve([]) + : table.getMany({ trans: req.trans, keys: effectiveKeys, cache: "immutable" }); + } + + function getFromTransactionCache(keys, cache, clone) { + try { + if (!cache) + return null; + if (cache.keys.length < keys.length) + return null; + var result = []; + for (var i = 0, j = 0; i < cache.keys.length && j < keys.length; ++i) { + if (cmp(cache.keys[i], keys[j]) !== 0) + continue; + result.push(clone ? deepClone(cache.values[i]) : cache.values[i]); + ++j; + } + return result.length === keys.length ? result : null; + } + catch (_a) { + return null; + } + } + var cacheExistingValuesMiddleware = { + stack: "dbcore", + level: -1, + create: function (core) { + return { + table: function (tableName) { + var table = core.table(tableName); + return __assign(__assign({}, table), { getMany: function (req) { + if (!req.cache) { + return table.getMany(req); + } + var cachedResult = getFromTransactionCache(req.keys, req.trans["_cache"], req.cache === "clone"); + if (cachedResult) { + return DexiePromise.resolve(cachedResult); + } + return table.getMany(req).then(function (res) { + req.trans["_cache"] = { + keys: req.keys, + values: req.cache === "clone" ? deepClone(res) : res, + }; + return res; + }); + }, mutate: function (req) { + if (req.type !== "add") + req.trans["_cache"] = null; + return table.mutate(req); + } }); + }, + }; + }, + }; + + var _a; + function isEmptyRange(node) { + return !("from" in node); + } + var RangeSet = function (fromOrTree, to) { + if (this) { + extend(this, arguments.length ? { d: 1, from: fromOrTree, to: arguments.length > 1 ? to : fromOrTree } : { d: 0 }); + } + else { + var rv = new RangeSet(); + if (fromOrTree && ("d" in fromOrTree)) { + extend(rv, fromOrTree); + } + return rv; + } + }; + props(RangeSet.prototype, (_a = { + add: function (rangeSet) { + mergeRanges(this, rangeSet); + return this; + }, + addKey: function (key) { + addRange(this, key, key); + return this; + }, + addKeys: function (keys) { + var _this = this; + keys.forEach(function (key) { return addRange(_this, key, key); }); + return this; + } + }, + _a[iteratorSymbol] = function () { + return getRangeSetIterator(this); + }, + _a)); + function addRange(target, from, to) { + var diff = cmp(from, to); + if (isNaN(diff)) + return; + if (diff > 0) + throw RangeError(); + if (isEmptyRange(target)) + return extend(target, { from: from, to: to, d: 1 }); + var left = target.l; + var right = target.r; + if (cmp(to, target.from) < 0) { + left + ? addRange(left, from, to) + : (target.l = { from: from, to: to, d: 1, l: null, r: null }); + return rebalance(target); + } + if (cmp(from, target.to) > 0) { + right + ? addRange(right, from, to) + : (target.r = { from: from, to: to, d: 1, l: null, r: null }); + return rebalance(target); + } + if (cmp(from, target.from) < 0) { + target.from = from; + target.l = null; + target.d = right ? right.d + 1 : 1; + } + if (cmp(to, target.to) > 0) { + target.to = to; + target.r = null; + target.d = target.l ? target.l.d + 1 : 1; + } + var rightWasCutOff = !target.r; + if (left && !target.l) { + mergeRanges(target, left); + } + if (right && rightWasCutOff) { + mergeRanges(target, right); + } + } + function mergeRanges(target, newSet) { + function _addRangeSet(target, _a) { + var from = _a.from, to = _a.to, l = _a.l, r = _a.r; + addRange(target, from, to); + if (l) + _addRangeSet(target, l); + if (r) + _addRangeSet(target, r); + } + if (!isEmptyRange(newSet)) + _addRangeSet(target, newSet); + } + function rangesOverlap(rangeSet1, rangeSet2) { + var i1 = getRangeSetIterator(rangeSet2); + var nextResult1 = i1.next(); + if (nextResult1.done) + return false; + var a = nextResult1.value; + var i2 = getRangeSetIterator(rangeSet1); + var nextResult2 = i2.next(a.from); + var b = nextResult2.value; + while (!nextResult1.done && !nextResult2.done) { + if (cmp(b.from, a.to) <= 0 && cmp(b.to, a.from) >= 0) + return true; + cmp(a.from, b.from) < 0 + ? (a = (nextResult1 = i1.next(b.from)).value) + : (b = (nextResult2 = i2.next(a.from)).value); + } + return false; + } + function getRangeSetIterator(node) { + var state = isEmptyRange(node) ? null : { s: 0, n: node }; + return { + next: function (key) { + var keyProvided = arguments.length > 0; + while (state) { + switch (state.s) { + case 0: + state.s = 1; + if (keyProvided) { + while (state.n.l && cmp(key, state.n.from) < 0) + state = { up: state, n: state.n.l, s: 1 }; + } + else { + while (state.n.l) + state = { up: state, n: state.n.l, s: 1 }; + } + case 1: + state.s = 2; + if (!keyProvided || cmp(key, state.n.to) <= 0) + return { value: state.n, done: false }; + case 2: + if (state.n.r) { + state.s = 3; + state = { up: state, n: state.n.r, s: 0 }; + continue; + } + case 3: + state = state.up; + } + } + return { done: true }; + }, + }; + } + function rebalance(target) { + var _a, _b; + var diff = (((_a = target.r) === null || _a === void 0 ? void 0 : _a.d) || 0) - (((_b = target.l) === null || _b === void 0 ? void 0 : _b.d) || 0); + var r = diff > 1 ? "r" : diff < -1 ? "l" : ""; + if (r) { + var l = r === "r" ? "l" : "r"; + var rootClone = __assign({}, target); + var oldRootRight = target[r]; + target.from = oldRootRight.from; + target.to = oldRootRight.to; + target[r] = oldRootRight[r]; + rootClone[r] = oldRootRight[l]; + target[l] = rootClone; + rootClone.d = computeDepth(rootClone); + } + target.d = computeDepth(target); + } + function computeDepth(_a) { + var r = _a.r, l = _a.l; + return (r ? (l ? Math.max(r.d, l.d) : r.d) : l ? l.d : 0) + 1; + } + + var observabilityMiddleware = { + stack: "dbcore", + level: 0, + create: function (core) { + var dbName = core.schema.name; + var FULL_RANGE = new RangeSet(core.MIN_KEY, core.MAX_KEY); + return __assign(__assign({}, core), { table: function (tableName) { + var table = core.table(tableName); + var schema = table.schema; + var primaryKey = schema.primaryKey; + var extractKey = primaryKey.extractKey, outbound = primaryKey.outbound; + var tableClone = __assign(__assign({}, table), { mutate: function (req) { + var trans = req.trans; + var mutatedParts = trans.mutatedParts || (trans.mutatedParts = {}); + var getRangeSet = function (indexName) { + var part = "idb://" + dbName + "/" + tableName + "/" + indexName; + return (mutatedParts[part] || + (mutatedParts[part] = new RangeSet())); + }; + var pkRangeSet = getRangeSet(""); + var delsRangeSet = getRangeSet(":dels"); + var type = req.type; + var _a = req.type === "deleteRange" + ? [req.range] + : req.type === "delete" + ? [req.keys] + : req.values.length < 50 + ? [[], req.values] + : [], keys = _a[0], newObjs = _a[1]; + var oldCache = req.trans["_cache"]; + return table.mutate(req).then(function (res) { + if (isArray(keys)) { + if (type !== "delete") + keys = res.results; + pkRangeSet.addKeys(keys); + var oldObjs = getFromTransactionCache(keys, oldCache); + if (!oldObjs && type !== "add") { + delsRangeSet.addKeys(keys); + } + if (oldObjs || newObjs) { + trackAffectedIndexes(getRangeSet, schema, oldObjs, newObjs); + } + } + else if (keys) { + var range = { from: keys.lower, to: keys.upper }; + delsRangeSet.add(range); + pkRangeSet.add(range); + } + else { + pkRangeSet.add(FULL_RANGE); + delsRangeSet.add(FULL_RANGE); + schema.indexes.forEach(function (idx) { return getRangeSet(idx.name).add(FULL_RANGE); }); + } + return res; + }); + } }); + var getRange = function (_a) { + var _b, _c; + var _d = _a.query, index = _d.index, range = _d.range; + return [ + index, + new RangeSet((_b = range.lower) !== null && _b !== void 0 ? _b : core.MIN_KEY, (_c = range.upper) !== null && _c !== void 0 ? _c : core.MAX_KEY), + ]; + }; + var readSubscribers = { + get: function (req) { return [primaryKey, new RangeSet(req.key)]; }, + getMany: function (req) { return [primaryKey, new RangeSet().addKeys(req.keys)]; }, + count: getRange, + query: getRange, + openCursor: getRange, + }; + keys(readSubscribers).forEach(function (method) { + tableClone[method] = function (req) { + var subscr = PSD.subscr; + if (subscr) { + var getRangeSet = function (indexName) { + var part = "idb://" + dbName + "/" + tableName + "/" + indexName; + return (subscr[part] || + (subscr[part] = new RangeSet())); + }; + var pkRangeSet_1 = getRangeSet(""); + var delsRangeSet_1 = getRangeSet(":dels"); + var _a = readSubscribers[method](req), queriedIndex = _a[0], queriedRanges = _a[1]; + getRangeSet(queriedIndex.name || "").add(queriedRanges); + if (!queriedIndex.isPrimaryKey) { + if (method === "count") { + delsRangeSet_1.add(FULL_RANGE); + } + else { + var keysPromise_1 = method === "query" && + outbound && + req.values && + table.query(__assign(__assign({}, req), { values: false })); + return table[method].apply(this, arguments).then(function (res) { + if (method === "query") { + if (outbound && req.values) { + return keysPromise_1.then(function (_a) { + var resultingKeys = _a.result; + pkRangeSet_1.addKeys(resultingKeys); + return res; + }); + } + var pKeys = req.values + ? res.result.map(extractKey) + : res.result; + if (req.values) { + pkRangeSet_1.addKeys(pKeys); + } + else { + delsRangeSet_1.addKeys(pKeys); + } + } + else if (method === "openCursor") { + var cursor_1 = res; + var wantValues_1 = req.values; + return (cursor_1 && + Object.create(cursor_1, { + key: { + get: function () { + delsRangeSet_1.addKey(cursor_1.primaryKey); + return cursor_1.key; + }, + }, + primaryKey: { + get: function () { + var pkey = cursor_1.primaryKey; + delsRangeSet_1.addKey(pkey); + return pkey; + }, + }, + value: { + get: function () { + wantValues_1 && pkRangeSet_1.addKey(cursor_1.primaryKey); + return cursor_1.value; + }, + }, + })); + } + return res; + }); + } + } + } + return table[method].apply(this, arguments); + }; + }); + return tableClone; + } }); + }, + }; + function trackAffectedIndexes(getRangeSet, schema, oldObjs, newObjs) { + function addAffectedIndex(ix) { + var rangeSet = getRangeSet(ix.name || ""); + function extractKey(obj) { + return obj != null ? ix.extractKey(obj) : null; + } + var addKeyOrKeys = function (key) { return ix.multiEntry && isArray(key) + ? key.forEach(function (key) { return rangeSet.addKey(key); }) + : rangeSet.addKey(key); }; + (oldObjs || newObjs).forEach(function (_, i) { + var oldKey = oldObjs && extractKey(oldObjs[i]); + var newKey = newObjs && extractKey(newObjs[i]); + if (cmp(oldKey, newKey) !== 0) { + if (oldKey != null) + addKeyOrKeys(oldKey); + if (newKey != null) + addKeyOrKeys(newKey); + } + }); + } + schema.indexes.forEach(addAffectedIndex); + } + + var Dexie$1 = (function () { + function Dexie(name, options) { + var _this = this; + this._middlewares = {}; + this.verno = 0; + var deps = Dexie.dependencies; + this._options = options = __assign({ + addons: Dexie.addons, autoOpen: true, + indexedDB: deps.indexedDB, IDBKeyRange: deps.IDBKeyRange }, options); + this._deps = { + indexedDB: options.indexedDB, + IDBKeyRange: options.IDBKeyRange + }; + var addons = options.addons; + this._dbSchema = {}; + this._versions = []; + this._storeNames = []; + this._allTables = {}; + this.idbdb = null; + this._novip = this; + var state = { + dbOpenError: null, + isBeingOpened: false, + onReadyBeingFired: null, + openComplete: false, + dbReadyResolve: nop, + dbReadyPromise: null, + cancelOpen: nop, + openCanceller: null, + autoSchema: true, + PR1398_maxLoop: 3 + }; + state.dbReadyPromise = new DexiePromise(function (resolve) { + state.dbReadyResolve = resolve; + }); + state.openCanceller = new DexiePromise(function (_, reject) { + state.cancelOpen = reject; + }); + this._state = state; + this.name = name; + this.on = Events(this, "populate", "blocked", "versionchange", "close", { ready: [promisableChain, nop] }); + this.on.ready.subscribe = override(this.on.ready.subscribe, function (subscribe) { + return function (subscriber, bSticky) { + Dexie.vip(function () { + var state = _this._state; + if (state.openComplete) { + if (!state.dbOpenError) + DexiePromise.resolve().then(subscriber); + if (bSticky) + subscribe(subscriber); + } + else if (state.onReadyBeingFired) { + state.onReadyBeingFired.push(subscriber); + if (bSticky) + subscribe(subscriber); + } + else { + subscribe(subscriber); + var db_1 = _this; + if (!bSticky) + subscribe(function unsubscribe() { + db_1.on.ready.unsubscribe(subscriber); + db_1.on.ready.unsubscribe(unsubscribe); + }); + } + }); + }; + }); + this.Collection = createCollectionConstructor(this); + this.Table = createTableConstructor(this); + this.Transaction = createTransactionConstructor(this); + this.Version = createVersionConstructor(this); + this.WhereClause = createWhereClauseConstructor(this); + this.on("versionchange", function (ev) { + if (ev.newVersion > 0) + console.warn("Another connection wants to upgrade database '" + _this.name + "'. Closing db now to resume the upgrade."); + else + console.warn("Another connection wants to delete database '" + _this.name + "'. Closing db now to resume the delete request."); + _this.close(); + }); + this.on("blocked", function (ev) { + if (!ev.newVersion || ev.newVersion < ev.oldVersion) + console.warn("Dexie.delete('" + _this.name + "') was blocked"); + else + console.warn("Upgrade '" + _this.name + "' blocked by other connection holding version " + ev.oldVersion / 10); + }); + this._maxKey = getMaxKey(options.IDBKeyRange); + this._createTransaction = function (mode, storeNames, dbschema, parentTransaction) { return new _this.Transaction(mode, storeNames, dbschema, _this._options.chromeTransactionDurability, parentTransaction); }; + this._fireOnBlocked = function (ev) { + _this.on("blocked").fire(ev); + connections + .filter(function (c) { return c.name === _this.name && c !== _this && !c._state.vcFired; }) + .map(function (c) { return c.on("versionchange").fire(ev); }); + }; + this.use(virtualIndexMiddleware); + this.use(hooksMiddleware); + this.use(observabilityMiddleware); + this.use(cacheExistingValuesMiddleware); + this.vip = Object.create(this, { _vip: { value: true } }); + addons.forEach(function (addon) { return addon(_this); }); + } + Dexie.prototype.version = function (versionNumber) { + if (isNaN(versionNumber) || versionNumber < 0.1) + throw new exceptions.Type("Given version is not a positive number"); + versionNumber = Math.round(versionNumber * 10) / 10; + if (this.idbdb || this._state.isBeingOpened) + throw new exceptions.Schema("Cannot add version when database is open"); + this.verno = Math.max(this.verno, versionNumber); + var versions = this._versions; + var versionInstance = versions.filter(function (v) { return v._cfg.version === versionNumber; })[0]; + if (versionInstance) + return versionInstance; + versionInstance = new this.Version(versionNumber); + versions.push(versionInstance); + versions.sort(lowerVersionFirst); + versionInstance.stores({}); + this._state.autoSchema = false; + return versionInstance; + }; + Dexie.prototype._whenReady = function (fn) { + var _this = this; + return (this.idbdb && (this._state.openComplete || PSD.letThrough || this._vip)) ? fn() : new DexiePromise(function (resolve, reject) { + if (_this._state.openComplete) { + return reject(new exceptions.DatabaseClosed(_this._state.dbOpenError)); + } + if (!_this._state.isBeingOpened) { + if (!_this._options.autoOpen) { + reject(new exceptions.DatabaseClosed()); + return; + } + _this.open().catch(nop); + } + _this._state.dbReadyPromise.then(resolve, reject); + }).then(fn); + }; + Dexie.prototype.use = function (_a) { + var stack = _a.stack, create = _a.create, level = _a.level, name = _a.name; + if (name) + this.unuse({ stack: stack, name: name }); + var middlewares = this._middlewares[stack] || (this._middlewares[stack] = []); + middlewares.push({ stack: stack, create: create, level: level == null ? 10 : level, name: name }); + middlewares.sort(function (a, b) { return a.level - b.level; }); + return this; + }; + Dexie.prototype.unuse = function (_a) { + var stack = _a.stack, name = _a.name, create = _a.create; + if (stack && this._middlewares[stack]) { + this._middlewares[stack] = this._middlewares[stack].filter(function (mw) { + return create ? mw.create !== create : + name ? mw.name !== name : + false; + }); + } + return this; + }; + Dexie.prototype.open = function () { + return dexieOpen(this); + }; + Dexie.prototype._close = function () { + var state = this._state; + var idx = connections.indexOf(this); + if (idx >= 0) + connections.splice(idx, 1); + if (this.idbdb) { + try { + this.idbdb.close(); + } + catch (e) { } + this._novip.idbdb = null; + } + state.dbReadyPromise = new DexiePromise(function (resolve) { + state.dbReadyResolve = resolve; + }); + state.openCanceller = new DexiePromise(function (_, reject) { + state.cancelOpen = reject; + }); + }; + Dexie.prototype.close = function () { + this._close(); + var state = this._state; + this._options.autoOpen = false; + state.dbOpenError = new exceptions.DatabaseClosed(); + if (state.isBeingOpened) + state.cancelOpen(state.dbOpenError); + }; + Dexie.prototype.delete = function () { + var _this = this; + var hasArguments = arguments.length > 0; + var state = this._state; + return new DexiePromise(function (resolve, reject) { + var doDelete = function () { + _this.close(); + var req = _this._deps.indexedDB.deleteDatabase(_this.name); + req.onsuccess = wrap(function () { + _onDatabaseDeleted(_this._deps, _this.name); + resolve(); + }); + req.onerror = eventRejectHandler(reject); + req.onblocked = _this._fireOnBlocked; + }; + if (hasArguments) + throw new exceptions.InvalidArgument("Arguments not allowed in db.delete()"); + if (state.isBeingOpened) { + state.dbReadyPromise.then(doDelete); + } + else { + doDelete(); + } + }); + }; + Dexie.prototype.backendDB = function () { + return this.idbdb; + }; + Dexie.prototype.isOpen = function () { + return this.idbdb !== null; + }; + Dexie.prototype.hasBeenClosed = function () { + var dbOpenError = this._state.dbOpenError; + return dbOpenError && (dbOpenError.name === 'DatabaseClosed'); + }; + Dexie.prototype.hasFailed = function () { + return this._state.dbOpenError !== null; + }; + Dexie.prototype.dynamicallyOpened = function () { + return this._state.autoSchema; + }; + Object.defineProperty(Dexie.prototype, "tables", { + get: function () { + var _this = this; + return keys(this._allTables).map(function (name) { return _this._allTables[name]; }); + }, + enumerable: false, + configurable: true + }); + Dexie.prototype.transaction = function () { + var args = extractTransactionArgs.apply(this, arguments); + return this._transaction.apply(this, args); + }; + Dexie.prototype._transaction = function (mode, tables, scopeFunc) { + var _this = this; + var parentTransaction = PSD.trans; + if (!parentTransaction || parentTransaction.db !== this || mode.indexOf('!') !== -1) + parentTransaction = null; + var onlyIfCompatible = mode.indexOf('?') !== -1; + mode = mode.replace('!', '').replace('?', ''); + var idbMode, storeNames; + try { + storeNames = tables.map(function (table) { + var storeName = table instanceof _this.Table ? table.name : table; + if (typeof storeName !== 'string') + throw new TypeError("Invalid table argument to Dexie.transaction(). Only Table or String are allowed"); + return storeName; + }); + if (mode == "r" || mode === READONLY) + idbMode = READONLY; + else if (mode == "rw" || mode == READWRITE) + idbMode = READWRITE; + else + throw new exceptions.InvalidArgument("Invalid transaction mode: " + mode); + if (parentTransaction) { + if (parentTransaction.mode === READONLY && idbMode === READWRITE) { + if (onlyIfCompatible) { + parentTransaction = null; + } + else + throw new exceptions.SubTransaction("Cannot enter a sub-transaction with READWRITE mode when parent transaction is READONLY"); + } + if (parentTransaction) { + storeNames.forEach(function (storeName) { + if (parentTransaction && parentTransaction.storeNames.indexOf(storeName) === -1) { + if (onlyIfCompatible) { + parentTransaction = null; + } + else + throw new exceptions.SubTransaction("Table " + storeName + + " not included in parent transaction."); + } + }); + } + if (onlyIfCompatible && parentTransaction && !parentTransaction.active) { + parentTransaction = null; + } + } + } + catch (e) { + return parentTransaction ? + parentTransaction._promise(null, function (_, reject) { reject(e); }) : + rejection(e); + } + var enterTransaction = enterTransactionScope.bind(null, this, idbMode, storeNames, parentTransaction, scopeFunc); + return (parentTransaction ? + parentTransaction._promise(idbMode, enterTransaction, "lock") : + PSD.trans ? + usePSD(PSD.transless, function () { return _this._whenReady(enterTransaction); }) : + this._whenReady(enterTransaction)); + }; + Dexie.prototype.table = function (tableName) { + if (!hasOwn(this._allTables, tableName)) { + throw new exceptions.InvalidTable("Table " + tableName + " does not exist"); + } + return this._allTables[tableName]; + }; + return Dexie; + }()); + + var symbolObservable = typeof Symbol !== "undefined" && "observable" in Symbol + ? Symbol.observable + : "@@observable"; + var Observable = (function () { + function Observable(subscribe) { + this._subscribe = subscribe; + } + Observable.prototype.subscribe = function (x, error, complete) { + return this._subscribe(!x || typeof x === "function" ? { next: x, error: error, complete: complete } : x); + }; + Observable.prototype[symbolObservable] = function () { + return this; + }; + return Observable; + }()); + + function extendObservabilitySet(target, newSet) { + keys(newSet).forEach(function (part) { + var rangeSet = target[part] || (target[part] = new RangeSet()); + mergeRanges(rangeSet, newSet[part]); + }); + return target; + } + + function liveQuery(querier) { + var hasValue = false; + var currentValue = undefined; + var observable = new Observable(function (observer) { + var scopeFuncIsAsync = isAsyncFunction(querier); + function execute(subscr) { + if (scopeFuncIsAsync) { + incrementExpectedAwaits(); + } + var exec = function () { return newScope(querier, { subscr: subscr, trans: null }); }; + var rv = PSD.trans + ? + usePSD(PSD.transless, exec) + : exec(); + if (scopeFuncIsAsync) { + rv.then(decrementExpectedAwaits, decrementExpectedAwaits); + } + return rv; + } + var closed = false; + var accumMuts = {}; + var currentObs = {}; + var subscription = { + get closed() { + return closed; + }, + unsubscribe: function () { + closed = true; + globalEvents.storagemutated.unsubscribe(mutationListener); + }, + }; + observer.start && observer.start(subscription); + var querying = false, startedListening = false; + function shouldNotify() { + return keys(currentObs).some(function (key) { + return accumMuts[key] && rangesOverlap(accumMuts[key], currentObs[key]); + }); + } + var mutationListener = function (parts) { + extendObservabilitySet(accumMuts, parts); + if (shouldNotify()) { + doQuery(); + } + }; + var doQuery = function () { + if (querying || closed) + return; + accumMuts = {}; + var subscr = {}; + var ret = execute(subscr); + if (!startedListening) { + globalEvents(DEXIE_STORAGE_MUTATED_EVENT_NAME, mutationListener); + startedListening = true; + } + querying = true; + Promise.resolve(ret).then(function (result) { + hasValue = true; + currentValue = result; + querying = false; + if (closed) + return; + if (shouldNotify()) { + doQuery(); + } + else { + accumMuts = {}; + currentObs = subscr; + observer.next && observer.next(result); + } + }, function (err) { + querying = false; + hasValue = false; + observer.error && observer.error(err); + subscription.unsubscribe(); + }); + }; + doQuery(); + return subscription; + }); + observable.hasValue = function () { return hasValue; }; + observable.getValue = function () { return currentValue; }; + return observable; + } + + var domDeps; + try { + domDeps = { + indexedDB: _global.indexedDB || _global.mozIndexedDB || _global.webkitIndexedDB || _global.msIndexedDB, + IDBKeyRange: _global.IDBKeyRange || _global.webkitIDBKeyRange + }; + } + catch (e) { + domDeps = { indexedDB: null, IDBKeyRange: null }; + } + + var Dexie = Dexie$1; + props(Dexie, __assign(__assign({}, fullNameExceptions), { + delete: function (databaseName) { + var db = new Dexie(databaseName, { addons: [] }); + return db.delete(); + }, + exists: function (name) { + return new Dexie(name, { addons: [] }).open().then(function (db) { + db.close(); + return true; + }).catch('NoSuchDatabaseError', function () { return false; }); + }, + getDatabaseNames: function (cb) { + try { + return getDatabaseNames(Dexie.dependencies).then(cb); + } + catch (_a) { + return rejection(new exceptions.MissingAPI()); + } + }, + defineClass: function () { + function Class(content) { + extend(this, content); + } + return Class; + }, ignoreTransaction: function (scopeFunc) { + return PSD.trans ? + usePSD(PSD.transless, scopeFunc) : + scopeFunc(); + }, vip: vip, async: function (generatorFn) { + return function () { + try { + var rv = awaitIterator(generatorFn.apply(this, arguments)); + if (!rv || typeof rv.then !== 'function') + return DexiePromise.resolve(rv); + return rv; + } + catch (e) { + return rejection(e); + } + }; + }, spawn: function (generatorFn, args, thiz) { + try { + var rv = awaitIterator(generatorFn.apply(thiz, args || [])); + if (!rv || typeof rv.then !== 'function') + return DexiePromise.resolve(rv); + return rv; + } + catch (e) { + return rejection(e); + } + }, + currentTransaction: { + get: function () { return PSD.trans || null; } + }, waitFor: function (promiseOrFunction, optionalTimeout) { + var promise = DexiePromise.resolve(typeof promiseOrFunction === 'function' ? + Dexie.ignoreTransaction(promiseOrFunction) : + promiseOrFunction) + .timeout(optionalTimeout || 60000); + return PSD.trans ? + PSD.trans.waitFor(promise) : + promise; + }, + Promise: DexiePromise, + debug: { + get: function () { return debug; }, + set: function (value) { + setDebug(value, value === 'dexie' ? function () { return true; } : dexieStackFrameFilter); + } + }, + derive: derive, extend: extend, props: props, override: override, + Events: Events, on: globalEvents, liveQuery: liveQuery, extendObservabilitySet: extendObservabilitySet, + getByKeyPath: getByKeyPath, setByKeyPath: setByKeyPath, delByKeyPath: delByKeyPath, shallowClone: shallowClone, deepClone: deepClone, getObjectDiff: getObjectDiff, cmp: cmp, asap: asap$1, + minKey: minKey, + addons: [], + connections: connections, + errnames: errnames, + dependencies: domDeps, + semVer: DEXIE_VERSION, version: DEXIE_VERSION.split('.') + .map(function (n) { return parseInt(n); }) + .reduce(function (p, c, i) { return p + (c / Math.pow(10, i * 2)); }) })); + Dexie.maxKey = getMaxKey(Dexie.dependencies.IDBKeyRange); + + if (typeof dispatchEvent !== 'undefined' && typeof addEventListener !== 'undefined') { + globalEvents(DEXIE_STORAGE_MUTATED_EVENT_NAME, function (updatedParts) { + if (!propagatingLocally) { + var event_1; + if (isIEOrEdge) { + event_1 = document.createEvent('CustomEvent'); + event_1.initCustomEvent(STORAGE_MUTATED_DOM_EVENT_NAME, true, true, updatedParts); + } + else { + event_1 = new CustomEvent(STORAGE_MUTATED_DOM_EVENT_NAME, { + detail: updatedParts + }); + } + propagatingLocally = true; + dispatchEvent(event_1); + propagatingLocally = false; + } + }); + addEventListener(STORAGE_MUTATED_DOM_EVENT_NAME, function (_a) { + var detail = _a.detail; + if (!propagatingLocally) { + propagateLocally(detail); + } + }); + } + function propagateLocally(updateParts) { + var wasMe = propagatingLocally; + try { + propagatingLocally = true; + globalEvents.storagemutated.fire(updateParts); + } + finally { + propagatingLocally = wasMe; + } + } + var propagatingLocally = false; + + if (typeof BroadcastChannel !== 'undefined') { + var bc_1 = new BroadcastChannel(STORAGE_MUTATED_DOM_EVENT_NAME); + if (typeof bc_1.unref === 'function') { + bc_1.unref(); + } + globalEvents(DEXIE_STORAGE_MUTATED_EVENT_NAME, function (changedParts) { + if (!propagatingLocally) { + bc_1.postMessage(changedParts); + } + }); + bc_1.onmessage = function (ev) { + if (ev.data) + propagateLocally(ev.data); + }; + } + else if (typeof self !== 'undefined' && typeof navigator !== 'undefined') { + globalEvents(DEXIE_STORAGE_MUTATED_EVENT_NAME, function (changedParts) { + try { + if (!propagatingLocally) { + if (typeof localStorage !== 'undefined') { + localStorage.setItem(STORAGE_MUTATED_DOM_EVENT_NAME, JSON.stringify({ + trig: Math.random(), + changedParts: changedParts, + })); + } + if (typeof self['clients'] === 'object') { + __spreadArray([], self['clients'].matchAll({ includeUncontrolled: true }), true).forEach(function (client) { + return client.postMessage({ + type: STORAGE_MUTATED_DOM_EVENT_NAME, + changedParts: changedParts, + }); + }); + } + } + } + catch (_a) { } + }); + if (typeof addEventListener !== 'undefined') { + addEventListener('storage', function (ev) { + if (ev.key === STORAGE_MUTATED_DOM_EVENT_NAME) { + var data = JSON.parse(ev.newValue); + if (data) + propagateLocally(data.changedParts); + } + }); + } + var swContainer = self.document && navigator.serviceWorker; + if (swContainer) { + swContainer.addEventListener('message', propagateMessageLocally); + } + } + function propagateMessageLocally(_a) { + var data = _a.data; + if (data && data.type === STORAGE_MUTATED_DOM_EVENT_NAME) { + propagateLocally(data.changedParts); + } + } + + DexiePromise.rejectionMapper = mapError; + setDebug(debug, dexieStackFrameFilter); + + var namedExports = /*#__PURE__*/Object.freeze({ + __proto__: null, + Dexie: Dexie$1, + liveQuery: liveQuery, + 'default': Dexie$1, + RangeSet: RangeSet, + mergeRanges: mergeRanges, + rangesOverlap: rangesOverlap + }); + + __assign(Dexie$1, namedExports, { default: Dexie$1 }); + + return Dexie$1; + +})); +//# sourceMappingURL=dexie.js.map diff --git a/src/lib/rlottie/RLottie.ts b/src/lib/rlottie/RLottie.ts index 4a6c8fc4..bd9fb720 100644 --- a/src/lib/rlottie/RLottie.ts +++ b/src/lib/rlottie/RLottie.ts @@ -159,6 +159,8 @@ class RLottie { } pause(viewId?: string) { + this.lastRenderAt = undefined; + if (viewId) { this.views.get(viewId)!.isPaused = true; @@ -492,7 +494,7 @@ class RLottie { const now = Date.now(); const currentSpeed = this.lastRenderAt ? this.msPerFrame / (now - this.lastRenderAt) : 1; - const delta = Math.min(1, (this.direction * this.speed) / currentSpeed); + const delta = (this.direction * this.speed) / currentSpeed; const expectedNextFrameIndex = Math.round(this.approxFrameIndex + delta); this.lastRenderAt = now; diff --git a/src/lib/teact/dom-events.ts b/src/lib/teact/dom-events.ts index f303fba1..605b7f49 100644 --- a/src/lib/teact/dom-events.ts +++ b/src/lib/teact/dom-events.ts @@ -1,15 +1,15 @@ import { DEBUG } from '../../config'; type Handler = (e: Event) => void; -type DelegationRegistry = Map; +type DelegationRegistry = Map; const NON_BUBBLEABLE_EVENTS = new Set(['scroll', 'mouseenter', 'mouseleave', 'load']); const documentEventCounters: Record = {}; const delegationRegistryByEventType: Record = {}; -const delegatedEventTypesByElement = new Map>(); +const delegatedEventTypesByElement = new Map>(); -export function addEventListener(element: HTMLElement, propName: string, handler: Handler, asCapture = false) { +export function addEventListener(element: Element, propName: string, handler: Handler, asCapture = false) { const eventType = resolveEventType(propName, element); if (canUseEventDelegation(eventType, element, asCapture)) { addDelegatedListener(eventType, element, handler); @@ -18,7 +18,7 @@ export function addEventListener(element: HTMLElement, propName: string, handler } } -export function removeEventListener(element: HTMLElement, propName: string, handler: Handler, asCapture = false) { +export function removeEventListener(element: Element, propName: string, handler: Handler, asCapture = false) { const eventType = resolveEventType(propName, element); if (canUseEventDelegation(eventType, element, asCapture)) { removeDelegatedListener(eventType, element); @@ -27,7 +27,7 @@ export function removeEventListener(element: HTMLElement, propName: string, hand } } -function resolveEventType(propName: string, element: HTMLElement) { +function resolveEventType(propName: string, element: Element) { const eventType = propName .replace(/^on/, '') .replace(/Capture$/, '').toLowerCase(); @@ -54,7 +54,7 @@ function resolveEventType(propName: string, element: HTMLElement) { return eventType; } -function canUseEventDelegation(realEventType: string, element: HTMLElement, asCapture: boolean) { +function canUseEventDelegation(realEventType: string, element: Element, asCapture: boolean) { return ( !asCapture && !NON_BUBBLEABLE_EVENTS.has(realEventType) @@ -63,7 +63,7 @@ function canUseEventDelegation(realEventType: string, element: HTMLElement, asCa ); } -function addDelegatedListener(eventType: string, element: HTMLElement, handler: Handler) { +function addDelegatedListener(eventType: string, element: Element, handler: Handler) { if (!documentEventCounters[eventType]) { documentEventCounters[eventType] = 0; document.addEventListener(eventType, handleEvent); @@ -74,7 +74,7 @@ function addDelegatedListener(eventType: string, element: HTMLElement, handler: documentEventCounters[eventType]++; } -function removeDelegatedListener(eventType: string, element: HTMLElement) { +function removeDelegatedListener(eventType: string, element: Element) { documentEventCounters[eventType]--; if (!documentEventCounters[eventType]) { // Synchronous deletion on 0 will cause perf degradation in the case of 1 element @@ -86,7 +86,7 @@ function removeDelegatedListener(eventType: string, element: HTMLElement) { delegatedEventTypesByElement.get(element)!.delete(eventType); } -export function removeAllDelegatedListeners(element: HTMLElement) { +export function removeAllDelegatedListeners(element: Element) { const eventTypes = delegatedEventTypesByElement.get(element); if (!eventTypes) { return; @@ -101,7 +101,7 @@ function handleEvent(realEvent: Event) { if (events) { let furtherCallsPrevented = false; - let current: HTMLElement = realEvent.target as HTMLElement; + let current: Element = realEvent.target as Element; const stopPropagation = () => { furtherCallsPrevented = true; @@ -138,7 +138,7 @@ function handleEvent(realEvent: Event) { } } - current = current.parentNode as HTMLElement; + current = current.parentNode as Element; } } } @@ -151,7 +151,7 @@ function resolveDelegationRegistry(eventType: string) { return delegationRegistryByEventType[eventType]; } -function resolveDelegatedEventTypes(element: HTMLElement) { +function resolveDelegatedEventTypes(element: Element) { const existing = delegatedEventTypesByElement.get(element); if (existing) { return existing; diff --git a/src/lib/teact/teact-dom.ts b/src/lib/teact/teact-dom.ts index 31214262..4c9e288c 100644 --- a/src/lib/teact/teact-dom.ts +++ b/src/lib/teact/teact-dom.ts @@ -34,6 +34,8 @@ interface SelectionState { isCaretAtEnd: boolean; } +type DOMElement = HTMLElement | SVGElement; + const FILTERED_ATTRIBUTES = new Set(['key', 'ref', 'teactFastList', 'teactOrderKey']); const HTML_ATTRIBUTES = new Set(['dir', 'role', 'form']); const CONTROLLABLE_TAGS = ['INPUT', 'TEXTAREA', 'SELECT']; @@ -73,7 +75,7 @@ function render($element: VirtualElement | undefined, parentEl: HTMLElement) { } function renderWithVirtual( - parentEl: HTMLElement, + parentEl: DOMElement, $current: VirtualElement | undefined, $new: T, $parent: VirtualElementParent | VirtualDomHead, @@ -83,17 +85,22 @@ function renderWithVirtual( nextSibling?: ChildNode; forceMoveToEnd?: boolean; fragment?: DocumentFragment; + isSvg?: true; } = {}, ): T { const { skipComponentUpdate, fragment } = options; - let { nextSibling } = options; + let { nextSibling, isSvg } = options; - const isCurrentComponent = $current && $current.type === VirtualType.Component; - const isNewComponent = $new && $new.type === VirtualType.Component; + const isCurrentComponent = $current?.type === VirtualType.Component; + const isNewComponent = $new?.type === VirtualType.Component; const $newAsReal = $new as VirtualElementReal; - const isCurrentFragment = $current && !isCurrentComponent && $current.type === VirtualType.Fragment; - const isNewFragment = $new && !isNewComponent && $new.type === VirtualType.Fragment; + const isCurrentFragment = !isCurrentComponent && $current?.type === VirtualType.Fragment; + const isNewFragment = !isNewComponent && $new?.type === VirtualType.Fragment; + + if ($new?.type === VirtualType.Tag && $new.tag === 'svg') { + isSvg = true; + } if ( !skipComponentUpdate @@ -129,7 +136,9 @@ function renderWithVirtual( $new = initComponent(parentEl, $new as VirtualElementComponent, $parent, index) as unknown as typeof $new; } - mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, { nextSibling, fragment }); + mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, { + nextSibling, fragment, isSvg, + }); } else { const canSetTextContent = !fragment && !nextSibling @@ -141,12 +150,12 @@ function renderWithVirtual( parentEl.textContent = $newAsReal.value; $newAsReal.target = parentEl.firstChild!; } else { - const node = createNode($newAsReal); + const node = createNode($newAsReal, isSvg); $newAsReal.target = node; insertBefore(fragment || parentEl, node, nextSibling); if ($newAsReal.type === VirtualType.Tag) { - setElementRef($newAsReal, node as HTMLElement); + setElementRef($newAsReal, node as DOMElement); } } } @@ -164,14 +173,16 @@ function renderWithVirtual( } remount(parentEl, $current, undefined); - mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, { nextSibling, fragment }); + mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, { + nextSibling, fragment, isSvg, + }); } else { - const node = createNode($newAsReal); + const node = createNode($newAsReal, isSvg); $newAsReal.target = node; remount(parentEl, $current, node, nextSibling); if ($newAsReal.type === VirtualType.Tag) { - setElementRef($newAsReal, node as HTMLElement); + setElementRef($newAsReal, node as DOMElement); } } } else { @@ -198,14 +209,14 @@ function renderWithVirtual( const $newAsTag = $new as VirtualElementTag; setElementRef($current, undefined); - setElementRef($newAsTag, currentTarget as HTMLElement); + setElementRef($newAsTag, currentTarget as DOMElement); if (nextSibling || options.forceMoveToEnd) { insertBefore(parentEl, currentTarget, nextSibling); } - updateAttributes($current, $newAsTag, currentTarget as HTMLElement); - renderChildren($current, $newAsTag, currentTarget as HTMLElement); + updateAttributes($current, $newAsTag, currentTarget as DOMElement, isSvg); + renderChildren($current, $newAsTag, currentTarget as DOMElement, undefined, undefined, isSvg); } } } @@ -215,7 +226,7 @@ function renderWithVirtual( } function initComponent( - parentEl: HTMLElement, + parentEl: DOMElement, $element: VirtualElementComponent, $parent: VirtualElementParent | VirtualDomHead, index: number, @@ -237,7 +248,7 @@ function updateComponent($current: VirtualElementComponent, $new: VirtualElement } function setupComponentUpdateListener( - parentEl: HTMLElement, + parentEl: DOMElement, $element: VirtualElementComponent, $parent: VirtualElementParent | VirtualDomHead, index: number, @@ -257,11 +268,12 @@ function setupComponentUpdateListener( } function mountChildren( - parentEl: HTMLElement, + parentEl: DOMElement, $element: VirtualElementComponent | VirtualElementFragment, options: { nextSibling?: ChildNode; fragment?: DocumentFragment; + isSvg?: true; }, ) { const { children } = $element; @@ -274,13 +286,13 @@ function mountChildren( } } -function unmountChildren(parentEl: HTMLElement, $element: VirtualElementComponent | VirtualElementFragment) { +function unmountChildren(parentEl: DOMElement, $element: VirtualElementComponent | VirtualElementFragment) { for (const $child of $element.children) { renderWithVirtual(parentEl, $child, undefined, $element, -1); } } -function createNode($element: VirtualElementReal): Node { +function createNode($element: VirtualElementReal, isSvg?: true): Node { if ($element.type === VirtualType.Empty) { return document.createTextNode(''); } @@ -290,7 +302,7 @@ function createNode($element: VirtualElementReal): Node { } const { tag, props, children } = $element; - const element = document.createElement(tag); + const element = isSvg ? document.createElementNS('http://www.w3.org/2000/svg', tag) : document.createElement(tag); processControlled(tag, props); @@ -299,7 +311,7 @@ function createNode($element: VirtualElementReal): Node { if (!props.hasOwnProperty(key)) continue; if (props[key] !== undefined) { - setAttribute(element, key, props[key]); + setAttribute(element, key, props[key], isSvg); } } @@ -307,7 +319,7 @@ function createNode($element: VirtualElementReal): Node { for (let i = 0, l = children.length; i < l; i++) { const $child = children[i]; - const $renderedChild = renderWithVirtual(element, undefined, $child, $element, i); + const $renderedChild = renderWithVirtual(element, undefined, $child, $element, i, { isSvg }); if ($renderedChild !== $child) { children[i] = $renderedChild; } @@ -317,7 +329,7 @@ function createNode($element: VirtualElementReal): Node { } function remount( - parentEl: HTMLElement, + parentEl: DOMElement, $current: VirtualElement, node: Node | undefined, componentNextSibling?: ChildNode, @@ -368,7 +380,7 @@ function unmountRealTree($element: VirtualElement) { } } -function insertBefore(parentEl: HTMLElement | DocumentFragment, node: Node, nextSibling?: ChildNode) { +function insertBefore(parentEl: DOMElement | DocumentFragment, node: Node, nextSibling?: ChildNode) { if (nextSibling) { parentEl.insertBefore(node, nextSibling); } else { @@ -388,9 +400,10 @@ function getNextSibling($current: VirtualElement): ChildNode | undefined { function renderChildren( $current: VirtualElementParent, $new: VirtualElementParent, - currentEl: HTMLElement, + currentEl: DOMElement, nextSibling?: ChildNode, forceMoveToEnd = false, + isSvg?: true, ) { if (DEBUG) { DEBUG_checkKeyUniqueness($new.children); @@ -421,7 +434,7 @@ function renderChildren( newChildren[i], $new, i, - i >= currentChildrenLength ? { fragment } : { nextSibling, forceMoveToEnd }, + i >= currentChildrenLength ? { fragment, isSvg } : { nextSibling, forceMoveToEnd, isSvg }, ); if ($renderedChild && $renderedChild !== newChildren[i]) { @@ -436,7 +449,7 @@ function renderChildren( // This function allows to prepend/append a bunch of new DOM nodes to the top/bottom of preserved ones. // It also allows to selectively move particular preserved nodes within their DOM list. -function renderFastListChildren($current: VirtualElementParent, $new: VirtualElementParent, currentEl: HTMLElement) { +function renderFastListChildren($current: VirtualElementParent, $new: VirtualElementParent, currentEl: DOMElement) { const currentChildren = $current.children; const newChildren = $new.children; @@ -551,7 +564,7 @@ function renderFastListChildren($current: VirtualElementParent, $new: VirtualEle } function renderFragment( - fragmentIndex: number, fragmentSize: number, parentEl: HTMLElement, $parent: VirtualElementParent, + fragmentIndex: number, fragmentSize: number, parentEl: DOMElement, $parent: VirtualElementParent, ) { const nextSibling = parentEl.childNodes[fragmentIndex]; @@ -578,13 +591,13 @@ function renderFragment( insertBefore(parentEl, fragment, nextSibling); } -function setElementRef($element: VirtualElementTag, htmlElement: HTMLElement | undefined) { +function setElementRef($element: VirtualElementTag, DOMElement: DOMElement | undefined) { const { ref } = $element.props; if (typeof ref === 'object') { - ref.current = htmlElement; + ref.current = DOMElement; } else if (typeof ref === 'function') { - ref(htmlElement); + ref(DOMElement); } } @@ -631,7 +644,7 @@ function processControlled(tag: string, props: AnyLiteral) { }; } -function processUncontrolledOnMount(element: HTMLElement, props: AnyLiteral) { +function processUncontrolledOnMount(element: DOMElement, props: AnyLiteral) { if (!CONTROLLABLE_TAGS.includes(element.tagName)) { return; } @@ -645,7 +658,7 @@ function processUncontrolledOnMount(element: HTMLElement, props: AnyLiteral) { } } -function updateAttributes($current: VirtualElementTag, $new: VirtualElementTag, element: HTMLElement) { +function updateAttributes($current: VirtualElementTag, $new: VirtualElementTag, element: DOMElement, isSvg?: true) { processControlled(element.tagName, $new.props); const currentEntries = Object.entries($current.props); @@ -669,14 +682,14 @@ function updateAttributes($current: VirtualElementTag, $new: VirtualElementTag, const currentValue = $current.props[key]; if (newValue !== undefined && newValue !== currentValue) { - setAttribute(element, key, newValue); + setAttribute(element, key, newValue, isSvg); } } } -function setAttribute(element: HTMLElement, key: string, value: any) { +function setAttribute(element: DOMElement, key: string, value: any, isSvg?: true) { if (key === 'className') { - updateClassName(element, value); + updateClassName(element, value, isSvg); } else if (key === 'value') { const inputEl = element as HTMLInputElement; @@ -703,14 +716,14 @@ function setAttribute(element: HTMLElement, key: string, value: any) { element.innerHTML = value.__html; } else if (key.startsWith('on')) { addEventListener(element, key, value, key.endsWith('Capture')); - } else if (key.startsWith('data-') || key.startsWith('aria-') || HTML_ATTRIBUTES.has(key)) { + } else if (isSvg || key.startsWith('data-') || key.startsWith('aria-') || HTML_ATTRIBUTES.has(key)) { element.setAttribute(key, value); } else if (!FILTERED_ATTRIBUTES.has(key)) { (element as any)[MAPPED_ATTRIBUTES[key] || key] = value; } } -function removeAttribute(element: HTMLElement, key: string, value: any) { +function removeAttribute(element: DOMElement, key: string, value: any) { if (key === 'className') { updateClassName(element, ''); } else if (key === 'value') { @@ -726,10 +739,16 @@ function removeAttribute(element: HTMLElement, key: string, value: any) { } } -function updateClassName(element: HTMLElement, value: string) { +function updateClassName(element: DOMElement, value: string, isSvg?: true) { + if (isSvg) { + element.setAttribute('class', value); + return; + } + + const htmlElement = element as HTMLElement; const extra = extraClasses.get(element); if (!extra) { - element.className = value; + htmlElement.className = value; return; } @@ -738,10 +757,10 @@ function updateClassName(element: HTMLElement, value: string) { extraArray.push(value); } - element.className = extraArray.join(' '); + htmlElement.className = extraArray.join(' '); } -function updateStyle(element: HTMLElement, value: string) { +function updateStyle(element: DOMElement, value: string) { element.style.cssText = value; const extraObject = extraStyles.get(element); @@ -750,7 +769,7 @@ function updateStyle(element: HTMLElement, value: string) { } } -export function addExtraClass(element: Element, className: string, forceSingle = false) { +export function addExtraClass(element: DOMElement, className: string, forceSingle = false) { if (!forceSingle) { const classNames = className.split(' '); if (classNames.length > 1) { @@ -772,7 +791,7 @@ export function addExtraClass(element: Element, className: string, forceSingle = } } -export function removeExtraClass(element: Element, className: string, forceSingle = false) { +export function removeExtraClass(element: DOMElement, className: string, forceSingle = false) { if (!forceSingle) { const classNames = className.split(' '); if (classNames.length > 1) { @@ -796,7 +815,7 @@ export function removeExtraClass(element: Element, className: string, forceSingl } } -export function toggleExtraClass(element: Element, className: string, force?: boolean, forceSingle = false) { +export function toggleExtraClass(element: DOMElement, className: string, force?: boolean, forceSingle = false) { if (!forceSingle) { const classNames = className.split(' '); if (classNames.length > 1) { @@ -817,12 +836,12 @@ export function toggleExtraClass(element: Element, className: string, force?: bo } } -export function setExtraStyles(element: HTMLElement, styles: Partial & AnyLiteral) { +export function setExtraStyles(element: DOMElement, styles: Partial & AnyLiteral) { extraStyles.set(element, styles); applyExtraStyles(element); } -function applyExtraStyles(element: HTMLElement) { +function applyExtraStyles(element: DOMElement) { const standardStyles = Object.entries(extraStyles.get(element)!).reduce>( (acc, [prop, value]) => { if (prop.startsWith('--')) { diff --git a/src/lib/teact/teact.ts b/src/lib/teact/teact.ts index b6526925..57ed281d 100644 --- a/src/lib/teact/teact.ts +++ b/src/lib/teact/teact.ts @@ -37,7 +37,7 @@ interface VirtualElementText { export interface VirtualElementTag { type: VirtualType.Tag; - target?: HTMLElement; + target?: HTMLElement | SVGElement; tag: string; props: Props; children: VirtualElementChildren; diff --git a/src/lib/teact/teactn.tsx b/src/lib/teact/teactn.tsx index ef888ce7..83f59960 100644 --- a/src/lib/teact/teactn.tsx +++ b/src/lib/teact/teactn.tsx @@ -14,6 +14,17 @@ import useUniqueId from '../../hooks/useUniqueId'; export default React; +interface Container { + mapStateToProps: MapStateToProps; + activationFn?: ActivationFn; + stuckTo?: any; + ownProps: Props; + mappedProps?: Props; + forceUpdate: Function; + DEBUG_updates: number; + DEBUG_componentName: string; +} + type GlobalState = AnyLiteral & { DEBUG_capturedId?: number }; @@ -24,7 +35,7 @@ export interface ActionOptions { forceOnHeavyAnimation?: boolean; // Workaround for iOS gesture history navigation forceSyncOnIOs?: boolean; - noUpdate?: boolean; + forceOutdated?: boolean; } type Actions = Record void>; @@ -35,10 +46,11 @@ type ActionHandler = ( payload: any, ) => GlobalState | void | Promise; -type DetachWhenChanged = (current: any) => void; -type MapStateToProps = ( - (global: GlobalState, ownProps: OwnProps, detachWhenChanged: DetachWhenChanged) => AnyLiteral - ); +type MapStateToProps = (global: GlobalState, ownProps: OwnProps) => AnyLiteral; +type StickToFirstFn = (value: any) => boolean; +type ActivationFn = ( + global: GlobalState, ownProps: OwnProps, stickToFirst: StickToFirstFn, +) => boolean; let currentGlobal = {} as GlobalState; @@ -51,28 +63,13 @@ const DEBUG_releaseCapturedIdThrottled = throttleWithTickEnd(() => { const actionHandlers: Record = {}; const callbacks: Function[] = [updateContainers]; -const immediateCallbacks: Function[] = []; const actions = {} as Actions; -const containers = new Map; - ownProps: Props; - mappedProps?: Props; - forceUpdate: Function; - isDetached: boolean; - detachReason: any; - detachWhenChanged: DetachWhenChanged; - DEBUG_updates: number; - DEBUG_componentName: string; -}>(); +const containers = new Map(); const runCallbacksThrottled = throttleWithTickEnd(runCallbacks); let forceOnHeavyAnimation = true; -function runImmediateCallbacks() { - immediateCallbacks.forEach((cb) => cb(currentGlobal)); -} - function runCallbacks() { if (forceOnHeavyAnimation) { forceOnHeavyAnimation = false; @@ -87,7 +84,10 @@ function runCallbacks() { export function setGlobal(newGlobal?: GlobalState, options?: ActionOptions) { if (typeof newGlobal === 'object' && newGlobal !== currentGlobal) { if (DEBUG) { - if (newGlobal.DEBUG_capturedId && newGlobal.DEBUG_capturedId !== DEBUG_currentCapturedId) { + if ( + !options?.forceOutdated + && newGlobal.DEBUG_capturedId && newGlobal.DEBUG_capturedId !== DEBUG_currentCapturedId + ) { throw new Error('[TeactN.setGlobal] Attempt to set an outdated global'); } @@ -96,8 +96,6 @@ export function setGlobal(newGlobal?: GlobalState, options?: ActionOptions) { currentGlobal = newGlobal; - if (!options?.noUpdate) runImmediateCallbacks(); - if (options?.forceSyncOnIOs) { forceOnHeavyAnimation = true; runCallbacks(); @@ -128,6 +126,10 @@ export function getActions() { return actions; } +export function forceOnHeavyAnimationOnce() { + forceOnHeavyAnimation = true; +} + let actionQueue: NoneToVoidFunction[] = []; function handleAction(name: string, payload?: ActionPayload, options?: ActionOptions) { @@ -164,21 +166,17 @@ function updateContainers() { // eslint-disable-next-line no-restricted-syntax for (const container of containers.values()) { const { - mapStateToProps, ownProps, mappedProps, forceUpdate, isDetached, detachWhenChanged, + mapStateToProps, ownProps, mappedProps, forceUpdate, } = container; - if (isDetached) { + if (!activateContainer(container, currentGlobal, ownProps)) { continue; } let newMappedProps; try { - newMappedProps = mapStateToProps(currentGlobal, ownProps, detachWhenChanged); - - if (container.isDetached) { - continue; - } + newMappedProps = mapStateToProps(currentGlobal, ownProps); } catch (err: any) { handleError(err); @@ -232,19 +230,20 @@ export function addActionHandler(name: ActionNames, handler: ActionHandler) { actionHandlers[name].push(handler); } -export function addCallback(cb: Function, isImmediate = false) { - (isImmediate ? immediateCallbacks : callbacks).push(cb); +export function addCallback(cb: Function) { + callbacks.push(cb); } -export function removeCallback(cb: Function, isImmediate = false) { - const index = (isImmediate ? immediateCallbacks : callbacks).indexOf(cb); +export function removeCallback(cb: Function) { + const index = callbacks.indexOf(cb); if (index !== -1) { - (isImmediate ? immediateCallbacks : callbacks).splice(index, 1); + callbacks.splice(index, 1); } } export function withGlobal( mapStateToProps: MapStateToProps = () => ({}), + activationFn?: ActivationFn, ) { return (Component: FC) => { function TeactNContainer(props: OwnProps) { @@ -261,20 +260,9 @@ export function withGlobal( if (!container) { container = { mapStateToProps, + activationFn, ownProps: props, forceUpdate, - isDetached: false, - detachReason: undefined, - // This allows to ignore changes in global during animation before unmount - detachWhenChanged: (current) => { - const { detachReason } = container!; - - if (detachReason === undefined && current !== undefined) { - container!.detachReason = current; - } else if (detachReason !== undefined && detachReason !== current) { - container!.isDetached = true; - } - }, DEBUG_updates: 0, DEBUG_componentName: Component.name, }; @@ -282,18 +270,18 @@ export function withGlobal( containers.set(id, container); } - if (!container.mappedProps || !arePropsShallowEqual(container.ownProps, props)) { - container.ownProps = props; - - if (!container.isDetached) { - try { - container.mappedProps = mapStateToProps(currentGlobal, props, container.detachWhenChanged); - } catch (err: any) { - handleError(err); - } + if (!container.mappedProps || ( + !arePropsShallowEqual(container.ownProps, props) && activateContainer(container, currentGlobal, props) + )) { + try { + container.mappedProps = mapStateToProps(currentGlobal, props); + } catch (err: any) { + handleError(err); } } + container.ownProps = props; + // eslint-disable-next-line react/jsx-props-no-spreading return ; } @@ -304,6 +292,21 @@ export function withGlobal( }; } +function activateContainer(container: Container, global: GlobalState, props: Props) { + const { activationFn, stuckTo } = container; + if (!activationFn) { + return true; + } + + return activationFn(global, props, (stickTo: any) => { + if (stickTo && !stuckTo) { + container.stuckTo = stickTo; + } + + return stickTo && (!stuckTo || stuckTo === stickTo); + }); +} + export function typify< ProjectGlobalState, ActionPayloads, @@ -337,8 +340,8 @@ export function typify< handler: ActionHandlers[ActionName], ) => void, withGlobal: withGlobal as ( - mapStateToProps: ( - (global: ProjectGlobalState, ownProps: OwnProps, detachWhenChanged: DetachWhenChanged) => AnyLiteral), + mapStateToProps: (global: ProjectGlobalState, ownProps: OwnProps) => AnyLiteral, + activationFn?: (global: ProjectGlobalState, ownProps: OwnProps, stickToFirst: StickToFirstFn) => boolean, ) => (Component: FC) => FC, }; } diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index 9cc50606..900feaef 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -21,6 +21,7 @@ --color-background-drop-down-search: #F1F5FA; --color-background-purple-1: #F9FAFE; --color-background-purple-2: #DCDEFF; + --color-app-background: var(--color-background-second); --color-input-button-text: #53657B; --color-input-button-background: #F6F8FB; @@ -87,6 +88,8 @@ --color-activity-purple-text: #6875E9; --color-activity-purple-background: #EDEEFC; --color-activity-red-background: #FDEAEA; + --color-activity-blue: #0088CC; + --color-activity-blue-background: rgba(0, 136, 204, 0.10); --color-transaction-gray-text: #53657B; --color-transaction-gray-background: #E2E9F1; @@ -94,6 +97,8 @@ --color-transaction-red-background: #F5E6E7; --color-transaction-green-text: #1EC160; --color-transaction-green-background: #DDF2E8; + --color-transaction-purple-text: #6673E6; + --color-transaction-purple-background: #E2E7F7; --color-separator: rgba(132, 146, 171, 0.3); --color-separator-input-stroke: #DFE7F0; @@ -135,14 +140,20 @@ --color-apy-active-text: #FFFFFF; --color-apy-active-background: #8892EB; + --color-flashlight-button-background: rgba(255, 255, 255, 0.35); + --color-flashlight-button-enabled-background: rgba(255, 255, 255, 0.95); + --color-flashlight-button-text: #000; + --color-flashlight-button-enabled-text: #53657B; + --default-shadow: 0 0 1.5625rem 0 #00000026; --spinner-white-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEwLjggMjIuNEM2IDIxLjkgMi4xIDE4IDEuNiAxMy4yLjkgNy4xIDUuNCAxLjkgMTEuMyAxLjVjLjQgMCAuNy0uMy43LS43IDAtLjQtLjQtLjgtLjgtLjhDNC44LjQtLjIgNS45IDAgMTIuNS4yIDE4LjYgNS40IDIzLjggMTEuNSAyNGM2LjYuMiAxMi00LjggMTIuNC0xMS4yIDAtLjQtLjMtLjgtLjgtLjgtLjQgMC0uNy4zLS43LjctLjMgNS45LTUuNSAxMC40LTExLjYgOS43eiIgZmlsbD0iI2ZmZmZmZiIvPjwvc3ZnPg==); --spinner-white-thin-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0iI2ZmZmZmZiIgZD0iTTEyIDIzQzUuOSAyMyAxIDE4LjEgMSAxMlM1LjkgMSAxMiAxVjBDNS40IDAgMCA1LjQgMCAxMnM1LjQgMTIgMTIgMTIgMTItNS40IDEyLTEyaC0xYzAgNi4xLTQuOSAxMS0xMSAxMXoiLz48L3N2Zz4=); --spinner-blue-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEwLjggMjIuNEM2IDIxLjkgMi4xIDE4IDEuNiAxMy4yLjkgNy4xIDUuNCAxLjkgMTEuMyAxLjVjLjQgMCAuNy0uMy43LS43IDAtLjQtLjQtLjgtLjgtLjhDNC44LjQtLjIgNS45IDAgMTIuNS4yIDE4LjYgNS40IDIzLjggMTEuNSAyNGM2LjYuMiAxMi00LjggMTIuNC0xMS4yIDAtLjQtLjMtLjgtLjgtLjgtLjQgMC0uNy4zLS43LjctLjMgNS45LTUuNSAxMC40LTExLjYgOS43eiIgZmlsbD0iIzAwODhDQyIvPjwvc3ZnPg==); --spinner-dark-blue-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEwLjggMjIuNEM2IDIxLjkgMi4xIDE4IDEuNiAxMy4yLjkgNy4xIDUuNCAxLjkgMTEuMyAxLjVjLjQgMCAuNy0uMy43LS43IDAtLjQtLjQtLjgtLjgtLjhDNC44LjQtLjIgNS45IDAgMTIuNS4yIDE4LjYgNS40IDIzLjggMTEuNSAyNGM2LjYuMiAxMi00LjggMTIuNC0xMS4yIDAtLjQtLjMtLjgtLjgtLjgtLjQgMC0uNy4zLS43LjctLjMgNS45LTUuNSAxMC40LTExLjYgOS43eiIgZmlsbD0iIzAwODhDQyIvPjwvc3ZnPg==); + --spinner-green-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEwLjggMjIuNEM2IDIxLjkgMi4xIDE4IDEuNiAxMy4yLjkgNy4xIDUuNCAxLjkgMTEuMyAxLjVjLjQgMCAuNy0uMy43LS43IDAtLjQtLjQtLjgtLjgtLjhDNC44LjQtLjIgNS45IDAgMTIuNS4yIDE4LjYgNS40IDIzLjggMTEuNSAyNGM2LjYuMiAxMi00LjggMTIuNC0xMS4yIDAtLjQtLjMtLjgtLjgtLjgtLjQgMC0uNy4zLS43LjctLjMgNS45LTUuNSAxMC40LTExLjYgOS43eiIgZmlsbD0iIzJDRDM2RiIvPjwvc3ZnPg==); - --layer-blackout-opacity: 0.3; + --layer-blackout-opacity: 0.1; --border-radius-tiny: 0.5rem; --border-radius-small: 0.625rem; @@ -153,6 +164,7 @@ --scrollbar-width: 0; --safe-area-bottom-value: env(safe-area-inset-bottom); + --safe-area-top-value: env(safe-area-inset-top); --z-below: -1; --z-modal: 100; @@ -160,9 +172,11 @@ --z-menu-bubble: 200; --z-notification: 250; --z-tooltip: 300; + --z-confetti: 400; --no-animation-transition: 200ms opacity ease; --layer-transition: 300ms cubic-bezier(0.33, 1, 0.68, 1); + --layer-transition-behind: 300ms cubic-bezier(0.33, 1, 0.68, 1); --slide-transition: 300ms cubic-bezier(0.25, 1, 0.5, 1); --select-transition: 200ms ease-out; --dropdown-transition: opacity 200ms cubic-bezier(0.2, 0, 0.2, 1), transform 200ms cubic-bezier(0.2, 0, 0.2, 1), @@ -170,13 +184,13 @@ --dropdown-transition-backwards: opacity 200ms ease-in, transform 200ms ease-in; &.is-ios { - --layer-transition: 450ms cubic-bezier(0.33, 1, 0.68, 1); + --layer-transition: 650ms cubic-bezier(0.22, 1, 0.36, 1); + --layer-transition-behind: 650ms cubic-bezier(0.33, 1, 0.68, 1); --slide-transition: 450ms cubic-bezier(0.25, 1, 0.5, 1); } &.is-android { - --layer-transition: 450ms cubic-bezier(0.25, 1, 0.5, 1); - --slide-transition: 400ms cubic-bezier(0.25, 1, 0.5, 1); + --slide-transition: 350ms cubic-bezier(0.16, 1, 0.3, 1); } &:global(.theme-dark) { @@ -243,10 +257,12 @@ --color-activity-gray-background: #2E3947; --color-activity-gray-text: #A3B8CA; --color-activity-green-background: #1F3838; - --color-activity-green-text: #2CD36F; + --color-activity-green-text: #2BC469; --color-activity-purple-text: #6875E9; --color-activity-purple-background: #2D3651; --color-activity-red-background: #422F39; + --color-activity-blue: #469CEC; + --color-activity-blue-background: rgba(70, 156, 236, 0.1); --color-transaction-gray-text: #AABCCC; --color-transaction-gray-background: #2E3947; @@ -254,6 +270,8 @@ --color-transaction-red-background: #452A2E; --color-transaction-green-text: #2CD36F; --color-transaction-green-background: #1F3838; + --color-transaction-purple-text: #8A95FF; + --color-transaction-purple-background: #2A3147; --color-separator: rgba(84, 96, 112, 0.3); --color-separator-input-stroke: #364354; diff --git a/src/styles/brilliant-icons.css b/src/styles/brilliant-icons.css index 602b00b0..84acab8a 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?bc5164b37bfd0b73b5ecadf95aa71fe5") format("woff"), -url("./brilliant-icons.woff2?bc5164b37bfd0b73b5ecadf95aa71fe5") format("woff2"); + src: url("./brilliant-icons.woff?c432e9e8a4a2ab95fcf5874fa9f9b512") format("woff"), +url("./brilliant-icons.woff2?c432e9e8a4a2ab95fcf5874fa9f9b512") format("woff2"); font-weight: normal; font-style: normal; } @@ -20,144 +20,168 @@ url("./brilliant-icons.woff2?bc5164b37bfd0b73b5ecadf95aa71fe5") format("woff2"); .icon-arrow-down::before { content: "\f102"; } -.icon-arrow-right::before { +.icon-arrow-right-swap::before { content: "\f103"; } -.icon-arrow-up::before { +.icon-arrow-right::before { content: "\f104"; } -.icon-caret-down::before { +.icon-arrow-up-swap::before { content: "\f105"; } -.icon-chevron-down::before { +.icon-arrow-up::before { content: "\f106"; } -.icon-chevron-left::before { +.icon-backspace::before { content: "\f107"; } -.icon-chevron-right::before { +.icon-caret-down::before { content: "\f108"; } -.icon-clock::before { +.icon-changelly::before { content: "\f109"; } -.icon-close-filled::before { +.icon-chevron-down::before { content: "\f10a"; } -.icon-close::before { +.icon-chevron-left::before { content: "\f10b"; } -.icon-cog::before { +.icon-chevron-right::before { content: "\f10c"; } -.icon-coinmarket::before { +.icon-clock::before { content: "\f10d"; } -.icon-copy::before { +.icon-close-filled::before { content: "\f10e"; } -.icon-dot::before { +.icon-close::before { content: "\f10f"; } -.icon-download::before { +.icon-cog::before { content: "\f110"; } -.icon-earn::before { +.icon-coinmarket::before { content: "\f111"; } -.icon-eye-closed::before { +.icon-copy::before { content: "\f112"; } -.icon-eye::before { +.icon-dot::before { content: "\f113"; } -.icon-github::before { +.icon-download::before { content: "\f114"; } -.icon-laptop::before { +.icon-earn::before { content: "\f115"; } -.icon-ledger::before { +.icon-eye-closed::before { content: "\f116"; } -.icon-lock::before { +.icon-eye::before { content: "\f117"; } -.icon-params::before { +.icon-face-id::before { content: "\f118"; } -.icon-paste::before { +.icon-flashlight::before { content: "\f119"; } -.icon-pen::before { +.icon-github::before { content: "\f11a"; } -.icon-percent::before { +.icon-laptop::before { content: "\f11b"; } -.icon-plus::before { +.icon-ledger::before { content: "\f11c"; } -.icon-qrcode::before { +.icon-lock::before { content: "\f11d"; } -.icon-question::before { +.icon-params::before { content: "\f11e"; } -.icon-receive-alt::before { +.icon-paste::before { content: "\f11f"; } -.icon-receive::before { +.icon-pen::before { content: "\f120"; } -.icon-replace::before { +.icon-percent::before { content: "\f121"; } -.icon-search::before { +.icon-plus::before { content: "\f122"; } -.icon-send-alt::before { +.icon-qr-scanner-alt::before { content: "\f123"; } -.icon-send::before { +.icon-qr-scanner::before { content: "\f124"; } -.icon-share::before { +.icon-question::before { content: "\f125"; } -.icon-sort::before { +.icon-receive-alt::before { content: "\f126"; } -.icon-star-filled::before { +.icon-receive::before { content: "\f127"; } -.icon-star::before { +.icon-replace::before { content: "\f128"; } -.icon-swap::before { +.icon-search::before { content: "\f129"; } -.icon-telegram::before { +.icon-send-alt::before { content: "\f12a"; } -.icon-ton::before { +.icon-send::before { content: "\f12b"; } -.icon-tonscan::before { +.icon-share::before { content: "\f12c"; } -.icon-trash::before { +.icon-sort::before { content: "\f12d"; } -.icon-update::before { +.icon-star-filled::before { content: "\f12e"; } -.icon-windows-close::before { +.icon-star::before { content: "\f12f"; } -.icon-windows-maximize::before { +.icon-swap::before { content: "\f130"; } -.icon-windows-minimize::before { +.icon-telegram::before { content: "\f131"; } +.icon-ton::before { + content: "\f132"; +} +.icon-tonscan::before { + content: "\f133"; +} +.icon-touch-id::before { + content: "\f134"; +} +.icon-trash::before { + content: "\f135"; +} +.icon-update::before { + content: "\f136"; +} +.icon-windows-close::before { + content: "\f137"; +} +.icon-windows-maximize::before { + content: "\f138"; +} +.icon-windows-minimize::before { + content: "\f139"; +} diff --git a/src/styles/brilliant-icons.woff b/src/styles/brilliant-icons.woff index 6a7db102..19187eca 100644 Binary files a/src/styles/brilliant-icons.woff and b/src/styles/brilliant-icons.woff differ diff --git a/src/styles/brilliant-icons.woff2 b/src/styles/brilliant-icons.woff2 index a123d39d..711bb42a 100644 Binary files a/src/styles/brilliant-icons.woff2 and b/src/styles/brilliant-icons.woff2 differ diff --git a/src/styles/index.scss b/src/styles/index.scss index 5b10cab1..ef6792d0 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -15,7 +15,9 @@ html, body { margin: 0; padding: 0; - background-color: var(--color-background-second); + background-color: var(--color-app-background); + + transition: background-color 350ms; @include respond-below(xs) { height: calc(var(--vh, 1vh) * 100); @@ -25,8 +27,6 @@ html, body { body { user-select: none; - overflow: hidden; - font-family: 'Nunito', -apple-system, BlinkMacSystemFont, "Apple Color Emoji", "Segoe UI", Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; font-size: 16px; @@ -52,10 +52,13 @@ html.is-electron { --custom-cursor: default; } -#root { - height: 100%; +// Required for closing over scrollable content (scroll tracking) +html.is-native-bottom-sheet { + overflow: auto; - background: var(--color-background-second); + body { + overflow: auto; + } } #root, @@ -170,3 +173,29 @@ a { .no-transitions * { transition: none !important; } + +.with-notch-on-scroll { + position: relative; + + &::after { + content: ''; + + position: absolute; + bottom: 0; + left: 0; + + width: 100%; + height: 0.0625rem; + + opacity: 0; + + /* stylelint-disable-next-line plugin/whole-pixel */ + box-shadow: 0 0.035rem 0 0 var(--color-separator); + + transition: opacity 200ms; + } + + &.is-scrolled::after { + opacity: 1; + } +} diff --git a/src/util/activeTabMonitor.ts b/src/util/activeTabMonitor.ts index 3edecc4f..c136a960 100644 --- a/src/util/activeTabMonitor.ts +++ b/src/util/activeTabMonitor.ts @@ -1,18 +1,22 @@ -import { DETACHED_TAB_URL } from './ledger/tab'; +import { ACTIVE_TAB_STORAGE_KEY } from '../config'; +import { + IS_DELEGATED_BOTTOM_SHEET, IS_ELECTRON, IS_LEDGER_EXTENSION_TAB, +} from './windowEnvironment'; + +const IS_DISABLED = IS_LEDGER_EXTENSION_TAB || IS_DELEGATED_BOTTOM_SHEET || IS_ELECTRON; -const STORAGE_KEY = 'mtw-active-tab'; const INTERVAL = 2000; const tabKey = String(Date.now() + Math.random()); -if (!window.location.href.includes(DETACHED_TAB_URL)) { - localStorage.setItem(STORAGE_KEY, tabKey); +if (!IS_DISABLED) { + localStorage.setItem(ACTIVE_TAB_STORAGE_KEY, tabKey); } let callback: NoneToVoidFunction; const interval = window.setInterval(() => { - if (callback && localStorage.getItem(STORAGE_KEY) !== tabKey && !window.location.href.includes(DETACHED_TAB_URL)) { + if (!IS_DISABLED && callback && localStorage.getItem(ACTIVE_TAB_STORAGE_KEY) !== tabKey) { callback(); clearInterval(interval); } diff --git a/src/util/animation.ts b/src/util/animation.ts index 79d4ae3b..d6d65264 100644 --- a/src/util/animation.ts +++ b/src/util/animation.ts @@ -48,15 +48,15 @@ export function animateInstantly(tick: Function, schedulerFn: Scheduler) { } } -export type TimingFn = (t: number) => number; +type TimingFn = (t: number) => number; -export type AnimateNumberProps = { - to: number | number[]; - from: number | number[]; +type AnimateNumberProps = { + to: T; + from: T; duration: number; - onUpdate: (value: any) => void; + onUpdate: (value: T) => void; timing?: TimingFn; - onEnd?: () => void; + onEnd?: (isCanceled?: boolean) => void; }; export const timingFunctions = { @@ -78,35 +78,41 @@ export const timingFunctions = { easeInOutQuint: (t: number) => (t < 0.5 ? 16 * t ** 5 : 1 + 16 * (--t) * t ** 4), }; -export function animateNumber({ +export function animateNumber({ timing = timingFunctions.linear, onUpdate, duration, onEnd, from, to, -}: AnimateNumberProps) { +}: AnimateNumberProps) { const t0 = Date.now(); - let canceled = false; + + let isCanceled = false; animateInstantly(() => { - if (canceled) return false; + if (isCanceled) return false; + const t1 = Date.now(); - let t = (t1 - t0) / duration; - if (t > 1) t = 1; + const t = Math.min((t1 - t0) / duration, 1); + const progress = timing(t); if (typeof from === 'number' && typeof to === 'number') { - onUpdate(from + ((to - from) * progress)); + onUpdate((from + ((to - from) * progress)) as T); } else if (Array.isArray(from) && Array.isArray(to)) { const result = from.map((f, i) => f + ((to[i] - f) * progress)); - onUpdate(result); + onUpdate(result as T); } - if (t === 1 && onEnd) onEnd(); + + if (t === 1) { + onEnd?.(); + } + return t < 1; }, requestMeasure); return () => { - canceled = true; - if (onEnd) onEnd(); + isCanceled = true; + onEnd?.(true); }; } diff --git a/src/util/arePropsShallowEqual.ts b/src/util/arePropsShallowEqual.ts index 56330a77..a6ba9453 100644 --- a/src/util/arePropsShallowEqual.ts +++ b/src/util/arePropsShallowEqual.ts @@ -38,7 +38,7 @@ export function logUnequalProps(currentProps: AnyLiteral, newProps: AnyLiteral, // eslint-disable-next-line no-console console.log(msg); - currentKeys.forEach((res, prop) => { + currentKeys.forEach((prop) => { if (currentProps[prop] !== newProps[prop]) { // eslint-disable-next-line no-console console.log(debugKey, prop, ':', currentProps[prop], '=>', newProps[prop]); diff --git a/src/util/authApi/index.ts b/src/util/authApi/index.ts new file mode 100644 index 00000000..89530034 --- /dev/null +++ b/src/util/authApi/index.ts @@ -0,0 +1,100 @@ +import { NativeBiometric } from '@capgo/capacitor-native-biometric'; + +import type { AuthConfig } from './types'; +import type { CredentialCreationResult } from './webAuthn'; + +import { APP_NAME, NATIVE_BIOMETRICS_SERVER, NATIVE_BIOMETRICS_USERNAME } from '../../config'; +import { logDebugError } from '../logs'; +import { randomBytes } from '../random'; +import webAuthn from './webAuthn'; + +const CREDENTIAL_SIZE = 32; + +async function setupBiometrics({ credential }: { credential?: CredentialCreationResult }) { + let result: { password: string; config: AuthConfig } | undefined; + + try { + if (!credential) { + const password = Buffer.from(randomBytes(CREDENTIAL_SIZE)).toString('hex'); + const encryptedPassword = await window.electron?.encryptPassword(password); + if (!encryptedPassword) { + return result; + } + + result = { + password, + config: { + kind: 'electron-safe-storage', + encryptedPassword, + }, + }; + } else { + result = await webAuthn.verify(credential); + } + } catch (err) { + logDebugError('setupBiometrics', err); + } + + return result; +} + +async function setupNativeBiometrics(password: string) { + await NativeBiometric.setCredentials({ + username: NATIVE_BIOMETRICS_USERNAME, + password, + server: NATIVE_BIOMETRICS_SERVER, + }); + + return { + password, + config: { + kind: 'native-biometrics', + } as AuthConfig, + }; +} + +function removeNativeBiometrics() { + return NativeBiometric.deleteCredentials({ + server: NATIVE_BIOMETRICS_SERVER, + }); +} + +async function getPassword(config: AuthConfig) { + let password: string | undefined; + + try { + if (config.kind === 'webauthn') { + password = await webAuthn.getPassword(config); + } else if (config.kind === 'electron-safe-storage') { + password = await window.electron?.decryptPassword(config.encryptedPassword); + } else if (config.kind === 'native-biometrics') { + const isVerified = await NativeBiometric.verifyIdentity({ + title: APP_NAME, + subtitle: '', + }) + .then(() => true) + .catch(() => false); + + if (!isVerified) return undefined; + + const credentials = await NativeBiometric.getCredentials({ + server: NATIVE_BIOMETRICS_SERVER, + }); + password = credentials.password; + } else { + throw new Error('Unexpected auth kind'); + } + } catch (err) { + logDebugError('submitTransferPassword', err); + } + + return password; +} + +export default { + setupBiometrics, + setupNativeBiometrics, + removeNativeBiometrics, + getPassword, + webAuthn, +}; diff --git a/src/util/authApi/types.ts b/src/util/authApi/types.ts new file mode 100644 index 00000000..0b72c117 --- /dev/null +++ b/src/util/authApi/types.ts @@ -0,0 +1,21 @@ +export type AuthConfig = AuthPassword | WebAuthn | ElectronSafeStorage | NativeBiometrics; + +export interface AuthPassword { + kind: 'password'; +} + +export interface WebAuthn { + kind: 'webauthn'; + type: 'largeBlob' | 'credBlob' | 'userHandle'; + credentialId: string; + transports?: AuthenticatorTransport[]; +} + +export interface ElectronSafeStorage { + kind: 'electron-safe-storage'; + encryptedPassword: string; +} + +export interface NativeBiometrics { + kind: 'native-biometrics'; +} diff --git a/src/util/authApi/webAuthn.ts b/src/util/authApi/webAuthn.ts new file mode 100644 index 00000000..cbe8cd6c --- /dev/null +++ b/src/util/authApi/webAuthn.ts @@ -0,0 +1,223 @@ +import type { AuthConfig, WebAuthn } from './types'; + +import { randomBytes } from '../random'; +import { pause } from '../schedulers'; + +declare global { + interface AuthenticationExtensionsClientInputs { + credBlob?: Uint8Array; // max 32 bytes + getCredBlob?: boolean; + hmacCreateSecret?: boolean; + hmacGetSecret?: { salt1: Uint8Array }; // 32-byte random data + } + + interface AuthenticationExtensionsClientOutputs { + credBlob?: boolean; + getCredBlob?: Uint8Array; + hmacCreateSecret?: boolean; + hmacGetSecret?: { output1: Uint8Array }; + } + + interface AuthenticatorResponse { + getTransports?: () => AuthenticatorTransport[]; + } +} + +export interface CredentialCreationResult { + type: 'credBlob' | 'userHandle'; + password: { + credBlob: string; + userHandle: string; + }; + credential: PublicKeyCredential; +} + +enum PubkeyAlg { + Ed25519 = -8, + ES256 = -7, + RS256 = -257, +} + +const CREDENTIAL_SIZE = 32; +const RP_NAME = 'MyTonWallet'; +const USER_NAME = 'MyTonWallet'; +const PAUSE = 300; +const CREDENTIAL_TIMEOUT = 120000; + +async function createCredential() { + const rpId = window.location.hostname; + + const userHandle = randomBytes(CREDENTIAL_SIZE); + const credBlob = randomBytes(CREDENTIAL_SIZE); + + const options: CredentialCreationOptions = { + publicKey: { + challenge: randomBytes(CREDENTIAL_SIZE), + rp: { + name: RP_NAME, + id: rpId, + }, + user: { + id: userHandle, + name: USER_NAME, + displayName: RP_NAME, + }, + pubKeyCredParams: [ + { + type: 'public-key', + alg: PubkeyAlg.ES256, + }, + { + type: 'public-key', + alg: PubkeyAlg.RS256, + }, + { + type: 'public-key', + alg: PubkeyAlg.Ed25519, + }, + ], + authenticatorSelection: { + requireResidentKey: true, + userVerification: 'preferred', + }, + extensions: { + credBlob, + hmacCreateSecret: true, + }, + timeout: CREDENTIAL_TIMEOUT, + excludeCredentials: [], + }, + }; + + const credential = (await navigator.credentials.create(options)) as PublicKeyCredential; + + if (!credential) { + throw new Error('Missing credential'); + } + + const extensions = credential.getClientExtensionResults(); + const type = extensions.credBlob ? 'credBlob' : 'userHandle'; + + return { + type, + password: { + credBlob: Buffer.from(credBlob).toString('hex'), + userHandle: Buffer.from(userHandle).toString('hex'), + }, + credential, + } as CredentialCreationResult; +} + +async function verify({ credential, password, type }: CredentialCreationResult) { + await pause(PAUSE); + + const transports = credential.response + && credential.response.getTransports + && credential.response.getTransports(); + + const credentialId = Buffer.from(credential.rawId).toString('hex'); + + const options: CredentialRequestOptions = { + publicKey: { + challenge: randomBytes(CREDENTIAL_SIZE), + allowCredentials: [ + { + id: credential.rawId, + type: 'public-key', + transports, + }, + ], + userVerification: 'required', + extensions: { + getCredBlob: true, + }, + }, + }; + + const assertion = (await navigator.credentials.get(options)) as PublicKeyCredential; + + if (!assertion) { + throw new Error('Missing authentication'); + } + + const response = assertion.response as AuthenticatorAssertionResponse; + let result: string | undefined; + switch (type) { + case 'userHandle': { + if (!response.userHandle) { + throw new Error('Missing stored userHandle'); + } + if (!Buffer.from(password.userHandle, 'hex').equals(Buffer.from(response.userHandle))) { + throw new Error('Stored blob not equals passed blob'); + } + result = password.userHandle; + break; + } + } + + if (!result) { + throw new Error('Missing stored blob'); + } + + const config: WebAuthn = { + kind: 'webauthn', + type, + credentialId, + transports, + }; + + return { config, password: result }; +} + +async function getPassword(config: AuthConfig) { + if (config.kind !== 'webauthn') { + throw new Error('Unexpected auth kind'); + } + + const { credentialId, transports, type } = config; + + const controller = new AbortController(); + const signal = controller.signal; + + const options: CredentialRequestOptions = { + publicKey: { + challenge: randomBytes(CREDENTIAL_SIZE), + allowCredentials: [ + { + id: Buffer.from(credentialId, 'hex'), + type: 'public-key', + transports, + }, + ], + userVerification: 'required', + extensions: { + getCredBlob: true, + }, + }, + signal, + }; + + const assertion = (await navigator.credentials.get(options)) as PublicKeyCredential; + + if (signal.aborted) { + throw new Error('Verification canceled'); + } + + const response = assertion.response as AuthenticatorAssertionResponse; + + const extensions = assertion.getClientExtensionResults(); + if (type === 'userHandle') { + if (!response.userHandle) { + throw new Error('missing userHandle'); + } + return Buffer.from(response.userHandle).toString('hex'); + } else { + return Buffer.from(extensions.getCredBlob ?? '').toString('hex'); + } +} + +export default { + createCredential, + verify, + getPassword, +}; diff --git a/src/util/betterView.ts b/src/util/betterView.ts new file mode 100644 index 00000000..cbde96f3 --- /dev/null +++ b/src/util/betterView.ts @@ -0,0 +1,92 @@ +import { animate } from './animation'; +import { fastRaf } from './schedulers'; +import { IS_IOS } from './windowEnvironment'; + +const TEST_INTERVAL = 5000; // 5 sec +const FRAMES_TO_TEST = 10; +const REDUCED_FPS = 35; + +let isImproved = false; + +export function betterView() { + if (!IS_IOS) return; + + let interval: number | undefined; + let lastFocusAt = Date.now(); + + function setupInterval() { + if (interval || isImproved) return; + + interval = window.setInterval(testAndImprove, TEST_INTERVAL); + } + + window.addEventListener('focus', () => { + const now = Date.now(); + if (now - lastFocusAt < 100) return; // iOS triggers two `focus` events for some reason + lastFocusAt = now; + + setupInterval(); + testAndImprove(); + }); + + window.addEventListener('blur', () => { + clearInterval(interval); + interval = undefined; + }); + + if (document.hasFocus()) { + setupInterval(); + testAndImprove(); + } +} + +async function testAndImprove() { + const fps = await testFps(); + if (fps <= REDUCED_FPS) { + improveView(); + } +} + +function testFps() { + return new Promise((resolve) => { + const frames: number[] = []; + let lastFrameAt = performance.now(); + + animate(() => { + const now = performance.now(); + frames.push(now - lastFrameAt); + lastFrameAt = now; + + if (frames.length === FRAMES_TO_TEST) { + const mean = frames.sort()[Math.floor(frames.length / 2)]; + resolve(Math.round(1000 / mean)); + return false; + } + + return true; + }, fastRaf); + }); +} + +function improveView() { + isImproved = true; + + const containerEl = document.createElement('div'); + containerEl.style.cssText = 'position: absolute; top: 0; left: 0; width: 0; height: 100%; overflow: hidden;'; + + const boosterEl = document.createElement('div'); + const height = window.screen.height * 1.5; + boosterEl.style.cssText = `width: 0; height: ${height}px; transform: translateX(100%); transition: transform 100ms;`; + boosterEl.innerHTML = ' '; + + containerEl.appendChild(boosterEl); + document.body.appendChild(containerEl); + + requestAnimationFrame(() => { + boosterEl.addEventListener('transitionend', () => { + containerEl.remove(); + }); + + boosterEl.style.transform = ''; + }); +} diff --git a/src/util/cacheApi.ts b/src/util/cacheApi.ts index a11229e3..abd078c8 100644 --- a/src/util/cacheApi.ts +++ b/src/util/cacheApi.ts @@ -1,4 +1,4 @@ -import { ELECTRON_HOST_URL, IS_ELECTRON } from '../config'; +import { ELECTRON_HOST_URL, IS_ELECTRON_BUILD } from '../config'; // eslint-disable-next-line no-restricted-globals const cacheApi = self.caches; @@ -10,7 +10,7 @@ export async function fetch(cacheName: string, key: string) { try { // To avoid the error "Request scheme 'webdocument' is unsupported" - const request = IS_ELECTRON + const request = IS_ELECTRON_BUILD ? `${ELECTRON_HOST_URL}/${key.replace(/:/g, '_')}` : new Request(key.replace(/:/g, '_')); const cache = await cacheApi.open(cacheName); @@ -37,7 +37,7 @@ export async function save(cacheName: string, key: string, data: AnyLiteral | Bl ? data : JSON.stringify(data); // To avoid the error "Request scheme 'webdocument' is unsupported" - const request = IS_ELECTRON + const request = IS_ELECTRON_BUILD ? `${ELECTRON_HOST_URL}/${key.replace(/:/g, '_')}` : new Request(key.replace(/:/g, '_')); const response = new Response(cacheData); diff --git a/src/util/callbacks.ts b/src/util/callbacks.ts index ef796fd9..c43b21ff 100644 --- a/src/util/callbacks.ts +++ b/src/util/callbacks.ts @@ -1,7 +1,7 @@ -export function createCallbackManager() { - const callbacks = new Set(); +export function createCallbackManager() { + const callbacks = new Set(); - function addCallback(cb: AnyToVoidFunction) { + function addCallback(cb: T) { callbacks.add(cb); return () => { @@ -9,11 +9,11 @@ export function createCallbackManager() { }; } - function removeCallback(cb: AnyToVoidFunction) { + function removeCallback(cb: T) { callbacks.delete(cb); } - function runCallbacks(...args: any[]) { + function runCallbacks(...args: Parameters) { callbacks.forEach((callback) => { callback(...args); }); @@ -31,7 +31,8 @@ export function createCallbackManager() { }; } -export type CallbackManager = ReturnType; +export type CallbackManager + = ReturnType>; export class EventEmitter { private channels = new Map(); diff --git a/src/util/capacitor.ts b/src/util/capacitor.ts new file mode 100644 index 00000000..6d597bc1 --- /dev/null +++ b/src/util/capacitor.ts @@ -0,0 +1,156 @@ +import type { URLOpenListenerEvent } from '@capacitor/app'; +import { App } from '@capacitor/app'; +import { Capacitor } from '@capacitor/core'; +import { Haptics, ImpactStyle } from '@capacitor/haptics'; +import { StatusBar, Style } from '@capacitor/status-bar'; +import { BiometryType, NativeBiometric } from '@capgo/capacitor-native-biometric'; +import { NavigationBar } from '@mauricewegner/capacitor-navigation-bar'; +import { SafeArea } from 'capacitor-plugin-safe-area'; +import { SplashScreen } from 'capacitor-splash-screen'; + +import type { Theme } from '../global/types'; + +import { callApi } from '../api'; +import { isTonConnectDeeplink } from './ton/deeplinks'; +import { pause } from './schedulers'; +import { tonConnectGetDeviceInfo } from './tonConnectEnvironment'; +import { IS_BIOMETRIC_AUTH_SUPPORTED, IS_DELEGATED_BOTTOM_SHEET } from './windowEnvironment'; + +let launchUrl: string | undefined; +const IOS_SPLASH_SCREEN_HIDE_DELAY = 500; +const IOS_SPLASH_SCREEN_HIDE_DURATION = 600; +export const VIBRATE_SUCCESS_END_PAUSE_MS = 1300; + +let platform: 'ios' | 'android' | undefined; +let isNativeBiometricAuthSupported = false; +let isFaceIdAvailable = false; +let isTouchIdAvailable = false; +let statusBarHeight = 0; + +export async function initCapacitor() { + platform = Capacitor.getPlatform() as 'ios' | 'android'; + + const biometricsAvailableResult = await NativeBiometric.isAvailable(); + + isNativeBiometricAuthSupported = biometricsAvailableResult.isAvailable; + isFaceIdAvailable = biometricsAvailableResult.biometryType === BiometryType.FACE_ID; + isTouchIdAvailable = biometricsAvailableResult.biometryType === BiometryType.TOUCH_ID; + + if (IS_DELEGATED_BOTTOM_SHEET) { + void SplashScreen.hide({ fadeOutDuration: 0 }); + return; + } + + if (platform === 'ios') { + setTimeout(() => { + void SplashScreen.hide({ fadeOutDuration: IOS_SPLASH_SCREEN_HIDE_DURATION }); + }, IOS_SPLASH_SCREEN_HIDE_DELAY); + } + + launchUrl = (await App.getLaunchUrl())?.url; + + App.addListener('appUrlOpen', (event: URLOpenListenerEvent) => { + processDeeplink(event.url); + }); + + if (launchUrl) { + void processDeeplink(launchUrl); + } + + if (platform === 'android') { + // Until this bug is fixed, the `overlay` must be `false` + // https://bugs.chromium.org/p/chromium/issues/detail?id=1094366 + void StatusBar.setOverlaysWebView({ overlay: false }); + void NavigationBar.setTransparency({ isTransparent: false }); + } + + SafeArea.getStatusBarHeight().then(({ statusBarHeight: height }) => { + statusBarHeight = height; + document.documentElement.style.setProperty('--status-bar-height', `${height}px`); + }); + + await SafeArea.addListener('safeAreaChanged', (data) => { + const { insets } = data; + + for (const [key, value] of Object.entries(insets)) { + document.documentElement.style.setProperty( + `--safe-area-${key}`, + `${value}px`, + ); + } + }); +} + +export async function processDeeplink(url: string) { + if (isTonConnectDeeplink(url)) { + const deviceInfo = tonConnectGetDeviceInfo(); + const returnStrategy = await callApi('startSseConnection', url, deviceInfo); + if (returnStrategy === 'ret') { + await App.minimizeApp(); + } + } +} + +export function switchStatusBar(currentAppTheme: Theme, isSystemDark: boolean, forceDarkBackground?: boolean) { + if (platform !== 'ios') return; + + const style = forceDarkBackground || currentAppTheme === 'dark' + ? Style.Dark + : (isSystemDark && currentAppTheme === 'system' ? Style.Dark : Style.Light); + + void StatusBar.setStyle({ style }); +} + +export function getLaunchUrl() { + return launchUrl; +} + +export function clearLaunchUrl() { + launchUrl = undefined; +} + +export function getCapacitorPlatform() { + return platform; +} + +export function getStatusBarHeight() { + return statusBarHeight; +} + +export async function vibrate() { + await Haptics.impact({ style: ImpactStyle.Light }); +} + +export async function vibrateOnError() { + for (let i = 0; i < 3; i++) { + await Haptics.impact({ style: ImpactStyle.Medium }); + await pause(150); + } +} + +export async function vibrateOnSuccess(withPauseOnEnd = false) { + await pause(300); + await Haptics.impact({ style: ImpactStyle.Heavy }); + await pause(150); + await Haptics.impact({ style: ImpactStyle.Light }); + + if (withPauseOnEnd) { + await pause(VIBRATE_SUCCESS_END_PAUSE_MS); + } +} + +export function getIsNativeBiometricAuthSupported() { + return isNativeBiometricAuthSupported; +} + +export function getIsBiometricAuthSupported() { + return IS_BIOMETRIC_AUTH_SUPPORTED || getIsNativeBiometricAuthSupported(); +} + +export function getIsFaceIdAvailable() { + return isFaceIdAvailable; +} + +export function getIsTouchIdAvailable() { + return isTouchIdAvailable; +} diff --git a/src/util/captureEvents.ts b/src/util/captureEvents.ts new file mode 100644 index 00000000..3a2bd28f --- /dev/null +++ b/src/util/captureEvents.ts @@ -0,0 +1,455 @@ +import { Lethargy } from './lethargy'; +import { clamp, round } from './math'; +import { debounce } from './schedulers'; +import { IS_IOS } from './windowEnvironment'; +import windowSize from './windowSize'; + +export enum SwipeDirection { + Up, + Down, + Left, + Right, +} + +export interface MoveOffsets { + dragOffsetX: number; + dragOffsetY: number; +} + +interface CaptureOptions { + onCapture?: (e: MouseEvent | TouchEvent | WheelEvent) => void; + onRelease?: (e: MouseEvent | TouchEvent | WheelEvent) => void; + onDrag?: ( + e: MouseEvent | TouchEvent | WheelEvent, + captureEvent: MouseEvent | TouchEvent | WheelEvent, + offsets: MoveOffsets, + cancelDrag?: (x: boolean, y: boolean) => void, + ) => void; + onSwipe?: (e: Event, direction: SwipeDirection, offsets: MoveOffsets) => boolean; + onZoom?: (e: TouchEvent | WheelEvent, params: { + // Absolute zoom level + zoom?: number; + // Relative zoom factor + zoomFactor?: number; + + // center coordinate of the initial pinch + initialCenterX: number; + initialCenterY: number; + + // offset of the pinch center (current from initial) + dragOffsetX: number; + dragOffsetY: number; + + // center coordinate of the current pinch + currentCenterX: number; + currentCenterY: number; + }) => void; + onClick?: (e: MouseEvent | TouchEvent) => void; + onDoubleClick?: (e: MouseEvent | RealTouchEvent | WheelEvent, params: { centerX: number; centerY: number }) => void; + includedClosestSelector?: string; + excludedClosestSelector?: string; + selectorToPreventScroll?: string; + withNativeDrag?: boolean; + maxZoom?: number; + minZoom?: number; + doubleTapZoom?: number; + initialZoom?: number; + isNotPassive?: boolean; + withCursor?: boolean; + swipeThreshold?: number; +} + +// https://stackoverflow.com/questions/11287877/how-can-i-get-e-offsetx-on-mobile-ipad +// Android does not have this value, and iOS has it but as read-only +export interface RealTouchEvent extends TouchEvent { + pageX?: number; + pageY?: number; +} + +type TSwipeAxis = + 'x' + | 'y' + | undefined; + +export const IOS_SCREEN_EDGE_THRESHOLD = 20; +const MOVE_THRESHOLD = 15; +const SWIPE_THRESHOLD_DEFAULT = 20; +const RELEASE_WHEEL_ZOOM_DELAY = 150; +const RELEASE_WHEEL_DRAG_DELAY = 150; + +function getDistance(a: Touch, b?: Touch) { + if (!b) return 0; + return Math.hypot((b.pageX - a.pageX), (b.pageY - a.pageY)); +} + +function getTouchCenter(a: Touch, b: Touch) { + return { + x: (a.pageX + b.pageX) / 2, + y: (a.pageY + b.pageY) / 2, + }; +} + +let lastClickTime = 0; +const lethargy = new Lethargy({ + stability: 5, + sensitivity: 25, + tolerance: 0.6, + delay: 150, +}); + +export function captureEvents(element: HTMLElement, options: CaptureOptions) { + let captureEvent: MouseEvent | RealTouchEvent | WheelEvent | undefined; + let hasMoved = false; + let hasSwiped = false; + let isZooming = false; + let initialDistance = 0; + let wheelZoom = options.initialZoom ?? 1; + let initialDragOffset = { + x: 0, + y: 0, + }; + let isDragCanceled = { + x: false, + y: false, + }; + const currentWindowSize = windowSize.get(); + let initialTouchCenter = { + x: currentWindowSize.width / 2, + y: currentWindowSize.height / 2, + }; + let initialSwipeAxis: TSwipeAxis | undefined; + const minZoom = options.minZoom ?? 1; + const maxZoom = options.maxZoom ?? 4; + + function onCapture(e: MouseEvent | RealTouchEvent) { + const target = e.target as HTMLElement; + const { + excludedClosestSelector, + includedClosestSelector, + withNativeDrag, + withCursor, + onDrag, + } = options; + + if ( + (excludedClosestSelector && (target.matches(excludedClosestSelector) || target.closest(excludedClosestSelector))) + || ( + includedClosestSelector && !(target.matches(includedClosestSelector) || target.closest(includedClosestSelector)) + ) + ) { + return; + } + + captureEvent = e; + + if (e.type === 'mousedown') { + if (!withNativeDrag && onDrag) { + e.preventDefault(); + } + + document.addEventListener('mousemove', onMove); + document.addEventListener('mouseup', onRelease); + } else if (e.type === 'touchstart') { + // We need to always listen on `touchstart` target: + // https://stackoverflow.com/questions/33298828/touch-move-event-dont-fire-after-touch-start-target-is-removed + target.addEventListener('touchmove', onMove, { passive: true }); + target.addEventListener('touchend', onRelease, { passive: true }); + target.addEventListener('touchcancel', onRelease, { passive: true }); + + if ('touches' in e) { + if (e.pageX === undefined) { + e.pageX = e.touches[0].pageX; + } + + if (e.pageY === undefined) { + e.pageY = e.touches[0].pageY; + } + + if (e.touches.length === 2) { + initialDistance = getDistance(e.touches[0], e.touches[1]); + initialTouchCenter = getTouchCenter(e.touches[0], e.touches[1]); + } + } + } + + if (withCursor) { + document.body.classList.add('cursor-grabbing'); + } + + options.onCapture?.(e); + } + + function onRelease(e?: MouseEvent | TouchEvent) { + if (captureEvent) { + if (options.withCursor) { + document.body.classList.remove('cursor-grabbing'); + } + + document.removeEventListener('mouseup', onRelease); + document.removeEventListener('mousemove', onMove); + (captureEvent.target as HTMLElement).removeEventListener('touchcancel', onRelease); + (captureEvent.target as HTMLElement).removeEventListener('touchend', onRelease); + (captureEvent.target as HTMLElement).removeEventListener('touchmove', onMove); + + if (IS_IOS && options.selectorToPreventScroll) { + Array.from(document.querySelectorAll(options.selectorToPreventScroll)) + .forEach((scrollable) => { + scrollable.style.overflow = ''; + }); + } + + if (e) { + if (hasMoved) { + if (options.onRelease) { + options.onRelease(e); + } + } else if (e.type === 'mouseup') { + if (options.onDoubleClick && Date.now() - lastClickTime < 300) { + options.onDoubleClick(e, { + centerX: captureEvent!.pageX!, + centerY: captureEvent!.pageY!, + }); + } else if (options.onClick && (!('button' in e) || e.button === 0)) { + options.onClick(e); + } + lastClickTime = Date.now(); + } + } + } + + hasMoved = false; + hasSwiped = false; + isZooming = false; + initialDistance = 0; + wheelZoom = clamp(wheelZoom, minZoom, maxZoom); + initialSwipeAxis = undefined; + initialDragOffset = { + x: 0, + y: 0, + }; + isDragCanceled = { + x: false, + y: false, + }; + const newWindowSize = windowSize.get(); + initialTouchCenter = { + x: newWindowSize.width / 2, + y: newWindowSize.height / 2, + }; + captureEvent = undefined; + } + + function onMove(e: MouseEvent | RealTouchEvent) { + if (captureEvent) { + if (e.type === 'touchmove' && ('touches' in e)) { + if (e.pageX === undefined) { + e.pageX = e.touches[0].pageX; + } + + if (e.pageY === undefined) { + e.pageY = e.touches[0].pageY; + } + + if (options.onZoom && initialDistance > 0 && e.touches.length === 2) { + const endDistance = getDistance(e.touches[0], e.touches[1]); + const touchCenter = getTouchCenter(e.touches[0], e.touches[1]); + const dragOffsetX = touchCenter.x - initialTouchCenter.x; + const dragOffsetY = touchCenter.y - initialTouchCenter.y; + const zoomFactor = endDistance / initialDistance; + options.onZoom(e, { + zoomFactor, + initialCenterX: initialTouchCenter.x, + initialCenterY: initialTouchCenter.y, + dragOffsetX, + dragOffsetY, + currentCenterX: touchCenter.x, + currentCenterY: touchCenter.y, + }); + if (zoomFactor !== 1) hasMoved = true; + } + } + + const dragOffsetX = e.pageX! - captureEvent.pageX!; + const dragOffsetY = e.pageY! - captureEvent.pageY!; + + if (Math.abs(dragOffsetX) >= MOVE_THRESHOLD || Math.abs(dragOffsetY) >= MOVE_THRESHOLD) { + hasMoved = true; + } + + let shouldPreventScroll = false; + + if (options.onDrag) { + options.onDrag(e, captureEvent, { + dragOffsetX, + dragOffsetY, + }); + shouldPreventScroll = true; + } + + if (options.onSwipe && !hasSwiped) { + hasSwiped = onSwipe(e, dragOffsetX, dragOffsetY); + shouldPreventScroll = hasSwiped; + } + + if (IS_IOS && shouldPreventScroll && options.selectorToPreventScroll) { + Array.from(document.querySelectorAll(options.selectorToPreventScroll)) + .forEach((scrollable) => { + scrollable.style.overflow = 'hidden'; + }); + } + } + } + + function onSwipe(e: MouseEvent | RealTouchEvent, dragOffsetX: number, dragOffsetY: number) { + // Avoid conflicts with swipe-to-back gestures + if (IS_IOS) { + const x = (e as RealTouchEvent).touches[0].pageX; + if (x <= IOS_SCREEN_EDGE_THRESHOLD || x >= windowSize.get().width - IOS_SCREEN_EDGE_THRESHOLD) { + return false; + } + } + + const xAbs = Math.abs(dragOffsetX); + const yAbs = Math.abs(dragOffsetY); + const threshold = options.swipeThreshold ?? SWIPE_THRESHOLD_DEFAULT; + + let axis: TSwipeAxis | undefined; + if (xAbs > yAbs && xAbs >= threshold) { + axis = 'x'; + } else if (yAbs > xAbs && yAbs >= threshold) { + axis = 'y'; + } + + if (!axis) { + return false; + } + + if (!initialSwipeAxis) { + initialSwipeAxis = axis; + } else if (initialSwipeAxis !== axis) { + // Prevent horizontal swipe after vertical to prioritize scroll + return false; + } + + return processSwipe(e, axis, dragOffsetX, dragOffsetY, options.onSwipe!); + } + + const releaseWheelDrag = debounce(onRelease, RELEASE_WHEEL_DRAG_DELAY, false); + const releaseWheelZoom = debounce(onRelease, RELEASE_WHEEL_ZOOM_DELAY, false); + + function onWheelCapture(e: WheelEvent) { + if (hasMoved) return; + onCapture(e); + hasMoved = true; + initialTouchCenter = { x: e.x, y: e.y }; + } + + function onWheelZoom(e: WheelEvent) { + if (!options.onZoom) return; + onWheelCapture(e); + const dragOffsetX = e.x - initialTouchCenter.x; + const dragOffsetY = e.y - initialTouchCenter.y; + const delta = clamp(e.deltaY, -25, 25); + wheelZoom -= delta * 0.01; + wheelZoom = clamp(wheelZoom, minZoom * 0.5, maxZoom * 3); + isZooming = true; + options.onZoom(e, { + zoom: round(wheelZoom, 2), + initialCenterX: initialTouchCenter.x, + initialCenterY: initialTouchCenter.y, + dragOffsetX, + dragOffsetY, + currentCenterX: e.x, + currentCenterY: e.y, + }); + releaseWheelZoom(e); + } + + function onWheelDrag(e: WheelEvent) { + if (!options.onDrag) return; + onWheelCapture(e); + // Ignore wheel inertia if drag is canceled in this direction + if (!isDragCanceled.x || Math.sign(initialDragOffset.x) === Math.sign(e.deltaX)) { + initialDragOffset.x -= e.deltaX; + } + if (!isDragCanceled.y || Math.sign(initialDragOffset.y) === Math.sign(e.deltaY)) { + initialDragOffset.y -= e.deltaY; + } + const { x, y } = initialDragOffset; + options.onDrag(e, captureEvent!, { + dragOffsetX: x, + dragOffsetY: y, + }, (dx, dy) => { + isDragCanceled = { x: dx, y: dy }; + }); + releaseWheelDrag(e); + } + + function onWheel(e: WheelEvent) { + if (!options.onZoom && !options.onDrag) return; + if (options.excludedClosestSelector && ( + (e.target as HTMLElement).matches(options.excludedClosestSelector) + || (e.target as HTMLElement).closest(options.excludedClosestSelector) + )) { + return; + } + e.preventDefault(); + e.stopPropagation(); + const { doubleTapZoom = 3 } = options; + if (options.onDoubleClick && Object.is(e.deltaX, -0) && Object.is(e.deltaY, -0) && e.ctrlKey) { + onWheelCapture(e); + wheelZoom = wheelZoom > 1 ? 1 : doubleTapZoom; + options.onDoubleClick(e, { centerX: e.pageX, centerY: e.pageY }); + hasMoved = false; + return; + } + const metaKeyPressed = e.metaKey || e.ctrlKey || e.shiftKey; + if (metaKeyPressed) { + onWheelZoom(e); + } + if (!metaKeyPressed && !isZooming) { + // Check if this event produced by user scroll and not by inertia + const isUserEvent = lethargy.check(e); + if (wheelZoom !== 1 || isUserEvent) { + onWheelDrag(e); + } + } + } + + element.addEventListener('wheel', onWheel); + element.addEventListener('mousedown', onCapture); + document.body.addEventListener('touchstart', onCapture, { passive: !options.isNotPassive }); + + return () => { + onRelease(); + element.removeEventListener('wheel', onWheel); + document.body.removeEventListener('touchstart', onCapture); + element.removeEventListener('mousedown', onCapture); + }; +} + +function processSwipe( + e: Event, + currentSwipeAxis: TSwipeAxis, + dragOffsetX: number, + dragOffsetY: number, + onSwipe: (e: Event, direction: SwipeDirection, offsets: MoveOffsets) => boolean, +) { + const offsets = { dragOffsetX, dragOffsetY }; + + if (currentSwipeAxis === 'x') { + if (dragOffsetX < 0) { + return onSwipe(e, SwipeDirection.Left, offsets); + } else { + return onSwipe(e, SwipeDirection.Right, offsets); + } + } else if (currentSwipeAxis === 'y') { + if (dragOffsetY < 0) { + return onSwipe(e, SwipeDirection.Up, offsets); + } else { + return onSwipe(e, SwipeDirection.Down, offsets); + } + } + + return false; +} diff --git a/src/util/captureSwipe.ts b/src/util/captureSwipe.ts deleted file mode 100644 index d6827b16..00000000 --- a/src/util/captureSwipe.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { IS_IOS } from './windowEnvironment'; -import windowSize from './windowSize'; - -export enum SwipeDirection { - Up, - Down, - Left, - Right, -} - -// https://stackoverflow.com/questions/11287877/how-can-i-get-e-offsetx-on-mobile-ipad -// Android does not have this value, and iOS has it but as read-only -export interface RealTouchEvent extends TouchEvent { - pageX?: number; - pageY?: number; -} - -type TSwipeAxis = - 'x' - | 'y' - | undefined; - -export const IOS_SCREEN_EDGE_THRESHOLD = 20; -const SWIPE_THRESHOLD = 50; - -export function captureSwipe(element: HTMLElement, handleSwipe: (e: Event, direction: SwipeDirection) => boolean) { - let captureEvent: MouseEvent | RealTouchEvent | undefined; - let hasSwiped = false; - let initialSwipeAxis: TSwipeAxis | undefined; - - function onCapture(e: MouseEvent | RealTouchEvent) { - captureEvent = e; - - if (e.type === 'touchstart') { - // We need to always listen on `touchstart` target: - // https://stackoverflow.com/questions/33298828/touch-move-event-dont-fire-after-touch-start-target-is-removed - const target = e.target as HTMLElement; - target.addEventListener('touchmove', onMove, { passive: true }); - target.addEventListener('touchend', onRelease); - target.addEventListener('touchcancel', onRelease); - - if ('touches' in e) { - if (e.pageX === undefined) { - e.pageX = e.touches[0].pageX; - } - - if (e.pageY === undefined) { - e.pageY = e.touches[0].pageY; - } - } - } - } - - function onRelease() { - if (captureEvent) { - (captureEvent.target as HTMLElement).removeEventListener('touchcancel', onRelease); - (captureEvent.target as HTMLElement).removeEventListener('touchend', onRelease); - (captureEvent.target as HTMLElement).removeEventListener('touchmove', onMove); - } - - hasSwiped = false; - initialSwipeAxis = undefined; - captureEvent = undefined; - } - - function onMove(e: MouseEvent | RealTouchEvent) { - if (captureEvent) { - if (e.type === 'touchmove' && ('touches' in e)) { - if (e.pageX === undefined) { - e.pageX = e.touches[0].pageX; - } - - if (e.pageY === undefined) { - e.pageY = e.touches[0].pageY; - } - } - - const dragOffsetX = e.pageX! - captureEvent.pageX!; - const dragOffsetY = e.pageY! - captureEvent.pageY!; - - if (!hasSwiped) { - hasSwiped = onSwipe(e, dragOffsetX, dragOffsetY); - } - } - } - - function onSwipe(e: MouseEvent | RealTouchEvent, dragOffsetX: number, dragOffsetY: number) { - // Avoid conflicts with swipe-to-back gestures - if (IS_IOS) { - const x = (e as RealTouchEvent).touches[0].pageX; - if (x <= IOS_SCREEN_EDGE_THRESHOLD || x >= windowSize.get().width - IOS_SCREEN_EDGE_THRESHOLD) { - return false; - } - } - - const xAbs = Math.abs(dragOffsetX); - const yAbs = Math.abs(dragOffsetY); - - if (dragOffsetX && dragOffsetY) { - const ratio = Math.max(xAbs, yAbs) / Math.min(xAbs, yAbs); - // Diagonal swipe - if (ratio < 2) { - return false; - } - } - - let axis: TSwipeAxis | undefined; - if (xAbs >= SWIPE_THRESHOLD) { - axis = 'x'; - } else if (yAbs >= SWIPE_THRESHOLD) { - axis = 'y'; - } - - if (!axis) { - return false; - } - - if (!initialSwipeAxis) { - initialSwipeAxis = axis; - } else if (initialSwipeAxis !== axis) { - // Prevent horizontal swipe after vertical to prioritize scroll - return false; - } - - return processSwipe(e, axis, dragOffsetX, dragOffsetY, handleSwipe); - } - - element.addEventListener('touchstart', onCapture, { passive: true }); - - return () => { - onRelease(); - element.removeEventListener('touchstart', onCapture); - }; -} - -function processSwipe( - e: Event, - currentSwipeAxis: TSwipeAxis, - dragOffsetX: number, - dragOffsetY: number, - onSwipe: (e: Event, direction: SwipeDirection) => boolean, -) { - if (currentSwipeAxis === 'x') { - if (dragOffsetX < 0) { - return onSwipe(e, SwipeDirection.Left); - } else { - return onSwipe(e, SwipeDirection.Right); - } - } else if (currentSwipeAxis === 'y') { - if (dragOffsetY < 0) { - return onSwipe(e, SwipeDirection.Up); - } else { - return onSwipe(e, SwipeDirection.Down); - } - } - - return false; -} diff --git a/src/util/clipboard.ts b/src/util/clipboard.ts index 91f3c488..1cf99d44 100644 --- a/src/util/clipboard.ts +++ b/src/util/clipboard.ts @@ -7,24 +7,8 @@ textCopyEl.setAttribute('readonly', ''); textCopyEl.tabIndex = -1; textCopyEl.className = 'visually-hidden'; -export const copyTextToClipboard = (str: string): void => { - textCopyEl.value = str; - document.body.appendChild(textCopyEl); - const selection = document.getSelection(); - - if (selection) { - // Store previous selection - const rangeToRestore = selection.rangeCount > 0 && selection.getRangeAt(0); - textCopyEl.select(); - document.execCommand('copy'); - // Restore the original selection - if (rangeToRestore) { - selection.removeAllRanges(); - selection.addRange(rangeToRestore); - } - } - - document.body.removeChild(textCopyEl); +export const copyTextToClipboard = (str: string): Promise => { + return navigator.clipboard.writeText(str); }; export const copyImageToClipboard = (imageUrl?: string) => { diff --git a/src/util/createPostMessageInterface.ts b/src/util/createPostMessageInterface.ts index bd8f225d..df86fa7b 100644 --- a/src/util/createPostMessageInterface.ts +++ b/src/util/createPostMessageInterface.ts @@ -151,7 +151,7 @@ async function onMessage( ); } } catch (err: any) { - logDebugError('onMessage:callMethod', err); + logDebugError(name, err); if (messageId) { sendToOrigin({ diff --git a/src/util/cssAnimationEndListeners.ts b/src/util/cssAnimationEndListeners.ts index bd89eb15..2731339d 100644 --- a/src/util/cssAnimationEndListeners.ts +++ b/src/util/cssAnimationEndListeners.ts @@ -4,13 +4,13 @@ const ANIMATION_END_DELAY = 50; export function waitForTransitionEnd( node: Node, handler: NoneToVoidFunction, propertyName?: string, fallbackMs?: number, ) { - waitForEndEvent('transitionend', node, handler, propertyName, fallbackMs); + return waitForEndEvent('transitionend', node, handler, propertyName, fallbackMs); } export function waitForAnimationEnd( node: Node, handler: NoneToVoidFunction, animationName?: string, fallbackMs?: number, ) { - waitForEndEvent('animationend', node, handler, animationName, fallbackMs); + return waitForEndEvent('animationend', node, handler, animationName, fallbackMs); } function waitForEndEvent( @@ -22,6 +22,10 @@ function waitForEndEvent( ) { let isHandled = false; + function cleanup() { + node.removeEventListener(eventType, handleAnimationEnd); + } + function handleAnimationEnd(e: TransitionEvent | AnimationEvent | Event) { if (isHandled || e.target !== e.currentTarget) { return; @@ -36,7 +40,7 @@ function waitForEndEvent( isHandled = true; - node.removeEventListener(eventType, handleAnimationEnd); + cleanup(); setTimeout(() => { handler(); @@ -49,9 +53,11 @@ function waitForEndEvent( setTimeout(() => { if (isHandled) return; - node.removeEventListener(eventType, handleAnimationEnd); + cleanup(); handler(); }, fallbackMs); } + + return cleanup; } diff --git a/src/util/cssColorToHex.ts b/src/util/cssColorToHex.ts new file mode 100644 index 00000000..2ee2bc57 --- /dev/null +++ b/src/util/cssColorToHex.ts @@ -0,0 +1,11 @@ +export default function cssColorToHex(cssColor: string) { + if (/^#[0-9A-F]{6}$/i.test(cssColor)) return cssColor; + + return `#${cssColor.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+\.{0,1}\d*))?\)$/)! + .slice(1) + .map((n: string, i: number) => (i === 3 ? Math.round(parseFloat(n) * 255) : parseFloat(n)) + .toString(16) + .padStart(2, '0') + .replace('NaN', '')) + .join('')}`; +} diff --git a/src/util/debugOverlay.ts b/src/util/debugOverlay.ts index e30378c0..2f9d9917 100644 --- a/src/util/debugOverlay.ts +++ b/src/util/debugOverlay.ts @@ -1,4 +1,5 @@ -import { throttle } from './schedulers'; +import { animate } from './animation'; +import { fastRaf, throttle } from './schedulers'; const KEYS_TO_IGNORE = new Set([ 'TeactMemoWrapper renders', @@ -87,10 +88,33 @@ export function renderCounters() { .join('\n'); } +export function debugFps() { + if (!loggerEl) { + setupOverlay(); + } + + let ticks: number[] = []; + let lastFrameAt = performance.now(); + + animate(() => { + const now = performance.now(); + ticks.push(now - lastFrameAt); + lastFrameAt = now; + + if (ticks.length > 100) { + ticks = ticks.slice(-100); + } + + const avg = ticks.reduce((acc, t) => acc + t, 0) / ticks.length; + loggerEl!.innerHTML = `${Math.round(1000 / avg)} FPS`; + return true; + }, fastRaf); +} + function setupOverlay() { loggerEl = document.createElement('div'); loggerEl.style.cssText = 'position: absolute; left: 0; bottom: 25px; z-index: 9998; width: 260px; height: 200px;' - + ' border: 1px solid #555; background: rgba(255, 255, 255, 0.9); overflow: auto; font-size: 10px;'; + + ' border: 1px solid #555; background: rgba(255, 255, 255, 0.9); overflow: auto; font-size: 50px; color: black;'; document.body.appendChild(loggerEl); const clearEl = document.createElement('a'); diff --git a/src/util/deepDiff.ts b/src/util/deepDiff.ts new file mode 100644 index 00000000..5d0ec2b3 --- /dev/null +++ b/src/util/deepDiff.ts @@ -0,0 +1,63 @@ +const EQUAL = Symbol('EQUAL'); + +function deepAreSortedArraysEqual>(array1: T, array2: T) { + if (array1.length !== array2.length) { + return false; + } + + return array1.every((item, i) => deepDiff(item, array2[i]) === EQUAL); +} + +export function deepDiff(value1: T, value2: T): Partial | typeof EQUAL { + const type1 = typeof value1; + const type2 = typeof value2; + + if (value1 === value2) { + return EQUAL; + } + + if (type1 !== type2) { + return value2; + } + + if (type2 !== 'object') { + return value2; + } + + if (Array.isArray(value1) && Array.isArray(value2)) { + if (deepAreSortedArraysEqual(value1, value2)) return EQUAL; + + return value2; + } + + const object1 = value1 as AnyLiteral; + const object2 = value2 as AnyLiteral; + const keys1 = Array.from(new Set([...Object.keys(object1), ...Object.keys(object2)])); + + const reduced = keys1.reduce((acc: any, el) => { + if (object1[el] === object2[el]) { + return acc; + } + + const o1has = object1.hasOwnProperty(el); + const o2has = object2.hasOwnProperty(el); + if (!o2has) { + acc[el] = { __delete: true }; + return acc; + } + if (!o1has && o2has) { + acc[el] = object2[el]; + return acc; + } + + const diff = deepDiff(object1[el], object2[el]); + if (diff !== EQUAL) acc[el] = diff; + return acc; + }, {}); + + if (Object.keys(reduced).length === 0) { + return EQUAL; + } + + return reduced; +} diff --git a/src/util/deepMerge.ts b/src/util/deepMerge.ts new file mode 100644 index 00000000..50fccd97 --- /dev/null +++ b/src/util/deepMerge.ts @@ -0,0 +1,34 @@ +import { omit } from './iteratees'; + +export function deepMerge(value1: T, value2: Partial): T { + const type1 = typeof value1; + const type2 = typeof value2; + if (type1 !== 'object') { + return value2 as T; + } + + if (Array.isArray(value2)) { + return value2 as T; + } + + if (type1 !== type2) { + return value2 as T; + } + + if (value1 === value2) { + return value2 as T; + } + + const object1 = value1 as AnyLiteral; + const object2 = value2 as AnyLiteral; + const keys = Object.keys(object2); + // eslint-disable-next-line no-underscore-dangle + const keysDeleted = keys.filter((k) => object2[k]?.__delete); + // eslint-disable-next-line no-underscore-dangle + const keysNotDeleted = keys.filter((k) => !object2[k]?.__delete); + return keysNotDeleted.reduce((acc: any, key) => { + acc[key] = deepMerge(object1[key], object2[key]); + + return acc; + }, { ...omit(object1, keysDeleted) }); +} diff --git a/src/util/formatNumber.ts b/src/util/formatNumber.ts index 8cae785f..ba104f36 100644 --- a/src/util/formatNumber.ts +++ b/src/util/formatNumber.ts @@ -1,8 +1,12 @@ -import { DEFAULT_DECIMAL_PLACES, DEFAULT_PRICE_CURRENCY } from '../config'; +import type { ApiBaseCurrency } from '../api/types'; + +import { DEFAULT_DECIMAL_PLACES, DEFAULT_PRICE_CURRENCY, SHORT_CURRENCY_SYMBOL_MAP } from '../config'; import withCache from './withCache'; +const SHORT_SYMBOLS = new Set(Object.values(SHORT_CURRENCY_SYMBOL_MAP)); + export const formatInteger = withCache((value: number, fractionDigits = 2, noRadix = false) => { - const dp = value > 1 ? fractionDigits : DEFAULT_DECIMAL_PLACES; + const dp = value >= 1 ? fractionDigits : DEFAULT_DECIMAL_PLACES; const fixed = value.toFixed(dp); let [wholePart, fractionPart = ''] = fixed.split('.'); @@ -23,7 +27,7 @@ export const formatInteger = withCache((value: number, fractionDigits = 2, noRad export function formatCurrency(value: number, currency: string, fractionDigits?: number) { const formatted = formatInteger(value, fractionDigits); - return currency === '$' ? `$${formatted}`.replace('$-', '-$') : `${formatted} ${currency}`; + return addCurrency(formatted, currency); } export function formatCurrencyExtended(value: number, currency: string, noSign = false, fractionDigits?: number) { @@ -32,8 +36,24 @@ export function formatCurrencyExtended(value: number, currency: string, noSign = return prefix + formatCurrency(noSign ? value : Math.abs(value), currency, fractionDigits); } -export function formatCurrencyForBigValue(value: number, threshold = 1000) { - const formattedValue = formatCurrency(value, DEFAULT_PRICE_CURRENCY); +export function formatCurrencySimple(value: number, currency: string, decimals?: number) { + const stringValue = clearZeros(value.toFixed(decimals ?? DEFAULT_DECIMAL_PLACES)); + return addCurrency(stringValue, currency); +} + +function addCurrency(value: number | string, currency: string) { + return SHORT_SYMBOLS.has(currency) + ? `${currency}${value}`.replace(`${currency}-`, `-${currency}`) + : `${value} ${currency}`; +} + +function clearZeros(value: string) { + if (value.indexOf('.') === -1) return value; + return value.replace(/\.?0*$/, ''); +} + +export function formatCurrencyForBigValue(value: number, currency: string, threshold = 1000) { + const formattedValue = formatCurrency(value, currency); if (value < threshold) { return formattedValue; @@ -74,3 +94,8 @@ function toSignificant(value: string, fractionDigits: number): string { return value.slice(0, digitsLastIndex).replace(/0+$/, ''); } + +export function getShortCurrencySymbol(currency?: ApiBaseCurrency) { + if (!currency) currency = DEFAULT_PRICE_CURRENCY; + return SHORT_CURRENCY_SYMBOL_MAP[currency as keyof typeof SHORT_CURRENCY_SYMBOL_MAP] ?? currency; +} diff --git a/src/util/getIsAppUpdateNeeded.ts b/src/util/getIsAppUpdateNeeded.ts new file mode 100644 index 00000000..23f5ab1a --- /dev/null +++ b/src/util/getIsAppUpdateNeeded.ts @@ -0,0 +1,7 @@ +const APP_VERSION_REGEX = /^\d+\.\d+(\.\d+)?$/; + +export default function getIsAppUpdateNeeded(remoteVersion: string, appVersion: string) { + const sanitizedRemoteVersion = remoteVersion.trim(); + + return APP_VERSION_REGEX.test(sanitizedRemoteVersion) && sanitizedRemoteVersion !== appVersion; +} diff --git a/src/util/iteratees.ts b/src/util/iteratees.ts index f5f6acf0..54d5eb03 100644 --- a/src/util/iteratees.ts +++ b/src/util/iteratees.ts @@ -8,7 +8,7 @@ interface OrderCallback { (member: T): any; } -export function buildCollectionByKey(collection: T[], key: keyof T) { +export function buildCollectionByKey(collection: T[], key: keyof T): CollectionByKey { return collection.reduce((byKey: CollectionByKey, member: T) => { byKey[member[key]] = member; diff --git a/src/util/langProvider.ts b/src/util/langProvider.ts index 0983e78e..745f79b8 100644 --- a/src/util/langProvider.ts +++ b/src/util/langProvider.ts @@ -2,7 +2,7 @@ import type { TeactNode } from '../lib/teact/teact'; import type { LangCode, LangPack, LangString } from '../global/types'; -import { IS_ELECTRON, LANG_CACHE_NAME, LANG_LIST } from '../config'; +import { IS_ELECTRON_BUILD, LANG_CACHE_NAME, LANG_LIST } from '../config'; import renderText from '../global/helpers/renderText'; // @ts-ignore this file is autogenerated import defaultLangPackJson from '../i18n/en.json'; @@ -109,7 +109,7 @@ async function fetchRemote(langCode: string): Promise { return defaultLangPack; } - const response = await fetch(`${IS_ELECTRON ? '.' : '..'}/i18n/${langCode}.json`); + const response = await fetch(`${IS_ELECTRON_BUILD ? '.' : '..'}/i18n/${langCode}.json`); if (!response.ok) { const message = `An error has occured: ${response.status}`; diff --git a/src/util/ledger/index.ts b/src/util/ledger/index.ts index 1b4c6354..50e1202a 100644 --- a/src/util/ledger/index.ts +++ b/src/util/ledger/index.ts @@ -22,19 +22,19 @@ import { 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 { + DEFAULT_IS_BOUNCEABLE, + TOKEN_TRANSFER_TON_AMOUNT, + TOKEN_TRANSFER_TON_FORWARD_AMOUNT, +} from '../../api/blockchains/ton/constants'; +import { ApiUserRejectsError, handleServerError } from '../../api/errors'; import { parseAccountId } from '../account'; -import { range } from '../iteratees'; import { logDebugError } from '../logs'; import { pause } from '../schedulers'; import { isValidLedgerComment } from './utils'; const CHAIN = 0; // workchain === -1 ? 255 : 0; const VERSION = 'v4R2'; -const ACCOUNTS_PAGE = 9; const ATTEMPTS = 10; const PAUSE = 125; const IS_BOUNCEABLE = false; @@ -145,9 +145,10 @@ export async function submitLedgerTransfer(options: ApiSubmitTransferOptions) { ]); let payload: TonPayloadFormat | undefined; - let isBounceable = Address.parseFriendly(toAddress).isBounceable; + const parsedAddress = Address.parseFriendly(toAddress); + let isBounceable = parsedAddress.isBounceable; // Force default bounceable address for `waitTxComplete` to work properly - const normalizedAddress = toBase64Address(toAddress); + const normalizedAddress = parsedAddress.address.toString({ urlSafe: true, bounceable: DEFAULT_IS_BOUNCEABLE }); if (slug !== TON_TOKEN_SLUG) { ({ toAddress, amount, payload } = await buildLedgerTokenTransfer( @@ -272,7 +273,7 @@ export async function signLedgerTransactions( ledgerPayload = undefined; break; } - case 'transfer-nft': { + case 'nft:transfer': { const { queryId, newOwner, @@ -296,7 +297,7 @@ export async function signLedgerTransactions( }; break; } - case 'transfer-tokens': { + case 'tokens:transfer': { const { queryId, amount: jettonAmount, @@ -389,12 +390,28 @@ export async function signLedgerProof(accountId: string, proof: ApiTonConnectPro return result.signature.toString('base64'); } -export function getFirstLedgerWallets(network: ApiNetwork) { - const accountIndexes = range(0, ACCOUNTS_PAGE); +export async function getNextLedgerWallets(network: ApiNetwork, lastExistingIndex = -1) { + const result: LedgerWalletInfo[] = []; + let index = lastExistingIndex + 1; + + try { + // eslint-disable-next-line no-constant-condition + while (true) { + const walletInfo = await getLedgerWalletInfo(network, index, IS_BOUNCEABLE); + if (walletInfo.balance !== '0') { + result.push(walletInfo); + index += 1; + continue; + } - return Promise.all(accountIndexes.map((index) => { - return getLedgerWalletInfo(network, index, IS_BOUNCEABLE); - })); + if (!result.length) { + result.push(walletInfo); + } + return result; + } + } catch (err) { + return handleServerError(err); + } } export async function getLedgerWalletInfo( @@ -403,7 +420,7 @@ export async function getLedgerWalletInfo( isBounceable: boolean, ): Promise { const { address, publicKey } = await getLedgerWalletAddress(accountIndex, isBounceable); - const balance = await getWalletBalance(network, address); + const balance = (await callApi('getWalletBalance', network, address))!; return { index: accountIndex, diff --git a/src/util/ledger/tab.ts b/src/util/ledger/tab.ts index bb5d75bc..661d7893 100644 --- a/src/util/ledger/tab.ts +++ b/src/util/ledger/tab.ts @@ -1,15 +1,29 @@ export const DETACHED_TAB_URL = '#detached'; +let ledgerTabId: number | undefined; + export function openLedgerTab() { return createLedgerTab(); } +export async function closeLedgerTab() { + if (!ledgerTabId) return; + + await chrome.tabs.query({ active: true }, () => { + if (!ledgerTabId) return; + + chrome.tabs.remove(ledgerTabId); + }); +} + export function onLedgerTabClose(id: number, onClose: () => void) { chrome.tabs.onRemoved.addListener((closedTabId: number) => { if (closedTabId !== id) { return; } + ledgerTabId = undefined; + onClose(); }); } @@ -17,5 +31,8 @@ export function onLedgerTabClose(id: number, onClose: () => void) { async function createLedgerTab() { 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!; + + ledgerTabId = tab.id!; + + return ledgerTabId; } diff --git a/src/util/lethargy.ts b/src/util/lethargy.ts new file mode 100644 index 00000000..c5019895 --- /dev/null +++ b/src/util/lethargy.ts @@ -0,0 +1,99 @@ +/** + * Lethargy help distinguish between scroll events initiated by the user, and those by inertial scrolling. + * Lethargy does not have external dependencies. + * + * @param stability - Specifies the length of the rolling average. + * In effect, the larger the value, the smoother the curve will be. + * This attempts to prevent anomalies from firing 'real' events. Valid values are all positive integers, + * but in most cases, you would need to stay between 5 and around 30. + * + * @param sensitivity - Specifies the minimum value for wheelDelta for it to register as a valid scroll event. + * Because the tail of the curve have low wheelDelta values, + * this will stop them from registering as valid scroll events. + * The unofficial standard wheelDelta is 120, so valid values are positive integers below 120. + * + * @param tolerance - Prevent small fluctuations from affecting results. + * Valid values are decimals from 0, but should ideally be between 0.05 and 0.3. + * + * Based on https://github.com/d4nyll/lethargy + */ + +export type LethargyConfig = { + stability?: number; + sensitivity?: number; + tolerance?: number; + delay?: number; +}; + +export class Lethargy { + stability: number; + + sensitivity: number; + + tolerance: number; + + delay: number; + + lastUpDeltas: Array; + + lastDownDeltas: Array; + + deltasTimestamp: Array; + + constructor({ + stability = 8, + sensitivity = 100, + tolerance = 1.1, + delay = 150, + }: LethargyConfig = {}) { + this.stability = stability; + this.sensitivity = sensitivity; + this.tolerance = tolerance; + this.delay = delay; + this.lastUpDeltas = new Array(this.stability * 2).fill(0); + this.lastDownDeltas = new Array(this.stability * 2).fill(0); + this.deltasTimestamp = new Array(this.stability * 2).fill(0); + } + + check(e: any) { + let lastDelta; + e = e.originalEvent || e; + if (e.wheelDelta !== undefined) { + lastDelta = e.wheelDelta; + } else if (e.deltaY !== undefined) { + lastDelta = e.deltaY * -40; + } else if (e.detail !== undefined || e.detail === 0) { + lastDelta = e.detail * -40; + } + this.deltasTimestamp.push(Date.now()); + this.deltasTimestamp.shift(); + if (lastDelta > 0) { + this.lastUpDeltas.push(lastDelta); + this.lastUpDeltas.shift(); + return this.isInertia(1); + } else { + this.lastDownDeltas.push(lastDelta); + this.lastDownDeltas.shift(); + return this.isInertia(-1); + } + } + + isInertia(direction: number) { + const lastDeltas = direction === -1 ? this.lastDownDeltas : this.lastUpDeltas; + if (lastDeltas[0] === undefined) return direction; + if ( + this.deltasTimestamp[this.stability * 2 - 2] + this.delay > Date.now() + && lastDeltas[0] === lastDeltas[this.stability * 2 - 1] + ) { + return false; + } + const lastDeltasOld = lastDeltas.slice(0, this.stability); + const lastDeltasNew = lastDeltas.slice(this.stability, this.stability * 2); + const oldSum = lastDeltasOld.reduce((t, s) => t + s); + const newSum = lastDeltasNew.reduce((t, s) => t + s); + const oldAverage = oldSum / lastDeltasOld.length; + const newAverage = newSum / lastDeltasNew.length; + return Math.abs(oldAverage) < Math.abs(newAverage * this.tolerance) + && this.sensitivity < Math.abs(newAverage); + } +} diff --git a/src/util/logs.ts b/src/util/logs.ts index c347f582..d5e8f351 100644 --- a/src/util/logs.ts +++ b/src/util/logs.ts @@ -10,6 +10,6 @@ export function logDebugError(message: string, ...args: any[]) { export function logDebug(message: any, ...args: any[]) { if (DEBUG) { // eslint-disable-next-line no-console - console.log('[DEBUG]', message, ...args); + console.log(`[DEBUG] ${message}`, ...args); } } diff --git a/src/util/math.ts b/src/util/math.ts index 40c989bf..c9561d22 100644 --- a/src/util/math.ts +++ b/src/util/math.ts @@ -1 +1,11 @@ +export const clamp = (num: number, min: number, max: number) => (Math.min(max, Math.max(min, num))); export const isBetween = (num: number, min: number, max: number) => (num >= min && num <= max); +export const round = (num: number, decimals: number = 0) => Math.round(num * 10 ** decimals) / 10 ** decimals; +export const lerp = (start: number, end: number, interpolationRatio: number) => { + return (1 - interpolationRatio) * start + interpolationRatio * end; +}; + +// Fractional values cause blurry text & canvas. Round to even to keep whole numbers while centering +export function roundToNearestEven(value: number) { + return Math.round(value / 2) * 2; +} diff --git a/src/util/metadata.ts b/src/util/metadata.ts new file mode 100644 index 00000000..1953191a --- /dev/null +++ b/src/util/metadata.ts @@ -0,0 +1,29 @@ +import { BRILLIANT_API_BASE_URL, IS_CAPACITOR } from '../config'; + +const IPFS_GATEWAY_BASE_URL: string = 'https://ipfs.io/ipfs/'; + +export function fetchJsonMetadata(url: string) { + url = fixIpfsUrl(url); + + const reserveUrl = `${BRILLIANT_API_BASE_URL}/utils/download-json?url=${url}`; + + if (IS_CAPACITOR) { + return fetchJson(reserveUrl); + } + + return fetchJson(url).catch(() => { + return fetchJson(reserveUrl); + }); +} + +export function fixIpfsUrl(url: string) { + return url.replace('ipfs://', IPFS_GATEWAY_BASE_URL); +} + +async function fetchJson(url: string) { + const response = await fetch(url); + if (!response.ok) { + throw Error(`Http error ${response.status}`); + } + return response.json(); +} diff --git a/src/util/modalSwipeManager.ts b/src/util/modalSwipeManager.ts new file mode 100644 index 00000000..689f6308 --- /dev/null +++ b/src/util/modalSwipeManager.ts @@ -0,0 +1,13 @@ +let counter = 0; + +export function disableSwipeToClose() { + counter += 1; +} + +export function enableSwipeToClose() { + counter -= 1; +} + +export function getIsSwipeToCloseDisabled() { + return counter > 0; +} diff --git a/src/util/multitab.ts b/src/util/multitab.ts new file mode 100644 index 00000000..6544a9f8 --- /dev/null +++ b/src/util/multitab.ts @@ -0,0 +1,80 @@ +import { addCallback } from '../lib/teact/teactn'; +import { getGlobal, setGlobal } from '../global'; + +import type { GlobalState } from '../global/types'; + +import { MULTITAB_DATA_CHANNEL_NAME } from '../config'; +import { deepDiff } from './deepDiff'; +import { deepMerge } from './deepMerge'; +import { omit } from './iteratees'; +import { IS_MULTITAB_SUPPORTED } from './windowEnvironment'; + +import { isBackgroundModeActive } from '../hooks/useBackgroundMode'; + +interface BroadcastChannelGlobalDiff { + type: 'globalDiffUpdate'; + diff: any; +} + +type BroadcastChannelMessage = BroadcastChannelGlobalDiff; +type EventListener = (type: 'message', listener: (event: { data: BroadcastChannelMessage }) => void) => void; + +export type TypedBroadcastChannel = { + postMessage: (message: BroadcastChannelMessage) => void; + addEventListener: EventListener; + removeEventListener: EventListener; +}; + +const channel = IS_MULTITAB_SUPPORTED + ? new BroadcastChannel(MULTITAB_DATA_CHANNEL_NAME) as TypedBroadcastChannel + : undefined; + +let currentGlobal = getGlobal(); + +export function initMultitab({ noPub, noSub }: { noPub?: boolean; noSub?: boolean } = {}) { + if (!channel) return; + + if (!noPub) { + addCallback(handleGlobalChange); + } + + if (!noSub) { + channel.addEventListener('message', handleMultitabMessage); + } +} + +function handleGlobalChange(global: GlobalState) { + if (global === currentGlobal) return; + + if (isBackgroundModeActive()) { + currentGlobal = global; + return; + } + + const diff = deepDiff(omitLocalOnlyKeys(currentGlobal), omitLocalOnlyKeys(global)); + + if (typeof diff !== 'symbol') { + channel!.postMessage({ + type: 'globalDiffUpdate', + diff, + }); + } + + currentGlobal = global; +} + +function omitLocalOnlyKeys(global: GlobalState) { + return omit(global, ['DEBUG_capturedId']); +} + +function handleMultitabMessage({ data }: { data: BroadcastChannelMessage }) { + switch (data.type) { + case 'globalDiffUpdate': { + currentGlobal = deepMerge(getGlobal(), data.diff); + + setGlobal(currentGlobal); + + break; + } + } +} diff --git a/src/util/processDeeplink.ts b/src/util/processDeeplink.ts new file mode 100644 index 00000000..1e038055 --- /dev/null +++ b/src/util/processDeeplink.ts @@ -0,0 +1,30 @@ +import { BottomSheet } from 'native-bottom-sheet'; +import { getActions } from '../global'; + +import { TON_TOKEN_SLUG } from '../config'; +import { bigStrToHuman } from '../global/helpers'; +import { parseTonDeeplink } from './ton/deeplinks'; +import { pause } from './schedulers'; +import { CAN_DELEGATE_BOTTOM_SHEET } from './windowEnvironment'; + +// Both to close current Transfer Modal and delay when app launch +const PAUSE = 700; +export async function processDeeplink(url: string) { + const params = parseTonDeeplink(url); + if (!params) return false; + + if (CAN_DELEGATE_BOTTOM_SHEET) { + await BottomSheet.release({ key: '*' }); + await pause(PAUSE); + } + + getActions().startTransfer({ + isPortrait: true, + tokenSlug: TON_TOKEN_SLUG, + toAddress: params.to, + amount: params.amount ? bigStrToHuman(params.amount) : undefined, + comment: params.comment, + }); + + return true; +} diff --git a/src/util/random.ts b/src/util/random.ts index f1dec00f..1c83e6ca 100644 --- a/src/util/random.ts +++ b/src/util/random.ts @@ -5,3 +5,8 @@ export function random(min: number, max: number) { export function sample(arr: T[]) { return arr[random(0, arr.length - 1)]; } + +export function randomBytes(size: number) { + // eslint-disable-next-line no-restricted-globals + return self.crypto.getRandomValues(new Uint8Array(size)); +} diff --git a/src/util/resetScroll.ts b/src/util/resetScroll.ts new file mode 100644 index 00000000..8e1045d9 --- /dev/null +++ b/src/util/resetScroll.ts @@ -0,0 +1,17 @@ +import { IS_IOS } from './windowEnvironment'; + +const resetScroll = (container: HTMLDivElement, scrollTop?: number) => { + if (IS_IOS) { + container.style.overflow = 'hidden'; + } + + if (scrollTop !== undefined) { + container.scrollTop = scrollTop; + } + + if (IS_IOS) { + container.style.overflow = ''; + } +}; + +export default resetScroll; diff --git a/src/util/resolveModalTransitionName.ts b/src/util/resolveModalTransitionName.ts new file mode 100644 index 00000000..40c6642b --- /dev/null +++ b/src/util/resolveModalTransitionName.ts @@ -0,0 +1,5 @@ +import { IS_ANDROID, IS_IOS } from './windowEnvironment'; + +export default function resolveModalTransitionName() { + return IS_ANDROID ? 'slideFadeAndroid' : IS_IOS ? 'slideLayers' : 'slideFade'; +} diff --git a/src/util/saveCaretPosition.ts b/src/util/saveCaretPosition.ts index a1d6b9ae..39f6f3f4 100644 --- a/src/util/saveCaretPosition.ts +++ b/src/util/saveCaretPosition.ts @@ -6,7 +6,7 @@ export function saveCaretPosition(context: HTMLElement, decimals: number) { const range = selection.getRangeAt(0); range.setStart(context, 0); - const clearedValue = range.toString().match(new RegExp(`(\\d+(?:\\.\\d{0,${decimals}})?)`)); + const clearedValue = range.toString().match(new RegExp(`(\\d+(?:[.,]\\d{0,${decimals}})?)`)); const len = clearedValue?.[0]?.length || range.toString().length || 0; return function restore() { diff --git a/src/util/schedulers.ts b/src/util/schedulers.ts index ee988e9e..8a4e22ee 100644 --- a/src/util/schedulers.ts +++ b/src/util/schedulers.ts @@ -104,7 +104,7 @@ export function rafPromise() { }); } -const FAST_RAF_TIMEOUT_FALLBACK_MS = 300; +const FAST_RAF_TIMEOUT_FALLBACK_MS = 35; // < 30 FPS let fastRafCallbacks: Set | undefined; let fastRafFallbackCallbacks: Set | undefined; diff --git a/src/util/swap/buildSwapId.ts b/src/util/swap/buildSwapId.ts new file mode 100644 index 00000000..fece7716 --- /dev/null +++ b/src/util/swap/buildSwapId.ts @@ -0,0 +1,3 @@ +export function buildSwapId(backendId: string) { + return `swap:${backendId}`; +} diff --git a/src/util/swap/getBlockchainNetworkIcon.ts b/src/util/swap/getBlockchainNetworkIcon.ts new file mode 100644 index 00000000..12eafe09 --- /dev/null +++ b/src/util/swap/getBlockchainNetworkIcon.ts @@ -0,0 +1,53 @@ +import avalancheBlockchainIcon from '../../assets/blockchain/chain_avalanche.png'; +import bitcoinBlockchainIcon from '../../assets/blockchain/chain_bitcoin.png'; +import bitcoincashBlockchainIcon from '../../assets/blockchain/chain_bitcoincash.png'; +import bnbBlockchainIcon from '../../assets/blockchain/chain_bnb.png'; +import cardanoBlockchainIcon from '../../assets/blockchain/chain_cardano.png'; +import cosmosBlockchainIcon from '../../assets/blockchain/chain_cosmos.png'; +import dashBlockchainIcon from '../../assets/blockchain/chain_dash.png'; +import dogeBlockchainIcon from '../../assets/blockchain/chain_doge.png'; +import eosBlockchainIcon from '../../assets/blockchain/chain_eos.png'; +import ethereumBlockchainIcon from '../../assets/blockchain/chain_ethereum.png'; +import ethereumclassicBlockchainIcon from '../../assets/blockchain/chain_ethereumclassic.png'; +import internetcomputerBlockchainIcon from '../../assets/blockchain/chain_internetcomputer.png'; +import iotaBlockchainIcon from '../../assets/blockchain/chain_iota.png'; +import litecoinBlockchainIcon from '../../assets/blockchain/chain_litecoin.png'; +import moneroBlockchainIcon from '../../assets/blockchain/chain_monero.png'; +import polkadotBlockchainIcon from '../../assets/blockchain/chain_polkadot.png'; +import rippleBlockchainIcon from '../../assets/blockchain/chain_ripple.png'; +import solanaBlockchainIcon from '../../assets/blockchain/chain_solana.png'; +import stellarBlockchainIcon from '../../assets/blockchain/chain_stellar.png'; +import tonBlockchainIcon from '../../assets/blockchain/chain_ton.png'; +import tronBlockchainIcon from '../../assets/blockchain/chain_tron.png'; +import zcashBlockchainIcon from '../../assets/blockchain/chain_zcash.png'; + +const BLOCKCHAIN_ICON_MAP: Record = { + avalanche: avalancheBlockchainIcon, + bitcoin: bitcoinBlockchainIcon, + bitcoin_cash: bitcoincashBlockchainIcon, + binance_smart_chain: bnbBlockchainIcon, + binance_dex: bnbBlockchainIcon, + cardano: cardanoBlockchainIcon, + cosmos: cosmosBlockchainIcon, + dash: dashBlockchainIcon, + doge: dogeBlockchainIcon, + eos: eosBlockchainIcon, + ethereum: ethereumBlockchainIcon, + ethereum_classic: ethereumclassicBlockchainIcon, + internet_computer: internetcomputerBlockchainIcon, + iota: iotaBlockchainIcon, + litecoin: litecoinBlockchainIcon, + monero: moneroBlockchainIcon, + polkadot: polkadotBlockchainIcon, + ripple: rippleBlockchainIcon, + solana: solanaBlockchainIcon, + stellar: stellarBlockchainIcon, + ton: tonBlockchainIcon, + tron: tronBlockchainIcon, + zcash: zcashBlockchainIcon, +}; +export default function getBlockchainNetworkIcon(networkName?: string) { + if (!networkName) return ''; + + return BLOCKCHAIN_ICON_MAP[networkName] ?? networkName; +} diff --git a/src/util/swap/getBlockchainNetworkName.ts b/src/util/swap/getBlockchainNetworkName.ts new file mode 100644 index 00000000..6f288532 --- /dev/null +++ b/src/util/swap/getBlockchainNetworkName.ts @@ -0,0 +1,31 @@ +const NETWORK_NAMES_EXCEPTIONS: Record = { + binance_smart_chain: 'Binance Smart Chain', + internet_computer: 'Internet Computer', + ethereum_classic: 'Ethereum Classic', + bitcoin_cash: 'Bitcoin Cash', + binance_dex: 'Binance Dex', + ton: 'TON', + bitcoin: 'Bitcoin', + ethereum: 'Ethereum', + solana: 'Solana', + tron: 'TRON', + stellar: 'Stellar', + doge: 'DOGE', + eos: 'EOS', + avalanche: 'Avalanche', + cardano: 'Cardano', + monero: 'Monero', + dash: 'Dash', + ripple: 'Ripple', + cosmos: 'Cosmos', + litecoin: 'Litecoin', + zcash: 'Zcash', + polkadot: 'Polkadot', + iota: 'IOTA', +}; + +export default function getBlockchainNetworkName(networkName?: string) { + if (!networkName) return ''; + + return NETWORK_NAMES_EXCEPTIONS[networkName] ?? networkName; +} diff --git a/src/util/swap/getSwapRate.ts b/src/util/swap/getSwapRate.ts new file mode 100644 index 00000000..870e4f6c --- /dev/null +++ b/src/util/swap/getSwapRate.ts @@ -0,0 +1,49 @@ +import type { ApiSwapAsset } from '../../api/types'; + +import { TON_SYMBOL } from '../../config'; +import { Big } from '../../lib/big.js'; +import { formatInteger } from '../formatNumber'; + +const BTC = new Set(['jWBTC', 'oWBTC', 'BTC']); +const USD = new Set(['jUSDT', 'oUSDT', 'USDT', 'jUSDC', 'oUSDC', 'USDC']); + +const LARGE_NUMBER = 1000; + +export default function getSwapRate( + fromAmount?: string, + toAmount?: string, + fromToken?: ApiSwapAsset, + toToken?: ApiSwapAsset, + shouldTrimLargeNumber = false, +) { + if (!fromAmount || !toAmount || !fromToken || !toToken) { + return undefined; + } + + let firstCurrencySymbol = fromToken.symbol; + let secondCurrencySymbol = toToken.symbol; + let price: string; + + if ( + BTC.has(secondCurrencySymbol) + || (USD.has(secondCurrencySymbol) && firstCurrencySymbol !== TON_SYMBOL) + || (USD.has(firstCurrencySymbol) && secondCurrencySymbol === TON_SYMBOL) + || (firstCurrencySymbol === TON_SYMBOL && !USD.has(secondCurrencySymbol)) + ) { + firstCurrencySymbol = toToken.symbol; + secondCurrencySymbol = fromToken.symbol; + const ratio = new Big(fromAmount).div(toAmount); + const isLargeNumber = shouldTrimLargeNumber && ratio.gte(LARGE_NUMBER); + price = formatInteger(ratio.toNumber(), isLargeNumber ? 0 : 2); + } else { + const ratio = new Big(toAmount).div(fromAmount); + const isLargeNumber = shouldTrimLargeNumber && ratio.gte(LARGE_NUMBER); + price = formatInteger(ratio.toNumber(), isLargeNumber ? 0 : 2); + } + + return { + firstCurrencySymbol, + secondCurrencySymbol, + price, + }; +} diff --git a/src/util/swipeController.ts b/src/util/swipeController.ts new file mode 100644 index 00000000..02268faa --- /dev/null +++ b/src/util/swipeController.ts @@ -0,0 +1,186 @@ +import type { MoveOffsets } from './captureEvents'; + +import { requestMeasure, requestMutation } from '../lib/fasterdom/fasterdom'; +import { animateNumber, timingFunctions } from './animation'; +import { captureEvents, SwipeDirection } from './captureEvents'; +import { waitForAnimationEnd } from './cssAnimationEndListeners'; +import { clamp } from './math'; +import { IS_IOS } from './windowEnvironment'; + +const INERTIA_DURATION = 300; +const INERTIA_EASING = timingFunctions.easeOutCubic; + +let isSwipeActive = false; +let swipeOffsets: MoveOffsets | undefined; +let onDrag: ((offsets: MoveOffsets) => void) | undefined; +let onRelease: ((onCancel: NoneToVoidFunction) => void) | undefined; +let cancelCurrentReleaseAnimation: NoneToVoidFunction | undefined; + +export function captureControlledSwipe( + element: HTMLElement, options: { + onSwipeLeftStart?: NoneToVoidFunction; + onSwipeRightStart?: NoneToVoidFunction; + onCancel: NoneToVoidFunction; + }, +) { + return captureEvents(element, { + swipeThreshold: 10, + + onSwipe(e, direction, offsets) { + if (direction === SwipeDirection.Left) { + options.onSwipeLeftStart?.(); + } else if (direction === SwipeDirection.Right) { + options.onSwipeRightStart?.(); + } else { + return false; + } + + if (IS_IOS) { + isSwipeActive = true; + swipeOffsets = offsets; + } + + return true; + }, + + onDrag(e, captureEvent, offsets) { + if (!isSwipeActive) return; + + onDrag?.(offsets); + }, + + onRelease() { + if (!isSwipeActive) return; + + isSwipeActive = false; + + onRelease?.(options.onCancel); + + onDrag = undefined; + onRelease = undefined; + }, + }); +} + +export function allowSwipeControlForTransition( + currentSlide: HTMLElement, + nextSlide: HTMLElement, + onCancelForTransition: NoneToVoidFunction, +) { + cancelCurrentReleaseAnimation?.(); + + if (!isSwipeActive) return; + + const targetPosition = extractAnimationEndPosition(currentSlide); + if (!targetPosition) return; + + currentSlide.getAnimations().forEach((a) => a.pause()); + nextSlide.getAnimations().forEach((a) => a.pause()); + + currentSlide.style.animationTimingFunction = 'linear'; + nextSlide.style.animationTimingFunction = 'linear'; + + let currentDirection: 1 | -1 | undefined; + + requestMeasure(() => { + const computedStyle = getComputedStyle(currentSlide); + const initialPositionPx = extractPositionFromMatrix(computedStyle.transform, targetPosition.axis); + const targetPositionPx = targetPosition.units === 'px' + ? targetPosition.value + : ((targetPosition.value / 100) * ( + targetPosition.axis === 'X' ? currentSlide.offsetWidth : currentSlide.offsetHeight + )); + const distance = targetPositionPx - initialPositionPx; + + let progress = 0; + + onDrag = ({ dragOffsetX, dragOffsetY }) => { + const dragOffset = targetPosition.axis === 'X' + ? dragOffsetX - swipeOffsets!.dragOffsetX + : dragOffsetY - swipeOffsets!.dragOffsetY; + + const newProgress = clamp(dragOffset / distance, 0, 1); + currentDirection = newProgress > progress ? 1 : -1; + progress = newProgress; + + updateAnimationProgress([currentSlide, nextSlide], progress); + }; + + onRelease = (onCancelForClient: NoneToVoidFunction) => { + const isRevertSwipe = currentDirection === -1; + + function cleanup() { + currentSlide.getAnimations().forEach((a) => a.cancel()); + nextSlide.getAnimations().forEach((a) => a.cancel()); + + requestMutation(() => { + currentSlide.style.animationTimingFunction = ''; + nextSlide.style.animationTimingFunction = ''; + }); + } + + if (!isRevertSwipe) { + // For some reason animations are not cleared when CSS class is removed + waitForAnimationEnd(currentSlide, cleanup); + } + + cancelCurrentReleaseAnimation = animateNumber({ + from: progress, + to: isRevertSwipe ? 0 : 1, + duration: INERTIA_DURATION, + timing: INERTIA_EASING, + onUpdate(releaseProgress) { + updateAnimationProgress([currentSlide, nextSlide], releaseProgress); + }, + onEnd(isCanceled = false) { + cancelCurrentReleaseAnimation = undefined; + + if (isCanceled || isRevertSwipe) { + cleanup(); + onCancelForTransition(); + onCancelForClient(); + } + }, + }); + }; + }); +} + +function updateAnimationProgress(elements: HTMLElement[], progress: number) { + elements.map((e) => e.getAnimations()).flat().forEach((animation) => { + animation.currentTime = (animation.effect!.getTiming().duration as number) * progress; + }); +} + +function extractAnimationEndPosition(element: HTMLElement) { + for (const animation of element.getAnimations()) { + if (!(animation.effect instanceof KeyframeEffect)) continue; + + for (const keyframe of animation.effect.getKeyframes()) { + if (keyframe.offset !== 1 || !keyframe.transform) continue; + + const position = extractPositionFromTransform(keyframe.transform as string); + if (position) { + return position; + } + } + } + + return undefined; +} + +function extractPositionFromTransform(transformRule: string) { + const match = transformRule.match(/([XY])\((-?\d+)(%|px)\)/); + if (!match) return undefined; + + return { + axis: match[1] as 'X' | 'Y', + value: Number(match[2]), + units: match[3], + }; +} + +function extractPositionFromMatrix(transform: string, axis: 'X' | 'Y') { + const matrix = transform.slice(7, -1).split(',').map(Number); + return matrix[axis === 'X' ? 4 : 5]; +} diff --git a/src/util/switchTheme.ts b/src/util/switchTheme.ts index 9d928c71..2d5309ef 100644 --- a/src/util/switchTheme.ts +++ b/src/util/switchTheme.ts @@ -1,6 +1,8 @@ import type { Theme } from '../global/types'; +import { IS_CAPACITOR } from '../config'; import { requestMeasure } from '../lib/fasterdom/fasterdom'; +import { switchStatusBar } from './capacitor'; const prefersDark = window.matchMedia('(prefers-color-scheme: dark)'); let currentTheme: Theme; @@ -19,6 +21,11 @@ function setThemeValue() { ); } +function handlePrefersColorSchemeChange() { + setThemeValue(); + setStatusBarStyle(); +} + function setThemeColor() { requestMeasure(() => { const color = getComputedStyle(document.documentElement) @@ -30,4 +37,10 @@ function setThemeColor() { }); } -prefersDark.addEventListener('change', setThemeValue); +export function setStatusBarStyle(forceDarkBackground?: boolean) { + if (!IS_CAPACITOR) return; + + switchStatusBar(currentTheme, prefersDark.matches, forceDarkBackground); +} + +prefersDark.addEventListener('change', handlePrefersColorSchemeChange); diff --git a/src/util/ton/deeplinks.ts b/src/util/ton/deeplinks.ts new file mode 100644 index 00000000..dadaf686 --- /dev/null +++ b/src/util/ton/deeplinks.ts @@ -0,0 +1,26 @@ +import { TON_PROTOCOL, TONCONNECT_PROTOCOL, TONCONNECT_UNIVERSAL_URL } from '../../config'; + +export function parseTonDeeplink(value: string | unknown) { + if (typeof value !== 'string' || !isTonDeeplink(value) || !value.includes('/transfer/')) { + return undefined; + } + + try { + const url = new URL(value); + return { + to: url.pathname.replace(/.*\//, ''), + amount: url.searchParams.get('amount') ?? undefined, + comment: url.searchParams.get('text') ?? undefined, + }; + } catch (err) { + return undefined; + } +} + +export function isTonDeeplink(url: string) { + return url.startsWith(TON_PROTOCOL); +} + +export function isTonConnectDeeplink(url: string) { + return url.startsWith(TONCONNECT_PROTOCOL) || url.startsWith(TONCONNECT_UNIVERSAL_URL); +} diff --git a/src/util/windowEnvironment.ts b/src/util/windowEnvironment.ts index 8ebe625c..6585b127 100644 --- a/src/util/windowEnvironment.ts +++ b/src/util/windowEnvironment.ts @@ -1,6 +1,6 @@ import type { LangCode } from '../global/types'; -import { IS_FIREFOX_EXTENSION, LANG_LIST } from '../config'; +import { IS_CAPACITOR, IS_FIREFOX_EXTENSION, LANG_LIST } from '../config'; import { requestForcedReflow, requestMutation } from '../lib/fasterdom/fasterdom'; const SAFE_AREA_INITIALIZATION_DELAY = 1000; @@ -8,6 +8,10 @@ const SAFE_AREA_INITIALIZATION_DELAY = 1000; export function getPlatform() { const { userAgent, platform } = window.navigator; + if (/Android/.test(userAgent)) return 'Android'; + + if (/Linux/.test(platform)) return 'Linux'; + const iosPlatforms = ['iPhone', 'iPad', 'iPod']; if ( iosPlatforms.indexOf(platform) !== -1 @@ -21,10 +25,6 @@ export function getPlatform() { const windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE']; if (windowsPlatforms.indexOf(platform) !== -1) return 'Windows'; - if (/Android/.test(userAgent)) return 'Android'; - - if (/Linux/.test(platform)) return 'Linux'; - return undefined; } @@ -44,13 +44,23 @@ export const IS_IOS = PLATFORM_ENV === 'iOS'; export const IS_ANDROID = PLATFORM_ENV === 'Android'; export const IS_SAFARI = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); export const IS_OPERA = navigator.userAgent.includes(' OPR/'); +export const IS_FIREFOX = navigator.userAgent.includes('Firefox/'); export const IS_TOUCH_ENV = window.matchMedia('(pointer: coarse)').matches; export const IS_CHROME_EXTENSION = Boolean(window.chrome?.system); +export const IS_ELECTRON = Boolean(window.electron); export const DEFAULT_LANG_CODE = 'en'; export const USER_AGENT_LANG_CODE = getBrowserLanguage(); export const DPR = window.devicePixelRatio || 1; - export const IS_LEDGER_SUPPORTED = !(IS_IOS || IS_ANDROID || IS_FIREFOX_EXTENSION); +export const IS_LEDGER_EXTENSION_TAB = global.location.hash.startsWith('#detached'); +// Disable biometric auth on electron for now until this issue is fixed: +// https://github.com/electron/electron/issues/24573 +export const IS_BIOMETRIC_AUTH_SUPPORTED = Boolean( + !IS_CAPACITOR && window.navigator.credentials && (!IS_ELECTRON || IS_MAC_OS), +); +export const IS_DELEGATED_BOTTOM_SHEET = IS_CAPACITOR && global.location.search.startsWith('?bottom-sheet'); +export const CAN_DELEGATE_BOTTOM_SHEET = IS_CAPACITOR && IS_IOS && !IS_DELEGATED_BOTTOM_SHEET; +export const IS_MULTITAB_SUPPORTED = 'BroadcastChannel' in window && !IS_LEDGER_EXTENSION_TAB; export function setScrollbarWidthProperty() { const el = document.createElement('div'); @@ -74,8 +84,14 @@ export function setPageSafeAreaProperty() { // WebKit has issues with this property on page load // https://bugs.webkit.org/show_bug.cgi?id=191872 setTimeout(() => { - const safeAreaBottom = parseInt(getComputedStyle(documentElement).getPropertyValue('--safe-area-bottom-value'), 10); + const safeAreaTop = getSafeAreaTop(); + const safeAreaBottom = getSafeAreaBottom(); + if (!Number.isNaN(safeAreaTop) && safeAreaTop > 0) { + requestMutation(() => { + documentElement.classList.add('with-safe-area-top'); + }); + } if (!Number.isNaN(safeAreaBottom) && safeAreaBottom > 0) { requestMutation(() => { documentElement.classList.add('with-safe-area-bottom'); @@ -84,4 +100,12 @@ export function setPageSafeAreaProperty() { }, SAFE_AREA_INITIALIZATION_DELAY); } +export function getSafeAreaTop() { + return parseInt(getComputedStyle(document.documentElement).getPropertyValue('--safe-area-top-value'), 10); +} + +export function getSafeAreaBottom() { + return parseInt(getComputedStyle(document.documentElement).getPropertyValue('--safe-area-bottom-value'), 10); +} + export const REM = parseInt(getComputedStyle(document.documentElement).fontSize, 10); diff --git a/src/util/windowSize.ts b/src/util/windowSize.ts index a7f8a2f6..89107650 100644 --- a/src/util/windowSize.ts +++ b/src/util/windowSize.ts @@ -1,65 +1,58 @@ +import { IS_CAPACITOR } from '../config'; import { requestMutation } from '../lib/fasterdom/fasterdom'; import { throttle } from './schedulers'; import { IS_ANDROID, IS_IOS } from './windowEnvironment'; -export type IDimensions = { - width: number; - height: number; -}; - const WINDOW_RESIZE_THROTTLE_MS = 250; const WINDOW_ORIENTATION_CHANGE_THROTTLE_MS = IS_IOS ? 350 : 250; const initialHeight = window.innerHeight; -let currentWindowSize = updateSizes(); - -const handleResize = throttle(() => { - currentWindowSize = updateSizes(); -}, WINDOW_RESIZE_THROTTLE_MS, true); -const handleViewportResize = throttle((e: Event) => { - const target = e.target as VisualViewport; - currentWindowSize = { - width: window.innerWidth, - height: target.height, - }; -}, WINDOW_RESIZE_THROTTLE_MS, true); +let currentWindowSize = updateSizes(); -const handleOrientationChange = throttle(() => { +window.addEventListener('orientationchange', throttle(() => { currentWindowSize = updateSizes(); -}, WINDOW_ORIENTATION_CHANGE_THROTTLE_MS, false); +}, WINDOW_ORIENTATION_CHANGE_THROTTLE_MS, false)); -window.addEventListener('orientationchange', handleOrientationChange); if (!IS_IOS) { - window.addEventListener('resize', handleResize); + window.addEventListener('resize', throttle(() => { + currentWindowSize = updateSizes(); + }, WINDOW_RESIZE_THROTTLE_MS, true)); } if ('visualViewport' in window && (IS_IOS || IS_ANDROID)) { - window.visualViewport!.addEventListener('resize', handleViewportResize); + window.visualViewport!.addEventListener('resize', throttle((e: Event) => { + const target = e.target as VisualViewport; + currentWindowSize = { + width: window.innerWidth, + height: target.height, + screenHeight: window.screen.height, + }; + }, WINDOW_RESIZE_THROTTLE_MS, true)); } -export function updateSizes(): IDimensions { - let height: number; - if (IS_IOS) { - height = window.visualViewport!.height + window.visualViewport!.pageTop; - } else { - height = window.innerHeight; - } - - requestMutation(() => { - const vh = height * 0.01; - document.documentElement.style.setProperty('--vh', `${vh}px`); - }); +export function updateSizes() { + patchVh(); return { width: window.innerWidth, height: window.innerHeight, + screenHeight: window.screen.height, }; } -const windowSize = { +export default { get: () => currentWindowSize, getIsKeyboardVisible: () => initialHeight > currentWindowSize.height, }; -export default windowSize; +function patchVh() { + if (!(IS_IOS || IS_ANDROID) || IS_CAPACITOR) return; + + const height = IS_IOS ? window.visualViewport!.height + window.visualViewport!.pageTop : window.innerHeight; + + requestMutation(() => { + const vh = height * 0.01; + document.documentElement.style.setProperty('--vh', `${vh}px`); + }); +} diff --git a/src/util/withCacheAsync.ts b/src/util/withCacheAsync.ts index 36b60a9f..a23bd399 100644 --- a/src/util/withCacheAsync.ts +++ b/src/util/withCacheAsync.ts @@ -1,9 +1,9 @@ const cache = new WeakMap>(); export default function withCacheAsync( - fn: T, canBeCached: (value: ReturnType) => boolean = (value) => !!value, + fn: T, canBeCached: (value: Awaited>) => boolean = (value) => !!value, ) { - return async (...args: Parameters): Promise> => { + return async (...args: Parameters): Promise>> => { let fnCache = cache.get(fn); const cacheKey = buildCacheKey(args); diff --git a/tsconfig.json b/tsconfig.json index d7883d7e..3e98d6be 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,6 @@ "noEmit": true, "jsx": "react" }, - "include": ["src", "tests", "plugins", "webpack.config.ts", "webpack-electron.config.ts"], + "include": ["src", "tests", "plugins", "webpack.config.ts", "webpack-electron.config.ts", "capacitor.config.ts"], "exclude": ["trash"] } diff --git a/webpack-electron.config.ts b/webpack-electron.config.ts index 386a1be1..28520896 100644 --- a/webpack-electron.config.ts +++ b/webpack-electron.config.ts @@ -1,7 +1,16 @@ import path from 'path'; import { EnvironmentPlugin } from 'webpack'; -const { APP_ENV = 'production' } = process.env; +import { PRODUCTION_URL } from './src/config'; + +// GitHub workflow uses an empty string as the default value if it's not in repository variables, so we cannot define a default value here +process.env.BASE_URL = process.env.BASE_URL || PRODUCTION_URL; + +const { + APP_ENV = 'production', + BASE_URL, + IS_PREVIEW, +} = process.env; export default { mode: 'production', @@ -23,7 +32,11 @@ export default { }, plugins: [ - new EnvironmentPlugin({ APP_ENV }), + new EnvironmentPlugin({ + APP_ENV, + BASE_URL, + IS_PREVIEW: false, + }), ], module: { diff --git a/webpack.config.ts b/webpack.config.ts index b35f620b..317438ef 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -19,18 +19,24 @@ import { ProvidePlugin, } from 'webpack'; -const { APP_ENV, HEAD } = process.env; +import { PRODUCTION_URL } from './src/config'; + +dotenv.config(); + +// GitHub workflow uses an empty string as the default value if it's not in repository variables, so we cannot define a default value here +process.env.BASE_URL = process.env.BASE_URL || PRODUCTION_URL; + +const { APP_ENV, BASE_URL, HEAD } = process.env; const IS_EXTENSION = process.env.IS_EXTENSION === '1'; -const IS_ELECTRON = process.env.IS_ELECTRON === '1'; +const IS_ELECTRON_BUILD = process.env.IS_ELECTRON_BUILD === '1'; const IS_FIREFOX_EXTENSION = process.env.IS_FIREFOX_EXTENSION === '1'; const IS_OPERA_EXTENSION = process.env.IS_OPERA_EXTENSION === '1'; const gitRevisionPlugin = new GitRevisionPlugin(); const branch = HEAD || gitRevisionPlugin.branch(); const appRevision = !branch || branch === 'HEAD' ? gitRevisionPlugin.commithash()?.substring(0, 7) : branch; -const STATOSCOPE_REFERENCE_URL = 'https://beta.mytonwallet.app/statoscope-build-statistics.json'; -const canUseStatoscope = !IS_EXTENSION && !IS_ELECTRON; -const canUseBuildReference = canUseStatoscope && APP_ENV === 'staging'; +const canUseStatoscope = !IS_EXTENSION && !IS_ELECTRON_BUILD; +const connectSrcExtraUrl = APP_ENV === 'development' ? process.env.CSP_CONNECT_SRC_EXTRA_URL ?? '' : ''; // 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. @@ -38,7 +44,7 @@ const canUseBuildReference = canUseStatoscope && APP_ENV === 'staging'; const CSP = ` default-src 'none'; manifest-src 'self'; - connect-src 'self' https: http://localhost:3000; + connect-src 'self' https: http://localhost:3000 ${connectSrcExtraUrl}; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' https://fonts.googleapis.com/; img-src 'self' data: https:; @@ -53,8 +59,6 @@ const appVersion = require('./package.json').version; const defaultI18nFilename = path.resolve(__dirname, './src/i18n/en.json'); -dotenv.config(); - export default function createConfig( _: any, { mode = 'production' }: { mode: 'none' | 'development' | 'production' }, @@ -141,7 +145,7 @@ export default function createConfig( modules: { exportLocalsConvention: 'camelCase', auto: true, - localIdentName: mode === 'production' ? '[hash:base64]' : '[name]__[local]', + localIdentName: APP_ENV === 'production' ? '[hash:base64]' : '[name]__[local]', }, }, }, @@ -181,25 +185,6 @@ export default function createConfig( }, plugins: [ - ...(canUseBuildReference ? [{ - apply: (compiler: Compiler) => { - compiler.hooks.compile.tap('Before Compilation', async () => { - try { - const stats = await fetch(STATOSCOPE_REFERENCE_URL).then((res) => res.text()); - // Quick and simple json validator - JSON.parse(stats); - fs.writeFileSync(path.resolve('./public/statoscope-master-reference.json'), stats); - // eslint-disable-next-line no-console - console.info('Reference statoscope stats fetched'); - } catch (err: any) { - fs.writeFileSync(path.resolve('./public/statoscope-master-reference.json'), '{}'); - - // eslint-disable-next-line no-console - console.warn('Failed to fetch reference statoscope stats: ', err.message); - } - }); - }, - }] : []), ...(IS_OPERA_EXTENSION ? [{ apply: (compiler: Compiler) => { compiler.hooks.afterDone.tap('After Compilation', async () => { @@ -241,8 +226,10 @@ export default function createConfig( include: 'allAssets', fileWhitelist: [ /duck_.*?\.png/, // Lottie thumbs - /theme_.*?\.png/, // All theme icons - /settings_.*?\.svg/, // All settings svg icons + /coin_.*?\.png/, // Coin icons + /theme_.*?\.png/, // Theme icons + /chain_.*?\.png/, // Chain icons + /settings_.*?\.svg/, // Settings icons (svg) ], as(entry: string) { if (/\.png$/.test(entry)) return 'image'; @@ -268,14 +255,20 @@ export default function createConfig( TONHTTPAPI_TESTNET_API_KEY: null, TONAPIIO_MAINNET_URL: null, TONAPIIO_TESTNET_URL: null, + TONINDEXER_MAINNET_URL: null, + TONINDEXER_TESTNET_URL: null, BRILLIANT_API_BASE_URL: null, PROXY_HOSTS: null, STAKING_POOLS: null, - IS_ELECTRON: false, + LIQUID_POOL: null, + LIQUID_JETTON: null, + IS_ELECTRON_BUILD: false, ELECTRON_TONHTTPAPI_MAINNET_API_KEY: null, ELECTRON_TONHTTPAPI_TESTNET_API_KEY: null, + BASE_URL, IS_EXTENSION: false, IS_FIREFOX_EXTENSION: false, + IS_CAPACITOR: false, }), /* eslint-enable no-null/no-null */ new DefinePlugin({ @@ -336,9 +329,6 @@ export default function createConfig( normalizeStats: true, open: false, extensions: [new WebpackContextExtension()], // eslint-disable-line @typescript-eslint/no-use-before-define - ...(canUseBuildReference ? { - additionalStats: ['./public/statoscope-master-reference.json'], - } : {}), })] : []), ...(IS_EXTENSION ? [ @@ -351,7 +341,7 @@ export default function createConfig( ], devtool: - IS_EXTENSION ? 'cheap-source-map' : APP_ENV === 'production' && IS_ELECTRON ? undefined : 'source-map', + IS_EXTENSION ? 'cheap-source-map' : APP_ENV === 'production' && IS_ELECTRON_BUILD ? undefined : 'source-map', }; }