diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index f59ec20aa..000000000 --- a/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -* \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 120000 index 000000000..3e4e48b0b --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.gitignore \ No newline at end of file diff --git a/.github/workflows/automated_integration_test.yml b/.github/workflows/automated_integration_test.yml index 96d6ef153..1e0d1054a 100644 --- a/.github/workflows/automated_integration_test.yml +++ b/.github/workflows/automated_integration_test.yml @@ -56,6 +56,7 @@ jobs: - name: Add secrets run: | + touch cw_bitcoin/lib/.secrets.g.dart touch cw_evm/lib/.secrets.g.dart touch cw_solana/lib/.secrets.g.dart touch cw_core/lib/.secrets.g.dart @@ -75,6 +76,8 @@ jobs: echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart + + echo "const breezApiKey = '${{ secrets.BREEZ_API_KEY }}';" >> cw_bitcoin/lib/.secrets.g.dart echo "${{ secrets.MAIN_SECRETS_FILE }}" | base64 --decode > lib/.secrets.g.dart diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 3743ed110..a7b1a9016 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -3,6 +3,10 @@ name: Cake Wallet Android on: pull_request: +permissions: + contents: read + packages: write + jobs: # ----------------------------------------- # PATH A: Internal PRs diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index cdfd6a485..d009e32ed 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -44,6 +44,7 @@ jobs: - name: Add secrets run: | + touch cw_bitcoin/lib/.secrets.g.dart touch cw_evm/lib/.secrets.g.dart touch cw_solana/lib/.secrets.g.dart touch cw_core/lib/.secrets.g.dart @@ -64,6 +65,8 @@ jobs: echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart + + echo "const breezApiKey = '${{ secrets.BREEZ_API_KEY }}';" >> cw_bitcoin/lib/.secrets.g.dart echo "${{ secrets.MAIN_SECRETS_FILE }}" | base64 --decode > lib/.secrets.g.dart diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml index 801acaf65..c84ab5920 100644 --- a/.github/workflows/reusable-build.yml +++ b/.github/workflows/reusable-build.yml @@ -1,5 +1,9 @@ name: Reusable Build Logic +permissions: + contents: read + packages: write + on: workflow_call: inputs: @@ -34,6 +38,7 @@ jobs: - /opt/cw_cache_android/root/go/pkg:/root/go/pkg - /opt/cw_cache_android/opt/generic_cache:/opt/generic_cache - /dev/kvm:/dev/kvm + - /var/run/docker.sock:/var/run/docker.sock strategy: matrix: api-level: [29] @@ -44,11 +49,14 @@ jobs: - uses: actions/checkout@v4 with: ref: ${{ inputs.ref }} - - name: configure git - run: | - git config --global --add safe.directory '*' - git config --global user.email "ci@cakewallet.com" - git config --global user.name "CakeWallet CI" + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Get the full commit message run: | FULL_MESSAGE="$(git log -1 --pretty=%B)" @@ -58,6 +66,7 @@ jobs: - name: Add secrets run: | + touch cw_bitcoin/lib/.secrets.g.dart touch cw_evm/lib/.secrets.g.dart touch cw_solana/lib/.secrets.g.dart touch cw_core/lib/.secrets.g.dart @@ -79,6 +88,8 @@ jobs: echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart + echo "const breezApiKey = '${{ secrets.BREEZ_API_KEY }}';" >> cw_bitcoin/lib/.secrets.g.dart + echo "${{ secrets.MAIN_SECRETS_FILE }}" | base64 --decode > lib/.secrets.g.dart - name: Generate KeyStore @@ -93,33 +104,12 @@ jobs: popd cp /opt/generic_cache/key.jks android/app - - name: Fetch prebuilt Torch + - name: Build dependencies utilizing ghcr cache run: | - set -x -e - pushd scripts - wget https://github.com/MrCyjaneK/torch_dart/releases/download/v1.0.17/torch_dart-v1.0.17.tar.gz -O torch_dart.tar.gz - mkdir torch_dart - pushd torch_dart - tar -xzf ../torch_dart.tar.gz - popd - rm ./torch_dart.tar.gz - popd - - - name: Fetch prebuilt Reown - run: | - set -x -e - pushd scripts - # cleaning - rm -rf reown_flutter - rm -f reown_flutter.tar.gz - - wget https://github.com/cake-tech/reown_flutter/releases/download/v0.0.4/reown_flutter-v0.0.4.tar.gz -O reown_flutter.tar.gz - mkdir reown_flutter - pushd reown_flutter - tar -xzf ../reown_flutter.tar.gz - popd - rm ./reown_flutter.tar.gz - popd + env \ + CW_DOCKER_REGISTRY=ghcr.io/cake-tech/cake_wallet \ + CW_DOCKER_USE_CLOUD=true \ + ./scripts/android/docker/build.sh - name: Execute Build and Setup Commands run: | @@ -128,49 +118,10 @@ jobs: ./app_config.sh popd - - name: Build monero_c - run: | - mkdir -p scripts/monero_c - pushd scripts/monero_c - wget https://github.com/MrCyjaneK/monero_c/releases/download/v0.18.4.0-RC9/release-bundle.zip - unzip release-bundle.zip - rm release-bundle.zip - unxz -fv release/*/*.xz - popd - - - name: Build Bitbox Flutter - run: | - set -x -e - pushd scripts - ./build_bitbox_flutter.sh - popd - - name: Build Zcash Lib - run: | - set -x -e - pushd scripts/android - ./build_zcash.sh - popd - - name: Install Flutter dependencies run: | flutter pub get - - name: Build mwebd - run: | - set -x -e - export MWEBD_HASH=$(cat scripts/android/build_mwebd.sh | grep 'git reset --hard' | xargs | awk '{ print $4 }') - echo MWEBD_HASH=$MWEBD_HASH >> /etc/environment - pushd scripts/android - ./build_mwebd.sh - popd - - - name: Build Decred - run: | - set -x -e - pushd scripts/android - ./build_decred.sh - popd - - name: Build generated code run: | flutter --version diff --git a/.gitignore b/.gitignore index e9d8156de..6de4cb247 100644 --- a/.gitignore +++ b/.gitignore @@ -97,6 +97,7 @@ android/app/key.jks **/tool/.solana-secrets-config.json **/tool/.nano-secrets-config.json **/tool/.tron-secrets-config.json +**/tool/.bitcoin-secrets-config.json **/lib/.secrets.g.dart **/cw_evm/lib/.secrets.g.dart **/cw_solana/lib/.secrets.g.dart diff --git a/Dockerfile b/Dockerfile index 9fa20c919..773ac6822 100644 --- a/Dockerfile +++ b/Dockerfile @@ -69,6 +69,8 @@ RUN set -o xtrace \ g++-x86-64-linux-gnu gcc-x86-64-linux-gnu \ # flatpak dependencies flatpak flatpak-builder binutils elfutils patch unzip xz-utils zstd \ + # docker dependencies + docker-cli rsync \ && apt clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \ && sh -c 'echo "en_US.UTF-8 UTF-8" > /etc/locale.gen' \ && locale-gen \ @@ -80,7 +82,7 @@ RUN flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub. && flatpak install -y flathub org.freedesktop.Sdk//${FLATPAK_RUNTIME_VERSION} # Install nodejs for Github Actions -RUN curl -fsSL https://deb.nodesource.com/setup_23.x | bash - && \ +RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - && \ apt-get install -y --no-install-recommends nodejs && \ apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* @@ -103,8 +105,8 @@ RUN ARCH=$(uname -m) && \ gomobile init RUN git config --global user.email "czarek@cakewallet.com" \ - && git config --global user.name "CakeWallet CI" - + && git config --global user.name "CakeWallet CI" \ + && git config --global --add safe.directory '*' # Install Android SDK commandline tools and emulator RUN ARCH=$(uname -m) && \ @@ -142,7 +144,8 @@ RUN ARCH=$(uname -m) && \ "build-tools;33.0.2" \ "build-tools;33.0.1" \ "build-tools;33.0.0" \ - "build-tools;35.0.0" + "build-tools;35.0.0" \ + "cmake;3.22.1" # Install extra NDK dependency for sp_scanner ENV ANDROID_NDK_VERSION=28.2.13676358 diff --git a/analysis_options.yaml b/analysis_options.yaml index 4f6f0f6e9..7f76d2721 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -38,6 +38,10 @@ linter: - prefer_final_fields +formatter: + page_width: 100 + + # analyzer: # strong-mode: # implicit-casts: false diff --git a/android/app/build.gradle b/android/app/build.gradle index 4602d5aa8..6c1540222 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -61,7 +61,7 @@ android { defaultConfig { applicationId appProperties['id'] minSdkVersion 24 - targetSdkVersion 35 + targetSdkVersion 36 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index 4981d5d19..a7364d670 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -117,6 +117,8 @@ + + diff --git a/android/gradle.properties b/android/gradle.properties index 8c323ab51..184d30421 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx4096M +org.gradle.jvmargs=-Xmx6144M android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/assets/fonts/IBM_Plex_Mono/IBMPlexMono-Bold.ttf b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-Bold.ttf new file mode 100644 index 000000000..247979cae Binary files /dev/null and b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-Bold.ttf differ diff --git a/assets/fonts/IBM_Plex_Mono/IBMPlexMono-BoldItalic.ttf b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-BoldItalic.ttf new file mode 100644 index 000000000..2321473f5 Binary files /dev/null and b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-BoldItalic.ttf differ diff --git a/assets/fonts/IBM_Plex_Mono/IBMPlexMono-ExtraLight.ttf b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-ExtraLight.ttf new file mode 100644 index 000000000..d6ab75db5 Binary files /dev/null and b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-ExtraLight.ttf differ diff --git a/assets/fonts/IBM_Plex_Mono/IBMPlexMono-ExtraLightItalic.ttf b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-ExtraLightItalic.ttf new file mode 100644 index 000000000..88308efd0 Binary files /dev/null and b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-ExtraLightItalic.ttf differ diff --git a/assets/fonts/IBM_Plex_Mono/IBMPlexMono-Italic.ttf b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-Italic.ttf new file mode 100644 index 000000000..e259e84c9 Binary files /dev/null and b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-Italic.ttf differ diff --git a/assets/fonts/IBM_Plex_Mono/IBMPlexMono-Light.ttf b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-Light.ttf new file mode 100644 index 000000000..0dcb2fba5 Binary files /dev/null and b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-Light.ttf differ diff --git a/assets/fonts/IBM_Plex_Mono/IBMPlexMono-LightItalic.ttf b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-LightItalic.ttf new file mode 100644 index 000000000..f4a5feaea Binary files /dev/null and b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-LightItalic.ttf differ diff --git a/assets/fonts/IBM_Plex_Mono/IBMPlexMono-Medium.ttf b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-Medium.ttf new file mode 100644 index 000000000..8253c5fba Binary files /dev/null and b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-Medium.ttf differ diff --git a/assets/fonts/IBM_Plex_Mono/IBMPlexMono-MediumItalic.ttf b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-MediumItalic.ttf new file mode 100644 index 000000000..528b13b01 Binary files /dev/null and b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-MediumItalic.ttf differ diff --git a/assets/fonts/IBM_Plex_Mono/IBMPlexMono-Regular.ttf b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-Regular.ttf new file mode 100644 index 000000000..601ae945e Binary files /dev/null and b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-Regular.ttf differ diff --git a/assets/fonts/IBM_Plex_Mono/IBMPlexMono-SemiBold.ttf b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-SemiBold.ttf new file mode 100644 index 000000000..5e0b41df1 Binary files /dev/null and b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-SemiBold.ttf differ diff --git a/assets/fonts/IBM_Plex_Mono/IBMPlexMono-SemiBoldItalic.ttf b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-SemiBoldItalic.ttf new file mode 100644 index 000000000..58243dd3d Binary files /dev/null and b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-SemiBoldItalic.ttf differ diff --git a/assets/fonts/IBM_Plex_Mono/IBMPlexMono-Thin.ttf b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-Thin.ttf new file mode 100644 index 000000000..e069a6474 Binary files /dev/null and b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-Thin.ttf differ diff --git a/assets/fonts/IBM_Plex_Mono/IBMPlexMono-ThinItalic.ttf b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-ThinItalic.ttf new file mode 100644 index 000000000..f3ed26b29 Binary files /dev/null and b/assets/fonts/IBM_Plex_Mono/IBMPlexMono-ThinItalic.ttf differ diff --git a/assets/fonts/WixMadeforText-Bold.ttf b/assets/fonts/WixMadeforText-Bold.ttf new file mode 100644 index 000000000..d20de33af Binary files /dev/null and b/assets/fonts/WixMadeforText-Bold.ttf differ diff --git a/assets/fonts/WixMadeforText-Medium.ttf b/assets/fonts/WixMadeforText-Medium.ttf new file mode 100644 index 000000000..a4119f4d5 Binary files /dev/null and b/assets/fonts/WixMadeforText-Medium.ttf differ diff --git a/assets/fonts/WixMadeforText-Regular.ttf b/assets/fonts/WixMadeforText-Regular.ttf new file mode 100644 index 000000000..7d43fdd3e Binary files /dev/null and b/assets/fonts/WixMadeforText-Regular.ttf differ diff --git a/assets/fonts/WixMadeforText-SemiBold.ttf b/assets/fonts/WixMadeforText-SemiBold.ttf new file mode 100644 index 000000000..e7ba4efc4 Binary files /dev/null and b/assets/fonts/WixMadeforText-SemiBold.ttf differ diff --git a/assets/fonts/WixMadeforText-VariableFont_wght.ttf b/assets/fonts/WixMadeforText-VariableFont_wght.ttf new file mode 100644 index 000000000..5994d24be Binary files /dev/null and b/assets/fonts/WixMadeforText-VariableFont_wght.ttf differ diff --git a/assets/images/btc_chain_qr_lightning.svg b/assets/images/btc_chain_qr_lightning.svg new file mode 100644 index 000000000..f46556caa --- /dev/null +++ b/assets/images/btc_chain_qr_lightning.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/cake_wordmark.png b/assets/images/cake_wordmark.png new file mode 100644 index 000000000..14bc2efa8 Binary files /dev/null and b/assets/images/cake_wordmark.png differ diff --git a/assets/images/crypto/lightning.webp b/assets/images/crypto/lightning.webp new file mode 100644 index 000000000..7218f3946 Binary files /dev/null and b/assets/images/crypto/lightning.webp differ diff --git a/assets/images/cupcake.png b/assets/images/cupcake.png new file mode 100644 index 000000000..ed90c9c92 Binary files /dev/null and b/assets/images/cupcake.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/Icon-App-1024x1024@1x.png b/assets/images/ios_icons/monero_ios_icons/Icon-App-1024x1024@1x.png index e62a1a374..22b43da1a 100644 Binary files a/assets/images/ios_icons/monero_ios_icons/Icon-App-1024x1024@1x.png and b/assets/images/ios_icons/monero_ios_icons/Icon-App-1024x1024@1x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/Icon-App-20x20@1x.png b/assets/images/ios_icons/monero_ios_icons/Icon-App-20x20@1x.png index 84202c5ac..55f006709 100644 Binary files a/assets/images/ios_icons/monero_ios_icons/Icon-App-20x20@1x.png and b/assets/images/ios_icons/monero_ios_icons/Icon-App-20x20@1x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/Icon-App-20x20@2x.png b/assets/images/ios_icons/monero_ios_icons/Icon-App-20x20@2x.png index be11384df..9ac20f5d6 100644 Binary files a/assets/images/ios_icons/monero_ios_icons/Icon-App-20x20@2x.png and b/assets/images/ios_icons/monero_ios_icons/Icon-App-20x20@2x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/Icon-App-20x20@3x.png b/assets/images/ios_icons/monero_ios_icons/Icon-App-20x20@3x.png index f3156ac34..d6a44407e 100644 Binary files a/assets/images/ios_icons/monero_ios_icons/Icon-App-20x20@3x.png and b/assets/images/ios_icons/monero_ios_icons/Icon-App-20x20@3x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/Icon-App-29x29@1x.png b/assets/images/ios_icons/monero_ios_icons/Icon-App-29x29@1x.png index 789fe1867..eae48aa12 100644 Binary files a/assets/images/ios_icons/monero_ios_icons/Icon-App-29x29@1x.png and b/assets/images/ios_icons/monero_ios_icons/Icon-App-29x29@1x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/Icon-App-29x29@2x.png b/assets/images/ios_icons/monero_ios_icons/Icon-App-29x29@2x.png index 1da0ad2c4..ffa4c0e80 100644 Binary files a/assets/images/ios_icons/monero_ios_icons/Icon-App-29x29@2x.png and b/assets/images/ios_icons/monero_ios_icons/Icon-App-29x29@2x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/Icon-App-29x29@3x.png b/assets/images/ios_icons/monero_ios_icons/Icon-App-29x29@3x.png index 8c6d5db5f..3f8c0acc0 100644 Binary files a/assets/images/ios_icons/monero_ios_icons/Icon-App-29x29@3x.png and b/assets/images/ios_icons/monero_ios_icons/Icon-App-29x29@3x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/Icon-App-40x40@1x.png b/assets/images/ios_icons/monero_ios_icons/Icon-App-40x40@1x.png index be11384df..9ac20f5d6 100644 Binary files a/assets/images/ios_icons/monero_ios_icons/Icon-App-40x40@1x.png and b/assets/images/ios_icons/monero_ios_icons/Icon-App-40x40@1x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/Icon-App-40x40@2x.png b/assets/images/ios_icons/monero_ios_icons/Icon-App-40x40@2x.png index c73a8cc90..14644910d 100644 Binary files a/assets/images/ios_icons/monero_ios_icons/Icon-App-40x40@2x.png and b/assets/images/ios_icons/monero_ios_icons/Icon-App-40x40@2x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/Icon-App-40x40@3x.png b/assets/images/ios_icons/monero_ios_icons/Icon-App-40x40@3x.png index a646a1c46..359371094 100644 Binary files a/assets/images/ios_icons/monero_ios_icons/Icon-App-40x40@3x.png and b/assets/images/ios_icons/monero_ios_icons/Icon-App-40x40@3x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/Icon-App-50x50@1x.png b/assets/images/ios_icons/monero_ios_icons/Icon-App-50x50@1x.png index ecdbae798..95ffad635 100644 Binary files a/assets/images/ios_icons/monero_ios_icons/Icon-App-50x50@1x.png and b/assets/images/ios_icons/monero_ios_icons/Icon-App-50x50@1x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/Icon-App-50x50@2x.png b/assets/images/ios_icons/monero_ios_icons/Icon-App-50x50@2x.png index 6979a7d72..f5270fda9 100644 Binary files a/assets/images/ios_icons/monero_ios_icons/Icon-App-50x50@2x.png and b/assets/images/ios_icons/monero_ios_icons/Icon-App-50x50@2x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/Icon-App-57x57@1x.png b/assets/images/ios_icons/monero_ios_icons/Icon-App-57x57@1x.png index d959aae2a..2c1b9be5b 100644 Binary files a/assets/images/ios_icons/monero_ios_icons/Icon-App-57x57@1x.png and b/assets/images/ios_icons/monero_ios_icons/Icon-App-57x57@1x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/Icon-App-57x57@2x.png b/assets/images/ios_icons/monero_ios_icons/Icon-App-57x57@2x.png index 773618487..d71092f8e 100644 Binary files a/assets/images/ios_icons/monero_ios_icons/Icon-App-57x57@2x.png and b/assets/images/ios_icons/monero_ios_icons/Icon-App-57x57@2x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/Icon-App-60x60@2x.png b/assets/images/ios_icons/monero_ios_icons/Icon-App-60x60@2x.png index a646a1c46..359371094 100644 Binary files a/assets/images/ios_icons/monero_ios_icons/Icon-App-60x60@2x.png and b/assets/images/ios_icons/monero_ios_icons/Icon-App-60x60@2x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/Icon-App-60x60@3x.png b/assets/images/ios_icons/monero_ios_icons/Icon-App-60x60@3x.png index 6b0eb93bb..300ae990b 100644 Binary files a/assets/images/ios_icons/monero_ios_icons/Icon-App-60x60@3x.png and b/assets/images/ios_icons/monero_ios_icons/Icon-App-60x60@3x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/Icon-App-72x72@1x.png b/assets/images/ios_icons/monero_ios_icons/Icon-App-72x72@1x.png index 362caddc5..bf884a35c 100644 Binary files a/assets/images/ios_icons/monero_ios_icons/Icon-App-72x72@1x.png and b/assets/images/ios_icons/monero_ios_icons/Icon-App-72x72@1x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/Icon-App-72x72@2x.png b/assets/images/ios_icons/monero_ios_icons/Icon-App-72x72@2x.png index 5cb072e13..a66916e64 100644 Binary files a/assets/images/ios_icons/monero_ios_icons/Icon-App-72x72@2x.png and b/assets/images/ios_icons/monero_ios_icons/Icon-App-72x72@2x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/Icon-App-76x76@1x.png b/assets/images/ios_icons/monero_ios_icons/Icon-App-76x76@1x.png index 6aff2cdf5..8a4813d1e 100644 Binary files a/assets/images/ios_icons/monero_ios_icons/Icon-App-76x76@1x.png and b/assets/images/ios_icons/monero_ios_icons/Icon-App-76x76@1x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/Icon-App-76x76@2x.png b/assets/images/ios_icons/monero_ios_icons/Icon-App-76x76@2x.png index bcb6b5aa5..577a8af9f 100644 Binary files a/assets/images/ios_icons/monero_ios_icons/Icon-App-76x76@2x.png and b/assets/images/ios_icons/monero_ios_icons/Icon-App-76x76@2x.png differ diff --git a/assets/images/ios_icons/monero_ios_icons/Icon-App-83.5x83.5@2x.png b/assets/images/ios_icons/monero_ios_icons/Icon-App-83.5x83.5@2x.png index bb0e648ba..77597a4cf 100644 Binary files a/assets/images/ios_icons/monero_ios_icons/Icon-App-83.5x83.5@2x.png and b/assets/images/ios_icons/monero_ios_icons/Icon-App-83.5x83.5@2x.png differ diff --git a/assets/images/lightning-icon.svg b/assets/images/lightning-icon.svg new file mode 100644 index 000000000..272baa52d --- /dev/null +++ b/assets/images/lightning-icon.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/macos_icons/monero_macos_icons/monero_macos_1024.png b/assets/images/macos_icons/monero_macos_icons/monero_macos_1024.png index f3f91af95..06f997e9e 100644 Binary files a/assets/images/macos_icons/monero_macos_icons/monero_macos_1024.png and b/assets/images/macos_icons/monero_macos_icons/monero_macos_1024.png differ diff --git a/assets/images/macos_icons/monero_macos_icons/monero_macos_128.png b/assets/images/macos_icons/monero_macos_icons/monero_macos_128.png index 48a1906e1..641dc5d45 100644 Binary files a/assets/images/macos_icons/monero_macos_icons/monero_macos_128.png and b/assets/images/macos_icons/monero_macos_icons/monero_macos_128.png differ diff --git a/assets/images/macos_icons/monero_macos_icons/monero_macos_16.png b/assets/images/macos_icons/monero_macos_icons/monero_macos_16.png index 5104ea7d3..38832f2da 100644 Binary files a/assets/images/macos_icons/monero_macos_icons/monero_macos_16.png and b/assets/images/macos_icons/monero_macos_icons/monero_macos_16.png differ diff --git a/assets/images/macos_icons/monero_macos_icons/monero_macos_256.png b/assets/images/macos_icons/monero_macos_icons/monero_macos_256.png index 6ae8b769b..7d067f3f5 100644 Binary files a/assets/images/macos_icons/monero_macos_icons/monero_macos_256.png and b/assets/images/macos_icons/monero_macos_icons/monero_macos_256.png differ diff --git a/assets/images/macos_icons/monero_macos_icons/monero_macos_32.png b/assets/images/macos_icons/monero_macos_icons/monero_macos_32.png index 60c30c609..def34e452 100644 Binary files a/assets/images/macos_icons/monero_macos_icons/monero_macos_32.png and b/assets/images/macos_icons/monero_macos_icons/monero_macos_32.png differ diff --git a/assets/images/macos_icons/monero_macos_icons/monero_macos_512.png b/assets/images/macos_icons/monero_macos_icons/monero_macos_512.png index 7072c07e7..8969e716f 100644 Binary files a/assets/images/macos_icons/monero_macos_icons/monero_macos_512.png and b/assets/images/macos_icons/monero_macos_icons/monero_macos_512.png differ diff --git a/assets/images/macos_icons/monero_macos_icons/monero_macos_64.png b/assets/images/macos_icons/monero_macos_icons/monero_macos_64.png index 74aa4e2ad..4c9152e7e 100644 Binary files a/assets/images/macos_icons/monero_macos_icons/monero_macos_64.png and b/assets/images/macos_icons/monero_macos_icons/monero_macos_64.png differ diff --git a/assets/images/monero.com_android_icon.png b/assets/images/monero.com_android_icon.png index 9e1fa0a65..77315b0c7 100644 Binary files a/assets/images/monero.com_android_icon.png and b/assets/images/monero.com_android_icon.png differ diff --git a/assets/images/monero.com_logo.png b/assets/images/monero.com_logo.png index 9e1fa0a65..77315b0c7 100644 Binary files a/assets/images/monero.com_logo.png and b/assets/images/monero.com_logo.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-anydpi-v26/ic_launcher.xml b/assets/images/monerocom_android_icon/mipmap-anydpi-v26/ic_launcher.xml index c8bd4b26c..90f958096 100644 --- a/assets/images/monerocom_android_icon/mipmap-anydpi-v26/ic_launcher.xml +++ b/assets/images/monerocom_android_icon/mipmap-anydpi-v26/ic_launcher.xml @@ -1,6 +1,5 @@ - - - + + \ No newline at end of file diff --git a/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher.png b/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher.png index 1785f4b05..3ed5f8878 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher.png and b/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_back.png b/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_back.png index 5b0fde827..e32edd842 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_back.png and b/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_back.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_fore.png b/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_fore.png index 884745809..25d55a9f8 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_fore.png and b/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_fore.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_mono.png b/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_mono.png deleted file mode 100644 index af8126ea9..000000000 Binary files a/assets/images/monerocom_android_icon/mipmap-hdpi/ic_launcher_adaptive_mono.png and /dev/null differ diff --git a/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher.png b/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher.png index 12c31ef57..c05a7c609 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher.png and b/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_back.png b/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_back.png index 5d25e42e7..6202eb67b 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_back.png and b/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_back.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_fore.png b/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_fore.png index 28bbd57b8..c8f5cc34f 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_fore.png and b/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_fore.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_mono.png b/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_mono.png deleted file mode 100644 index 0778b841f..000000000 Binary files a/assets/images/monerocom_android_icon/mipmap-mdpi/ic_launcher_adaptive_mono.png and /dev/null differ diff --git a/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher.png b/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher.png index 2d8878946..4e11807e3 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher.png and b/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_back.png b/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_back.png index c4b66dc58..cd5b663af 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_back.png and b/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_back.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_fore.png b/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_fore.png index 811848c27..2a5697c60 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_fore.png and b/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_fore.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_mono.png b/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_mono.png deleted file mode 100644 index 318914c59..000000000 Binary files a/assets/images/monerocom_android_icon/mipmap-xhdpi/ic_launcher_adaptive_mono.png and /dev/null differ diff --git a/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher.png b/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher.png index 2ceba55ad..5037424f9 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher.png and b/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_back.png b/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_back.png index 75dc0219d..5e7ee9d60 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_back.png and b/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_back.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_fore.png b/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_fore.png index e078655c9..5aaab9e32 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_fore.png and b/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_fore.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_mono.png b/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_mono.png deleted file mode 100644 index b1165abe8..000000000 Binary files a/assets/images/monerocom_android_icon/mipmap-xxhdpi/ic_launcher_adaptive_mono.png and /dev/null differ diff --git a/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher.png b/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher.png index beca2d29b..72c94fc36 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher.png and b/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_back.png b/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_back.png index 46b1e2cb1..d82cb42cf 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_back.png and b/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_back.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png b/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png index 255b1b71f..6e0415b8b 100644 Binary files a/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png and b/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png differ diff --git a/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_mono.png b/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_mono.png deleted file mode 100644 index ea11a01d3..000000000 Binary files a/assets/images/monerocom_android_icon/mipmap-xxxhdpi/ic_launcher_adaptive_mono.png and /dev/null differ diff --git a/assets/images/paxg.webp b/assets/images/paxg.webp new file mode 100644 index 000000000..9b89a14fa Binary files /dev/null and b/assets/images/paxg.webp differ diff --git a/assets/images/xaut.webp b/assets/images/xaut.webp new file mode 100644 index 000000000..74f514d2e Binary files /dev/null and b/assets/images/xaut.webp differ diff --git a/assets/lightning_deposit_help.svg b/assets/lightning_deposit_help.svg new file mode 100644 index 000000000..5cecc0bab --- /dev/null +++ b/assets/lightning_deposit_help.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/3dots.svg b/assets/new-ui/3dots.svg new file mode 100644 index 000000000..5b8896d06 --- /dev/null +++ b/assets/new-ui/3dots.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/new-ui/3dots_vertical.svg b/assets/new-ui/3dots_vertical.svg new file mode 100644 index 000000000..dfee4c2b4 --- /dev/null +++ b/assets/new-ui/3dots_vertical.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/Contacts.svg b/assets/new-ui/Contacts.svg new file mode 100644 index 000000000..335def2b5 --- /dev/null +++ b/assets/new-ui/Contacts.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/account.svg b/assets/new-ui/account.svg new file mode 100644 index 000000000..39668b220 --- /dev/null +++ b/assets/new-ui/account.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/new-ui/add-label.svg b/assets/new-ui/add-label.svg new file mode 100644 index 000000000..ec06b3fdf --- /dev/null +++ b/assets/new-ui/add-label.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/addr-book.svg b/assets/new-ui/addr-book.svg new file mode 100644 index 000000000..34f72ae93 --- /dev/null +++ b/assets/new-ui/addr-book.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/address-type-picker-icons/btc_lightning.svg b/assets/new-ui/address-type-picker-icons/btc_lightning.svg new file mode 100644 index 000000000..e67468b05 --- /dev/null +++ b/assets/new-ui/address-type-picker-icons/btc_lightning.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/new-ui/address-type-picker-icons/btc_standard.svg b/assets/new-ui/address-type-picker-icons/btc_standard.svg new file mode 100644 index 000000000..8878b4275 --- /dev/null +++ b/assets/new-ui/address-type-picker-icons/btc_standard.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/new-ui/address-type-picker-icons/legacy.svg b/assets/new-ui/address-type-picker-icons/legacy.svg new file mode 100644 index 000000000..b1b24555b --- /dev/null +++ b/assets/new-ui/address-type-picker-icons/legacy.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/new-ui/address-type-picker-icons/litecoin.svg b/assets/new-ui/address-type-picker-icons/litecoin.svg new file mode 100644 index 000000000..92dbe0580 --- /dev/null +++ b/assets/new-ui/address-type-picker-icons/litecoin.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/address-type-picker-icons/mweb.svg b/assets/new-ui/address-type-picker-icons/mweb.svg new file mode 100644 index 000000000..ca2c3e1bf --- /dev/null +++ b/assets/new-ui/address-type-picker-icons/mweb.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/new-ui/address-type-picker-icons/payjoin.svg b/assets/new-ui/address-type-picker-icons/payjoin.svg new file mode 100644 index 000000000..f9133b188 --- /dev/null +++ b/assets/new-ui/address-type-picker-icons/payjoin.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/new-ui/address-type-picker-icons/segwit.svg b/assets/new-ui/address-type-picker-icons/segwit.svg new file mode 100644 index 000000000..df2f29fdf --- /dev/null +++ b/assets/new-ui/address-type-picker-icons/segwit.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/new-ui/address-type-picker-icons/silent.svg b/assets/new-ui/address-type-picker-icons/silent.svg new file mode 100644 index 000000000..3e4d4f196 --- /dev/null +++ b/assets/new-ui/address-type-picker-icons/silent.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/new-ui/address-type-picker-icons/taproot.svg b/assets/new-ui/address-type-picker-icons/taproot.svg new file mode 100644 index 000000000..3580b3d74 --- /dev/null +++ b/assets/new-ui/address-type-picker-icons/taproot.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/new-ui/address-type-picker-icons/zec/public.svg b/assets/new-ui/address-type-picker-icons/zec/public.svg new file mode 100644 index 000000000..156a79e42 --- /dev/null +++ b/assets/new-ui/address-type-picker-icons/zec/public.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/address-type-picker-icons/zec/sapling.svg b/assets/new-ui/address-type-picker-icons/zec/sapling.svg new file mode 100644 index 000000000..4be2a420e --- /dev/null +++ b/assets/new-ui/address-type-picker-icons/zec/sapling.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/address-type-picker-icons/zec/shielded.svg b/assets/new-ui/address-type-picker-icons/zec/shielded.svg new file mode 100644 index 000000000..d106011bc --- /dev/null +++ b/assets/new-ui/address-type-picker-icons/zec/shielded.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/address-type-picker-icons/zec/transparent.svg b/assets/new-ui/address-type-picker-icons/zec/transparent.svg new file mode 100644 index 000000000..033af6be1 --- /dev/null +++ b/assets/new-ui/address-type-picker-icons/zec/transparent.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/address-type-picker-icons/zec/unified.svg b/assets/new-ui/address-type-picker-icons/zec/unified.svg new file mode 100644 index 000000000..0703d3019 --- /dev/null +++ b/assets/new-ui/address-type-picker-icons/zec/unified.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/address_hide.svg b/assets/new-ui/address_hide.svg new file mode 100644 index 000000000..60c1f52cd --- /dev/null +++ b/assets/new-ui/address_hide.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/address_set_label.svg b/assets/new-ui/address_set_label.svg new file mode 100644 index 000000000..5274019cb --- /dev/null +++ b/assets/new-ui/address_set_label.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/new-ui/app_settings.svg b/assets/new-ui/app_settings.svg new file mode 100644 index 000000000..9dd7abd4f --- /dev/null +++ b/assets/new-ui/app_settings.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/new-ui/arrow_forward.svg b/assets/new-ui/arrow_forward.svg new file mode 100644 index 000000000..85eecec32 --- /dev/null +++ b/assets/new-ui/arrow_forward.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/new-ui/arrow_right.svg b/assets/new-ui/arrow_right.svg new file mode 100644 index 000000000..fa9e3f408 --- /dev/null +++ b/assets/new-ui/arrow_right.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/balance_card_backgrounds/arbitrum.svg b/assets/new-ui/balance_card_backgrounds/arbitrum.svg new file mode 100644 index 000000000..bca0e614e --- /dev/null +++ b/assets/new-ui/balance_card_backgrounds/arbitrum.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/balance_card_backgrounds/base.svg b/assets/new-ui/balance_card_backgrounds/base.svg new file mode 100644 index 000000000..b20ef135a --- /dev/null +++ b/assets/new-ui/balance_card_backgrounds/base.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/balance_card_backgrounds/bitcoin-cash.svg b/assets/new-ui/balance_card_backgrounds/bitcoin-cash.svg new file mode 100644 index 000000000..2782d584a --- /dev/null +++ b/assets/new-ui/balance_card_backgrounds/bitcoin-cash.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/balance_card_backgrounds/bitcoin.svg b/assets/new-ui/balance_card_backgrounds/bitcoin.svg new file mode 100644 index 000000000..086df61e0 --- /dev/null +++ b/assets/new-ui/balance_card_backgrounds/bitcoin.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/balance_card_backgrounds/bnb.svg b/assets/new-ui/balance_card_backgrounds/bnb.svg new file mode 100644 index 000000000..9db48be4b --- /dev/null +++ b/assets/new-ui/balance_card_backgrounds/bnb.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/balance_card_backgrounds/decred.svg b/assets/new-ui/balance_card_backgrounds/decred.svg new file mode 100644 index 000000000..d6d3fbdb1 --- /dev/null +++ b/assets/new-ui/balance_card_backgrounds/decred.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/balance_card_backgrounds/dogecoin.svg b/assets/new-ui/balance_card_backgrounds/dogecoin.svg new file mode 100644 index 000000000..2503163f9 --- /dev/null +++ b/assets/new-ui/balance_card_backgrounds/dogecoin.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/balance_card_backgrounds/ethereum.svg b/assets/new-ui/balance_card_backgrounds/ethereum.svg new file mode 100644 index 000000000..f4f7ede64 --- /dev/null +++ b/assets/new-ui/balance_card_backgrounds/ethereum.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/balance_card_backgrounds/lightning.svg b/assets/new-ui/balance_card_backgrounds/lightning.svg new file mode 100644 index 000000000..f068bd053 --- /dev/null +++ b/assets/new-ui/balance_card_backgrounds/lightning.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/balance_card_backgrounds/litecoin.svg b/assets/new-ui/balance_card_backgrounds/litecoin.svg new file mode 100644 index 000000000..9d670e9c2 --- /dev/null +++ b/assets/new-ui/balance_card_backgrounds/litecoin.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/balance_card_backgrounds/monero.svg b/assets/new-ui/balance_card_backgrounds/monero.svg new file mode 100644 index 000000000..2cdfa76f2 --- /dev/null +++ b/assets/new-ui/balance_card_backgrounds/monero.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/balance_card_backgrounds/nano.svg b/assets/new-ui/balance_card_backgrounds/nano.svg new file mode 100644 index 000000000..7334524bc --- /dev/null +++ b/assets/new-ui/balance_card_backgrounds/nano.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/balance_card_backgrounds/polygon.svg b/assets/new-ui/balance_card_backgrounds/polygon.svg new file mode 100644 index 000000000..c09773fdf --- /dev/null +++ b/assets/new-ui/balance_card_backgrounds/polygon.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/balance_card_backgrounds/solana.svg b/assets/new-ui/balance_card_backgrounds/solana.svg new file mode 100644 index 000000000..887421976 --- /dev/null +++ b/assets/new-ui/balance_card_backgrounds/solana.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/balance_card_backgrounds/tron.svg b/assets/new-ui/balance_card_backgrounds/tron.svg new file mode 100644 index 000000000..d13c399d4 --- /dev/null +++ b/assets/new-ui/balance_card_backgrounds/tron.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/balance_card_backgrounds/wownero.svg b/assets/new-ui/balance_card_backgrounds/wownero.svg new file mode 100644 index 000000000..02cae7454 --- /dev/null +++ b/assets/new-ui/balance_card_backgrounds/wownero.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/balance_card_backgrounds/zano.svg b/assets/new-ui/balance_card_backgrounds/zano.svg new file mode 100644 index 000000000..1c020cf47 --- /dev/null +++ b/assets/new-ui/balance_card_backgrounds/zano.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/balance_card_backgrounds/zcash.svg b/assets/new-ui/balance_card_backgrounds/zcash.svg new file mode 100644 index 000000000..ebb20f588 --- /dev/null +++ b/assets/new-ui/balance_card_backgrounds/zcash.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/balance_card_icons/arbitrum.svg b/assets/new-ui/balance_card_icons/arbitrum.svg new file mode 100644 index 000000000..e0bfb4726 --- /dev/null +++ b/assets/new-ui/balance_card_icons/arbitrum.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/balance_card_icons/base.svg b/assets/new-ui/balance_card_icons/base.svg new file mode 100644 index 000000000..a7c5b7f02 --- /dev/null +++ b/assets/new-ui/balance_card_icons/base.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/balance_card_icons/bitcoin.svg b/assets/new-ui/balance_card_icons/bitcoin.svg new file mode 100644 index 000000000..135ecb5bc --- /dev/null +++ b/assets/new-ui/balance_card_icons/bitcoin.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/new-ui/balance_card_icons/bitcoin_cash.svg b/assets/new-ui/balance_card_icons/bitcoin_cash.svg new file mode 100644 index 000000000..e97c5bdb0 --- /dev/null +++ b/assets/new-ui/balance_card_icons/bitcoin_cash.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/new-ui/balance_card_icons/bnb.svg b/assets/new-ui/balance_card_icons/bnb.svg new file mode 100644 index 000000000..bac89c817 --- /dev/null +++ b/assets/new-ui/balance_card_icons/bnb.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/balance_card_icons/decred.svg b/assets/new-ui/balance_card_icons/decred.svg new file mode 100644 index 000000000..ffcb9e856 --- /dev/null +++ b/assets/new-ui/balance_card_icons/decred.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/balance_card_icons/dogecoin.svg b/assets/new-ui/balance_card_icons/dogecoin.svg new file mode 100644 index 000000000..7850a63fb --- /dev/null +++ b/assets/new-ui/balance_card_icons/dogecoin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/new-ui/balance_card_icons/ethereum.svg b/assets/new-ui/balance_card_icons/ethereum.svg new file mode 100644 index 000000000..1fa6a3e27 --- /dev/null +++ b/assets/new-ui/balance_card_icons/ethereum.svg @@ -0,0 +1,5 @@ + + + diff --git a/assets/new-ui/balance_card_icons/lightning.svg b/assets/new-ui/balance_card_icons/lightning.svg new file mode 100644 index 000000000..304eb6281 --- /dev/null +++ b/assets/new-ui/balance_card_icons/lightning.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/balance_card_icons/litecoin.svg b/assets/new-ui/balance_card_icons/litecoin.svg new file mode 100644 index 000000000..fa244bb73 --- /dev/null +++ b/assets/new-ui/balance_card_icons/litecoin.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/balance_card_icons/monero.svg b/assets/new-ui/balance_card_icons/monero.svg new file mode 100644 index 000000000..30a738bc8 --- /dev/null +++ b/assets/new-ui/balance_card_icons/monero.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/new-ui/balance_card_icons/nano.svg b/assets/new-ui/balance_card_icons/nano.svg new file mode 100644 index 000000000..0396bfea2 --- /dev/null +++ b/assets/new-ui/balance_card_icons/nano.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/new-ui/balance_card_icons/polygon.svg b/assets/new-ui/balance_card_icons/polygon.svg new file mode 100644 index 000000000..09fbc646d --- /dev/null +++ b/assets/new-ui/balance_card_icons/polygon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/new-ui/balance_card_icons/solana.svg b/assets/new-ui/balance_card_icons/solana.svg new file mode 100644 index 000000000..0c0b8d5ec --- /dev/null +++ b/assets/new-ui/balance_card_icons/solana.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/balance_card_icons/tron.svg b/assets/new-ui/balance_card_icons/tron.svg new file mode 100644 index 000000000..0ee440f6e --- /dev/null +++ b/assets/new-ui/balance_card_icons/tron.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/new-ui/balance_card_icons/wownero.svg b/assets/new-ui/balance_card_icons/wownero.svg new file mode 100644 index 000000000..8ecee9c6b --- /dev/null +++ b/assets/new-ui/balance_card_icons/wownero.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/new-ui/balance_card_icons/zano.svg b/assets/new-ui/balance_card_icons/zano.svg new file mode 100644 index 000000000..1cb3932c1 --- /dev/null +++ b/assets/new-ui/balance_card_icons/zano.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/new-ui/balance_card_icons/zcash.svg b/assets/new-ui/balance_card_icons/zcash.svg new file mode 100644 index 000000000..251ec4bcd --- /dev/null +++ b/assets/new-ui/balance_card_icons/zcash.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/bitcoin.svg b/assets/new-ui/bitcoin.svg new file mode 100644 index 000000000..391f3c289 --- /dev/null +++ b/assets/new-ui/bitcoin.svg @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/assets/new-ui/blank.svg b/assets/new-ui/blank.svg new file mode 100644 index 000000000..cb30dd870 --- /dev/null +++ b/assets/new-ui/blank.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/new-ui/btcqr.png b/assets/new-ui/btcqr.png new file mode 100644 index 000000000..a5c34ed1f Binary files /dev/null and b/assets/new-ui/btcqr.png differ diff --git a/assets/new-ui/by-cakelabs.svg b/assets/new-ui/by-cakelabs.svg new file mode 100644 index 000000000..004e534aa --- /dev/null +++ b/assets/new-ui/by-cakelabs.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/new-ui/cake_squircle_icon.svg b/assets/new-ui/cake_squircle_icon.svg new file mode 100644 index 000000000..8bafc44c4 --- /dev/null +++ b/assets/new-ui/cake_squircle_icon.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/cakelabs-icon.svg b/assets/new-ui/cakelabs-icon.svg new file mode 100644 index 000000000..21b6c2a5e --- /dev/null +++ b/assets/new-ui/cakelabs-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/new-ui/cakewallet-wordmark-light.svg b/assets/new-ui/cakewallet-wordmark-light.svg new file mode 100644 index 000000000..7d649c449 --- /dev/null +++ b/assets/new-ui/cakewallet-wordmark-light.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/cakewallet-wordmark.svg b/assets/new-ui/cakewallet-wordmark.svg new file mode 100644 index 000000000..08d6e195b --- /dev/null +++ b/assets/new-ui/cakewallet-wordmark.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/chain_badges/arbitrum.svg b/assets/new-ui/chain_badges/arbitrum.svg new file mode 100644 index 000000000..9a46c53d4 --- /dev/null +++ b/assets/new-ui/chain_badges/arbitrum.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/chain_badges/base.svg b/assets/new-ui/chain_badges/base.svg new file mode 100644 index 000000000..aab32426a --- /dev/null +++ b/assets/new-ui/chain_badges/base.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/chain_badges/bnb smart chain.svg b/assets/new-ui/chain_badges/bnb smart chain.svg new file mode 100644 index 000000000..bb03a4a82 --- /dev/null +++ b/assets/new-ui/chain_badges/bnb smart chain.svg @@ -0,0 +1,6 @@ + + + + diff --git a/assets/new-ui/chain_badges/ethereum.svg b/assets/new-ui/chain_badges/ethereum.svg new file mode 100644 index 000000000..0eef0c4a3 --- /dev/null +++ b/assets/new-ui/chain_badges/ethereum.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/new-ui/chain_badges/lightning.svg b/assets/new-ui/chain_badges/lightning.svg new file mode 100644 index 000000000..85430ad66 --- /dev/null +++ b/assets/new-ui/chain_badges/lightning.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/new-ui/chain_badges/mweb.svg b/assets/new-ui/chain_badges/mweb.svg new file mode 100644 index 000000000..75e6a990c --- /dev/null +++ b/assets/new-ui/chain_badges/mweb.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/assets/new-ui/chain_badges/polygon.svg b/assets/new-ui/chain_badges/polygon.svg new file mode 100644 index 000000000..8d9d4ba7c --- /dev/null +++ b/assets/new-ui/chain_badges/polygon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/chain_badges/solana.svg b/assets/new-ui/chain_badges/solana.svg new file mode 100644 index 000000000..749821f5f --- /dev/null +++ b/assets/new-ui/chain_badges/solana.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/new-ui/chain_badges/tron.svg b/assets/new-ui/chain_badges/tron.svg new file mode 100644 index 000000000..e8891cd41 --- /dev/null +++ b/assets/new-ui/chain_badges/tron.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/chain_badges/zano.svg b/assets/new-ui/chain_badges/zano.svg new file mode 100644 index 000000000..902bda58c --- /dev/null +++ b/assets/new-ui/chain_badges/zano.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/changelog/README b/assets/new-ui/changelog/README new file mode 100644 index 000000000..21ef5fe38 --- /dev/null +++ b/assets/new-ui/changelog/README @@ -0,0 +1,14 @@ +to change the changelog, write text/changelog_en.json in following format: + +[ + { + "icon": "the name of icon for the changelog item, must be present in icons/, do NOT include .svg", + "title": "title of item", + "description": "description of item" + } + +] + +all 3 elements are optional, ui will just omit them if you don't provide them. + +after you write it, run "dart run tool/translate_changelog.dart" to fill in the rest of the languages diff --git a/assets/new-ui/changelog/icons/cards.svg b/assets/new-ui/changelog/icons/cards.svg new file mode 100644 index 000000000..3ab43395f --- /dev/null +++ b/assets/new-ui/changelog/icons/cards.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/changelog/icons/nav.svg b/assets/new-ui/changelog/icons/nav.svg new file mode 100644 index 000000000..118ec1877 --- /dev/null +++ b/assets/new-ui/changelog/icons/nav.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/changelog/icons/pallete.svg b/assets/new-ui/changelog/icons/pallete.svg new file mode 100644 index 000000000..a7a5633bf --- /dev/null +++ b/assets/new-ui/changelog/icons/pallete.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/changelog/icons/star.svg b/assets/new-ui/changelog/icons/star.svg new file mode 100644 index 000000000..a6899d53f --- /dev/null +++ b/assets/new-ui/changelog/icons/star.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/changelog/icons/trans.svg b/assets/new-ui/changelog/icons/trans.svg new file mode 100644 index 000000000..0746ff96d --- /dev/null +++ b/assets/new-ui/changelog/icons/trans.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/changelog/text/changelog_ar.json b/assets/new-ui/changelog/text/changelog_ar.json new file mode 100644 index 000000000..0902875fb --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_ar.json @@ -0,0 +1 @@ +[{"icon":"star","title":"مظهر جديد","description":"أعيد تصميمها من الألف إلى الياء"},{"icon":"nav","title":"سهولة التنقل","description":"تحسين شريط التنقل والتخطيطات والتدفقات"},{"icon":"cards","title":"تحسين إدارة الحساب","description":"أسهل من أي وقت مضى"},{"icon":"pallete","title":"تخصيص بطاقة الرصيد","description":"اجعلها لك!"},{"icon":"trans","title":"عرض المعاملات السريعة","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_bg.json b/assets/new-ui/changelog/text/changelog_bg.json new file mode 100644 index 000000000..eddfae82a --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_bg.json @@ -0,0 +1 @@ +[{"icon":"star","title":"Нов облик","description":"Преработен от нулата"},{"icon":"nav","title":"По-лесна навигация","description":"Подобрена навигационна лента, оформления и потоци"},{"icon":"cards","title":"Подобрено управление на акаунта","description":"По-лесно от всякога"},{"icon":"pallete","title":"Персонализиране на балансова карта","description":"Направи го твой!"},{"icon":"trans","title":"Изглед на бързи транзакции","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_cs.json b/assets/new-ui/changelog/text/changelog_cs.json new file mode 100644 index 000000000..4a7d18453 --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_cs.json @@ -0,0 +1 @@ +[{"icon":"star","title":"Nový vzhled","description":"Od základu předělaný"},{"icon":"nav","title":"Jednodušší navigace","description":"Vylepšený navigační panel, rozvržení a toky"},{"icon":"cards","title":"Vylepšená správa účtu","description":"Snazší než kdy jindy"},{"icon":"pallete","title":"Přizpůsobení karty zůstatku","description":"Udělejte to po svém!"},{"icon":"trans","title":"Rychlé zobrazení transakcí","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_de.json b/assets/new-ui/changelog/text/changelog_de.json new file mode 100644 index 000000000..5e53dcbc9 --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_de.json @@ -0,0 +1 @@ +[{"icon":"star","title":"Neuer Look","description":"Von Grund auf neu gestaltet"},{"icon":"nav","title":"Einfachere Navigation","description":"Verbesserte Navigationsleiste, Layouts und Abläufe"},{"icon":"cards","title":"Verbesserte Kontoverwaltung","description":"Einfacher als je zuvor"},{"icon":"pallete","title":"Anpassung der Guthabenkarte","description":"Machen Sie es zu Ihrem!"},{"icon":"trans","title":"Schnelle Transaktionsansicht","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_en.json b/assets/new-ui/changelog/text/changelog_en.json new file mode 100644 index 000000000..9c9bf4ce2 --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_en.json @@ -0,0 +1,27 @@ +[ + { + "icon": "star", + "title": "New look", + "description": "Redesigned from the ground up" + }, + { + "icon": "nav", + "title": "Easier navigation", + "description": "Improved navbar, layouts and flows" + }, + { + "icon": "cards", + "title": "Improved account management", + "description": "Easier than ever" + }, + { + "icon": "pallete", + "title": "Balance card customization", + "description": "Make it yours!" + }, + { + "icon": "trans", + "title": "Quick transactions view", + "description": "" + } +] diff --git a/assets/new-ui/changelog/text/changelog_es.json b/assets/new-ui/changelog/text/changelog_es.json new file mode 100644 index 000000000..1739b62cb --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_es.json @@ -0,0 +1 @@ +[{"icon":"star","title":"Nueva apariencia","description":"Rediseñado desde cero"},{"icon":"nav","title":"Navegación más fácil","description":"Barra de navegación, diseños y flujos mejorados."},{"icon":"cards","title":"Gestión de cuentas mejorada","description":"Más fácil que nunca"},{"icon":"pallete","title":"Personalización de la tarjeta de saldo","description":"¡Hazlo tuyo!"},{"icon":"trans","title":"Vista rápida de transacciones","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_fa.json b/assets/new-ui/changelog/text/changelog_fa.json new file mode 100644 index 000000000..cf2694f63 --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_fa.json @@ -0,0 +1 @@ +[{"icon":"star","title":"ظاهر جدید","description":"بازطراحی شده از پایه"},{"icon":"nav","title":"ناوبری آسان تر","description":"نوار ناوبری، طرح‌بندی‌ها و جریان‌ها بهبود یافته است"},{"icon":"cards","title":"بهبود مدیریت حساب","description":"راحت تر از همیشه"},{"icon":"pallete","title":"سفارشی سازی کارت موجودی","description":"آن را مال خودت کن!"},{"icon":"trans","title":"نمای معاملات سریع","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_fr.json b/assets/new-ui/changelog/text/changelog_fr.json new file mode 100644 index 000000000..c1718c0a7 --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_fr.json @@ -0,0 +1 @@ +[{"icon":"star","title":"Nouveau look","description":"Repensé de fond en comble"},{"icon":"nav","title":"Navigation plus facile","description":"Barre de navigation, mises en page et flux améliorés"},{"icon":"cards","title":"Gestion de compte améliorée","description":"Plus facile que jamais"},{"icon":"pallete","title":"Personnalisation de la carte de solde","description":"Faites-en le vôtre !"},{"icon":"trans","title":"Vue des transactions rapides","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_gn.json b/assets/new-ui/changelog/text/changelog_gn.json new file mode 100644 index 000000000..023dc5339 --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_gn.json @@ -0,0 +1 @@ +[{"icon":"star","title":"Ojehecha pyahu","description":"Ojejapo jey yvýguio"},{"icon":"nav","title":"Navegación ndahasýiva","description":"Oñemoporãve navbar, diseño ha flujo"},{"icon":"cards","title":"Oñemoporãve cuenta gestión rehegua","description":"Ndahasýive yma guarégui"},{"icon":"pallete","title":"Tarjeta de saldo personalización rehegua","description":"¡Ejapo chugui nde mba'e!"},{"icon":"trans","title":"Umi transacción pya’e jehechauka","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_ha.json b/assets/new-ui/changelog/text/changelog_ha.json new file mode 100644 index 000000000..a41b71ff2 --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_ha.json @@ -0,0 +1 @@ +[{"icon":"star","title":"Sabon kallo","description":"An sake tsara shi daga ƙasa zuwa sama"},{"icon":"nav","title":"Sauƙin kewayawa","description":"Ingantattun navbar, shimfidu da gudana"},{"icon":"cards","title":"Inganta sarrafa asusun","description":"Sauki fiye da kowane lokaci"},{"icon":"pallete","title":"Daidaiton katin ma'auni","description":"Maida shi naku!"},{"icon":"trans","title":"Duban ma'amaloli da sauri","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_hi.json b/assets/new-ui/changelog/text/changelog_hi.json new file mode 100644 index 000000000..32427753b --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_hi.json @@ -0,0 +1 @@ +[{"icon":"star","title":"नया रूप","description":"ज़मीन से ऊपर तक पुनः डिज़ाइन किया गया"},{"icon":"nav","title":"आसान नेविगेशन","description":"बेहतर नेवबार, लेआउट और प्रवाह"},{"icon":"cards","title":"बेहतर खाता प्रबंधन","description":"पहले से कहीं ज्यादा आसान"},{"icon":"pallete","title":"बैलेंस कार्ड अनुकूलन","description":"इसे अपना बनाएं!"},{"icon":"trans","title":"त्वरित लेनदेन दृश्य","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_hr.json b/assets/new-ui/changelog/text/changelog_hr.json new file mode 100644 index 000000000..61d33f11b --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_hr.json @@ -0,0 +1 @@ +[{"icon":"star","title":"Novi izgled","description":"Redizajniran iz temelja"},{"icon":"nav","title":"Lakša navigacija","description":"Poboljšana navigacijska traka, izgledi i tokovi"},{"icon":"cards","title":"Poboljšano upravljanje računom","description":"Lakše nego ikada"},{"icon":"pallete","title":"Prilagodba kartice stanja","description":"Učinite ga svojim!"},{"icon":"trans","title":"Pregled brzih transakcija","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_hy.json b/assets/new-ui/changelog/text/changelog_hy.json new file mode 100644 index 000000000..038e8629c --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_hy.json @@ -0,0 +1 @@ +[{"icon":"star","title":"Նոր տեսք","description":"Վերանախագծված է ի սկզբանե"},{"icon":"nav","title":"Ավելի հեշտ նավարկություն","description":"Բարելավված navbar, դասավորություններ և հոսքեր"},{"icon":"cards","title":"Բարելավված հաշվի կառավարում","description":"Ավելի հեշտ, քան երբևէ"},{"icon":"pallete","title":"Հաշվեկշռային քարտի հարմարեցում","description":"Դարձրե՛ք այն ձերը:"},{"icon":"trans","title":"Արագ գործարքների դիտում","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_id.json b/assets/new-ui/changelog/text/changelog_id.json new file mode 100644 index 000000000..e59825b8b --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_id.json @@ -0,0 +1 @@ +[{"icon":"star","title":"Tampilan baru","description":"Didesain ulang dari awal"},{"icon":"nav","title":"Navigasi yang lebih mudah","description":"Peningkatan bilah navigasi, tata letak, dan alur"},{"icon":"cards","title":"Peningkatan manajemen akun","description":"Lebih mudah dari sebelumnya"},{"icon":"pallete","title":"Kustomisasi kartu saldo","description":"Jadikan itu milikmu!"},{"icon":"trans","title":"Tampilan transaksi cepat","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_it.json b/assets/new-ui/changelog/text/changelog_it.json new file mode 100644 index 000000000..324cfd15e --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_it.json @@ -0,0 +1 @@ +[{"icon":"star","title":"Nuovo aspetto","description":"Riprogettato da zero"},{"icon":"nav","title":"Navigazione più semplice","description":"Barra di navigazione, layout e flussi migliorati"},{"icon":"cards","title":"Gestione dell'account migliorata","description":"Più facile che mai"},{"icon":"pallete","title":"Personalizzazione della carta saldo","description":"Rendilo tuo!"},{"icon":"trans","title":"Visualizzazione rapida delle transazioni","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_ja.json b/assets/new-ui/changelog/text/changelog_ja.json new file mode 100644 index 000000000..a9a24bde1 --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_ja.json @@ -0,0 +1 @@ +[{"icon":"star","title":"新しい外観","description":"根本から再設計"},{"icon":"nav","title":"より簡単なナビゲーション","description":"ナビゲーションバー、レイアウト、フローの改善"},{"icon":"cards","title":"アカウント管理の改善","description":"これまでよりも簡単に"},{"icon":"pallete","title":"バランスカードのカスタマイズ","description":"それをあなたのものにしてください!"},{"icon":"trans","title":"クイックトランザクションビュー","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_ko.json b/assets/new-ui/changelog/text/changelog_ko.json new file mode 100644 index 000000000..d729e1351 --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_ko.json @@ -0,0 +1 @@ +[{"icon":"star","title":"새로운 모습","description":"처음부터 새롭게 디자인되었습니다."},{"icon":"nav","title":"더욱 쉬워진 탐색","description":"향상된 탐색 모음, 레이아웃 및 흐름"},{"icon":"cards","title":"향상된 계정 관리","description":"그 어느 때보다 쉬워졌습니다."},{"icon":"pallete","title":"잔액 카드 사용자 정의","description":"당신의 것으로 만들어보세요!"},{"icon":"trans","title":"빠른 거래 보기","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_my.json b/assets/new-ui/changelog/text/changelog_my.json new file mode 100644 index 000000000..959683d32 --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_my.json @@ -0,0 +1 @@ +[{"icon":"star","title":"အမြင်သစ်","description":"မြေပြင်မှ ပြန်လည်ဒီဇိုင်းထုတ်ထားသည်။"},{"icon":"nav","title":"ပိုမိုလွယ်ကူသောလမ်းကြောင်း","description":"navbar၊ အပြင်အဆင်များနှင့် စီးဆင်းမှုများကို မြှင့်တင်ထားသည်။"},{"icon":"cards","title":"ပိုမိုကောင်းမွန်သော အကောင့်စီမံခန့်ခွဲမှု","description":"အရင်ကထက် ပိုလွယ်တယ်။"},{"icon":"pallete","title":"လက်ကျန်ကတ် စိတ်ကြိုက်ပြုလုပ်ခြင်း။","description":"အဲဒါကို မင်းအတွက် လုပ်ပါ။"},{"icon":"trans","title":"လျင်မြန်သော ငွေပေးငွေယူ မြင်ကွင်း","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_nl.json b/assets/new-ui/changelog/text/changelog_nl.json new file mode 100644 index 000000000..533436ad5 --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_nl.json @@ -0,0 +1 @@ +[{"icon":"star","title":"Nieuwe look","description":"Opnieuw ontworpen vanaf de grond af"},{"icon":"nav","title":"Gemakkelijkere navigatie","description":"Verbeterde navigatiebalk, lay-outs en stromen"},{"icon":"cards","title":"Verbeterd accountbeheer","description":"Makkelijker dan ooit"},{"icon":"pallete","title":"Aanpassing van de saldokaart","description":"Maak het de jouwe!"},{"icon":"trans","title":"Snelle transactieweergave","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_pl.json b/assets/new-ui/changelog/text/changelog_pl.json new file mode 100644 index 000000000..72ba30563 --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_pl.json @@ -0,0 +1 @@ +[{"icon":"star","title":"Nowy wygląd","description":"Przeprojektowany od podstaw"},{"icon":"nav","title":"Łatwiejsza nawigacja","description":"Ulepszony pasek nawigacyjny, układy i przepływy"},{"icon":"cards","title":"Ulepszone zarządzanie kontem","description":"Łatwiejsze niż kiedykolwiek"},{"icon":"pallete","title":"Dostosowanie karty salda","description":"Zrób to swoje!"},{"icon":"trans","title":"Szybki podgląd transakcji","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_pt.json b/assets/new-ui/changelog/text/changelog_pt.json new file mode 100644 index 000000000..3c14c9f3b --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_pt.json @@ -0,0 +1 @@ +[{"icon":"star","title":"Novo visual","description":"Redesenhado do zero"},{"icon":"nav","title":"Navegação mais fácil","description":"Barra de navegação, layouts e fluxos aprimorados"},{"icon":"cards","title":"Gerenciamento de conta aprimorado","description":"Mais fácil do que nunca"},{"icon":"pallete","title":"Personalização do cartão de saldo","description":"Faça o seu!"},{"icon":"trans","title":"Visualização rápida de transações","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_ru.json b/assets/new-ui/changelog/text/changelog_ru.json new file mode 100644 index 000000000..6a0b87077 --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_ru.json @@ -0,0 +1 @@ +[{"icon":"star","title":"Новый облик","description":"Переработан с нуля"},{"icon":"nav","title":"Упрощенная навигация","description":"Улучшенная панель навигации, макеты и потоки."},{"icon":"cards","title":"Улучшенное управление аккаунтом","description":"Легче, чем когда-либо"},{"icon":"pallete","title":"Настройка балансовой карты","description":"Сделайте это своим!"},{"icon":"trans","title":"Быстрый просмотр транзакций","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_th.json b/assets/new-ui/changelog/text/changelog_th.json new file mode 100644 index 000000000..133a885ba --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_th.json @@ -0,0 +1 @@ +[{"icon":"star","title":"รูปลักษณ์ใหม่","description":"ออกแบบใหม่ตั้งแต่ต้นจนจบ"},{"icon":"nav","title":"การนำทางที่ง่ายขึ้น","description":"ปรับปรุงแถบนำทาง เค้าโครง และโฟลว์"},{"icon":"cards","title":"ปรับปรุงการจัดการบัญชี","description":"ง่ายกว่าที่เคย"},{"icon":"pallete","title":"การปรับแต่งการ์ดบาลานซ์","description":"ทำให้มันเป็นของคุณ!"},{"icon":"trans","title":"มุมมองธุรกรรมด่วน","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_tl.json b/assets/new-ui/changelog/text/changelog_tl.json new file mode 100644 index 000000000..45ecd1f86 --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_tl.json @@ -0,0 +1 @@ +[{"icon":"star","title":"Bagong hitsura","description":"Muling idinisenyo mula sa simula"},{"icon":"nav","title":"Mas madaling nabigasyon","description":"Pinahusay na navbar, mga layout at daloy"},{"icon":"cards","title":"Pinahusay na pamamahala ng account","description":"Mas madali kaysa dati"},{"icon":"pallete","title":"Pag-customize ng balanse ng card","description":"Gawin ito sa iyo!"},{"icon":"trans","title":"Mabilis na pagtingin sa mga transaksyon","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_tr.json b/assets/new-ui/changelog/text/changelog_tr.json new file mode 100644 index 000000000..e7172734d --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_tr.json @@ -0,0 +1 @@ +[{"icon":"star","title":"Yeni görünüm","description":"Baştan sona yeniden tasarlandı"},{"icon":"nav","title":"Daha kolay gezinme","description":"Geliştirilmiş gezinme çubuğu, düzenler ve akışlar"},{"icon":"cards","title":"Geliştirilmiş hesap yönetimi","description":"Her zamankinden daha kolay"},{"icon":"pallete","title":"Bakiye kartı özelleştirme","description":"Senin olsun!"},{"icon":"trans","title":"Hızlı işlemler görünümü","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_uk.json b/assets/new-ui/changelog/text/changelog_uk.json new file mode 100644 index 000000000..c5a80a28a --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_uk.json @@ -0,0 +1 @@ +[{"icon":"star","title":"Новий вигляд","description":"Перероблено з нуля"},{"icon":"nav","title":"Простіша навігація","description":"Покращено панель навігації, макети та потоки"},{"icon":"cards","title":"Покращене керування обліковим записом","description":"Легше, ніж будь-коли"},{"icon":"pallete","title":"Налаштування картки балансу","description":"Зробіть це своїм!"},{"icon":"trans","title":"Швидкий перегляд транзакцій","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_ur.json b/assets/new-ui/changelog/text/changelog_ur.json new file mode 100644 index 000000000..44bbbff36 --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_ur.json @@ -0,0 +1 @@ +[{"icon":"star","title":"نئی شکل","description":"زمین سے دوبارہ ڈیزائن کیا گیا۔"},{"icon":"nav","title":"آسان نیویگیشن","description":"بہتر نوبار، ترتیب اور بہاؤ"},{"icon":"cards","title":"بہتر اکاؤنٹ مینیجمنٹ","description":"پہلے سے کہیں زیادہ آسان"},{"icon":"pallete","title":"بیلنس کارڈ حسب ضرورت","description":"اسے اپنا بنائیں!"},{"icon":"trans","title":"فوری لین دین کا نظارہ","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_vi.json b/assets/new-ui/changelog/text/changelog_vi.json new file mode 100644 index 000000000..dac1545f0 --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_vi.json @@ -0,0 +1 @@ +[{"icon":"star","title":"Giao diện mới","description":"Được thiết kế lại từ đầu"},{"icon":"nav","title":"Điều hướng dễ dàng hơn","description":"Thanh điều hướng, bố cục và luồng được cải thiện"},{"icon":"cards","title":"Cải thiện quản lý tài khoản","description":"Dễ dàng hơn bao giờ hết"},{"icon":"pallete","title":"Tùy chỉnh thẻ số dư","description":"Hãy biến nó thành của bạn!"},{"icon":"trans","title":"Xem giao dịch nhanh","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_yo.json b/assets/new-ui/changelog/text/changelog_yo.json new file mode 100644 index 000000000..6744a259b --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_yo.json @@ -0,0 +1 @@ +[{"icon":"star","title":"Iwo tuntun","description":"Atunse lati ilẹ soke"},{"icon":"nav","title":"Rọrun lilọ","description":"Ilọsiwaju navbar, ipalemo ati awọn sisan"},{"icon":"cards","title":"Imudara iṣakoso akọọlẹ","description":"Rọrun ju lailai"},{"icon":"pallete","title":"Iwontunwonsi kaadi isọdi","description":"Ṣe tirẹ!"},{"icon":"trans","title":"Wiwo awọn iṣowo yarayara","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_zh-cn.json b/assets/new-ui/changelog/text/changelog_zh-cn.json new file mode 100644 index 000000000..321552a90 --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_zh-cn.json @@ -0,0 +1 @@ +[{"icon":"star","title":"新面貌","description":"从头开始重新设计"},{"icon":"nav","title":"更轻松的导航","description":"改进的导航栏、布局和流程"},{"icon":"cards","title":"改进账户管理","description":"比以往更容易"},{"icon":"pallete","title":"余额卡定制","description":"让它成为你的!"},{"icon":"trans","title":"快速交易视图","description":""}] \ No newline at end of file diff --git a/assets/new-ui/changelog/text/changelog_zh-tw.json b/assets/new-ui/changelog/text/changelog_zh-tw.json new file mode 100644 index 000000000..4e33fb9ec --- /dev/null +++ b/assets/new-ui/changelog/text/changelog_zh-tw.json @@ -0,0 +1 @@ +[{"icon":"star","title":"新面貌","description":"從頭開始重新設計"},{"icon":"nav","title":"更輕鬆的導航","description":"改進的導覽列、佈局和流程"},{"icon":"cards","title":"改進帳戶管理","description":"比以往更容易"},{"icon":"pallete","title":"餘額卡定制","description":"讓它成為你的!"},{"icon":"trans","title":"快速交易視圖","description":""}] \ No newline at end of file diff --git a/assets/new-ui/checkmark.svg b/assets/new-ui/checkmark.svg new file mode 100644 index 000000000..84a7a26bf --- /dev/null +++ b/assets/new-ui/checkmark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/new-ui/chooser.svg b/assets/new-ui/chooser.svg new file mode 100644 index 000000000..ca5c07d48 --- /dev/null +++ b/assets/new-ui/chooser.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/new-ui/contacts_outlined.svg b/assets/new-ui/contacts_outlined.svg new file mode 100644 index 000000000..9d2dfea28 --- /dev/null +++ b/assets/new-ui/contacts_outlined.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/copy-icon.svg b/assets/new-ui/copy-icon.svg new file mode 100644 index 000000000..43df029ef --- /dev/null +++ b/assets/new-ui/copy-icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/assets/new-ui/copy.svg b/assets/new-ui/copy.svg new file mode 100644 index 000000000..1a92a5234 --- /dev/null +++ b/assets/new-ui/copy.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/new-ui/crypto-minimal-icons/Monero.svg b/assets/new-ui/crypto-minimal-icons/Monero.svg new file mode 100644 index 000000000..c88c3ff93 --- /dev/null +++ b/assets/new-ui/crypto-minimal-icons/Monero.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/dropdown_arrow.svg b/assets/new-ui/dropdown_arrow.svg new file mode 100644 index 000000000..2d5da041a --- /dev/null +++ b/assets/new-ui/dropdown_arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/exchange.svg b/assets/new-ui/exchange.svg new file mode 100644 index 000000000..0bf048de8 --- /dev/null +++ b/assets/new-ui/exchange.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/new-ui/exchange_providers.svg b/assets/new-ui/exchange_providers.svg new file mode 100644 index 000000000..296e68d2a --- /dev/null +++ b/assets/new-ui/exchange_providers.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + diff --git a/assets/new-ui/frozen.svg b/assets/new-ui/frozen.svg new file mode 100644 index 000000000..3c88fe62a --- /dev/null +++ b/assets/new-ui/frozen.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/help.svg b/assets/new-ui/help.svg new file mode 100644 index 000000000..080ec86c1 --- /dev/null +++ b/assets/new-ui/help.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/hide_keyboard.svg b/assets/new-ui/hide_keyboard.svg new file mode 100644 index 000000000..8e3eacda7 --- /dev/null +++ b/assets/new-ui/hide_keyboard.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/history-received.svg b/assets/new-ui/history-received.svg new file mode 100644 index 000000000..bb02c1714 --- /dev/null +++ b/assets/new-ui/history-received.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/new-ui/history-receiving.svg b/assets/new-ui/history-receiving.svg new file mode 100644 index 000000000..cec2958dc --- /dev/null +++ b/assets/new-ui/history-receiving.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/history-sending.svg b/assets/new-ui/history-sending.svg new file mode 100644 index 000000000..f40ec89a0 --- /dev/null +++ b/assets/new-ui/history-sending.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/history-sent.svg b/assets/new-ui/history-sent.svg new file mode 100644 index 000000000..e1635e38b --- /dev/null +++ b/assets/new-ui/history-sent.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/new-ui/icon-accounts.svg b/assets/new-ui/icon-accounts.svg new file mode 100644 index 000000000..d7ccfc273 --- /dev/null +++ b/assets/new-ui/icon-accounts.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/new-ui/info.svg b/assets/new-ui/info.svg new file mode 100644 index 000000000..945d67e52 --- /dev/null +++ b/assets/new-ui/info.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/label.svg b/assets/new-ui/label.svg new file mode 100644 index 000000000..9354d82fd --- /dev/null +++ b/assets/new-ui/label.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/lightning.svg b/assets/new-ui/lightning.svg new file mode 100644 index 000000000..9788dcc17 --- /dev/null +++ b/assets/new-ui/lightning.svg @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/assets/new-ui/lightning_deposit_help.svg b/assets/new-ui/lightning_deposit_help.svg new file mode 100644 index 000000000..f2d078adb --- /dev/null +++ b/assets/new-ui/lightning_deposit_help.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/lightning_username_setup.svg b/assets/new-ui/lightning_username_setup.svg new file mode 100644 index 000000000..e3a40e8ea --- /dev/null +++ b/assets/new-ui/lightning_username_setup.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/lightning_username_setup_light.svg b/assets/new-ui/lightning_username_setup_light.svg new file mode 100644 index 000000000..0f303b2b9 --- /dev/null +++ b/assets/new-ui/lightning_username_setup_light.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/lightning_withdraw_help.svg b/assets/new-ui/lightning_withdraw_help.svg new file mode 100644 index 000000000..5307562b5 --- /dev/null +++ b/assets/new-ui/lightning_withdraw_help.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/link_arrow.svg b/assets/new-ui/link_arrow.svg new file mode 100644 index 000000000..feb8a6079 --- /dev/null +++ b/assets/new-ui/link_arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/mask.svg b/assets/new-ui/mask.svg new file mode 100644 index 000000000..0c6979cfc --- /dev/null +++ b/assets/new-ui/mask.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/mweb_help.svg b/assets/new-ui/mweb_help.svg new file mode 100644 index 000000000..63201d0ca --- /dev/null +++ b/assets/new-ui/mweb_help.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/mweb_sync.svg b/assets/new-ui/mweb_sync.svg new file mode 100644 index 000000000..ef5962723 --- /dev/null +++ b/assets/new-ui/mweb_sync.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/new-ui/navbar/apps.svg b/assets/new-ui/navbar/apps.svg new file mode 100644 index 000000000..1ee1ce2d1 --- /dev/null +++ b/assets/new-ui/navbar/apps.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/navbar/charts.svg b/assets/new-ui/navbar/charts.svg new file mode 100644 index 000000000..cc86078e6 --- /dev/null +++ b/assets/new-ui/navbar/charts.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/new-ui/navbar/contacts.svg b/assets/new-ui/navbar/contacts.svg new file mode 100644 index 000000000..855cfed4c --- /dev/null +++ b/assets/new-ui/navbar/contacts.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/navbar/home.svg b/assets/new-ui/navbar/home.svg new file mode 100644 index 000000000..1b9b7a559 --- /dev/null +++ b/assets/new-ui/navbar/home.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/navbar/wallets.svg b/assets/new-ui/navbar/wallets.svg new file mode 100644 index 000000000..1754984fa --- /dev/null +++ b/assets/new-ui/navbar/wallets.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/offline.svg b/assets/new-ui/offline.svg new file mode 100644 index 000000000..bf4b82b7a --- /dev/null +++ b/assets/new-ui/offline.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/new-ui/options.svg b/assets/new-ui/options.svg new file mode 100644 index 000000000..82026cdc4 --- /dev/null +++ b/assets/new-ui/options.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/options_slider.svg b/assets/new-ui/options_slider.svg new file mode 100644 index 000000000..85a9e6b91 --- /dev/null +++ b/assets/new-ui/options_slider.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/paste.svg b/assets/new-ui/paste.svg new file mode 100644 index 000000000..03d177321 --- /dev/null +++ b/assets/new-ui/paste.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/payjoin.svg b/assets/new-ui/payjoin.svg new file mode 100644 index 000000000..18af49edf --- /dev/null +++ b/assets/new-ui/payjoin.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/pjmodal_btc.svg b/assets/new-ui/pjmodal_btc.svg new file mode 100644 index 000000000..4de18fbd5 --- /dev/null +++ b/assets/new-ui/pjmodal_btc.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/new-ui/pjmodal_pj.svg b/assets/new-ui/pjmodal_pj.svg new file mode 100644 index 000000000..dc9a27413 --- /dev/null +++ b/assets/new-ui/pjmodal_pj.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/new-ui/randomize.svg b/assets/new-ui/randomize.svg new file mode 100644 index 000000000..f5e6375c9 --- /dev/null +++ b/assets/new-ui/randomize.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/receive.svg b/assets/new-ui/receive.svg new file mode 100644 index 000000000..fc420ad69 --- /dev/null +++ b/assets/new-ui/receive.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/new-ui/refund_address.svg b/assets/new-ui/refund_address.svg new file mode 100644 index 000000000..6e9ac86af --- /dev/null +++ b/assets/new-ui/refund_address.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/remove_recipient.svg b/assets/new-ui/remove_recipient.svg new file mode 100644 index 000000000..fbbd992c9 --- /dev/null +++ b/assets/new-ui/remove_recipient.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/scan.svg b/assets/new-ui/scan.svg new file mode 100644 index 000000000..9e19071ce --- /dev/null +++ b/assets/new-ui/scan.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/new-ui/select_wallet.svg b/assets/new-ui/select_wallet.svg new file mode 100644 index 000000000..f0198b7a2 --- /dev/null +++ b/assets/new-ui/select_wallet.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/send.svg b/assets/new-ui/send.svg new file mode 100644 index 000000000..cde7609f6 --- /dev/null +++ b/assets/new-ui/send.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/send_from_external.svg b/assets/new-ui/send_from_external.svg new file mode 100644 index 000000000..3e1d4a99b --- /dev/null +++ b/assets/new-ui/send_from_external.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/set-amount.svg b/assets/new-ui/set-amount.svg new file mode 100644 index 000000000..d45a9e220 --- /dev/null +++ b/assets/new-ui/set-amount.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/settings.png b/assets/new-ui/settings.png new file mode 100644 index 000000000..b6cfb5b08 Binary files /dev/null and b/assets/new-ui/settings.png differ diff --git a/assets/new-ui/settings_row_icons/backup.svg b/assets/new-ui/settings_row_icons/backup.svg new file mode 100644 index 000000000..93a642726 --- /dev/null +++ b/assets/new-ui/settings_row_icons/backup.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/settings_row_icons/coin-control.svg b/assets/new-ui/settings_row_icons/coin-control.svg new file mode 100644 index 000000000..f3b2f0eaf --- /dev/null +++ b/assets/new-ui/settings_row_icons/coin-control.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/new-ui/settings_row_icons/connections.svg b/assets/new-ui/settings_row_icons/connections.svg new file mode 100644 index 000000000..5dd144c56 --- /dev/null +++ b/assets/new-ui/settings_row_icons/connections.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/settings_row_icons/cupcake.svg b/assets/new-ui/settings_row_icons/cupcake.svg new file mode 100644 index 000000000..b4b37263d --- /dev/null +++ b/assets/new-ui/settings_row_icons/cupcake.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/settings_row_icons/defaults.svg b/assets/new-ui/settings_row_icons/defaults.svg new file mode 100644 index 000000000..5faa39cce --- /dev/null +++ b/assets/new-ui/settings_row_icons/defaults.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/new-ui/settings_row_icons/display.svg b/assets/new-ui/settings_row_icons/display.svg new file mode 100644 index 000000000..5d81c899b --- /dev/null +++ b/assets/new-ui/settings_row_icons/display.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/settings_row_icons/info.svg b/assets/new-ui/settings_row_icons/info.svg new file mode 100644 index 000000000..8645ccb97 --- /dev/null +++ b/assets/new-ui/settings_row_icons/info.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/settings_row_icons/lightning_username.svg b/assets/new-ui/settings_row_icons/lightning_username.svg new file mode 100644 index 000000000..e0c34d1e4 --- /dev/null +++ b/assets/new-ui/settings_row_icons/lightning_username.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/settings_row_icons/mweb.svg b/assets/new-ui/settings_row_icons/mweb.svg new file mode 100644 index 000000000..8aaf50286 --- /dev/null +++ b/assets/new-ui/settings_row_icons/mweb.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/settings_row_icons/nodes.svg b/assets/new-ui/settings_row_icons/nodes.svg new file mode 100644 index 000000000..99c6ce0de --- /dev/null +++ b/assets/new-ui/settings_row_icons/nodes.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/settings_row_icons/other.svg b/assets/new-ui/settings_row_icons/other.svg new file mode 100644 index 000000000..fa353b578 --- /dev/null +++ b/assets/new-ui/settings_row_icons/other.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/settings_row_icons/payjoin.svg b/assets/new-ui/settings_row_icons/payjoin.svg new file mode 100644 index 000000000..bb586f767 --- /dev/null +++ b/assets/new-ui/settings_row_icons/payjoin.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/settings_row_icons/privacy.svg b/assets/new-ui/settings_row_icons/privacy.svg new file mode 100644 index 000000000..763bfbaf3 --- /dev/null +++ b/assets/new-ui/settings_row_icons/privacy.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/settings_row_icons/security.svg b/assets/new-ui/settings_row_icons/security.svg new file mode 100644 index 000000000..994f17f25 --- /dev/null +++ b/assets/new-ui/settings_row_icons/security.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/settings_row_icons/seed.svg b/assets/new-ui/settings_row_icons/seed.svg new file mode 100644 index 000000000..a3a97efbf --- /dev/null +++ b/assets/new-ui/settings_row_icons/seed.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/settings_row_icons/silent-payments.svg b/assets/new-ui/settings_row_icons/silent-payments.svg new file mode 100644 index 000000000..a4f786ac1 --- /dev/null +++ b/assets/new-ui/settings_row_icons/silent-payments.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/new-ui/settings_row_icons/support.svg b/assets/new-ui/settings_row_icons/support.svg new file mode 100644 index 000000000..c18a590e5 --- /dev/null +++ b/assets/new-ui/settings_row_icons/support.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/silent_sync.svg b/assets/new-ui/silent_sync.svg new file mode 100644 index 000000000..8b1e9df7f --- /dev/null +++ b/assets/new-ui/silent_sync.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/swap_amounts.svg b/assets/new-ui/swap_amounts.svg new file mode 100644 index 000000000..70178be50 --- /dev/null +++ b/assets/new-ui/swap_amounts.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/switch.svg b/assets/new-ui/switch.svg new file mode 100644 index 000000000..5c07ad628 --- /dev/null +++ b/assets/new-ui/switch.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/new-ui/switcher-bitcoin.svg b/assets/new-ui/switcher-bitcoin.svg new file mode 100644 index 000000000..d529c77e2 --- /dev/null +++ b/assets/new-ui/switcher-bitcoin.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/switcher-lightning.svg b/assets/new-ui/switcher-lightning.svg new file mode 100644 index 000000000..f3816e6b0 --- /dev/null +++ b/assets/new-ui/switcher-lightning.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/token-received.svg b/assets/new-ui/token-received.svg new file mode 100644 index 000000000..66e349f7d --- /dev/null +++ b/assets/new-ui/token-received.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/new-ui/token-sent.svg b/assets/new-ui/token-sent.svg new file mode 100644 index 000000000..414086dd0 --- /dev/null +++ b/assets/new-ui/token-sent.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/new-ui/top-settings.svg b/assets/new-ui/top-settings.svg new file mode 100644 index 000000000..ba716f8d5 --- /dev/null +++ b/assets/new-ui/top-settings.svg @@ -0,0 +1,5 @@ + + + diff --git a/assets/new-ui/tor.svg b/assets/new-ui/tor.svg new file mode 100644 index 000000000..71c681363 --- /dev/null +++ b/assets/new-ui/tor.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/tor_sync.svg b/assets/new-ui/tor_sync.svg new file mode 100644 index 000000000..742f82560 --- /dev/null +++ b/assets/new-ui/tor_sync.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/trade_providers/chainflip.svg b/assets/new-ui/trade_providers/chainflip.svg new file mode 100644 index 000000000..02d7b2683 --- /dev/null +++ b/assets/new-ui/trade_providers/chainflip.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/trade_providers/changenow.svg b/assets/new-ui/trade_providers/changenow.svg new file mode 100644 index 000000000..a10edce47 --- /dev/null +++ b/assets/new-ui/trade_providers/changenow.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/trade_providers/exolix.svg b/assets/new-ui/trade_providers/exolix.svg new file mode 100644 index 000000000..c4a817faa --- /dev/null +++ b/assets/new-ui/trade_providers/exolix.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/trade_providers/jupiter.svg b/assets/new-ui/trade_providers/jupiter.svg new file mode 100644 index 000000000..5cf8e78f3 --- /dev/null +++ b/assets/new-ui/trade_providers/jupiter.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/trade_providers/letsexchange.svg b/assets/new-ui/trade_providers/letsexchange.svg new file mode 100644 index 000000000..fad737e94 --- /dev/null +++ b/assets/new-ui/trade_providers/letsexchange.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/new-ui/trade_providers/near-intents.svg b/assets/new-ui/trade_providers/near-intents.svg new file mode 100644 index 000000000..295576ccb --- /dev/null +++ b/assets/new-ui/trade_providers/near-intents.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/new-ui/trade_providers/sideshift.svg b/assets/new-ui/trade_providers/sideshift.svg new file mode 100644 index 000000000..c4837270b --- /dev/null +++ b/assets/new-ui/trade_providers/sideshift.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/new-ui/trade_providers/simpleswap.svg b/assets/new-ui/trade_providers/simpleswap.svg new file mode 100644 index 000000000..42fb9708c --- /dev/null +++ b/assets/new-ui/trade_providers/simpleswap.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/new-ui/trade_providers/stealthex.svg b/assets/new-ui/trade_providers/stealthex.svg new file mode 100644 index 000000000..67201413e --- /dev/null +++ b/assets/new-ui/trade_providers/stealthex.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/trade_providers/swaps_xyz.svg b/assets/new-ui/trade_providers/swaps_xyz.svg new file mode 100644 index 000000000..e0daf3c7b --- /dev/null +++ b/assets/new-ui/trade_providers/swaps_xyz.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/trade_providers/swaptrade.svg b/assets/new-ui/trade_providers/swaptrade.svg new file mode 100644 index 000000000..1755e6e63 --- /dev/null +++ b/assets/new-ui/trade_providers/swaptrade.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/trade_providers/thorchain.svg b/assets/new-ui/trade_providers/thorchain.svg new file mode 100644 index 000000000..2b382620a --- /dev/null +++ b/assets/new-ui/trade_providers/thorchain.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/trade_providers/trocador.svg b/assets/new-ui/trade_providers/trocador.svg new file mode 100644 index 000000000..f51c6edb7 --- /dev/null +++ b/assets/new-ui/trade_providers/trocador.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/new-ui/trade_providers/uniswap.svg b/assets/new-ui/trade_providers/uniswap.svg new file mode 100644 index 000000000..a28decebb --- /dev/null +++ b/assets/new-ui/trade_providers/uniswap.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/trade_providers/xoswap.svg b/assets/new-ui/trade_providers/xoswap.svg new file mode 100644 index 000000000..22aab16b8 --- /dev/null +++ b/assets/new-ui/trade_providers/xoswap.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/new-ui/unmask.svg b/assets/new-ui/unmask.svg new file mode 100644 index 000000000..71e4c9ccf --- /dev/null +++ b/assets/new-ui/unmask.svg @@ -0,0 +1,5 @@ + + + diff --git a/assets/new-ui/wallet-trezor.svg b/assets/new-ui/wallet-trezor.svg new file mode 100644 index 000000000..d0747c444 --- /dev/null +++ b/assets/new-ui/wallet-trezor.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/new-ui/wallet_settings.svg b/assets/new-ui/wallet_settings.svg new file mode 100644 index 000000000..c5ff54e96 --- /dev/null +++ b/assets/new-ui/wallet_settings.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/new-ui/warning.svg b/assets/new-ui/warning.svg new file mode 100644 index 000000000..e86a57689 --- /dev/null +++ b/assets/new-ui/warning.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index fd5850ef8..fb15ed987 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,4 +1,2 @@ -Add BNB Smart Chain -Zcash improvements -EVM chains enhancements -Bug fixes \ No newline at end of file +Add support for Bitcoin Lightning +New Design diff --git a/cw_bitcoin/lib/bitcoin_receive_page_option.dart b/cw_bitcoin/lib/bitcoin_receive_page_option.dart index 8491ae8e3..7fe2f8e75 100644 --- a/cw_bitcoin/lib/bitcoin_receive_page_option.dart +++ b/cw_bitcoin/lib/bitcoin_receive_page_option.dart @@ -1,25 +1,51 @@ import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_bitcoin/lightning/lightning_addres_type.dart'; import 'package:cw_core/receive_page_option.dart'; class BitcoinReceivePageOption implements ReceivePageOption { - static const p2wpkh = BitcoinReceivePageOption._('Segwit (P2WPKH) (Default)'); - static const p2sh = BitcoinReceivePageOption._('Segwit-Compatible (P2SH)'); - static const p2tr = BitcoinReceivePageOption._('Taproot (P2TR)'); - static const p2wsh = BitcoinReceivePageOption._('Segwit (P2WSH)'); - static const p2pkh = BitcoinReceivePageOption._('Legacy (P2PKH)'); - static const mweb = BitcoinReceivePageOption._('MWEB'); + static const p2wpkh = BitcoinReceivePageOption._('Standard', + description: "Default (P2WPKH)", + iconPath: "assets/new-ui/address-type-picker-icons/btc_standard.svg", + isCommon: true, + addAddressWord: true); + static const p2sh = BitcoinReceivePageOption._('Segwit-Compatible', + description: "P2SH", iconPath: "assets/new-ui/address-type-picker-icons/segwit.svg"); + static const p2tr = BitcoinReceivePageOption._('Taproot', + description: "P2TR", iconPath: "assets/new-ui/address-type-picker-icons/taproot.svg"); + static const p2wsh = BitcoinReceivePageOption._('Segwit', + description: "P2WSH", iconPath: "assets/new-ui/address-type-picker-icons/segwit.svg"); + static const p2pkh = BitcoinReceivePageOption._('Legacy', + description: "P2PKH", iconPath: "assets/new-ui/address-type-picker-icons/legacy.svg"); + static const mweb = BitcoinReceivePageOption._('Private', + description: "MWEB", + iconPath: "assets/new-ui/address-type-picker-icons/mweb.svg", + isCommon: true, + addAddressWord: true); - static const silent_payments = BitcoinReceivePageOption._('Silent Payments'); + static const silent_payments = BitcoinReceivePageOption._('Silent Payments', + description: "Privacy-preserving static address", + iconPath: "assets/new-ui/address-type-picker-icons/silent.svg", + isCommon: true); + static const lightning = BitcoinReceivePageOption._('Lightning', + description: "Instant, low fee payments", + iconPath: "assets/new-ui/address-type-picker-icons/btc_lightning.svg", + isCommon: true); - const BitcoinReceivePageOption._(this.value); + const BitcoinReceivePageOption._(this.value, + {this.iconPath, this.description, this.isCommon = false, this.addAddressWord = false}); final String value; + final String? iconPath; + final String? description; + final bool isCommon; + final bool addAddressWord; String toString() { return value; } static const all = [ + BitcoinReceivePageOption.lightning, BitcoinReceivePageOption.silent_payments, BitcoinReceivePageOption.p2wpkh, BitcoinReceivePageOption.p2tr, @@ -42,7 +68,7 @@ class BitcoinReceivePageOption implements ReceivePageOption { BitcoinReceivePageOption.p2wpkh, BitcoinReceivePageOption.mweb, ]; - + BitcoinAddressType toType() { switch (this) { case BitcoinReceivePageOption.p2tr: @@ -55,6 +81,8 @@ class BitcoinReceivePageOption implements ReceivePageOption { return P2shAddressType.p2wpkhInP2sh; case BitcoinReceivePageOption.silent_payments: return SilentPaymentsAddresType.p2sp; + case BitcoinReceivePageOption.lightning: + return LightningAddressType.p2l; case BitcoinReceivePageOption.mweb: return SegwitAddresType.mweb; case BitcoinReceivePageOption.p2wpkh: @@ -77,6 +105,8 @@ class BitcoinReceivePageOption implements ReceivePageOption { return BitcoinReceivePageOption.p2sh; case SilentPaymentsAddresType.p2sp: return BitcoinReceivePageOption.silent_payments; + case LightningAddressType.p2l: + return BitcoinReceivePageOption.lightning; case SegwitAddresType.p2wpkh: default: return BitcoinReceivePageOption.p2wpkh; diff --git a/cw_bitcoin/lib/bitcoin_transaction_priority.dart b/cw_bitcoin/lib/bitcoin_transaction_priority.dart index d93070a78..46e4ca8e4 100644 --- a/cw_bitcoin/lib/bitcoin_transaction_priority.dart +++ b/cw_bitcoin/lib/bitcoin_transaction_priority.dart @@ -2,16 +2,16 @@ import 'package:cw_core/transaction_priority.dart'; import 'package:flutter/foundation.dart'; class BitcoinTransactionPriority extends TransactionPriority { - const BitcoinTransactionPriority({required String title, required int raw}) - : super(title: title, raw: raw); + const BitcoinTransactionPriority({required String title, required int raw, String? description, String? hint}) + : super(title: title, raw: raw, description: description, hint: hint); static const List all = [fast, medium, slow, custom]; static const BitcoinTransactionPriority slow = - BitcoinTransactionPriority(title: 'Slow', raw: 0); + BitcoinTransactionPriority(title: 'Slow', description: "2 sat/byte", hint: "~ 24 h", raw: 0); static const BitcoinTransactionPriority medium = - BitcoinTransactionPriority(title: 'Medium', raw: 1); + BitcoinTransactionPriority(title: 'Medium', description: "3 sat/byte", hint: "~ 1 h", raw: 1); static const BitcoinTransactionPriority fast = - BitcoinTransactionPriority(title: 'Fast', raw: 2); + BitcoinTransactionPriority(title: 'Fast', description: "4 sat/byte", hint: "~ 30 min", raw: 2); static const BitcoinTransactionPriority custom = BitcoinTransactionPriority(title: 'Custom', raw: 3); diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 495357425..ffb86a25b 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:bip39/bip39.dart' as bip39; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_bitcoin/.secrets.g.dart' as secrets; import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; @@ -10,9 +11,11 @@ import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_derivations.dart'; +import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; import 'package:cw_bitcoin/hardware/bitcoin_hardware_wallet_service.dart'; +import 'package:cw_bitcoin/lightning/lightning_wallet.dart'; import 'package:cw_bitcoin/hardware/bitcoin_ledger_service.dart'; import 'package:cw_bitcoin/payjoin/manager.dart'; import 'package:cw_bitcoin/payjoin/storage.dart'; @@ -24,9 +27,12 @@ import 'package:cw_bitcoin/psbt/v0_finalizer.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/output_info.dart'; +import 'package:cw_core/parse_fixed.dart'; import 'package:cw_core/payjoin_session.dart'; import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/utils/zpub.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; @@ -57,12 +63,15 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { BasedUtxoNetwork? networkParam, List? initialAddresses, ElectrumBalance? initialBalance, + ElectrumBalance? initialLightningBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, String? passphrase, List? initialSilentAddresses, int initialSilentAddressIndex = 0, bool? alwaysScan, + bool? useLightning, + String? cachedLightningAddress, }) : super( mnemonic: mnemonic, passphrase: passphrase, @@ -80,10 +89,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialBalance: initialBalance, seedBytes: seedBytes, encryptionFileUtils: encryptionFileUtils, - currency: networkParam == BitcoinNetwork.testnet - ? CryptoCurrency.tbtc - : CryptoCurrency.btc, + currency: + networkParam == BitcoinNetwork.testnet ? CryptoCurrency.tbtc : CryptoCurrency.btc, alwaysScan: alwaysScan, + useLightning: useLightning ?? true, ) { // in a standard BIP44 wallet, mainHd derivation path = m/84'/0'/0'/0 (account 0, index unspecified here) // the sideHd derivation path = m/84'/0'/0'/1 (account 1, index unspecified here) @@ -91,25 +100,67 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { // String sideDerivationPath = derivationPath.substring(0, derivationPath.length - 1) + "1"; // final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType); - payjoinManager = PayjoinManager(PayjoinStorage(payjoinBox), this); - walletAddresses = BitcoinWalletAddresses(walletInfo, - initialAddresses: initialAddresses, - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex, - initialSilentAddresses: initialSilentAddresses, - initialSilentAddressIndex: initialSilentAddressIndex, - mainHd: hd, - sideHd: accountHD.childKey(Bip32KeyIndex(1)), - network: networkParam ?? network, - masterHd: - seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null, - isHardwareWallet: walletInfo.isHardwareWallet, - payjoinManager: payjoinManager); + if (mnemonic != null && this.useLightning) { + try { + lightningWallet = LightningWallet( + mnemonic: mnemonic, + passphrase: passphrase, + seedBytes: seedBytes, + apiKey: secrets.breezApiKey, + lnurlDomain: "cake.cash", + cachedAddress: cachedLightningAddress, + ); + } catch (e) { + printV(e); + lightningWallet = null; + } + } else { + lightningWallet = null; + } + payjoinManager = PayjoinManager(PayjoinStorage(payjoinBox), this); + walletAddresses = BitcoinWalletAddresses( + walletInfo, + initialAddresses: initialAddresses, + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex, + initialSilentAddresses: initialSilentAddresses, + initialSilentAddressIndex: initialSilentAddressIndex, + mainHd: hd, + sideHd: accountHD.childKey(Bip32KeyIndex(1)), + network: networkParam ?? network, + masterHd: seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null, + isHardwareWallet: walletInfo.isHardwareWallet, + payjoinManager: payjoinManager, + lightningWallet: lightningWallet, + ); + + if (lightningWallet != null) { + walletAddresses.setLightningAddress(walletInfo.name); + } autorun((_) { - this.walletAddresses.isEnabledAutoGenerateSubaddress = - this.isEnabledAutoGenerateSubaddress; + this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; }); + + reaction((_) => this.useLightning, (bool useLightning) { + if (useLightning) { + if (mnemonic != null) { + lightningWallet = LightningWallet( + mnemonic: mnemonic, + passphrase: passphrase, + seedBytes: seedBytes, + apiKey: secrets.breezApiKey, + lnurlDomain: "cake.cash", + ); + } + } else { + lightningWallet = null; + } + }); + + if (initialLightningBalance != null) { + balance[CryptoCurrency.btcln] = initialLightningBalance; + } } @override @@ -145,8 +196,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { break; case DerivationType.electrum: default: - seedBytes = - await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); + seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); break; } @@ -168,6 +218,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { addressPageType: addressPageType, networkParam: network, payjoinBox: payjoinBox, + useLightning: true, ); } @@ -219,10 +270,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { final derivationInfo = await walletInfo.getDerivationInfo(); // set the default if not present: - derivationInfo.derivationPath ??= - snp?.derivationPath ?? electrum_path; - derivationInfo.derivationType ??= - snp?.derivationType ?? DerivationType.electrum; + derivationInfo.derivationPath ??= snp?.derivationPath ?? electrum_path; + derivationInfo.derivationType ??= snp?.derivationType ?? DerivationType.electrum; if (derivationInfo.derivationType == DerivationType.unknown) { if (snp?.derivationPath == electrum_path || snp?.derivationType == DerivationType.electrum) { derivationInfo.derivationPath = electrum_path; @@ -241,8 +290,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { if (mnemonic != null) { switch (derivationInfo.derivationType) { case DerivationType.electrum: - seedBytes = - await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); + seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); break; case DerivationType.bip39: default: @@ -266,6 +314,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialSilentAddresses: snp?.silentAddresses, initialSilentAddressIndex: snp?.silentAddressIndex ?? 0, initialBalance: snp?.balance, + initialLightningBalance: snp?.lightningBalance, encryptionFileUtils: encryptionFileUtils, seedBytes: seedBytes, initialRegularAddressIndex: snp?.regularAddressIndex, @@ -273,20 +322,79 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { addressPageType: snp?.addressPageType, networkParam: network, alwaysScan: snp?.alwaysScan, - payjoinBox: payjoinBox); + useLightning: snp?.useLightning, + cachedLightningAddress: snp?.cachedLightningAddress, + payjoinBox: payjoinBox, + ); } @override Future close({bool shouldCleanup = false}) async { payjoinManager.cleanupSessions(); + await lightningWallet?.close(); super.close(shouldCleanup: shouldCleanup); } + @override + Future fetchBalances() async { + final balance = await super.fetchBalances(); + if (lightningWallet == null) { + return balance; + } + + final lBalance = await lightningWallet!.getBalance(); + + this.balance[CryptoCurrency.btcln] = + ElectrumBalance(confirmed: lBalance.toInt(), unconfirmed: 0, frozen: 0); + + return ElectrumBalance( + confirmed: balance.confirmed, + unconfirmed: balance.unconfirmed, + frozen: balance.frozen.toInt(), + ); + } + + @override + @action + Future subscribeForUpdates() async { + if (lightningWallet != null) { + lightningWallet!.setEventListener( + onTransactionEvent: (tx) async { + if (transactionHistory.transactions[tx.id]?.isPending != tx.isPending) { + transactionHistory.addOne(tx); + await transactionHistory.save(); + await fetchBalances(); + } + }, + onBalanceChangedEvent: fetchBalances, + ); + } + + return super.subscribeForUpdates(); + } + + @override + Future> fetchTransactions() async { + if (lightningWallet != null) { + final existingTx = transactionHistory.transactions.values + .where((e) => (e.additionalInfo["isLightning"] as bool?) == true) + .lastOrNull; + + lightningWallet!.getTransactionHistory(fromDate: existingTx?.date).then((lnHistory) async { + transactionHistory.addMany(lnHistory); + await transactionHistory.save(); + }).onError((_, __) {}); + } + + return super.fetchTransactions(); + } + + late LightningWallet? lightningWallet; + late final PayjoinManager payjoinManager; bool get isPayjoinAvailable => unspentCoinsInfo.values - .where((element) => - element.walletId == id && element.isSending && !element.isFrozen) + .where((element) => element.walletId == id && element.isSending && !element.isFrozen) .isNotEmpty; Future buildPsbt({ @@ -304,10 +412,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { }) async { final psbtReadyInputs = []; for (final utxo in utxos) { - final rawTx = - await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); - final publicKeyAndDerivationPath = - publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; + final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); + final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; psbtReadyInputs.add(PSBTReadyUtxoWithAddress( utxo: utxo.utxo, @@ -367,9 +473,28 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { @override Future createTransaction(Object credentials) async { credentials = credentials as BitcoinTransactionCredentials; + final lnAddr = credentials.outputs.first.isParsedAddress ? credentials.outputs.first.extractedAddress! : credentials.outputs.first.address; - final tx = (await super.createTransaction(credentials)) - as PendingBitcoinTransaction; + final isLNCompatible = await lightningWallet?.isCompatible(lnAddr); + if ((credentials.coinTypeToSpendFrom == UnspentCoinType.lightning && lightningWallet != null) || + isLNCompatible == true) { + + BigInt amount; + if (credentials.outputs.first.sendAll) { + amount = (await lightningWallet!.getBalance()) - BigInt.from(10); + } else { + amount = parseFixed( + credentials.outputs.first.cryptoAmount?.isNotEmpty == true + ? credentials.outputs.first.cryptoAmount! + : "0", + 8); + } + + return lightningWallet!.createTransaction(lnAddr, + amount > BigInt.zero ? amount : null, credentials.priority); + } + + final tx = (await super.createTransaction(credentials)) as PendingBitcoinTransaction; final payjoinUri = credentials.payjoinUri; if (payjoinUri == null && !tx.shouldCommitUR()) return tx; @@ -394,8 +519,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { masterFingerprint: Uint8List.fromList([0, 0, 0, 0])); if (tx.shouldCommitUR()) { - tx.unsignedPsbt = transaction.asPsbtV0(); - return tx; + tx.unsignedPsbt = transaction.asPsbtV0(); + return tx; } final originalPsbt = @@ -419,8 +544,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Future commitPsbt(String finalizedPsbt) { final psbt = PsbtV2()..deserializeV0(base64.decode(finalizedPsbt)); - final btcTx = - BtcTransaction.fromRaw(BytesUtils.toHexString(psbt.extract())); + final btcTx = BtcTransaction.fromRaw(BytesUtils.toHexString(psbt.extract())); return PendingBitcoinTransaction( btcTx, @@ -435,8 +559,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { ).commit(); } - Future signPsbt( - String preProcessedPsbt, List utxos) async { + Future signPsbt(String preProcessedPsbt, List utxos) async { final psbt = PsbtV2()..deserializeV0(base64Decode(preProcessedPsbt)); await psbt.signWithUTXO(utxos, (txDigest, utxo, key, sighash) { @@ -499,15 +622,13 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Future signMessage(String message, {String? address = null}) async { if (walletInfo.isHardwareWallet) { final addressEntry = address != null - ? walletAddresses.allAddresses - .firstWhere((element) => element.address == address) + ? walletAddresses.allAddresses.firstWhere((element) => element.address == address) : null; final index = addressEntry?.index ?? 0; final isChange = addressEntry?.isHidden == true ? 1 : 0; final derivationInfo = await walletInfo.getDerivationInfo(); final accountPath = derivationInfo.derivationPath; - final derivationPath = - accountPath != null ? "$accountPath/$isChange/$index" : null; + final derivationPath = accountPath != null ? "$accountPath/$isChange/$index" : null; final signature = await hardwareWalletService! .signMessage(message: ascii.encode(message), derivationPath: derivationPath); diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index fcd0b7d8c..5d051827a 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -1,14 +1,21 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/bip/bip/bip32/bip32.dart'; +import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; +import 'package:cw_bitcoin/lightning/lightning_addres_type.dart'; import 'package:cw_bitcoin/payjoin/manager.dart'; import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/parse_fixed.dart'; +import 'package:cw_core/payment_uris.dart'; +import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; import 'package:payjoin_flutter/receive.dart' as payjoin; +import 'lightning/utils.dart'; + part 'bitcoin_wallet_addresses.g.dart'; class BitcoinWalletAddresses = BitcoinWalletAddressesBase with _$BitcoinWalletAddresses; @@ -27,6 +34,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S super.initialSilentAddresses, super.initialSilentAddressIndex = 0, super.masterHd, + super.lightningWallet, }) : super(walletInfo); final PayjoinManager payjoinManager; @@ -88,4 +96,66 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S if (!_isPayjoinConnectivityError(e.toString())) rethrow; } } + + @override + List get receivePageOptions { + if (isHardwareWallet) { + return [ + ...BitcoinReceivePageOption.allViewOnly, + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ]; + } + return [ + ...BitcoinReceivePageOption.all, + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ]; + } + + @override + PaymentURI getPaymentUri(String amount) { + if (addressPageType is LightningAddressType && lightningWallet != null) { + final lnUrl = getLnurlOfLightningAddress(address); + return LightningPaymentRequest(address: address, lnURL: lnUrl, amount: amount); + } + return BitcoinURI(address: address, amount: amount, pjUri: payjoinEndpoint ?? ''); + } + + Future getPaymentRequestUri(String amount) async { + if (addressPageType is LightningAddressType && lightningWallet != null) { + final amountSats = amount.isNotEmpty ? parseFixed(amount, 8) : null; + final lnUrl = getLnurlOfLightningAddress(address); + if (amountSats == null) { + return LightningPaymentRequest(address: address, lnURL: lnUrl, amount: amount); + } + final invoice = await lightningWallet!.getBolt11Invoice(amountSats, "Send to Cake Wallet"); + return LightningPaymentRequest( + address: address, lnURL: lnUrl, amount: amount, bolt11Invoice: invoice); + } + return getPaymentUri(amount); + } + + @override + bool containsAddress(String address) => super.containsAddress(address) || address == lightningAddress; + + @override + String get addressForBuy { + try { + return super + .allAddresses + .firstWhere((e) => e.type == SegwitAddresType.p2wpkh && !e.isHidden && !e.isUsed) + .address; + } catch (_) { + return super.addressForBuy; + } + } + + @override + String get addressForExchange { + if (addressPageType == LightningAddressType.p2l) { + final addresses = receiveAddresses + .where((element) => element.type == SegwitAddresType.p2wpkh && !element.isUsed); + return addresses.first.address; + } + return address; + } } diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index ebb075912..6163a8a82 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -15,7 +15,6 @@ import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:hive/hive.dart'; -import 'package:collection/collection.dart'; import 'package:bip39/bip39.dart' as bip39; class BitcoinWalletService extends WalletService< @@ -177,6 +176,7 @@ class BitcoinWalletService extends WalletService< networkParam: network, encryptionFileUtils: encryptionFileUtilsFor(isDirect), payjoinBox: payjoinSessionSource, + useLightning: false, ); await wallet.save(); await wallet.init(); @@ -200,6 +200,7 @@ class BitcoinWalletService extends WalletService< networkParam: network, encryptionFileUtils: encryptionFileUtilsFor(isDirect), payjoinBox: payjoinSessionSource, + useLightning: false, ); await wallet.save(); diff --git a/cw_bitcoin/lib/electrum_balance.dart b/cw_bitcoin/lib/electrum_balance.dart index aeb06f1f0..a1529f92f 100644 --- a/cw_bitcoin/lib/electrum_balance.dart +++ b/cw_bitcoin/lib/electrum_balance.dart @@ -1,25 +1,19 @@ import 'dart:convert'; -import 'package:cw_bitcoin/bitcoin_amount_format.dart'; + import 'package:cw_core/balance.dart'; class ElectrumBalance extends Balance { ElectrumBalance({ required this.confirmed, required this.unconfirmed, - required this.frozen, + required int frozen, this.secondConfirmed = 0, this.secondUnconfirmed = 0, - }) : super( - confirmed, - unconfirmed, - secondAvailable: secondConfirmed, - secondAdditional: secondUnconfirmed, - ); + }) : this._frozen = frozen, super.fromInt(confirmed, unconfirmed, + secondAvailable: secondConfirmed, secondAdditional: secondUnconfirmed, frozen: frozen); static ElectrumBalance? fromJSON(String? jsonSource) { - if (jsonSource == null) { - return null; - } + if (jsonSource == null) return null; final decoded = json.decode(jsonSource) as Map; @@ -34,36 +28,21 @@ class ElectrumBalance extends Balance { int confirmed; int unconfirmed; - final int frozen; + final int _frozen; + + @override + BigInt get frozen => BigInt.from(_frozen); + int secondConfirmed = 0; int secondUnconfirmed = 0; @override - String get formattedAvailableBalance => bitcoinAmountToString(amount: ((confirmed + unconfirmed) - frozen) ); - - @override - String get formattedAdditionalBalance => bitcoinAmountToString(amount: unconfirmed); - - @override - String get formattedUnAvailableBalance { - final frozenFormatted = bitcoinAmountToString(amount: frozen); - return frozenFormatted == '0.0' ? '' : frozenFormatted; - } - - @override - String get formattedSecondAvailableBalance => bitcoinAmountToString(amount: secondConfirmed); - - @override - String get formattedSecondAdditionalBalance => bitcoinAmountToString(amount: secondUnconfirmed); - - @override - String get formattedFullAvailableBalance => - bitcoinAmountToString(amount: (confirmed + unconfirmed) + secondConfirmed - frozen); + BigInt get fullAvailableBalance => BigInt.from((confirmed + unconfirmed) + secondConfirmed - _frozen); String toJSON() => json.encode({ 'confirmed': confirmed, 'unconfirmed': unconfirmed, - 'frozen': frozen, + 'frozen': _frozen, 'secondConfirmed': secondConfirmed, 'secondUnconfirmed': secondUnconfirmed, }); diff --git a/cw_bitcoin/lib/electrum_transaction_history.dart b/cw_bitcoin/lib/electrum_transaction_history.dart index d096d0e7b..6457832ec 100644 --- a/cw_bitcoin/lib/electrum_transaction_history.dart +++ b/cw_bitcoin/lib/electrum_transaction_history.dart @@ -1,15 +1,13 @@ import 'dart:convert'; -import 'package:cw_core/encryption_file_utils.dart'; + import 'package:cw_bitcoin/electrum_transaction_info.dart'; +import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/transaction_history.dart'; -import 'package:cw_core/utils/file.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; -import 'package:cw_core/transaction_history.dart'; -import 'package:cw_bitcoin/electrum_transaction_info.dart'; part 'electrum_transaction_history.g.dart'; diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index 9c0beadde..cee1f9b9f 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -209,7 +209,7 @@ class ElectrumTransactionInfo extends TransactionInfo { return ElectrumTransactionInfo( type, id: data['id'] as String, - height: data['height'] as int, + height: data['height'] as int?, amount: data['amount'] as int, fee: data['fee'] as int, direction: parseTransactionDirectionFromInt(data['direction'] as int), @@ -237,11 +237,11 @@ class ElectrumTransactionInfo extends TransactionInfo { @override String amountFormatted() => - '${formatAmount(bitcoinAmountToString(amount: amount))} ${walletTypeToCryptoCurrency(type).title}'; + '${walletTypeToCryptoCurrency(type).formatAmount(BigInt.from(amount))} ${walletTypeToCryptoCurrency(type).title}'; @override String? feeFormatted() => fee != null - ? '${formatAmount(bitcoinAmountToString(amount: fee!))} ${walletTypeToCryptoCurrency(type).title}' + ? '${walletTypeToCryptoCurrency(type).formatAmount(BigInt.from(fee!))} ${walletTypeToCryptoCurrency(type).title}' : ''; @override diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 972cfe5ea..603571d36 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -4,6 +4,13 @@ import 'dart:io'; import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_core/hardware/hardware_wallet_service.dart'; +import 'package:cw_core/root_dir.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_bitcoin/bitcoin_wallet.dart'; +import 'package:cw_bitcoin/litecoin_wallet.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:collection/collection.dart'; import 'package:cw_bitcoin/address_from_output.dart'; @@ -11,7 +18,6 @@ import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; -import 'package:cw_bitcoin/bitcoin_wallet.dart'; import 'package:cw_bitcoin/bitcoin_wallet_keys.dart'; import 'package:cw_bitcoin/electrum.dart' as electrum; import 'package:cw_bitcoin/electrum_balance.dart'; @@ -20,25 +26,20 @@ import 'package:cw_bitcoin/electrum_transaction_history.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/exceptions.dart'; -import 'package:cw_bitcoin/litecoin_wallet.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/get_height_by_date.dart'; -import 'package:cw_core/hardware/hardware_wallet_service.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/output_info.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pending_transaction.dart'; -import 'package:cw_core/root_dir.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/unspent_coins_info.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/utils/socket_health_logger.dart'; import 'package:cw_core/utils/tor/abstract.dart'; import 'package:cw_core/wallet_base.dart'; @@ -50,7 +51,6 @@ import 'package:hex/hex.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:rxdart/subjects.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:sp_scanner/sp_scanner.dart'; part 'electrum_wallet.g.dart'; @@ -76,6 +76,7 @@ abstract class ElectrumWalletBase ElectrumBalance? initialBalance, CryptoCurrency? currency, bool? alwaysScan, + this.useLightning = true, }) : _masterHD = getMasterHD(seedBytes, network, walletInfo.hardwareWalletType), accountHD = getAccountHDWallet( currency, network, seedBytes, xpub, derivationInfo, walletInfo.hardwareWalletType), @@ -183,6 +184,9 @@ abstract class ElectrumWalletBase @observable bool? alwaysScan; + @observable + bool useLightning; + final Bip32Slip10Secp256k1? _masterHD; final Bip32Slip10Secp256k1 accountHD; final String? _mnemonic; @@ -633,7 +637,9 @@ abstract class ElectrumWalletBase if (server.toLowerCase().contains('electrs')) { node!.isElectrs = true; - node!.save(); + if (node!.isInBox) { + node!.save(); + } return node!.isElectrs!; } } @@ -725,6 +731,7 @@ abstract class ElectrumWalletBase case UnspentCoinType.nonMweb: return utx.bitcoinAddressRecord.type != SegwitAddresType.mweb; case UnspentCoinType.any: + case UnspentCoinType.lightning: return true; } }).toList(); @@ -1465,12 +1472,15 @@ abstract class ElectrumWalletBase ? SegwitAddresType.p2wpkh.toString() : walletInfo.addressPageType.toString(), 'balance': balance[currency]?.toJSON(), + 'lightningBalance': balance[CryptoCurrency.btcln]?.toJSON(), 'derivationTypeIndex': derivationInfo.derivationType?.index, 'derivationPath': derivationInfo.derivationPath, 'silent_addresses': walletAddresses.silentAddresses.map((addr) => addr.toJSON()).toList(), 'silent_address_index': walletAddresses.currentSilentAddressIndex.toString(), 'mweb_addresses': walletAddresses.mwebAddresses.map((addr) => addr.toJSON()).toList(), 'alwaysScan': alwaysScan, + 'useLightning': useLightning, + 'cachedLightningAddress': walletAddresses.lightningAddress }); int feeRate(TransactionPriority priority) { @@ -3181,8 +3191,9 @@ Future _handleScanSilentPayments(ScanData scanData) async { final addToWallet = {}; receivers.forEach((receiver) { + final preparedList = outputPubkeys.keys.toList().map((e) => [e]).toList(); // NOTE: scanOutputs, from sp_scanner package, called from rust here - final scanResult = scanOutputs([outputPubkeys.keys.toList()], tweak, receiver); + final scanResult = scanOutputs(preparedList, tweak, receiver); if (scanResult.isEmpty) return; diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index d4927e7a2..404d4e982 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -1,20 +1,25 @@ import 'dart:io' show Platform; +import 'dart:math'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/bitcoin_unspent.dart'; +import 'package:cw_bitcoin/lightning/lightning_addres_type.dart'; +import 'package:cw_bitcoin/lightning/lightning_wallet.dart'; +import 'package:cw_core/pathForWallet.dart'; import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_core/wallet_addresses.dart'; +import 'package:cw_core/generate_name.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; part 'electrum_wallet_addresses.g.dart'; -class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses; +abstract class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses; const List BITCOIN_ADDRESS_TYPES = [ SegwitAddresType.p2wpkh, @@ -52,6 +57,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { List? initialMwebAddresses, Bip32Slip10Secp256k1? masterHd, BitcoinAddressType? initialAddressPageType, + this.lightningWallet, }) : _addresses = ObservableList.of((initialAddresses ?? []).toSet()), addressesByReceiveType = ObservableList.of(([]).toSet()), @@ -65,7 +71,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { currentChangeAddressIndexByType = initialChangeAddressIndex ?? {}, _addressPageType = initialAddressPageType ?? (walletInfo.addressPageType != null - ? BitcoinAddressType.fromValue(walletInfo.addressPageType!) + ? walletInfo.addressPageType == LightningAddressType.p2l.value + ? LightningAddressType.p2l + : BitcoinAddressType.fromValue(walletInfo.addressPageType!) : SegwitAddresType.p2wpkh), silentAddresses = ObservableList.of( (initialSilentAddresses ?? []).toSet()), @@ -112,7 +120,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { )); } } - updateAddressesByMatch(); } @@ -124,14 +131,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final ObservableList addressesByReceiveType; final ObservableList receiveAddresses; final ObservableList changeAddresses; + // TODO: add this variable in `bitcoin_wallet_addresses` and just add a cast in cw_bitcoin to use it final ObservableList silentAddresses; + // TODO: add this variable in `litecoin_wallet_addresses` and just add a cast in cw_bitcoin to use it final ObservableList mwebAddresses; final BasedUtxoNetwork network; final Bip32Slip10Secp256k1 mainHd; final Bip32Slip10Secp256k1 sideHd; final bool isHardwareWallet; + final LightningWallet? lightningWallet; @observable ObservableMap lockedReceiveAddressByType; @@ -148,6 +158,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @observable String? activeSilentAddress; + @observable + String? lightningAddress; + @computed List get allAddresses => _addresses; @@ -162,6 +175,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return silentAddress.toString(); } + if (addressPageType == LightningAddressType.p2l) { + return lightningAddress ?? + "Error: Unable to fetch your Lightning address, please check your network connection."; + } + final typeMatchingAddresses = _addresses.where((addr) => !addr.isHidden && _isAddressPageTypeMatch(addr)).toList(); final typeMatchingReceiveAddresses = @@ -229,7 +247,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { addressRecord.type == addressPageType) { lockedReceiveAddressByType[addressPageType] = addr; } - } catch (e) { printV("ElectrumWalletAddressBase: set address ($addr): $e"); } @@ -465,7 +482,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final firstSilentAddressRecord = silentAddresses.firstOrNull; if (firstSilentAddressRecord != null) { - if (firstSilentAddressRecord.address != address) { addressesMap[firstSilentAddressRecord.address] = firstSilentAddressRecord.name.isEmpty ? "Silent Payments" @@ -716,6 +732,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { }); } + @override + String get addressForBuy => super.addressForBuy; + @action Future setAddressType(BitcoinAddressType type) async { _addressPageType = type; @@ -743,4 +762,40 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { silentAddresses.remove(addressRecord); updateAddressesByMatch(); } + + @action + Future setLightningAddress(String walletName, {String newAddress = ""}) async { + if (lightningWallet == null) return; + + final path = await pathForWalletDir(name: walletName, type: WalletType.bitcoin); + final initialized = await lightningWallet!.init(path); + + if (!initialized) { + printV("Failed to initialize the lightning wallet"); + return; + } + + lightningAddress = await lightningWallet!.getAddress(); + + late final String username; + + if (newAddress.isEmpty) { + if (lightningAddress != null) return; + + final randomNumber = Random.secure().nextInt(9999); + final randomName = await generateName(); + username = "${randomName.replaceAll(" ", "")}$randomNumber".toLowerCase(); + } else { + username = newAddress; + } + + try { + printV(username); + lightningAddress = await lightningWallet!.registerAddress(username); + } catch (e) { + printV(e); + printV(username); + rethrow; + } + } } diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 990719089..810706374 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -6,7 +6,6 @@ import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/utils/file.dart'; import 'package:cw_core/wallet_type.dart'; class ElectrumWalletSnapshot { @@ -25,9 +24,12 @@ class ElectrumWalletSnapshot { required this.silentAddressIndex, required this.mwebAddresses, required this.alwaysScan, + required this.useLightning, + this.cachedLightningAddress, this.passphrase, this.derivationType, this.derivationPath, + this.lightningBalance, }); final String name; @@ -48,8 +50,11 @@ class ElectrumWalletSnapshot { List silentAddresses; List mwebAddresses; bool alwaysScan; + bool useLightning; + String? cachedLightningAddress; ElectrumBalance balance; + ElectrumBalance? lightningBalance; Map regularAddressIndex; Map changeAddressIndex; int silentAddressIndex; @@ -84,9 +89,13 @@ class ElectrumWalletSnapshot { .toList(); final alwaysScan = data['alwaysScan'] as bool? ?? false; + final useLightning = data['useLightning'] as bool? ?? true; + final cachedLightningAddress = data['cachedLightningAddress'] as String?; final balance = ElectrumBalance.fromJSON(data['balance'] as String?) ?? ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); + final lightningBalance = ElectrumBalance.fromJSON(data['lightningBalance'] as String?); + var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; var changeAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; var silentAddressIndex = 0; @@ -120,6 +129,7 @@ class ElectrumWalletSnapshot { xpub: xpub, addresses: addresses, balance: balance, + lightningBalance: lightningBalance, regularAddressIndex: regularAddressIndexByType, changeAddressIndex: changeAddressIndexByType, addressPageType: data['address_page_type'] as String?, @@ -129,6 +139,8 @@ class ElectrumWalletSnapshot { silentAddressIndex: silentAddressIndex, mwebAddresses: mwebAddresses, alwaysScan: alwaysScan, + useLightning: useLightning, + cachedLightningAddress: cachedLightningAddress, ); } } diff --git a/cw_bitcoin/lib/lightning/lightning_addres_type.dart b/cw_bitcoin/lib/lightning/lightning_addres_type.dart new file mode 100644 index 000000000..733d9338b --- /dev/null +++ b/cw_bitcoin/lib/lightning/lightning_addres_type.dart @@ -0,0 +1,26 @@ +import 'package:bitcoin_base/src/bitcoin/address/address.dart'; + +class LightningAddressType implements BitcoinAddressType { + const LightningAddressType._(this.value); + static const LightningAddressType p2l = LightningAddressType._("Lightning"); + + static const String Bolt11InvoiceMatcher = r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]+$'; + static const String Bolt12OfferMatcher = r'^(lightning:)?(lno1)[a-z0-9]+$'; + static const String LNURLMatcher = r'^(lightning:)?(lnurl)[a-z0-9]+$'; + + @override + bool get isP2sh => false; + @override + bool get isSegwit => false; + + @override + final String value; + + @override + int get hashLength { + return 32; + } + + @override + String toString() => value; +} diff --git a/cw_bitcoin/lib/lightning/lightning_wallet.dart b/cw_bitcoin/lib/lightning/lightning_wallet.dart new file mode 100644 index 000000000..e450f070a --- /dev/null +++ b/cw_bitcoin/lib/lightning/lightning_wallet.dart @@ -0,0 +1,421 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:breez_sdk_spark_flutter/breez_sdk_spark.dart'; +import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; +import 'package:cw_bitcoin/electrum_transaction_info.dart'; +import 'package:cw_bitcoin/lightning/pending_lightning_transaction.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_type.dart'; + +bool _breezSdkSparkLibUninitialized = true; +Stream? _logStream; + +class LightningWallet { + final String mnemonic; + final String? passphrase; + final Uint8List? seedBytes; + final String apiKey; + final String lnurlDomain; + final Network network; + late BreezSdk sdk; + + String? cachedAddress; + + static int MAX_RETRIES = 10; + + LightningWallet({ + required this.mnemonic, + this.passphrase, + this.seedBytes, + required this.apiKey, + required this.lnurlDomain, + this.network = Network.mainnet, + this.cachedAddress + }); + + StreamSubscription? _eventSubscription; + Stream? _eventStream; + + StreamSubscription? _logSubscription; + + void _subscribeToLogStream(File logFile) { + _logSubscription = _logStream?.listen((logEntry) { + logFile.writeAsString("[${logEntry.level}] ${logEntry.line}\n", mode: FileMode.append); + }, onError: (e) { + logFile.writeAsString("[ERROR] $e\n", mode: FileMode.append); + }); + } + + Future init(String appPath) async { + try { + if (_breezSdkSparkLibUninitialized) { + await BreezSdkSparkLib.init(); + _breezSdkSparkLibUninitialized = false; + } + + final seed = seedBytes != null + ? Seed.entropy(seedBytes!) + : Seed.mnemonic(mnemonic: mnemonic, passphrase: passphrase); + final config = defaultConfig(network: Network.mainnet).copyWith( + lnurlDomain: lnurlDomain, + apiKey: apiKey, + privateEnabledDefault: true, + maxDepositClaimFee: MaxFee.rate(satPerVbyte: BigInt.from(5)) + ); + + final connectRequest = ConnectRequest( + config: config, + seed: seed, + storageDir: "$appPath/.breez/", + ); + + sdk = await connect(request: connectRequest); + + _eventStream ??= sdk.addEventListener().asBroadcastStream(); + _logStream ??= initLogging().asBroadcastStream(); + + await sdk.syncWallet(request: SyncWalletRequest()); + + try { + final logFile = File("$appPath/lightning.log") + ..createSync(); + _subscribeToLogStream(logFile); + } catch (e) { + printV(e); + } + + return true; + } catch (e) { + printV(e); + return false; + } + } + + Future close() async { + _eventSubscription?.cancel(); + await sdk.disconnect(); + _logSubscription?.cancel(); + } + + Future getAddress() async { + var retries = 0; + while (retries < MAX_RETRIES) { + try { + final address = (await sdk.getLightningAddress())?.lightningAddress; + + if (address != null) { + cachedAddress = address; + return address; + } + } catch (_) {} // No need to log here since it should be in the lightning log + retries++; + await Future.delayed(Duration(milliseconds: 500)); + } + + return cachedAddress; + } + + Future getDepositAddress() async => (await sdk.receivePayment( + request: ReceivePaymentRequest(paymentMethod: ReceivePaymentMethod.bitcoinAddress()))) + .paymentRequest; + + Future getBalance() async { + try { + return (await sdk.getInfo(request: GetInfoRequest(ensureSynced: true))).balanceSats; + } on SdkError_Generic catch (_) { + } on SdkError_NetworkError catch (_) {} + + return BigInt.zero; + } + + Future registerAddress(String username) async { + return (await sdk.registerLightningAddress( + request: RegisterLightningAddressRequest(username: username))) + .lightningAddress; + } + + Future getBolt11Invoice(BigInt? amount, String description) async { + final response = await sdk.receivePayment( + request: ReceivePaymentRequest( + paymentMethod: ReceivePaymentMethod.bolt11Invoice( + description: description, + amountSats: amount, + ), + ), + ); + + return response.paymentRequest; + } + + Future isCompatible(String input) async { + try { + final inputType = await sdk.parse(input: input); + return (inputType is InputType_Bolt11Invoice) || + (inputType is InputType_LightningAddress) || + (inputType is InputType_LnurlPay); + } catch (_) { + return false; + } + } + + Future createTransaction( + String address, BigInt? amountSats, BitcoinTransactionPriority? priority) async { + final inputType = await sdk.parse(input: address); + + if (inputType is InputType_Bolt11Invoice) { + final request = PrepareSendPaymentRequest( + paymentRequest: inputType.field0.invoice.bolt11, amount: amountSats); + final prepareResponse = await sdk.prepareSendPayment(request: request); + + final paymentMethod = prepareResponse.paymentMethod; + if (paymentMethod is SendPaymentMethod_Bolt11Invoice) { + final lightningFeeSats = paymentMethod.lightningFeeSats; + final sparkTransferFeeSats = paymentMethod.sparkTransferFeeSats; + + return PendingLightningTransaction( + id: paymentMethod.invoiceDetails.paymentHash, + amount: amountSats?.toInt() ?? + ((paymentMethod.invoiceDetails.amountMsat?.toInt() ?? 0) / 1000).round(), + fee: lightningFeeSats.toInt() + (sparkTransferFeeSats?.toInt() ?? 0), + commitOverride: () async { + try { + final res = await sdk.sendPayment( + request: SendPaymentRequest(prepareResponse: prepareResponse)); + printV(res.payment.status.name); + } on SdkError_SparkError catch (e) { + if (e.field0.contains("AlreadyExists")) { + throw Exception("Invoice already paid"); + } + rethrow; + } + }, + ); + } + } else if (inputType is InputType_LightningAddress || inputType is InputType_LnurlPay) { + final optionalValidateSuccessActionUrl = true; + + PrepareLnurlPayRequest request; + if (inputType is InputType_LightningAddress) { + request = PrepareLnurlPayRequest( + amountSats: amountSats!, + payRequest: inputType.field0.payRequest, + validateSuccessActionUrl: optionalValidateSuccessActionUrl, + ); + } else { + request = PrepareLnurlPayRequest( + amountSats: amountSats!, + payRequest: (inputType as InputType_LnurlPay).field0, + validateSuccessActionUrl: optionalValidateSuccessActionUrl, + ); + } + + final prepareResponse = await sdk.prepareLnurlPay(request: request); + + final feeSats = prepareResponse.feeSats; + + return PendingLightningTransaction( + id: prepareResponse.invoiceDetails.paymentHash, + amount: ((prepareResponse.invoiceDetails.amountMsat?.toInt() ?? 0) / 1000).round(), + fee: feeSats.toInt(), + commitOverride: () async { + final res = + await sdk.lnurlPay(request: LnurlPayRequest(prepareResponse: prepareResponse)); + printV(res.payment.status.name); + }, + ); + } else if (inputType is InputType_BitcoinAddress) { + final request = + PrepareSendPaymentRequest(paymentRequest: inputType.field0.address, amount: amountSats); + final prepareResponse = await sdk.prepareSendPayment(request: request); + + final paymentMethod = prepareResponse.paymentMethod; + if (paymentMethod is SendPaymentMethod_BitcoinAddress) { + final feeQuote = paymentMethod.feeQuote; + + OnchainConfirmationSpeed onchainConfirmationSpeed; + int fee; + switch (priority) { + case BitcoinTransactionPriority.fast: + fee = (feeQuote.speedFast.userFeeSat + feeQuote.speedFast.l1BroadcastFeeSat).toInt(); + onchainConfirmationSpeed = OnchainConfirmationSpeed.fast; + break; + case BitcoinTransactionPriority.medium: + fee = + (feeQuote.speedMedium.userFeeSat + feeQuote.speedMedium.l1BroadcastFeeSat).toInt(); + onchainConfirmationSpeed = OnchainConfirmationSpeed.medium; + break; + case BitcoinTransactionPriority.slow: + default: + fee = (feeQuote.speedSlow.userFeeSat + feeQuote.speedSlow.l1BroadcastFeeSat).toInt(); + onchainConfirmationSpeed = OnchainConfirmationSpeed.slow; + } + + return PendingLightningTransaction( + id: "", // ToDo: Find out where to get it + amount: prepareResponse.amount.toInt(), + fee: fee, + commitOverride: () async { + final options = + SendPaymentOptions.bitcoinAddress(confirmationSpeed: onchainConfirmationSpeed); + await sdk.sendPayment( + request: SendPaymentRequest(prepareResponse: prepareResponse, options: options)); + }, + ); + } + } + + // If not returned earlier + throw UnimplementedError(); + } + + Future> getTransactionHistory({DateTime? fromDate}) async { + final request = ListPaymentsRequest( + typeFilter: [PaymentType.send, PaymentType.receive], + // statusFilter: [PaymentStatus.completed], + fromTimestamp: + fromDate != null ? BigInt.from((fromDate.millisecondsSinceEpoch / 1000).round()) : null, + assetFilter: AssetFilter.bitcoin(), + offset: 0, + limit: 50, + sortAscending: false, // Sort order (true = oldest first, false = newest first) + ); + final response = await sdk.listPayments(request: request); + final payments = response.payments; + + final txHistory = {}; + for (final payment in payments) { + txHistory[payment.id] = _getElectrumTransactionInfoFromPayment(payment); + } + + return txHistory; + } + + /// Return a list of UnclaimedDeposits including a possible reason why they where not auto-claimed + /// A unclaimed deposit is a [Map] consisting of the following datatypes + /// + /// | ----------------- | --------- |--------------------------------------------------- | + /// | key-name | data-type | description | + /// | ----------------- | --------- |--------------------------------------------------- | + /// | txId | String | The txId of the deposit transaction | + /// | vout | int | The output index of the deposit | + /// | amount | BigInt | Amount of the deposit in sats. | + /// | claimError | String? | The type of Claim error | + /// | actualFee | BigInt? | The actualFee in case of a DepositClaimFeeExceeded | + /// | claimErrorMessage | String? | The claimErrorMessage in case of a Generic Error | + /// + Future>> getUnclaimedDeposits() async { + final unclaimedDeposits = >[]; + final response = await sdk.listUnclaimedDeposits(request: ListUnclaimedDepositsRequest()); + for (final deposit in response.deposits) { + final unclaimedDeposit = { + "txId": deposit.txid, + "vout": deposit.vout, + "amount": deposit.amountSats, + }; + + final claimError = deposit.claimError; + if (claimError is DepositClaimError_MaxDepositClaimFeeExceeded) { + unclaimedDeposit["claimError"] = "DepositClaimError_MaxDepositClaimFeeExceeded"; + unclaimedDeposit["actualFee"] = claimError.requiredFeeSats; + } else if (claimError is DepositClaimError_MissingUtxo) { + unclaimedDeposit["claimError"] = "MissingUtxo"; + } else if (claimError is DepositClaimError_Generic) { + unclaimedDeposit["claimError"] = "Generic"; + unclaimedDeposit["claimErrorMessage"] = claimError.message; + } + } + + return unclaimedDeposits; + } + + Future claimDeposit(String txId, int vout, BigInt newFee) async { + final response = await sdk.claimDeposit( + request: ClaimDepositRequest( + txid: txId, + vout: vout, + maxFee: MaxFee.fixed(amount: newFee), + ), + ); + + return _getElectrumTransactionInfoFromPayment(response.payment); + } + + Future refundDeposit(String txId, int vout, String destinationAddress, + BigInt feeRate) async { + final response = await sdk.refundDeposit(request: RefundDepositRequest( + txid: txId, + vout: vout, + destinationAddress: destinationAddress, + fee: Fee.rate(satPerVbyte: feeRate), + ),); + + return response.txHex; + } + + void setEventListener( + {required Function(ElectrumTransactionInfo) onTransactionEvent, required Function onBalanceChangedEvent}) { + _eventSubscription = _eventStream?.listen((sdkEvent) { + if (sdkEvent is SdkEvent_PaymentSucceeded) { + onTransactionEvent(_getElectrumTransactionInfoFromPayment(sdkEvent.payment)); + } else if (sdkEvent is SdkEvent_PaymentPending) { + onTransactionEvent(_getElectrumTransactionInfoFromPayment(sdkEvent.payment)); + } else if (sdkEvent is SdkEvent_ClaimedDeposits) { + onBalanceChangedEvent(); + } + }); + } + + ElectrumTransactionInfo _getElectrumTransactionInfoFromPayment(Payment payment) { + TransactionDirection direction = TransactionDirection.outgoing; + + if (payment.paymentType == PaymentType.receive) { + direction = TransactionDirection.incoming; + } + if (payment.method == PaymentMethod.deposit) { + direction = TransactionDirection.incoming; + } + + return ElectrumTransactionInfo( + WalletType.bitcoin, + id: payment.id, + amount: payment.amount.toInt(), + direction: direction, + isPending: payment.status == PaymentStatus.pending, + fee: payment.fees.toInt(), + date: DateTime.fromMillisecondsSinceEpoch(payment.timestamp.toInt() * 1000), + confirmations: payment.status == PaymentStatus.pending ? 0 : 10, + additionalInfo: {"isLightning": true}, + ); + } +} + +extension _ConfigCopyWith on Config { + Config copyWith({ + String? apiKey, + String? lnurlDomain, + Network? network, + int? syncIntervalSecs, + MaxFee? maxDepositClaimFee, + bool? preferSparkOverLightning, + bool? useDefaultExternalInputParsers, + bool? privateEnabledDefault, + OptimizationConfig? optimizationConfig, + int? maxConcurrentClaims, + }) => + Config( + lnurlDomain: lnurlDomain ?? this.lnurlDomain, + apiKey: apiKey ?? this.apiKey, + network: network ?? this.network, + syncIntervalSecs: syncIntervalSecs ?? this.syncIntervalSecs, + maxDepositClaimFee: maxDepositClaimFee ?? this.maxDepositClaimFee, + preferSparkOverLightning: preferSparkOverLightning ?? this.preferSparkOverLightning, + useDefaultExternalInputParsers: + useDefaultExternalInputParsers ?? this.useDefaultExternalInputParsers, + privateEnabledDefault: privateEnabledDefault ?? this.privateEnabledDefault, + optimizationConfig: optimizationConfig ?? this.optimizationConfig, + maxConcurrentClaims: maxConcurrentClaims ?? this.maxConcurrentClaims, + ); +} diff --git a/cw_bitcoin/lib/lightning/pending_lightning_transaction.dart b/cw_bitcoin/lib/lightning/pending_lightning_transaction.dart new file mode 100644 index 000000000..cb75b7d2b --- /dev/null +++ b/cw_bitcoin/lib/lightning/pending_lightning_transaction.dart @@ -0,0 +1,50 @@ +import 'package:cw_bitcoin/bitcoin_amount_format.dart'; +import 'package:cw_core/pending_transaction.dart'; + +class PendingLightningTransaction with PendingTransaction { + PendingLightningTransaction({ + required this.id, + required this.amount, + required this.fee, + this.isSendAll = false, + required this.commitOverride, + }); + + final int amount; + final int fee; + final bool isSendAll; + Future Function() commitOverride; + final List _listeners =[]; + + @override + final String id; + + @override + String get hex => ""; + + @override + String get amountFormatted => bitcoinAmountToString(amount: amount); + + @override + String get feeFormatted => "$feeFormattedValue BTC"; + + @override + String get feeFormattedValue => bitcoinAmountToString(amount: fee); + + @override + int? get outputCount => 1; + + @override + Future commit() async { + await commitOverride.call(); + _listeners.forEach((e) => e.call()); + } + + @override + bool shouldCommitUR() => false; + + @override + Future> commitUR() => throw UnimplementedError(); + + void addListener(void Function() listener) => _listeners.add(listener); +} diff --git a/cw_bitcoin/lib/lightning/utils.dart b/cw_bitcoin/lib/lightning/utils.dart new file mode 100644 index 000000000..e60ee1e2f --- /dev/null +++ b/cw_bitcoin/lib/lightning/utils.dart @@ -0,0 +1,9 @@ +import 'package:cw_core/lnurl.dart'; + +String getLnurlOfLightningAddress(String lightningAddress) { + final parts = lightningAddress.split("@"); + + final name = parts.first; + final domain = parts.last; + return LNURL.encode("https://$domain/.well-known/lnurlp/$name"); +} diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 0c2612a12..7d98a52a3 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -989,7 +989,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return ElectrumBalance( confirmed: confirmed, unconfirmed: unconfirmed, - frozen: balance.frozen, + frozen: balance.frozen.toInt(), secondConfirmed: confirmedMweb, secondUnconfirmed: unconfirmedMweb, ); diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index b0725f109..147281a7b 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -5,9 +5,12 @@ import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; -import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; +import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/payment_uris.dart'; +import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; @@ -53,6 +56,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with List get scanSecret => (scanSecretOverride != null && scanSecretOverride?.isNotEmpty == true) ? hex.decode(scanSecretOverride!) : mwebHd?.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw ?? List.filled(32, 0); + List get spendPubkey => (spendPubkeyOverride != null && spendPubkeyOverride?.isNotEmpty == true) ? hex.decode(spendPubkeyOverride!) : mwebHd?.childKey(Bip32KeyIndex(0x80000001)).publicKey.pubKey.compressed ?? List.filled(32, 0); @@ -225,4 +229,22 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with .where((element) => element.type == SegwitAddresType.p2wpkh && !element.isUsed); return addresses.first.address; } + + @override + List get receivePageOptions { + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows || isHardwareWallet) { + return [ + ...BitcoinReceivePageOption.allLitecoin + .where((element) => element != BitcoinReceivePageOption.mweb), + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ]; + } + return [ + ...BitcoinReceivePageOption.allLitecoin, + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ]; + } + + @override + PaymentURI getPaymentUri(String amount) => LitecoinURI(amount: amount, address: address); } diff --git a/cw_bitcoin/lib/payjoin/manager.dart b/cw_bitcoin/lib/payjoin/manager.dart index ca7e262c1..437fed3ff 100644 --- a/cw_bitcoin/lib/payjoin/manager.dart +++ b/cw_bitcoin/lib/payjoin/manager.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'dart:isolate'; import 'dart:math'; import 'dart:typed_data'; @@ -13,6 +14,7 @@ import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart'; import 'package:cw_bitcoin/payjoin/storage.dart'; import 'package:cw_bitcoin/psbt/signer.dart'; import 'package:cw_bitcoin/psbt/utils.dart'; +import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:payjoin_flutter/common.dart'; import 'package:payjoin_flutter/receive.dart'; @@ -39,9 +41,47 @@ class PayjoinManager { static const payjoinDirectoryUrl = 'https://payjo.in'; - Future initPayjoin() => pj_config.PConfig.initializeApp(); + var _logStreamController = StreamController.broadcast(); + Stream get logStream => _logStreamController.stream; + StreamSubscription? _logSubscription; + + Future initPayjoin() async { + await initLogging(); + + await pj_config.PConfig.initializeApp(); + } + + Future initLogging() async { + _logSubscription?.cancel(); + _logStreamController = StreamController.broadcast(); + + try { + final path = await pathForWalletDir(name: _wallet.name, type: _wallet.type); + File("$path/payjoin.log") + .create() + .then(_subscribeToLogStream) + .onError((e, s) async => printV("$e\n$s")); + } catch (e) { + printV(e); + } + } + + void _subscribeToLogStream(File logFile) { + _logSubscription = logStream.listen((logEntry) { + logFile.writeAsString("$logEntry\n", mode: FileMode.writeOnlyAppend); + }); + } + + void writePayjoinLog(String message) { + if (_logStreamController.isClosed) return; + + try { + _logStreamController.add(message); + } catch (_) {} + } Future resumeSessions() async { + await initLogging(); final allSessions = _payjoinStorage.readAllOpenSessions(_wallet.id); final spawnedSessions = allSessions.map((session) { @@ -112,12 +152,21 @@ class PayjoinManager { try { switch (message['type'] as PayjoinSenderRequestTypes) { case PayjoinSenderRequestTypes.requestPosted: + writePayjoinLog("Sender($pjUri) PayjoinSenderRequestTypes.requestPosted"); return; case PayjoinSenderRequestTypes.psbtToSign: + writePayjoinLog("Sender($pjUri) PayjoinSenderRequestTypes.psbtToSign"); + final proposalPsbt = message['psbt'] as String; + writePayjoinLog("Sender($pjUri) proposedPSBT: $proposalPsbt"); + final utxos = _wallet.getUtxoWithPrivateKeys(); final finalizedPsbt = await _wallet.signPsbt(proposalPsbt, utxos); + writePayjoinLog("Sender($pjUri) finalizedPsbt: $finalizedPsbt"); + final txId = getTxIdFromPsbtV0(finalizedPsbt); + writePayjoinLog("Sender($pjUri) expected: $txId"); + _wallet.commitPsbt(finalizedPsbt); _cleanupSession(pjUri); @@ -125,6 +174,7 @@ class PayjoinManager { completer.complete(); } } catch (e) { + writePayjoinLog("[ERROR] Sender($pjUri) $e"); _cleanupSession(pjUri); await _payjoinStorage.markSenderSessionUnrecoverable(pjUri, e.toString()); completer.complete(); @@ -132,11 +182,16 @@ class PayjoinManager { } else if (message is PayjoinSessionError) { _cleanupSession(pjUri); if (message is UnrecoverableError) { + writePayjoinLog("[ERROR] Sender($pjUri) UnrecoverableError(${message.message})"); await _payjoinStorage.markSenderSessionUnrecoverable(pjUri, message.message); completer.complete(); } else if (message is RecoverableError) { + writePayjoinLog("[ERROR] Sender($pjUri) RecoverableError(${message.message})"); + completer.complete(); } else { + writePayjoinLog("[ERROR] Sender($pjUri) GenericError(${message.message})"); + completer.completeError(message); } } @@ -166,6 +221,8 @@ class PayjoinManager { } Future initReceiver(String address, [bool isTestnet = false, int retryCount = 0]) async { + if (retryCount > 0) writePayjoinLog("Retrying initReceiver ${retryCount + 1} attempt"); + try { final ohttpKeys = await PayjoinUri.fetchOhttpKeys( ohttpRelay: await randomOhttpRelayUrl(), @@ -186,7 +243,9 @@ class PayjoinManager { return receiver; } catch (e) { + writePayjoinLog(e.toString()); if (e.toString().contains("error sending request for url") && retryCount < 5) { + return initReceiver(address, isTestnet, ++retryCount); } else { rethrow; @@ -251,6 +310,8 @@ class PayjoinManager { case PayjoinReceiverRequestTypes.processPsbt: final psbt = message['psbt'] as String; + writePayjoinLog("Receiver(${receiver.id()}) PayjoinReceiverRequestTypes.processPsbt: $psbt"); + final signedPsbt = await _wallet.signPsbt(psbt, utxos); mainToIsolateSendPort?.send({ 'requestId': message['requestId'], @@ -261,25 +322,35 @@ class PayjoinManager { case PayjoinReceiverRequestTypes.proposalSent: _cleanupSession(receiver.id()); final psbt = message['psbt'] as String; + writePayjoinLog("Receiver(${receiver.id()}) PayjoinReceiverRequestTypes.proposalSent: $psbt"); + await _payjoinStorage.markReceiverSessionComplete( receiver.id(), getTxIdFromPsbtV0(psbt), rawAmount); completer.complete(); } } catch (e) { _cleanupSession(receiver.id()); - await _payjoinStorage.markReceiverSessionUnrecoverable( - receiver.id(), e.toString()); + writePayjoinLog("[ERROR] Receiver(${receiver.id()}) $e"); + + await _payjoinStorage.markReceiverSessionUnrecoverable(receiver.id(), e.toString()); completer.completeError(e); } } else if (message is PayjoinSessionError) { _cleanupSession(receiver.id()); if (message is UnrecoverableError) { - await _payjoinStorage.markReceiverSessionUnrecoverable( - receiver.id(), message.message); + writePayjoinLog( + "[ERROR] Receiver(${receiver.id()}) UnrecoverableError(${message.message})"); + + await _payjoinStorage.markReceiverSessionUnrecoverable(receiver.id(), message.message); completer.complete(); } else if (message is RecoverableError) { + writePayjoinLog( + "[ERROR] Receiver(${receiver.id()}) RecoverableError(${message.message})"); + completer.complete(); } else { + writePayjoinLog("[ERROR] Receiver(${receiver.id()}) GenericError(${message.message})"); + completer.completeError(message); } } else if (message is SendPort) { @@ -302,9 +373,13 @@ class PayjoinManager { for (final sessionId in sessionIds) { _cleanupSession(sessionId); } + + _logSubscription?.cancel(); + _logStreamController.close(); } void _cleanupSession(String sessionId) { + writePayjoinLog("Cleaning $sessionId"); _activePollers[sessionId]?.close(); _activePollers.remove(sessionId); } diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 66b66ebd7..a9b741dc7 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -104,10 +104,9 @@ class PendingBitcoinTransaction with PendingTransaction { try { final change = _tx.outputs.firstWhere((out) => out.isChange); if (changeAddressOverride != null) { - return PendingChange( - changeAddressOverride!, BtcUtils.fromSatoshi(change.amount)); + return PendingChange(changeAddressOverride!, change.amount); } - return PendingChange(change.scriptPubKey.toAddress(network: network), BtcUtils.fromSatoshi(change.amount)); + return PendingChange(change.scriptPubKey.toAddress(network: network), change.amount); } catch (_) { return null; } diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 95195d53c..45e828335 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -56,7 +56,7 @@ packages: source: git version: "1.0.0" bech32: - dependency: "direct main" + dependency: transitive description: path: "." ref: HEAD @@ -130,6 +130,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + breez_sdk_spark_flutter: + dependency: "direct main" + description: + path: "." + ref: "9ad3fc1cdc7f09009786cf4960a0b7697e9a249f" + resolved-ref: "9ad3fc1cdc7f09009786cf4960a0b7697e9a249f" + url: "https://github.com/cake-tech/breez-sdk-spark-flutter" + source: git + version: "0.9.1" bs58check: dependency: transitive description: diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 78a5e0deb..ea6d8ce50 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -37,9 +37,6 @@ dependencies: git: url: https://github.com/cake-tech/sp_scanner ref: sp_v4.0.1 - bech32: - git: - url: https://github.com/cake-tech/bech32.git payjoin_flutter: git: url: https://github.com/konstantinullrich/payjoin-flutter @@ -72,6 +69,10 @@ dependencies: git: url: https://github.com/mrcyjanek/bbqrdart ref: e867e3d0156d0b29858100f30adc2625b9dae586 + breez_sdk_spark_flutter: + git: + url: https://github.com/cake-tech/breez-sdk-spark-flutter + ref: 9ad3fc1cdc7f09009786cf4960a0b7697e9a249f dev_dependencies: flutter_test: diff --git a/cw_bitcoin/test/cw_bitcoin_test.dart b/cw_bitcoin/test/cw_bitcoin_test.dart index 2a7ad6fe4..3fb24b185 100644 --- a/cw_bitcoin/test/cw_bitcoin_test.dart +++ b/cw_bitcoin/test/cw_bitcoin_test.dart @@ -1,12 +1,23 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:cw_bitcoin/cw_bitcoin.dart'; - void main() { - test('adds one to input values', () { - final calculator = Calculator(); - expect(calculator.addOne(2), 3); - expect(calculator.addOne(-7), -6); - expect(calculator.addOne(0), 1); + group('lightning matchers', () { + final RegExp lightningInvoiceRegex = + RegExp(r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]+$', caseSensitive: false); + + test('Valid invoice', () { + final content = + "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw508d6qejxtdg4y5r3zarvary0c5xw7kpqdxssqfsqqqyqqqqlgqqqqqeqqjq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgq9qrsgqfsqqqyqqqqlgqqqqqeqqjq9qrsgq"; + expect(lightningInvoiceRegex.hasMatch(content), true); + }); + test('Valid invoice with prefix', () { + final content = + "lightning:lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw508d6qejxtdg4y5r3zarvary0c5xw7kpqdxssqfsqqqyqqqqlgqqqqqeqqjq9qrsgq"; + expect(lightningInvoiceRegex.hasMatch(content), true); + }); + test('Invalid invoice', () { + final content = "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"; // This is a Bitcoin address + expect(lightningInvoiceRegex.hasMatch(content), false); + }); }); } diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart index fe0ebc828..25c11b763 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -2,6 +2,7 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; @@ -28,4 +29,7 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi required Bip32Slip10Secp256k1 hd, BitcoinAddressType? addressType}) => generateP2PKHAddress(hd: hd, index: index, network: network); + + @override + PaymentURI getPaymentUri(String amount) => BitcoinCashURI(address: address, amount: amount); } diff --git a/cw_core/lib/balance.dart b/cw_core/lib/balance.dart index 7350c80f1..8c7820dba 100644 --- a/cw_core/lib/balance.dart +++ b/cw_core/lib/balance.dart @@ -1,17 +1,26 @@ +/// Balance Data class with all amounts in the lowest possible currency (e.g. satoshis or wei) abstract class Balance { - const Balance(this.available, this.additional, {this.secondAvailable, this.secondAdditional}); + const Balance(this.available, this.additional, {this.secondAvailable, this.secondAdditional, this.frozen}); - final int available; + Balance.fromInt(int available, int additional, {int? secondAvailable, int? secondAdditional, int? frozen}) : + available = BigInt.from(available), + additional = BigInt.from(additional), + secondAvailable = secondAvailable == null ? null : BigInt.from(secondAvailable), + secondAdditional = secondAdditional == null ? null : BigInt.from(secondAdditional), + frozen = frozen == null ? null : BigInt.from(frozen) {} - final int additional; - final int? secondAvailable; - final int? secondAdditional; - String get formattedAvailableBalance; - String get formattedAdditionalBalance; + final BigInt available; + final BigInt additional; + + final BigInt? secondAvailable; + final BigInt? secondAdditional; + + final BigInt? frozen; + + BigInt get fullAvailableBalance => available; + + @deprecated String get formattedUnAvailableBalance => ''; - String get formattedSecondAvailableBalance => ''; - String get formattedSecondAdditionalBalance => ''; - String get formattedFullAvailableBalance => formattedAvailableBalance; } diff --git a/cw_core/lib/balance_card_style_settings.dart b/cw_core/lib/balance_card_style_settings.dart new file mode 100644 index 000000000..6412f0d77 --- /dev/null +++ b/cw_core/lib/balance_card_style_settings.dart @@ -0,0 +1,85 @@ +import 'package:cw_core/card_design.dart'; +import 'package:cw_core/db/sqlite.dart'; +import 'package:sqflite/sqflite.dart'; + +class BalanceCardStyleSettings { + final int walletInfoId; + final int accountIndex; + final int gradientIndex; + final bool useSpecialDesign; + final String backgroundImagePath; + final int cardOrder; + + BalanceCardStyleSettings( + {required this.walletInfoId, + required this.accountIndex, + required this.gradientIndex, + required this.useSpecialDesign, + required this.backgroundImagePath, + required this.cardOrder}); + + static const tableName = "BalanceCardStyleSettings"; + + Map toJson() { + final ret = { + "walletInfoId": walletInfoId, + "accountIndex": accountIndex, + "gradientIndex": gradientIndex, + "useSpecialDesign": useSpecialDesign ? 1 : 0, + "backgroundImagePath": backgroundImagePath, + "cardOrder": cardOrder, + }; + return ret; + } + + static BalanceCardStyleSettings fromJson(Map json) { + return BalanceCardStyleSettings( + walletInfoId: json["walletInfoId"] as int, + accountIndex: json["accountIndex"] as int, + gradientIndex: json["gradientIndex"] as int, + useSpecialDesign: json["useSpecialDesign"] == 1, + backgroundImagePath: json["backgroundImagePath"] as String? ?? "", + cardOrder: json["cardOrder"] as int? ?? -1, + ); + } + + static BalanceCardStyleSettings fromCardDesign( + int walletInfoId, int accountIndex, int cardOrder, CardDesign design) { + return BalanceCardStyleSettings( + walletInfoId: walletInfoId, + accountIndex: accountIndex, + gradientIndex: CardDesign.allGradients.indexOf(design.gradient), + useSpecialDesign: design.backgroundType == CardDesignBackgroundTypes.svgFull, + backgroundImagePath: + design.backgroundType == CardDesignBackgroundTypes.image ? design.imagePath : "", + cardOrder: cardOrder, + ); + } + + static Future get(int walletInfoId, int accountIndex) async { + final json = await db!.query( + tableName, + where: "walletInfoId = ? AND accountIndex = ?", + whereArgs: [walletInfoId, accountIndex], + ); + + if (json.isEmpty) { + return null; + } + + return fromJson(json.first); + } + + static Future> getAll(int walletInfoId) async { + final json = await db!.query( + tableName, + where: "walletInfoId = ?", + whereArgs: [walletInfoId], + ); + return List.generate(json.length, (index) => BalanceCardStyleSettings.fromJson(json[index])); + } + + Future insert() async { + db!.insert(tableName, toJson(), conflictAlgorithm: ConflictAlgorithm.replace); + } +} \ No newline at end of file diff --git a/cw_core/lib/card_design.dart b/cw_core/lib/card_design.dart new file mode 100644 index 000000000..5786abebb --- /dev/null +++ b/cw_core/lib/card_design.dart @@ -0,0 +1,433 @@ +import 'package:cw_core/balance_card_style_settings.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:flutter/material.dart'; + +enum CardDesignBackgroundTypes { image, svgIcon, svgFull } + +class CardColorCombination { + final Color textColor; + final Color textColorSecondary; + final Color backgroundImageColor; + + const CardColorCombination( + {required this.textColor, + required this.textColorSecondary, + required this.backgroundImageColor}); + + static const light = CardColorCombination( + textColor: Color.fromARGB(200, 255, 255, 255), + textColorSecondary: Colors.white54, + backgroundImageColor: Color.fromARGB(200, 0, 0, 0) + ); + + static const dark = CardColorCombination( + textColor: Color.fromARGB(200, 0, 0, 0), + textColorSecondary: Colors.black45, + backgroundImageColor: Color.fromARGB(200, 255, 255, 255), + ); + + static const black = CardColorCombination( + textColor: Color.fromARGB(200, 255, 255, 255), + textColorSecondary: Colors.white54, + backgroundImageColor: Color.fromARGB(200, 255, 255, 255), + ); +} + +class CardDesign { + final Gradient gradient; + final String imagePath; + final CardDesignBackgroundTypes backgroundType; + final CardColorCombination colors; + + const CardDesign( + {this.backgroundType = CardDesignBackgroundTypes.svgIcon, + this.gradient = const LinearGradient(colors: [Colors.black], begin: Alignment.topCenter, end: Alignment.bottomCenter), + this.imagePath = "assets/new-ui/blank.svg", + this.colors = CardColorCombination.dark}); + + static const LinearGradient gradientOrange = LinearGradient( + colors: [Color(0xFFFF7C02), Color(0xFFFF5602)], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ); + + static const LinearGradient gradientYellow = LinearGradient( + colors: [Color(0xFFFFD000), Color(0xFFFFAA00)], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ); + static const LinearGradient gradientGreen = LinearGradient( + colors: [Color(0xFF5AA438), Color(0xFF5AA438)], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ); + + static const LinearGradient gradientBlue = LinearGradient( + colors: [Color(0xFF4EBEFF), Color(0xFF1D78FF)], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ); + + static const LinearGradient gradientPurple = LinearGradient( + colors: [Color(0xFF9E30FF), Color(0xFF7100BD)], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ); + + static const LinearGradient gradientPink = LinearGradient( + colors: [Color(0xFFFF6CD3), Color(0xFFF200A9)], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ); + + static const LinearGradient gradientRed = LinearGradient( + colors: [Color(0xFFFF2222), Color(0xFFA10000)], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ); + + static const LinearGradient gradientSilver = LinearGradient( + colors: [Color(0xFFE0E8FF), Color(0xFF6D8ADE)], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ); + + static const LinearGradient gradientGold = LinearGradient( + colors: [Color(0xFFE9CA74), Color(0xFF886A14)], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ); + + static const LinearGradient gradientBlack = LinearGradient( + colors: [Color(0xFF2A2A2A), Color(0xFF000000)], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ); + + static const List allGradients = [ + gradientOrange, + gradientYellow, + gradientGreen, + gradientBlue, + gradientPurple, + gradientPink, + gradientRed, + gradientSilver, + gradientGold, + gradientBlack, + ]; + + static const genericDefault = CardDesign(gradient: gradientBlue); + + static const btc = CardDesign( + imagePath: "assets/new-ui/balance_card_icons/bitcoin.svg"); + + static const eth = CardDesign( + imagePath: "assets/new-ui/balance_card_icons/ethereum.svg"); + + static const btcln = CardDesign( + imagePath: "assets/new-ui/balance_card_icons/lightning.svg"); + + static const xmr = CardDesign( + imagePath: "assets/new-ui/balance_card_icons/monero.svg"); + + static const ltc = CardDesign( + imagePath: "assets/new-ui/balance_card_icons/litecoin.svg"); + + static const bch = CardDesign( + imagePath: "assets/new-ui/balance_card_icons/bitcoin_cash.svg"); + + static const doge = CardDesign( + imagePath: "assets/new-ui/balance_card_icons/dogecoin.svg"); + + static const base = CardDesign( + imagePath: "assets/new-ui/balance_card_icons/base.svg"); + + static const pol = CardDesign( + imagePath: "assets/new-ui/balance_card_icons/polygon.svg"); + + static const sol = CardDesign( + imagePath: "assets/new-ui/balance_card_icons/solana.svg"); + + static const tron = CardDesign( + imagePath: "assets/new-ui/balance_card_icons/tron.svg"); + + static const nano = CardDesign( + imagePath: "assets/new-ui/balance_card_icons/nano.svg"); + + static const zano = CardDesign( + imagePath: "assets/new-ui/balance_card_icons/zano.svg"); + + static const wow = CardDesign( + imagePath: "assets/new-ui/balance_card_icons/wownero.svg"); + + static const dcr = CardDesign( + imagePath: "assets/new-ui/balance_card_icons/decred.svg"); + + static const arbitrum = CardDesign( + imagePath: "assets/new-ui/balance_card_icons/arbitrum.svg"); + + static const zec = CardDesign( + imagePath: "assets/new-ui/balance_card_icons/zcash.svg"); + + static const bnb = CardDesign( + imagePath: "assets/new-ui/balance_card_icons/bnb.svg"); + + + + static const ethSpecial = CardDesign( + gradient: const LinearGradient( + colors: [Color(0xFF6259FF), Color(0xFF3B20E6)], + begin: Alignment.topCenter, end: Alignment.bottomCenter + ), + backgroundType: CardDesignBackgroundTypes.svgFull, + colors: CardColorCombination.light, + imagePath: "assets/new-ui/balance_card_backgrounds/ethereum.svg"); + + static const btcSpecial = CardDesign( + gradient: const LinearGradient( + colors: [Color(0xFFFFBF00), Color(0xFFFF6A00)], + begin: Alignment.topCenter, end: Alignment.bottomCenter + ), + backgroundType: CardDesignBackgroundTypes.svgFull, + imagePath: "assets/new-ui/balance_card_backgrounds/bitcoin.svg"); + + static const xmrSpecial = CardDesign( + gradient: const LinearGradient( + colors: [Color(0xFFFF5900), Color(0xFFE62E00)], + begin: Alignment.topCenter, end: Alignment.bottomCenter + ), + backgroundType: CardDesignBackgroundTypes.svgFull, + imagePath: "assets/new-ui/balance_card_backgrounds/monero.svg"); + + static const ltcSpecial = CardDesign( + gradient: const LinearGradient( + colors: [Color(0xFF2145BF), Color(0xFF072071)], + begin: Alignment.topCenter, end: Alignment.bottomCenter + ), + backgroundType: CardDesignBackgroundTypes.svgFull, + colors: CardColorCombination.light, + imagePath: "assets/new-ui/balance_card_backgrounds/litecoin.svg"); + + static const lnSpecial = CardDesign( + gradient: const LinearGradient( + colors: [Color(0xFFFFBF00), Color(0xFFFF6A00)], + begin: Alignment.topCenter, end: Alignment.bottomCenter + ), + backgroundType: CardDesignBackgroundTypes.svgFull, + colors: CardColorCombination.dark, + imagePath: "assets/new-ui/balance_card_backgrounds/lightning.svg"); + + static const tronSpecial = CardDesign( + gradient: const LinearGradient( + colors: [Color(0xFFFF1313), Color(0xFFB40000)], + begin: Alignment.topCenter, end: Alignment.bottomCenter + ), + backgroundType: CardDesignBackgroundTypes.svgFull, + colors: CardColorCombination.light, + imagePath: "assets/new-ui/balance_card_backgrounds/tron.svg"); + + static const solSpecial = CardDesign( + gradient: const LinearGradient( + colors: [Color(0xFF4701AA), Color(0xFF19004B)], + begin: Alignment.topCenter, end: Alignment.bottomCenter + ), + backgroundType: CardDesignBackgroundTypes.svgFull, + colors: CardColorCombination.light, + imagePath: "assets/new-ui/balance_card_backgrounds/solana.svg"); + + static const bchSpecial = CardDesign( + gradient: const LinearGradient( + colors: [Color(0xFF36DA4C), Color(0xFF008D57)], + begin: Alignment.topCenter, end: Alignment.bottomCenter + ), + backgroundType: CardDesignBackgroundTypes.svgFull, + imagePath: "assets/new-ui/balance_card_backgrounds/bitcoin-cash.svg"); + + static const wowSpecial = CardDesign( + gradient: const LinearGradient( + colors: [Color(0xFFFF6CD3), Color(0xFFF200A9)], + begin: Alignment.topCenter, end: Alignment.bottomCenter + ), + backgroundType: CardDesignBackgroundTypes.svgFull, + imagePath: "assets/new-ui/balance_card_backgrounds/wownero.svg"); + + static const dogeSpecial = CardDesign( + gradient: const LinearGradient( + colors: [Color(0xFFCBA818), Color(0xFF885D00)], + begin: Alignment.topCenter, end: Alignment.bottomCenter + ), + backgroundType: CardDesignBackgroundTypes.svgFull, + imagePath: "assets/new-ui/balance_card_backgrounds/dogecoin.svg"); + + static const nanoSpecial = CardDesign( + gradient: const LinearGradient( + colors: [Color(0xFF209CE9), Color(0xFF0073CB)], + begin: Alignment.topCenter, end: Alignment.bottomCenter + ), + backgroundType: CardDesignBackgroundTypes.svgFull, + imagePath: "assets/new-ui/balance_card_backgrounds/nano.svg"); + + static const polSpecial = CardDesign( + gradient: const LinearGradient( + colors: [Color(0xFF863FE3), Color(0xFF59098E)], + begin: Alignment.topCenter, end: Alignment.bottomCenter + ), + backgroundType: CardDesignBackgroundTypes.svgFull, + colors: CardColorCombination.light, + imagePath: "assets/new-ui/balance_card_backgrounds/polygon.svg"); + + static const dcrSpecial = CardDesign( + gradient: const LinearGradient( + colors: [Color(0xFF2871FF), Color(0xFF0057FF)], + begin: Alignment.topCenter, end: Alignment.bottomCenter + ), + colors: CardColorCombination.light, + backgroundType: CardDesignBackgroundTypes.svgFull, + imagePath: "assets/new-ui/balance_card_backgrounds/decred.svg"); + + static const zanoSpecial = CardDesign( + gradient: const LinearGradient( + colors: [Color(0xFF0004B4), Color(0xFF170069)], + begin: Alignment.topCenter, end: Alignment.bottomCenter + ), + colors: CardColorCombination.black, + backgroundType: CardDesignBackgroundTypes.svgFull, + imagePath: "assets/new-ui/balance_card_backgrounds/zano.svg"); + + static const baseSpecial = CardDesign( + gradient: const LinearGradient( + colors: [Color(0xFF003CFF), Color(0xFF000087)], + begin: Alignment.topCenter, end: Alignment.bottomCenter + ), + colors: CardColorCombination.light, + backgroundType: CardDesignBackgroundTypes.svgFull, + imagePath: "assets/new-ui/balance_card_backgrounds/base.svg"); + + static const arbitrumSpecial = CardDesign( + gradient: const LinearGradient( + colors: [Color(0xFF173F76), Color(0xFF031A39)], + begin: Alignment.topCenter, end: Alignment.bottomCenter + ), + colors: CardColorCombination.light, + backgroundType: CardDesignBackgroundTypes.svgFull, + imagePath: "assets/new-ui/balance_card_backgrounds/arbitrum.svg"); + + static const zecSpecial = CardDesign( + gradient: const LinearGradient( + colors: [Color(0xFFF6C527), Color(0xFFCD8C00)], + begin: Alignment.topCenter, end: Alignment.bottomCenter + ), + backgroundType: CardDesignBackgroundTypes.svgFull, + imagePath: "assets/new-ui/balance_card_backgrounds/zcash.svg"); + + static const bnbSpecial = CardDesign( + gradient: const LinearGradient( + colors: [Color(0xFF603000), Color(0xFFF0B90B)], + begin: Alignment.topCenter, end: Alignment.bottomCenter + ), + colors: CardColorCombination.light, + backgroundType: CardDesignBackgroundTypes.svgFull, + imagePath: "assets/new-ui/balance_card_backgrounds/bnb.svg"); + + CardDesign withGradient(Gradient gradient) => CardDesign( + gradient: gradient, colors: preferredColorCombinations[gradient] ?? colors, imagePath: imagePath, backgroundType: backgroundType); + + CardDesign withGradientAndColorCombination(Gradient gradient, CardColorCombination cardColorCombination) => CardDesign( + gradient: gradient, colors: cardColorCombination, imagePath: imagePath, backgroundType: backgroundType); + + static const List all = [genericDefault, btc, eth, xmr, ltc, eth, pol, doge, base, sol, btcln, tron, zano, dcr, arbitrum, zec, bnb, ethSpecial, btcSpecial, xmrSpecial, ltcSpecial, lnSpecial, tronSpecial, bchSpecial, wowSpecial, dogeSpecial, polSpecial, dcrSpecial, zanoSpecial, arbitrumSpecial, zecSpecial, bnbSpecial]; + + static CardDesign forCurrencySpecial(CryptoCurrency currency) { + return specialDesignsForCurrencies[currency] ?? genericDefault; + } + + static CardDesign forCurrencyIcon(CryptoCurrency currency) { + return iconDesignsForCurrencies[currency] ?? genericDefault; + } + + static const Map iconDesignsForCurrencies = { + CryptoCurrency.xmr: xmr, + CryptoCurrency.btc: btc, + CryptoCurrency.eth: eth, + CryptoCurrency.ltc: ltc, + CryptoCurrency.btcln: btcln, + CryptoCurrency.trx: tron, + CryptoCurrency.sol: sol, + CryptoCurrency.maticpoly: pol, + CryptoCurrency.baseEth: base, + CryptoCurrency.bch: bch, + CryptoCurrency.wow: wow, + CryptoCurrency.doge: doge, + CryptoCurrency.nano: nano, + CryptoCurrency.zano: zano, + CryptoCurrency.arbEth: arbitrum, + CryptoCurrency.zec: zec, + CryptoCurrency.dcr: dcr, + CryptoCurrency.bnb: bnb, + + }; + + static const Map specialDesignsForCurrencies = { + CryptoCurrency.xmr: xmrSpecial, + CryptoCurrency.btc: btcSpecial, + CryptoCurrency.eth: ethSpecial, + CryptoCurrency.ltc: ltcSpecial, + CryptoCurrency.btcln: lnSpecial, + CryptoCurrency.trx: tronSpecial, + CryptoCurrency.sol: solSpecial, + CryptoCurrency.bch: bchSpecial, + CryptoCurrency.wow: wowSpecial, + CryptoCurrency.doge: dogeSpecial, + CryptoCurrency.nano: nanoSpecial, + CryptoCurrency.maticpoly: polSpecial, + CryptoCurrency.dcr: dcrSpecial, + CryptoCurrency.zano: zanoSpecial, + CryptoCurrency.baseEth: baseSpecial, + CryptoCurrency.arbEth: arbitrumSpecial, + CryptoCurrency.zec: zecSpecial, + CryptoCurrency.bnb: bnbSpecial, + }; + + static Map preferredColorCombinations = { + CardDesign.gradientOrange: CardColorCombination.dark, + CardDesign.gradientYellow: CardColorCombination.dark, + CardDesign.gradientGreen: CardColorCombination.light, + CardDesign.gradientBlue: CardColorCombination.dark, + CardDesign.gradientPurple: CardColorCombination.light, + CardDesign.gradientPink: CardColorCombination.dark, + CardDesign.gradientRed: CardColorCombination.light, + CardDesign.gradientSilver: CardColorCombination.dark, + CardDesign.gradientGold: CardColorCombination.dark, + CardDesign.gradientBlack: CardColorCombination.black, + }; + + static CardDesign fromStyleSettings( + BalanceCardStyleSettings? setting, CryptoCurrency walletCurrency) { + if (setting == null) { + return CardDesign.forCurrencySpecial(walletCurrency); + } else if (setting.backgroundImagePath.isNotEmpty) { + return CardDesign( + imagePath: setting.backgroundImagePath, + ); + } else if (setting.useSpecialDesign && setting.gradientIndex != -1) { + return CardDesign.forCurrencySpecial(walletCurrency) + .withGradient(CardDesign.allGradients[setting.gradientIndex]); + } else if (!setting.useSpecialDesign && setting.gradientIndex == -1) { + final specialColors = + specialDesignsForCurrencies[walletCurrency] ?? + genericDefault; + return CardDesign.forCurrencyIcon(walletCurrency) + .withGradientAndColorCombination(specialColors.gradient, specialColors.colors); + } else if (setting.useSpecialDesign) { + return CardDesign.forCurrencySpecial(walletCurrency); + } else if (setting.gradientIndex != -1) { + return CardDesign.forCurrencyIcon(walletCurrency) + .withGradient(CardDesign.allGradients[setting.gradientIndex]); + } else { + printV("somehow, the user saved the design settings with literally no customization?"); + return CardDesign.forCurrencySpecial(walletCurrency); + } + } +} \ No newline at end of file diff --git a/cw_core/lib/crypto_amount_format.dart b/cw_core/lib/crypto_amount_format.dart index 1be927c2a..1fb0d5ecf 100644 --- a/cw_core/lib/crypto_amount_format.dart +++ b/cw_core/lib/crypto_amount_format.dart @@ -1,3 +1,5 @@ +import 'package:intl/intl.dart'; + double cryptoAmountToDouble({required num amount, required num divider}) => amount / divider; extension MaxDecimals on String { @@ -14,4 +16,43 @@ extension MaxDecimals on String { return parts.join("."); } + + + /// Format a stringified number to a localized representation + /// 1.000.000,00 in de_DE + /// 1’000’000.00 in de_CH + /// 1,000,000.00 in en_US + /// + /// Can also handle amounts suffixed with a Symbol + /// 1.000.000,00 XMR in de_DE + /// 1’000’000.00 XMR in de_CH + /// 1,000,000.00 XMR in en_US + /// + /// Can also handle amounts prefixed with a size relational operator + /// < 1.000.000,00 XMR in de_DE + /// > 1’000’000.00 XMR in de_CH + /// < 1,000,000.00 XMR in en_US + /// + /// DO NOT PARSE THE LOCALIZED STRING TO A NUMBER IF YOU WANT TO KEEP YOUR SANITY! + String withLocalSeperator([String? locale]) { + if (contains(" ")) { + final parts = split(" "); + + if ([">", "<"].contains(parts.first)) { + final result = [parts.first, parts[1].withLocalSeperator(locale)]; + if (parts.length > 2) result.addAll(parts.sublist(2)); + + return result.join(" "); + } + + return [parts.first.withLocalSeperator(locale), ...parts.sublist(1)].join(" "); + } + + final formater = NumberFormat("#,###", locale); + final parts = replaceAll(",", "").split("."); + if (parts.first.contains("< 0")) parts.first = "0"; + + return [formater.format(int.tryParse(parts.first) ?? 0), ...parts.sublist(1)] + .join(formater.symbols.DECIMAL_SEP); + } } diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 96826cca2..03e48eebc 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -1,10 +1,9 @@ import 'package:cw_core/currency.dart'; import 'package:cw_core/enumerable_item.dart'; import 'package:collection/collection.dart'; +import 'package:cw_core/format_fixed.dart'; import 'package:cw_core/parse_fixed.dart'; -import 'format_fixed.dart'; - class CryptoCurrency extends EnumerableItem with Serializable implements Currency { const CryptoCurrency({ String title = '', @@ -13,16 +12,20 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen required this.decimals, this.fullName, this.iconPath, + this.flatIconPath, this.tag, + this.chainIconPath, this.enabled = false, this.isPotentialScam = false, - }) - : super(title: title, raw: raw); + }) : super(title: title, raw: raw); final String name; final String? tag; final String? fullName; final String? iconPath; + final String? flatIconPath; + final String? chainIconPath; + @override final int decimals; final bool enabled; final bool isPotentialScam; @@ -144,20 +147,20 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const zcashCurrencies = [ zec, zaddr, tzec ]; // title, tag (if applicable), fullName (if unique), raw, name, iconPath - static const xmr = CryptoCurrency(title: 'XMR', fullName: 'Monero', raw: 0, name: 'xmr', iconPath: 'assets/images/crypto/monero.webp', decimals: 12); + static const xmr = CryptoCurrency(title: 'XMR', fullName: 'Monero', raw: 0, name: 'xmr', iconPath: 'assets/images/crypto/monero.webp', decimals: 12,flatIconPath: "assets/new-ui/balance_card_icons/monero.svg"); static const ada = CryptoCurrency(title: 'ADA', fullName: 'Cardano', raw: 1, name: 'ada', iconPath: 'assets/images/ada_icon.png', decimals: 6); - static const bch = CryptoCurrency(title: 'BCH', fullName: 'Bitcoin Cash', raw: 2, name: 'bch', iconPath: 'assets/images/crypto/bitcoin-cash.webp', decimals: 8); - static const bnb = CryptoCurrency(title: 'BNB', tag: 'BSC', fullName: 'Binance Coin', raw: 3, name: 'bnb', iconPath: 'assets/images/crypto/BNB.webp', decimals: 8); - static const btc = CryptoCurrency(title: 'BTC', fullName: 'Bitcoin', raw: 4, name: 'btc', iconPath: 'assets/images/crypto/bitcoin.webp', decimals: 8); + static const bch = CryptoCurrency(title: 'BCH', fullName: 'Bitcoin Cash', raw: 2, name: 'bch', iconPath: 'assets/images/crypto/bitcoin-cash.webp', decimals: 8,flatIconPath: "assets/new-ui/balance_card_icons/bitcoin_cash.svg"); + static const bnb = CryptoCurrency(title: 'BNB', tag: 'BSC', fullName: 'BNB', raw: 3, name: 'bnb', iconPath: 'assets/images/crypto/BNB.webp', decimals: 18, flatIconPath: "assets/new-ui/balance_card_icons/bnb.svg", chainIconPath: "assets/new-ui/chain_badges/bnb smart chain.svg"); + static const btc = CryptoCurrency(title: 'BTC', fullName: 'Bitcoin', raw: 4, name: 'btc', iconPath: 'assets/images/crypto/bitcoin.webp', decimals: 8,flatIconPath: "assets/new-ui/balance_card_icons/bitcoin.svg"); static const dai = CryptoCurrency(title: 'DAI', tag: 'ETH', fullName: 'Dai', raw: 5, name: 'dai', iconPath: 'assets/images/crypto/dai.webp', decimals: 18); static const dash = CryptoCurrency(title: 'DASH', fullName: 'Dash', raw: 6, name: 'dash', iconPath: 'assets/images/dash_icon.png', decimals: 8); static const eos = CryptoCurrency(title: 'EOS', fullName: 'EOS', raw: 7, name: 'eos', iconPath: 'assets/images/eos_icon.png', decimals: 4); - static const eth = CryptoCurrency(title: 'ETH', fullName: 'Ethereum', raw: 8, name: 'eth', iconPath: 'assets/images/crypto/ethereum.webp', decimals: 18); - static const ltc = CryptoCurrency(title: 'LTC', fullName: 'Litecoin', raw: 9, name: 'ltc', iconPath: 'assets/images/crypto/litecoin.webp', decimals: 8); - static const nano = CryptoCurrency(title: 'XNO', fullName: 'Nano', raw: 10, name: 'xno', iconPath: 'assets/images/crypto/nano.webp', decimals: 30); - static const trx = CryptoCurrency(title: 'TRX', fullName: 'TRON', raw: 11, name: 'trx', iconPath: 'assets/images/crypto/tron.webp', decimals: 6); - static const usdt = CryptoCurrency(title: 'USDT', tag: 'OMNI', fullName: 'USDT Tether', raw: 12, name: 'usdt', iconPath: 'assets/images/crypto/tether.webp', decimals: 6); - static const usdterc20 = CryptoCurrency(title: 'USDT', tag: 'ETH', fullName: 'USDT Tether', raw: 13, name: 'usdterc20', iconPath: 'assets/images/crypto/tether.webp', decimals: 6); + static const eth = CryptoCurrency(title: 'ETH', fullName: 'Ethereum', raw: 8, name: 'eth', iconPath: 'assets/images/crypto/ethereum.webp', decimals: 18,flatIconPath: "assets/new-ui/balance_card_icons/ethereum.svg", chainIconPath: "assets/new-ui/chain_badges/ethereum.svg"); + static const ltc = CryptoCurrency(title: 'LTC', fullName: 'Litecoin', raw: 9, name: 'ltc', iconPath: 'assets/images/crypto/litecoin.webp', decimals: 8,flatIconPath: "assets/new-ui/balance_card_icons/litecoin.svg", chainIconPath: "assets/new-ui/chain_badges/mweb.svg"); + static const nano = CryptoCurrency(title: 'XNO', fullName: 'Nano', raw: 10, name: 'xno', iconPath: 'assets/images/crypto/nano.webp', decimals: 30,flatIconPath: "assets/new-ui/balance_card_icons/nano.svg"); + static const trx = CryptoCurrency(title: 'TRX', fullName: 'TRON', raw: 11, name: 'trx', iconPath: 'assets/images/crypto/tron.webp', decimals: 6,flatIconPath: "assets/new-ui/balance_card_icons/tron.svg", chainIconPath: "assets/new-ui/chain_badges/tron.svg"); + static const usdt = CryptoCurrency(title: 'USDT', tag: 'OMNI', fullName: 'Tether', raw: 12, name: 'usdt', iconPath: 'assets/images/crypto/tether.webp', decimals: 6); + static const usdterc20 = CryptoCurrency(title: 'USDT', tag: 'ETH', fullName: 'Tether', raw: 13, name: 'usdterc20', iconPath: 'assets/images/crypto/tether.webp', decimals: 6); static const xlm = CryptoCurrency(title: 'XLM', fullName: 'Stellar', raw: 14, name: 'xlm', iconPath: 'assets/images/xlm_icon.png', decimals: 7); static const xrp = CryptoCurrency(title: 'XRP', fullName: 'Ripple', raw: 15, name: 'xrp', iconPath: 'assets/images/xrp_icon.png', decimals: 6); static const xhv = CryptoCurrency(title: 'XHV', fullName: 'Haven Protocol', raw: 16, name: 'xhv', iconPath: 'assets/images/xhv_logo.png', decimals: 12); @@ -180,39 +183,39 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const avaxc = CryptoCurrency(title: 'AVAX', tag: 'AVAXC', fullName: 'Avalanche', raw: 31, name: 'avaxc', iconPath: 'assets/images/avaxc_icon.png', decimals: 9); static const btt = CryptoCurrency(title: 'BTT', tag: 'ETH', fullName: 'BitTorrent', raw: 32, name: 'btt', iconPath: 'assets/images/btt_icon.png', decimals: 18); static const bttc = CryptoCurrency(title: 'BTTC', tag: 'TRX', fullName: 'BitTorrent-NEW', raw: 33, name: 'bttc', iconPath: 'assets/images/btt_icon.png', decimals: 18); - static const doge = CryptoCurrency(title: 'DOGE', fullName: 'Dogecoin', raw: 34, name: 'doge', iconPath: 'assets/images/crypto/dogecoin.webp', decimals: 8); + static const doge = CryptoCurrency(title: 'DOGE', fullName: 'Dogecoin', raw: 34, name: 'doge', iconPath: 'assets/images/crypto/dogecoin.webp', decimals: 8,flatIconPath: "assets/new-ui/balance_card_icons/dogecoin.svg"); static const firo = CryptoCurrency(title: 'FIRO', raw: 35, name: 'firo', iconPath: 'assets/images/firo_icon.png', decimals: 8); - static const usdttrc20 = CryptoCurrency(title: 'USDT', tag: 'TRX', fullName: 'USDT Tether', raw: 36, name: 'usdttrc20', iconPath: 'assets/images/crypto/tether.webp', decimals: 6); + static const usdttrc20 = CryptoCurrency(title: 'USDT', tag: 'TRX', fullName: 'Tether', raw: 36, name: 'usdttrc20', iconPath: 'assets/images/crypto/tether.webp', decimals: 6); static const hbar = CryptoCurrency(title: 'HBAR', fullName: 'Hedera', raw: 37, name: 'hbar', iconPath: 'assets/images/hbar_icon.png', decimals: 8); static const sc = CryptoCurrency(title: 'SC', fullName: 'Siacoin', raw: 38, name: 'sc', iconPath: 'assets/images/sc_icon.png', decimals: 16); - static const sol = CryptoCurrency(title: 'SOL', fullName: 'Solana', raw: 39, name: 'sol', iconPath: 'assets/images/crypto/solana.webp', decimals: 9); - static const usdc = CryptoCurrency(title: 'USDC', tag: 'ETH', fullName: 'USD Coin', raw: 40, name: 'usdc', iconPath: 'assets/images/crypto/usdc.webp', decimals: 6); + static const sol = CryptoCurrency(title: 'SOL', fullName: 'Solana', raw: 39, name: 'sol', iconPath: 'assets/images/crypto/solana.webp', decimals: 9,flatIconPath: "assets/new-ui/balance_card_icons/solana.svg", chainIconPath: "assets/new-ui/chain_badges/solana.svg"); + static const usdc = CryptoCurrency(title: 'USDC', tag: 'ETH', fullName: 'USDC', raw: 40, name: 'usdc', iconPath: 'assets/images/crypto/usdc.webp', decimals: 6); static const usdcsol = CryptoCurrency(title: 'USDC', tag: 'SOL', fullName: 'USDC Coin', raw: 41, name: 'usdcsol', iconPath: 'assets/images/crypto/usdc.webp', decimals: 6); static const zaddr = CryptoCurrency(title: 'ZZEC', tag: 'ZEC', fullName: 'Shielded Zcash', raw: 42, name: 'zaddr', iconPath: 'assets/images/zec_icon.png', decimals: 8); static const tzec = CryptoCurrency(title: 'tZEC', tag: 'ZEC', fullName: 'Transparent Zcash', raw: 43, name: 'zec', iconPath: 'assets/images/zec_icon.png', decimals: 8); static const zen = CryptoCurrency(title: 'ZEN', fullName: 'Horizen', raw: 44, name: 'zen', iconPath: 'assets/images/zen_icon.png', decimals: 8); static const xvg = CryptoCurrency(title: 'XVG', fullName: 'Verge', raw: 45, name: 'xvg', iconPath: 'assets/images/xvg_icon.png', decimals: 8); - static const usdcpoly = CryptoCurrency(title: 'USDC', tag: 'POL', fullName: 'USD Coin', raw: 46, name: 'usdcpoly', iconPath: 'assets/images/crypto/usdc.webp', decimals: 6); - static const dcr = CryptoCurrency(title: 'DCR', fullName: 'Decred', raw: 47, name: 'dcr', iconPath: 'assets/images/crypto/decred.webp', decimals: 8); + static const usdcpoly = CryptoCurrency(title: 'USDC', tag: 'POL', fullName: 'USDC', raw: 46, name: 'usdcpoly', iconPath: 'assets/images/crypto/usdc.webp', decimals: 6); + static const dcr = CryptoCurrency(title: 'DCR', fullName: 'Decred', raw: 47, name: 'dcr', iconPath: 'assets/images/crypto/decred.webp', decimals: 8,flatIconPath: "assets/new-ui/balance_card_icons/decred.svg"); static const kmd = CryptoCurrency(title: 'KMD', fullName: 'Komodo', raw: 48, name: 'kmd', iconPath: 'assets/images/kmd_icon.png', decimals: 8); static const mana = CryptoCurrency(title: 'MANA', tag: 'ETH', fullName: 'Decentraland', raw: 49, name: 'mana', iconPath: 'assets/images/mana_icon.png', decimals: 18); - static const maticpoly = CryptoCurrency(title: 'POL', tag: 'POL', fullName: 'Polygon', raw: 50, name: 'maticpoly', iconPath: 'assets/images/crypto/polygon.webp', decimals: 18); - static const matic = CryptoCurrency(title: 'MATIC', tag: 'ETH', fullName: 'Polygon', raw: 51, name: 'matic', iconPath: 'assets/images/crypto/polygon.webp', decimals: 18); + static const maticpoly = CryptoCurrency(title: 'POL', tag: 'POL', fullName: 'Polygon', raw: 50, name: 'maticpoly', iconPath: 'assets/images/crypto/polygon.webp', decimals: 18, flatIconPath: "assets/new-ui/balance_card_icons/polygon.svg", chainIconPath: "assets/new-ui/chain_badges/polygon.svg"); + static const matic = CryptoCurrency(title: 'MATIC', tag: 'ETH', fullName: 'Polygon', raw: 51, name: 'matic', iconPath: 'assets/images/crypto/polygon.webp', decimals: 18, flatIconPath: "assets/new-ui/balance_card_icons/polygon.svg"); static const mkr = CryptoCurrency(title: 'MKR', tag: 'ETH', fullName: 'Maker', raw: 52, name: 'mkr', iconPath: 'assets/images/mkr_icon.png', decimals: 18); static const near = CryptoCurrency(title: 'NEAR', fullName: 'NEAR Protocol', raw: 53, name: 'near', iconPath: 'assets/images/near_icon.png', decimals: 24); static const oxt = CryptoCurrency(title: 'OXT', tag: 'ETH', fullName: 'Orchid', raw: 54, name: 'oxt', iconPath: 'assets/images/oxt_icon.png', decimals: 18); - static const paxg = CryptoCurrency(title: 'PAXG', tag: 'ETH', fullName: 'Pax Gold', raw: 55, name: 'paxg', iconPath: 'assets/images/paxg_icon.png', decimals: 18); + static const paxg = CryptoCurrency(title: 'PAXG', tag: 'ETH', fullName: 'Pax Gold', raw: 55, name: 'paxg', iconPath: 'assets/images/paxg.webp', decimals: 18); static const pivx = CryptoCurrency(title: 'PIVX', raw: 56, name: 'pivx', iconPath: 'assets/images/pivx_icon.png', decimals: 8); static const rune = CryptoCurrency(title: 'RUNE', fullName: 'Thorchain', raw: 57, name: 'rune', iconPath: 'assets/images/rune_icon.png', decimals: 18); static const rvn = CryptoCurrency(title: 'RVN', fullName: 'Ravencoin', raw: 58, name: 'rvn', iconPath: 'assets/images/rvn_icon.png', decimals: 8); static const scrt = CryptoCurrency(title: 'SCRT', fullName: 'Secret Network', raw: 59, name: 'scrt', iconPath: 'assets/images/scrt_icon.png', decimals: 6); static const uni = CryptoCurrency(title: 'UNI', tag: 'ETH', fullName: 'Uniswap', raw: 60, name: 'uni', iconPath: 'assets/images/uni_icon.png', decimals: 18); static const stx = CryptoCurrency(title: 'STX', fullName: 'Stacks', raw: 61, name: 'stx', iconPath: 'assets/images/stx_icon.png', decimals: 8); - static const btcln = CryptoCurrency(title: 'BTC', tag: 'LN', fullName: 'Bitcoin Lightning Network', raw: 62, name: 'btcln', iconPath: 'assets/images/crypto/bitcoin.webp', decimals: 8); + static const btcln = CryptoCurrency(title: 'BTC', tag: 'LN', fullName: 'Bitcoin Lightning Network', raw: 62, name: 'btcln', iconPath: 'assets/images/crypto/lightning.webp', decimals: 8); static const shib = CryptoCurrency(title: 'SHIB', tag: 'ETH', fullName: 'Shiba Inu', raw: 63, name: 'shib', iconPath: 'assets/images/shib_icon.png', decimals: 18); static const aave = CryptoCurrency(title: 'AAVE', tag: 'ETH', fullName: 'Aave', raw: 64, name: 'aave', iconPath: 'assets/images/aave_icon.png', decimals: 18); - static const arb = CryptoCurrency(title: 'ARB', fullName: 'Arbitrum', raw: 65, name: 'arb', iconPath: 'assets/images/crypto/arbitrum.webp', decimals: 18); + static const arb = CryptoCurrency(title: 'ARB', fullName: 'Arbitrum', raw: 65, name: 'arb', iconPath: 'assets/images/crypto/arbitrum.webp', decimals: 18, flatIconPath: "assets/new-ui/balance_card_icons/arbitrum.svg", chainIconPath: "assets/new-ui/chain_badges/arbitrum.svg"); static const bat = CryptoCurrency(title: 'BAT', tag: 'ETH', fullName: 'Basic Attention Token', raw: 66, name: 'bat', iconPath: 'assets/images/bat_icon.png', decimals: 18); static const comp = CryptoCurrency(title: 'COMP', tag: 'ETH', fullName: 'Compound', raw: 67, name: 'comp', iconPath: 'assets/images/comp_icon.png', decimals: 18); static const cro = CryptoCurrency(title: 'CRO', tag: 'ETH', fullName: 'Crypto.com Cronos', raw: 68, name: 'cro', iconPath: 'assets/images/cro_icon.png', decimals: 8); @@ -233,17 +236,17 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const zrx = CryptoCurrency(title: 'ZRX', tag: 'ETH', fullName: '0x Protocol', raw: 83, name: 'zrx', iconPath: 'assets/images/zrx_icon.png', decimals: 18); static const dydx = CryptoCurrency(title: 'DYDX', tag: 'ETH', fullName: 'dYdX', raw: 84, name: 'dydx', iconPath: 'assets/images/dydx_icon.png', decimals: 18); static const steth = CryptoCurrency(title: 'STETH', tag: 'ETH', fullName: 'Lido Staked Ethereum', raw: 85, name: 'steth', iconPath: 'assets/images/crypto/ethereum.webp', decimals: 18); - static const banano = CryptoCurrency(title: 'BAN', fullName: 'Banano', raw: 86, name: 'banano', iconPath: 'assets/images/nano_icon.png', decimals: 29); - static const usdtPoly = CryptoCurrency(title: 'USDT', tag: 'POL', fullName: 'Tether USD (PoS)', raw: 87, name: 'usdtpoly', iconPath: 'assets/images/crypto/tether.webp', decimals: 6); - static const usdcEPoly = CryptoCurrency(title: 'USDC.E', tag: 'POL', fullName: 'USD Coin (PoS)', raw: 88, name: 'usdcepoly', iconPath: 'assets/images/crypto/usdc.webp', decimals: 6); + static const banano = CryptoCurrency(title: 'BAN', fullName: 'Banano', raw: 86, name: 'banano', iconPath: 'assets/images/nano_icon.png', decimals: 29,flatIconPath: "assets/new-ui/balance_card_icons/nano.svg"); + static const usdtPoly = CryptoCurrency(title: 'USDT', tag: 'POL', fullName: 'Tether (PoS)', raw: 87, name: 'usdtpoly', iconPath: 'assets/images/crypto/tether.webp', decimals: 6); + static const usdcEPoly = CryptoCurrency(title: 'USDC.E', tag: 'POL', fullName: 'USDC (PoS)', raw: 88, name: 'usdcepoly', iconPath: 'assets/images/crypto/usdc.webp', decimals: 6); static const kaspa = CryptoCurrency(title: 'KAS', fullName: 'Kaspa', raw: 89, name: 'kas', iconPath: 'assets/images/kaspa_icon.png', decimals: 8); static const digibyte = CryptoCurrency(title: 'DGB', fullName: 'DigiByte', raw: 90, name: 'dgb', iconPath: 'assets/images/digibyte.png', decimals: 8); - static const usdtSol = CryptoCurrency(title: 'USDT', tag: 'SOL', fullName: 'USDT Tether', raw: 91, name: 'usdtsol', iconPath: 'assets/images/crypto/tether.webp', decimals: 6); + static const usdtSol = CryptoCurrency(title: 'USDT', tag: 'SOL', fullName: 'Tether', raw: 91, name: 'usdtsol', iconPath: 'assets/images/crypto/tether.webp', decimals: 6); static const usdcTrc20 = CryptoCurrency(title: 'USDC', tag: 'TRX', fullName: 'USDC Coin', raw: 92, name: 'usdctrc20', iconPath: 'assets/images/crypto/usdc.webp', decimals: 6); static const tbtc = CryptoCurrency(title: 'tBTC', fullName: 'Testnet Bitcoin', raw: 93, name: 'tbtc', iconPath: 'assets/images/tbtc.png', decimals: 8); static const wow = CryptoCurrency(title: 'WOW', fullName: 'Wownero', raw: 94, name: 'wow', iconPath: 'assets/images/crypto/wownero.webp', decimals: 11); static const ton = CryptoCurrency(title: 'TON', fullName: 'Toncoin', raw: 95, name: 'ton', iconPath: 'assets/images/ton_icon.png', decimals: 8); - static const zano = CryptoCurrency(title: 'ZANO', tag: 'ZANO', fullName: 'Zano', raw: 96, name: 'zano', iconPath: 'assets/images/crypto/zano.webp', decimals: 12); + static const zano = CryptoCurrency(title: 'ZANO', tag: 'ZANO', fullName: 'Zano', raw: 96, name: 'zano', iconPath: 'assets/images/crypto/zano.webp', decimals: 12,flatIconPath: "assets/new-ui/balance_card_icons/zano.svg", chainIconPath: "assets/new-ui/chain_badges/zano.svg"); static const flip = CryptoCurrency(title: 'FLIP', tag: 'ETH', fullName: 'Chainflip', raw: 97, name: 'flip', iconPath: 'assets/images/flip_icon.png', decimals: 18); static const deuro = CryptoCurrency(title: 'DEURO', tag: 'ETH', fullName: 'Decentralized Euro', raw: 98, name: 'deuro', iconPath: 'assets/images/deuro_icon.png', decimals: 18); static const usdtbsc = CryptoCurrency(title: 'USDT', tag: 'BSC', fullName: 'USDT Binance coin', raw: 99, name: 'usdtbsc', iconPath: 'assets/images/crypto/tether.webp', decimals: 18); @@ -251,11 +254,13 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const deps = CryptoCurrency(title: 'DEPS', tag: 'ETH', fullName: 'Decentralized Euro Protocol Share', raw: 101, name: 'deps', iconPath: 'assets/images/deps_icon.png', decimals: 18); static const kbtc = CryptoCurrency(title: 'KBTC', tag: 'ETH', fullName: 'Kraken Wrapped Bitcoin', raw: 102, name: 'kbtc', iconPath: 'assets/images/kbtc_icon.png', decimals: 8); static const cbbtc = CryptoCurrency(title: 'CBBTC', tag: 'ETH', fullName: 'Coinbase Wrapped BTC', raw: 103, name: 'cbbtc', iconPath: 'assets/images/cbbtc_icon.png', decimals: 8); - static const baseEth = CryptoCurrency(title: 'ETH', tag: 'BASE', fullName: 'Ethereum', raw: 104, name: 'baseth', iconPath: 'assets/images/crypto/base_icon.webp', decimals: 18); + static const baseEth = CryptoCurrency(title: 'ETH', tag: 'BASE', fullName: 'Ethereum', raw: 104, name: 'baseth', iconPath: 'assets/images/crypto/ethereum.webp', decimals: 18, flatIconPath: "assets/new-ui/balance_card_icons/base.svg", chainIconPath: "assets/new-ui/chain_badges/base.svg"); static const usde = CryptoCurrency(title: 'USDE', tag: 'BASE', fullName: 'Ethena USDE', raw: 105, name: 'usde', iconPath: 'assets/images/crypto/ethena-usde-logo.png', decimals: 18); - static const arbEth = CryptoCurrency(title: 'ETH', tag: 'ARB', fullName: 'Ethereum (Arbitrum One)', raw: 106, name: 'arbeth', iconPath: 'assets/images/crypto/ethereum.webp', decimals: 18); - static const zec = CryptoCurrency(title: 'ZEC', fullName: 'Zcash', raw: 107, name: 'zec', iconPath: 'assets/images/zec_icon.png', decimals: 8); + static const arbEth = CryptoCurrency(title: 'ETH', tag: 'ARB', fullName: 'Ethereum', raw: 106, name: 'arbeth', iconPath: 'assets/images/crypto/ethereum.webp', decimals: 18, flatIconPath: "assets/new-ui/balance_card_icons/arbitrum.svg"); + static const zec = CryptoCurrency(title: 'ZEC', fullName: 'Zcash', raw: 107, name: 'zec', iconPath: 'assets/images/zec_icon.png', decimals: 8,flatIconPath: "assets/new-ui/balance_card_icons/zcash.svg"); static const usdcArb = CryptoCurrency(title: 'USDC', tag: 'ARB', fullName: 'USDC Coin', raw: 108, name: 'usdcarb', iconPath: 'assets/images/crypto/usdc.webp', decimals: 6); + static const ltcmweb = CryptoCurrency(title: 'LTC', fullName: 'Litecoin MWeb', raw: 107, name: 'ltcmweb', iconPath: 'assets/images/crypto/litecoin.webp', decimals: 8); + static final Map _rawCurrencyMap = [...all, ...havenCurrencies, ...zcashCurrencies].fold>({}, (acc, item) { @@ -293,6 +298,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen 'wrappedether': weth, 'shibainu': shib, 'zcash': zec, + 'lightning': btcln, 'base': baseEth, 'arbitrum': arbEth, }; @@ -321,6 +327,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen return CryptoCurrency.all.firstWhere((element) => element.title.toLowerCase() == name.toLowerCase() && (element.tag == null || + element.tag == name || element.tag == walletCurrency?.title || element.tag == walletCurrency?.tag)); } catch (_) {} @@ -353,11 +360,13 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen }) { if (raw == null || raw.isEmpty) return null; + if(tag?.toLowerCase() == "lightning") tag = "LN"; + if (tag != null && tag.isNotEmpty) { final match = CryptoCurrency.all.firstWhereOrNull( (e) => e.title.toUpperCase() == raw.toUpperCase() && - e.tag?.toUpperCase() == tag.toUpperCase(), + e.tag?.toUpperCase() == tag?.toUpperCase(), ); if (match != null) return match; return null; @@ -397,19 +406,18 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen int? decimals, bool? enabled, bool? isPotentialScam, - }) { - return CryptoCurrency( - title: title ?? this.title, - raw: raw ?? this.raw, - name: name ?? this.name, - fullName: fullName ?? this.fullName, - iconPath: iconPath ?? this.iconPath, - tag: tag ?? this.tag, - decimals: decimals ?? this.decimals, - enabled: enabled ?? this.enabled, - isPotentialScam: isPotentialScam ?? this.isPotentialScam, - ); - } + }) => + CryptoCurrency( + title: title ?? this.title, + raw: raw ?? this.raw, + name: name ?? this.name, + fullName: fullName ?? this.fullName, + iconPath: iconPath ?? this.iconPath, + tag: tag ?? this.tag, + decimals: decimals ?? this.decimals, + enabled: enabled ?? this.enabled, + isPotentialScam: isPotentialScam ?? this.isPotentialScam, + ); @override String toString() => title; diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart index 65b752484..f6a884954 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -97,3 +97,16 @@ CryptoCurrency getCryptoCurrencyForWalletListItem(WalletType type, {bool isTestn return walletTypeToCryptoCurrency(type, isTestnet: isTestnet, chainId: chainId); } + +String getCryptoCurrencyIconForWalletListItem(WalletType type, + {bool isTestnet = false, int? chainId}) { + if (type == WalletType.arbitrum) { + return CryptoCurrency.arb.iconPath!; + } + + if (type == WalletType.base) { + return "assets/images/crypto/base_icon.webp"; + } + + return walletTypeToCryptoCurrency(type, isTestnet: isTestnet, chainId: chainId).iconPath ?? ""; +} diff --git a/cw_core/lib/db/sqlite.dart b/cw_core/lib/db/sqlite.dart index 6c875e578..15601c6f9 100644 --- a/cw_core/lib/db/sqlite.dart +++ b/cw_core/lib/db/sqlite.dart @@ -2,9 +2,26 @@ import 'dart:io'; import 'package:cw_core/root_dir.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; -late Database db; +Database? db; + +Future _addColumnIfNotExists( + Database db, { + required String table, + required String column, + required String definition, +}) async { + final result = await db.rawQuery("PRAGMA table_info($table)"); + final columnExists = result.any((row) => row['name'] == column); + + if (!columnExists) { + await db.execute( + 'ALTER TABLE $table ADD COLUMN $column $definition;', + ); + } +} Future initDb({String? pathOverride}) async { if (Platform.isLinux || Platform.isWindows) { @@ -21,8 +38,50 @@ Future initDb({String? pathOverride}) async { dbFileOld.deleteSync(); } } + await db?.close(); + db = await openDatabase(dbFile.path, version: 3, + onUpgrade: (Database db, int oldVersion, int newVersion) async { + printV("migrating: $oldVersion, $newVersion"); + if (oldVersion <= 1) { + await db.execute(''' +DELETE FROM WalletInfo +WHERE walletInfoId NOT IN ( + SELECT MIN(walletInfoId) + FROM WalletInfo + GROUP BY id +); - db = await openDatabase(dbFile.path, version: 1, +CREATE UNIQUE INDEX IF NOT EXISTS idx_walletinfo_id_unique +ON WalletInfo (id); +'''); + } + if (oldVersion <= 2) { + await db.execute(''' +CREATE TABLE IF NOT EXISTS BalanceCardStyleSettings ( + walletInfoId INTEGER, + accountIndex INTEGER DEFAULT -1, + gradientIndex INTEGER DEFAULT -1, + useSpecialDesign BOOLEAN DEFAULT FALSE, + backgroundImagePath TEXT DEFAULT "", + PRIMARY KEY (walletInfoId, accountIndex), + FOREIGN KEY (walletInfoId) REFERENCES WalletInfo(walletInfoId) +); +'''); + await _addColumnIfNotExists( + db, + table: 'WalletInfo', + column: 'receiveInfoboxDismissed', + definition: 'BOOLEAN DEFAULT FALSE', + ); + + await _addColumnIfNotExists( + db, + table: 'BalanceCardStyleSettings', + column: 'cardOrder', + definition: 'INTEGER DEFAULT 0', + ); + } + }, onCreate: (Database db, int version) async { await db.execute( ''' @@ -47,7 +106,8 @@ CREATE TABLE WalletInfo ( parentAddress TEXT, hashedWalletIdentifier TEXT, isNonSeedWallet INTEGER DEFAULT (0) NOT NULL, - sortOrder INTEGER DEFAULT (0) NOT NULL + sortOrder INTEGER DEFAULT (0) NOT NULL, + receiveInfoboxDismissed BOOLEAN DEFAULT FALSE ); '''); @@ -100,6 +160,22 @@ CREATE TABLE "WalletInfoAddressMap" ( ); ''' ); + await db.execute(''' +CREATE UNIQUE INDEX IF NOT EXISTS idx_walletinfo_id_unique +ON WalletInfo (id); +'''); + await db.execute(''' +CREATE TABLE BalanceCardStyleSettings ( + walletInfoId INTEGER, + accountIndex INTEGER DEFAULT -1, + gradientIndex INTEGER DEFAULT -1, + useSpecialDesign BOOLEAN DEFAULT FALSE, + backgroundImagePath TEXT DEFAULT "", + cardOrder INTEGER DEFAULT 0, + PRIMARY KEY (walletInfoId, accountIndex), + FOREIGN KEY (walletInfoId) REFERENCES WalletInfo(walletInfoId) +); + '''); } ); } @@ -121,10 +197,10 @@ Future> _getTableNames(Database db) async { } Future> _dumpDb() async { - final tableNames = await _getTableNames(db); + final tableNames = await _getTableNames(db!); final ret = {}; for (final tableName in tableNames) { - ret[tableName] = await db.query(tableName); + ret[tableName] = await db!.query(tableName); } return ret; } diff --git a/cw_core/lib/generate_name.dart b/cw_core/lib/generate_name.dart new file mode 100644 index 000000000..5f00c5589 --- /dev/null +++ b/cw_core/lib/generate_name.dart @@ -0,0 +1,22 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:flutter/services.dart'; + +extension StringExtension on String { + String capitalized() => "${this[0].toUpperCase()}${this.substring(1)}"; +} + +Future generateName() async { + final randomThing = Random(); + final adjectiveStringRaw = await rootBundle.loadString("assets/text/Wallet_Adjectives.txt"); + final nounStringRaw = await rootBundle.loadString("assets/text/Wallet_Nouns.txt"); + + final ls = LineSplitter(); + final adjectives = ls.convert(adjectiveStringRaw); + final nouns = ls.convert(nounStringRaw); + + final chosenAdjective = adjectives[randomThing.nextInt(adjectives.length)].capitalized(); + final chosenNoun = nouns[randomThing.nextInt(nouns.length)].capitalized(); + return "$chosenAdjective $chosenNoun"; +} diff --git a/cw_core/lib/hardware/device_connection_type.dart b/cw_core/lib/hardware/device_connection_type.dart index 74f043a81..39e0400c9 100644 --- a/cw_core/lib/hardware/device_connection_type.dart +++ b/cw_core/lib/hardware/device_connection_type.dart @@ -36,6 +36,12 @@ enum DeviceConnectionType { WalletType.polygon, ].contains(walletType); break; + case HardwareWalletType.cupcake: + case HardwareWalletType.coldcard: + case HardwareWalletType.seedsigner: + case HardwareWalletType.keystone: + // This should not be thrown since it should never reach this code for these HardwareWalletTypes + throw UnimplementedError(); } return isSupported diff --git a/cw_core/lib/lnurl.dart b/cw_core/lib/lnurl.dart new file mode 100644 index 000000000..c04bfc684 --- /dev/null +++ b/cw_core/lib/lnurl.dart @@ -0,0 +1,162 @@ +import 'dart:convert'; + +import 'package:bech32/bech32.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; + +const _BOLT_PREFIXES = ["lnbcrt", "lntbs", "lnbc", "lntb"]; +const _LUD17_PREFIXES = ['lnurlw', 'lnurlc', 'lnurlp', 'keyauth']; + +bool isBolt11ZeroInvoice(String invoice) { + final request = Bech32Codec().decode(invoice.replaceFirst("lightning:", ""), invoice.length); + + final prefix = _BOLT_PREFIXES + .firstWhere((prefix) => request.hrp.startsWith(prefix), orElse: () => ""); + + return request.hrp.length == prefix.length; +} + +/// Get the amount of a Bolt 11 Invoice in +int? _getAmountBolt11Msat(String invoice) { + final request = Bech32Codec().decode(invoice.replaceFirst("lightning:", ""), invoice.length); + + final prefix = _BOLT_PREFIXES.firstWhere( + (prefix) => request.hrp.startsWith(prefix), + orElse: () => throw FormatException('Invalid BOLT11 invoice: unknown HRP prefix.'), + ); + + final amountPart = request.hrp.substring(prefix.length); + if (amountPart.isEmpty) return null; // zero-amount invoice + + final hasMultiplier = RegExp(r'[munp]$').hasMatch(amountPart); + final multiplier = hasMultiplier ? amountPart[amountPart.length - 1] : ''; + final numberStr = hasMultiplier ? amountPart.substring(0, amountPart.length - 1) : amountPart; + + if (numberStr.isEmpty || !RegExp(r'^\d+$').hasMatch(numberStr)) { + throw FormatException('Invalid BOLT11 invoice: invalid amount number in HRP.'); + } + + final amount = int.parse(numberStr); + + switch (multiplier) { + case '': // bitcoin + return amount * 100000000000; + case 'm': // milli-bitcoin + return amount * 100000000; + case 'u': // micro-bitcoin + return amount * 100000; + case 'n': // nano-bitcoin + return amount * 100; + case 'p': + if (amount % 10 != 0) { + throw FormatException('Invalid BOLT11 invoice: amount not representable in whole msat.'); + } + return amount ~/ 10; + default: + throw FormatException('Invalid BOLT11 invoice: unknown amount multiplier.'); + } +} + +int? getBolt11Amount(String invoice) { + final msat = _getAmountBolt11Msat(invoice); + if (msat == null || msat % 1000 != 0) return null; + return msat ~/ 1000; +} + +class LNURL { + static Future getPayRequestAmount(String lnurl) async { + final url = decode(lnurl); + final response = await ProxyWrapper().get(clearnetUri: url); + + if (response.statusCode != 200) return null; + + try { + final body = jsonDecode(response.body) as Map; + final tag = body["tag"] as String?; + + if (tag != "payRequest") return null; + + final msat = body["minSendable"] as int?; + final maxSendable = body["maxSendable"] as int?; + + // if minSendable and maxSendable are the same we assume a specific payment request + if (msat != maxSendable) + return null; + + if (msat == null || msat % 1000 != 0) return null; + return msat ~/ 1000; + } catch (_) { + return null; + } + } + + static String encode(String url) { + final raw = _convert(utf8.encode(url), 8, 5, true); + return const Bech32Codec().encode(Bech32('lnurl', raw), 999); + } + + static Uri decode(String encodedUrl) { + Uri decodedUri; + + /// The URL doesn't have to be encoded at all as per LUD-17: Protocol schemes and raw (non bech32-encoded) URLs. + /// https://github.com/lnurl/luds/blob/luds/17.md + /// Handle non bech32-encoded LNURL + decodedUri = Uri.parse(encodedUrl); + for (final prefix in _LUD17_PREFIXES) { + if (decodedUri.scheme.contains(prefix)) { + decodedUri = decodedUri.replace(scheme: prefix); + } + } + if (_LUD17_PREFIXES.contains(decodedUri.scheme)) { + /// If the non-bech32 LNURL is a Tor address, the port has to be http instead of https for the clearnet LNURL so check if the host ends with '.onion' or '.onion.' + decodedUri = decodedUri.replace( + scheme: decodedUri.host.endsWith('onion') || decodedUri.host.endsWith('onion.') + ? 'http' + : 'https'); + } else { + /// Try to parse the input as a lnUrl. Will throw an error if it fails. + final lnUrl = _findLnUrl(encodedUrl); + + /// Decode the lnurl using bech32 + final bech32 = const Bech32Codec().decode(lnUrl, lnUrl.length); + decodedUri = Uri.parse(utf8.decode(_convert(bech32.data, 5, 8, false))); + } + return decodedUri; + } +} + +/// Parse and return a given lnurl string if it's valid. Will remove +/// `lightning:` from the beginning of it if present. +String _findLnUrl(String input) { + final res = RegExp(r',*?((lnurl)([0-9]+[a-z0-9]+))').allMatches(input.toLowerCase()); + + if (res.length != 1) + throw ArgumentError('Not a valid lnurl string'); + return res.first.group(0)!; +} + +/// Taken from bech32 (bitcoinjs): https://github.com/bitcoinjs/bech32 +List _convert(List data, int inBits, int outBits, bool pad) { + var value = 0; + var bits = 0; + final maxV = (1 << outBits) - 1; + + final result = []; + for (final dataValue in data) { + value = (value << inBits) | dataValue; + bits += inBits; + + while (bits >= outBits) { + bits -= outBits; + result.add((value >> bits) & maxV); + } + } + + if (pad) { + if (bits > 0) result.add((value << (outBits - bits)) & maxV); + } else { + if (bits >= inBits) throw Exception('[BECH32] Excess padding'); + if ((value << (outBits - bits)) & maxV > 0) throw Exception('[BECH32] Non-zero padding'); + } + + return result; +} diff --git a/cw_core/lib/monero_balance.dart b/cw_core/lib/monero_balance.dart index 34f51faf9..3a85d3cb9 100644 --- a/cw_core/lib/monero_balance.dart +++ b/cw_core/lib/monero_balance.dart @@ -3,25 +3,15 @@ import 'package:cw_core/monero_amount_format.dart'; class MoneroBalance extends Balance { MoneroBalance({required this.fullBalance, required this.unlockedBalance, this.frozenBalance = 0}) - : formattedUnconfirmedBalance = moneroAmountToString(amount: fullBalance - unlockedBalance), - formattedUnlockedBalance = moneroAmountToString(amount: unlockedBalance), - formattedFrozenBalance = moneroAmountToString(amount: frozenBalance), - super(unlockedBalance, fullBalance); + : formattedFrozenBalance = moneroAmountToString(amount: frozenBalance), + super.fromInt(unlockedBalance, fullBalance - unlockedBalance, frozen: frozenBalance); final int fullBalance; final int unlockedBalance; final int frozenBalance; - final String formattedUnconfirmedBalance; - final String formattedUnlockedBalance; final String formattedFrozenBalance; @override String get formattedUnAvailableBalance => formattedFrozenBalance == '0.0' ? '' : formattedFrozenBalance; - - @override - String get formattedAvailableBalance => formattedUnlockedBalance; - - @override - String get formattedAdditionalBalance => formattedUnconfirmedBalance; } diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index edd06bb70..6ed3964a2 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -20,6 +20,7 @@ Uri createUriFromElectrumAddress(String address, String path) => @HiveType(typeId: Node.typeId) class Node extends HiveObject with Keyable { Node({ + this.label, this.login, this.password, this.useSSL, @@ -56,6 +57,7 @@ class Node extends HiveObject with Keyable { : uriRaw = map['uri'] as String? ?? '', path = map['path'] as String? ?? '', login = map['login'] as String?, + label = map['label'] as String?, password = map['password'] as String?, useSSL = map['useSSL'] as bool?, trusted = map['trusted'] as bool? ?? false, @@ -101,6 +103,9 @@ class Node extends HiveObject with Keyable { @HiveField(11, defaultValue: false) bool isEnabledForAutoSwitching; + @HiveField(12, defaultValue: '') + String? label; + bool get isSSL => useSSL ?? false; bool get useSocksProxy => socksProxyAddress == null ? false : socksProxyAddress!.isNotEmpty; @@ -142,6 +147,7 @@ class Node extends HiveObject with Keyable { other is Node && (other.uriRaw == uriRaw && other.login == login && + other.label == label && other.password == password && other.typeRaw == typeRaw && other.useSSL == useSSL && @@ -153,6 +159,7 @@ class Node extends HiveObject with Keyable { int get hashCode => uriRaw.hashCode ^ login.hashCode ^ + label.hashCode ^ password.hashCode ^ typeRaw.hashCode ^ useSSL.hashCode ^ diff --git a/lib/core/payment_uris.dart b/cw_core/lib/payment_uris.dart similarity index 95% rename from lib/core/payment_uris.dart rename to cw_core/lib/payment_uris.dart index 1a5613433..6ad43a875 100644 --- a/lib/core/payment_uris.dart +++ b/cw_core/lib/payment_uris.dart @@ -383,14 +383,14 @@ class ERC681URI extends PaymentURI { static int _getChainID(String path) { return int.parse(RegExp( - r'@\d*', - ).firstMatch(path)?.group(0)?.replaceAll("@", "") ?? + r'@\d*', + ).firstMatch(path)?.group(0)?.replaceAll("@", "") ?? "1"); } static (bool, String) _getTargetAddress(String path) { final targetAddress = - RegExp(r'^(0x)?[0-9a-f]{40}', caseSensitive: false).firstMatch(path)!.group(0)!; + RegExp(r'^(0x)?[0-9a-f]{40}', caseSensitive: false).firstMatch(path)!.group(0)!; return (path.contains("/"), targetAddress); } @@ -468,3 +468,19 @@ class ERC681URI extends PaymentURI { } } } + +class LightningPaymentRequest extends PaymentURI { + LightningPaymentRequest({ + required super.address, + required super.amount, + required this.lnURL, + this.bolt11Invoice, + }); + + final String lnURL; + final String? bolt11Invoice; + + @override + String toString() => bolt11Invoice ?? "lightning:$lnURL"; +} + diff --git a/cw_core/lib/pending_transaction.dart b/cw_core/lib/pending_transaction.dart index 78cd4c874..89bd75922 100644 --- a/cw_core/lib/pending_transaction.dart +++ b/cw_core/lib/pending_transaction.dart @@ -1,6 +1,6 @@ class PendingChange { final String address; - final String amount; + final BigInt amount; PendingChange(this.address, this.amount); } @@ -16,6 +16,13 @@ mixin PendingTransaction { int? get outputCount => null; PendingChange? change; + String get roundedAmountFormatted { + if (amountFormatted.split(".").last.length <= 8) { + return amountFormatted; + } + return double.parse(amountFormatted).toStringAsFixed(8); + } + bool shouldCommitUR() => false; Future commit(); diff --git a/cw_core/lib/receive_page_option.dart b/cw_core/lib/receive_page_option.dart index f7d69bf0a..aacbe4918 100644 --- a/cw_core/lib/receive_page_option.dart +++ b/cw_core/lib/receive_page_option.dart @@ -6,9 +6,14 @@ class ReceivePageOption implements Enumerate { static const anonPayInvoice = ReceivePageOption._('anonPayInvoice'); static const anonPayDonationLink = ReceivePageOption._('anonPayDonationLink'); - const ReceivePageOption._(this.value); + const ReceivePageOption._(this.value, + {this.iconPath, this.description, this.isCommon = false, this.addAddressWord = false}); final String value; + final String? iconPath; + final String? description; + final bool isCommon; + final bool addAddressWord; String toString() { return value; @@ -17,6 +22,7 @@ class ReceivePageOption implements Enumerate { const ReceivePageOptions = [ ReceivePageOption.mainnet, - ReceivePageOption.anonPayInvoice, - ReceivePageOption.anonPayDonationLink + // FIXME anonpay disabed for new ui beta. restore after screens are ready + // ReceivePageOption.anonPayInvoice, + // ReceivePageOption.anonPayDonationLink ]; diff --git a/cw_core/lib/sync_status.dart b/cw_core/lib/sync_status.dart index e85db9a0f..0d7538a41 100644 --- a/cw_core/lib/sync_status.dart +++ b/cw_core/lib/sync_status.dart @@ -74,7 +74,7 @@ class SyncingSyncStatus extends SyncStatus { } // Show the placeholder ETA while gathering data - return '--:--'; + return 'Syncing...'; } String? getFormattedEta() { @@ -130,16 +130,16 @@ class SyncingSyncStatus extends SyncStatus { } String _formatDuration(Duration duration) { - String twoDigits(int n) => n.toString().padLeft(2, '0'); - - final hours = twoDigits(duration.inHours); - final minutes = twoDigits(duration.inMinutes.remainder(60)); - final seconds = twoDigits(duration.inSeconds.remainder(60)); - - if (hours == '00') { - return '${minutes}m${seconds}s'; + final hours = duration.inHours; + final minutes = duration.inMinutes.remainder(60); + final seconds = duration.inSeconds.remainder(60); + if (minutes == 0 && hours == 0) { + return '${seconds}s'; } - return '${hours}h${minutes}m${seconds}s'; + if (hours == 0) { + return '${minutes}min ${seconds}s'; + } + return '${hours}h ${minutes}min ${seconds}s'; } DateTime calculateEta() { diff --git a/cw_core/lib/transaction_priority.dart b/cw_core/lib/transaction_priority.dart index c173f1ddd..ddd2752c7 100644 --- a/cw_core/lib/transaction_priority.dart +++ b/cw_core/lib/transaction_priority.dart @@ -2,5 +2,8 @@ import 'package:cw_core/enumerable_item.dart'; abstract class TransactionPriority extends EnumerableItem with Serializable { - const TransactionPriority({required String title, required int raw}) : super(title: title, raw: raw); + final String? description; + final String? hint; + + const TransactionPriority({required String title, required int raw, this.description, this.hint}) : super(title: title, raw: raw); } diff --git a/cw_core/lib/unspent_coin_type.dart b/cw_core/lib/unspent_coin_type.dart index a042610fc..859457c49 100644 --- a/cw_core/lib/unspent_coin_type.dart +++ b/cw_core/lib/unspent_coin_type.dart @@ -1 +1 @@ -enum UnspentCoinType { mweb, nonMweb, any } \ No newline at end of file +enum UnspentCoinType { mweb, nonMweb, any, lightning } diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart index 4d4d2c0a5..ccbb8aa34 100644 --- a/cw_core/lib/wallet_addresses.dart +++ b/cw_core/lib/wallet_addresses.dart @@ -1,9 +1,11 @@ +import 'package:cw_core/payment_uris.dart'; +import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; abstract class WalletAddresses { - WalletAddresses(this.walletInfo) + WalletAddresses(this.walletInfo, [this.isTestnet = false]) : addressesMap = {}, allAddressesMap = {}, addressInfos = {}, @@ -17,11 +19,13 @@ abstract class WalletAddresses { final WalletInfo walletInfo; + final bool isTestnet; + String get address; String get latestAddress { - if (walletInfo.type == WalletType.monero || walletInfo.type == WalletType.wownero) { - if (addressesMap.keys.length == 0) return address; + if ([WalletType.monero, WalletType.wownero].contains(walletInfo.type)) { + if (addressesMap.keys.isEmpty) return address; return addressesMap[addressesMap.keys.last] ?? address; } return _localAddress ?? address; @@ -35,6 +39,8 @@ abstract class WalletAddresses { String get addressForExchange => address; + String get addressForBuy => address; + Map addressesMap; Map allAddressesMap; @@ -79,4 +85,14 @@ abstract class WalletAddresses { bool containsAddress(String address) => addressesMap.containsKey(address) || allAddressesMap.containsKey(address); + + List get receivePageOptions => ReceivePageOptions; + + /// Get a [PaymentURI] for the current [address] + /// e.g. ethereum:0x0 + PaymentURI getPaymentUri(String amount); + + /// Get a [PaymentURI] for the current [address] asynchronously + /// this can be used if a payment requires a api call beforehand + Future getPaymentRequestUri(String amount) async => getPaymentUri(amount); } diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index e47760846..c7ed4eccb 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -69,6 +69,8 @@ abstract class WalletBase walletInfo.isHardwareWallet; + bool get isSoftwareWallet => walletInfo.hardwareWalletType == null; + HardwareWalletType? get hardwareWalletType => walletInfo.hardwareWalletType; bool get hasRescan => false; diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index a8d6c18cb..c42f6856a 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -1,12 +1,12 @@ import 'dart:async'; +import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/db/sqlite.dart'; import 'package:cw_core/hive_type_ids.dart'; import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_info_legacy.dart' as wiLegacy; import 'package:cw_core/wallet_type.dart'; import 'package:sqflite/sqflite.dart'; -import 'package:cw_core/cake_hive.dart'; -import 'package:cw_core/wallet_info_legacy.dart' as wiLegacy; Future performHiveMigration() async { try { @@ -70,17 +70,19 @@ class WalletInfoAddressInfo { String address; String label; - static String get tableName => 'walletInfoAddressInfo'; + static String get tableName => 'walletInfoAddressInfo'; + static String get selfIdColumn => "${tableName}Id"; static Future> selectList(int walletInfoId) async { - final query = await db.query(tableName, where: 'walletInfoId = ?', whereArgs: [walletInfoId]); + final query = await db!.query(tableName, where: 'walletInfoId = ?', whereArgs: [walletInfoId]); return List.generate(query.length, (index) => WalletInfoAddressInfo.fromJson(query[index])); } static Future deleteByWalletInfoId(int walletInfoId) async { - return await db.delete(tableName, where: 'walletInfoId = ?', whereArgs: [walletInfoId]); + return await db!.delete(tableName, where: 'walletInfoId = ?', whereArgs: [walletInfoId]); } + static Future insert({ required int walletInfoId, required int mapKey, @@ -88,7 +90,7 @@ class WalletInfoAddressInfo { required String address, required String label, }) async { - return await db.insert(tableName, { + return await db!.insert(tableName, { "walletInfoId": walletInfoId, "mapKey": mapKey, "mapValueAccountIndex": accountIndex, @@ -128,24 +130,26 @@ class WalletInfoAddressMap { required this.addressValue, }); - int id; int walletInfoId; String addressKey; String addressValue; - static String get tableName => 'walletInfoAddressMap'; - static String get selfIdColumn => "${tableName}Id"; + static String get tableName => 'walletInfoAddressMap'; + + static String get selfIdColumn => "${tableName}Id"; static Future> selectList(int walletInfoId) async { - final query = await db.query(tableName, where: 'walletInfoId = ?', whereArgs: [walletInfoId]); + final query = await db!.query(tableName, where: 'walletInfoId = ?', whereArgs: [walletInfoId]); return List.generate(query.length, (index) => WalletInfoAddressMap.fromJson(query[index])); } + static Future deleteByWalletInfoId(int walletInfoId) async { - return await db.delete(tableName, where: 'walletInfoId = ?', whereArgs: [walletInfoId]); + return await db!.delete(tableName, where: 'walletInfoId = ?', whereArgs: [walletInfoId]); } + static Future insert(int walletInfoId, String addressKey, String addressValue) async { - return await db.insert(tableName, { + return await db!.insert(tableName, { "walletInfoId": walletInfoId, "addressKey": addressKey, "addressValue": addressValue, @@ -184,28 +188,37 @@ class WalletInfoAddress { WalletInfoAddressType type; String address; - static String get tableName => 'walletInfoAddress'; + static String get tableName => 'walletInfoAddress'; + static String get selfIdColumn => "${tableName}Id"; - static Future> selectList(int walletInfoId, WalletInfoAddressType type) async { - final query = await db.query(tableName, where: 'walletInfoId = ? AND type = ?', whereArgs: [walletInfoId, type.index]); + static Future> selectList( + int walletInfoId, WalletInfoAddressType type) async { + final query = await db!.query(tableName, + where: 'walletInfoId = ? AND type = ?', whereArgs: [walletInfoId, type.index]); return List.generate(query.length, (index) => WalletInfoAddress.fromJson(query[index])); } - static Future deleteByAddress(int walletInfoId, WalletInfoAddressType type, String address) async { - return await db.delete(tableName, where: 'walletInfoId = ? AND type = ? AND address = ?', whereArgs: [walletInfoId, type.index, address]); + static Future deleteByAddress( + int walletInfoId, WalletInfoAddressType type, String address) async { + return await db!.delete(tableName, + where: 'walletInfoId = ? AND type = ? AND address = ?', + whereArgs: [walletInfoId, type.index, address]); } static Future deleteByType(int walletInfoId, WalletInfoAddressType type) async { - return await db.delete(tableName, where: 'walletInfoId = ? AND type = ?', whereArgs: [walletInfoId, type.index]); + return await db!.delete(tableName, + where: 'walletInfoId = ? AND type = ?', whereArgs: [walletInfoId, type.index]); } static Future insert(int walletInfoId, WalletInfoAddressType type, String address) async { - final select = await db.query(tableName, where: 'walletInfoId = ? AND type = ? AND address = ?', whereArgs: [walletInfoId, type.index, address]); + final select = await db!.query(tableName, + where: 'walletInfoId = ? AND type = ? AND address = ?', + whereArgs: [walletInfoId, type.index, address]); if (select.isNotEmpty) { return select[0][selfIdColumn] as int; } - return await db.insert(tableName, { + return await db!.insert(tableName, { "walletInfoId": walletInfoId, "type": type.index, "address": address, @@ -245,7 +258,8 @@ class DerivationInfo { int id; - static String get tableName => 'walletInfoDerivationInfo'; + static String get tableName => 'walletInfoDerivationInfo'; + static String get selfIdColumn => "${tableName}Id"; String address; @@ -257,12 +271,12 @@ class DerivationInfo { String? description; static Future> selectList(String where, List whereArgs) async { - final query = await db.query( + final query = await db!.query( tableName, columns: [ selfIdColumn, 'address', - 'balance', + 'balance', 'transactionsCount', 'derivationType', 'derivationPath', @@ -288,7 +302,7 @@ class DerivationInfo { }; } - factory DerivationInfo.fromJson(Map json ) { + factory DerivationInfo.fromJson(Map json) { return DerivationInfo( id: json[selfIdColumn] as int, derivationType: DerivationType.values[json['derivationType'] as int? ?? 0], @@ -306,7 +320,7 @@ class DerivationInfo { if (json[selfIdColumn] == 0) { json[selfIdColumn] = null; } - id = await db.insert(tableName, json, conflictAlgorithm: ConflictAlgorithm.replace); + id = await db!.insert(tableName, json, conflictAlgorithm: ConflictAlgorithm.replace); return id; } } @@ -333,6 +347,8 @@ class WalletInfo { this.isNonSeedWallet, this.sortOrder, this.addressPageType, + this.receiveInfoboxDismissed + ) : _yatLastUsedAddressController = StreamController.broadcast(); factory WalletInfo.external({ @@ -354,6 +370,7 @@ class WalletInfo { String? hashedWalletIdentifier, bool? isNonSeedWallet, int? sortOrder, + bool? receiveInfoboxDismissed, }) { return WalletInfo( 0, @@ -376,10 +393,12 @@ class WalletInfo { isNonSeedWallet ?? false, sortOrder ?? 0, null, + receiveInfoboxDismissed ?? false ); } - static String get tableName => 'walletInfo'; + static String get tableName => 'walletInfo'; + static String get selfIdColumn => "${tableName}Id"; int internalId; @@ -393,6 +412,8 @@ class WalletInfo { String dirPath; String path; String address; + bool receiveInfoboxDismissed; + Future> getAddresses() async { final list = await WalletInfoAddressMap.selectList(internalId); return Map.fromEntries(list.map((e) => MapEntry(e.addressKey, e.addressValue))); @@ -409,6 +430,7 @@ class WalletInfo { String? yatEid; String? yatLastUsedAddressRaw; bool? showIntroCakePayCard; + Future>> getAddressInfos() async { final list = await WalletInfoAddressInfo.selectList(internalId); final ret = >{}; @@ -439,6 +461,7 @@ class WalletInfo { final list = await WalletInfoAddress.selectList(internalId, WalletInfoAddressType.used); return list.map((e) => e.address).toSet(); } + Future setUsedAddresses(List addresses) async { await WalletInfoAddress.deleteByType(internalId, WalletInfoAddressType.used); for (final address in addresses) { @@ -450,6 +473,7 @@ class WalletInfo { final list = await WalletInfoAddress.selectList(internalId, WalletInfoAddressType.hidden); return list.map((e) => e.address).toSet(); } + Future setHiddenAddresses(List addresses) async { await WalletInfoAddress.deleteByType(internalId, WalletInfoAddressType.hidden); for (final address in addresses) { @@ -461,6 +485,7 @@ class WalletInfo { final list = await WalletInfoAddress.selectList(internalId, WalletInfoAddressType.manual); return list.map((e) => e.address).toSet(); } + Future setManualAddresses(List addresses) async { await WalletInfoAddress.deleteByType(internalId, WalletInfoAddressType.manual); for (final address in addresses) { @@ -476,11 +501,13 @@ class WalletInfo { String? network; int derivationInfoId; DerivationInfo? _derivationInfo; + Future getDerivationInfo() async { if (_derivationInfo != null) { return _derivationInfo!; } - final list = await DerivationInfo.selectList('walletInfoDerivationInfoId = ?', [derivationInfoId]); + final list = + await DerivationInfo.selectList('walletInfoDerivationInfoId = ?', [derivationInfoId]); if (list.isEmpty) { final di = DerivationInfo( id: 0, @@ -493,11 +520,12 @@ class WalletInfo { _derivationInfo = list[0]; return _derivationInfo!; } + HardwareWalletType? hardwareWalletType; String? parentAddress; String? hashedWalletIdentifier; bool isNonSeedWallet; - + int sortOrder; String get yatLastUsedAddress => yatLastUsedAddressRaw ?? ''; @@ -549,6 +577,8 @@ class WalletInfo { "isNonSeedWallet": isNonSeedWallet ? 1 : 0, "sortOrder": sortOrder, "addressPageType": addressPageType, + "receiveInfoboxDismissed": receiveInfoboxDismissed ? 1 : 0, + }; factory WalletInfo.fromJson(Map json) { @@ -567,12 +597,15 @@ class WalletInfo { json['yatLastUsedAddressRaw'] as String?, (json['showIntroCakePayCard'] as int) == 1, json['walletInfoDerivationInfoId'] as int, - json['hardwareWalletType'] == null ? null : HardwareWalletType.values[json['hardwareWalletType'] as int], + json['hardwareWalletType'] == null + ? null + : HardwareWalletType.values[json['hardwareWalletType'] as int], json['parentAddress'] as String?, json['hashedWalletIdentifier'] as String?, (json['isNonSeedWallet'] as int) == 1, json['sortOrder'] as int? ?? 0, json['addressPageType'] as String? ?? null, + json['receiveInfoboxDismissed'] != 0, ); } @@ -584,16 +617,17 @@ class WalletInfo { if (_derivationInfo != null) { derivationInfoId = await _derivationInfo!.save(); } - internalId = await db.insert(tableName, json, conflictAlgorithm: ConflictAlgorithm.replace); + internalId = await db!.insert(tableName, json, conflictAlgorithm: ConflictAlgorithm.replace); return internalId; } static Future delete(WalletInfo walletInfo) async { - return await db.delete(tableName, where: 'id = ?', whereArgs: [walletInfo.id]); + return await db!.delete(tableName, where: 'id = ?', whereArgs: [walletInfo.id]); } - static Future> selectList(String where, List whereArgs, {String orderBy = 'sortOrder'}) async { - final list = await db.query( + static Future> selectList(String where, List whereArgs, + {String orderBy = 'sortOrder'}) async { + final list = await db!.query( tableName, where: where.isNotEmpty ? where : "1 = 1", whereArgs: whereArgs.isNotEmpty ? whereArgs : null, diff --git a/cw_core/lib/wallet_info_legacy.dart b/cw_core/lib/wallet_info_legacy.dart index 08207f2b0..3b8a78a0a 100644 --- a/cw_core/lib/wallet_info_legacy.dart +++ b/cw_core/lib/wallet_info_legacy.dart @@ -271,6 +271,7 @@ class WalletInfo extends HiveObject { isNonSeedWallet, 0, addressPageType, + false ); final wiId = await walletInfo.save(); for (final address in usedAddresses ?? []) { diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index e40885610..bee9391e8 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -256,7 +256,7 @@ String walletTypeToDisplayName(WalletType type) { case WalletType.dogecoin: return 'Dogecoin (DOGE)'; case WalletType.base: - return 'Base (BASE)'; + return 'Base'; case WalletType.arbitrum: return 'Arbitrum (ARB)'; case WalletType.zcash: @@ -268,7 +268,7 @@ String walletTypeToDisplayName(WalletType type) { } } -WalletType? cryptoCurrencyToWalletType(CryptoCurrency type) { +WalletType? _cryptoCurrencyToWalletType(CryptoCurrency type) { switch (type) { case CryptoCurrency.xmr: return WalletType.monero; @@ -313,3 +313,15 @@ WalletType? cryptoCurrencyToWalletType(CryptoCurrency type) { return null; } } + +WalletType? cryptoCurrencyOrTokenToWalletType(CryptoCurrency type) { + if(type.tag == CryptoCurrency.bnb.tag) { + return _cryptoCurrencyToWalletType(CryptoCurrency.bnb); + } + + if(type.tag != null && ![CryptoCurrency.btcln.tag, CryptoCurrency.bnb.tag].contains(type.tag)) { + return _cryptoCurrencyToWalletType(CryptoCurrency.fromString(type.tag!)); + } else { + return _cryptoCurrencyToWalletType(type); + } +} diff --git a/cw_core/lib/wownero_balance.dart b/cw_core/lib/wownero_balance.dart index 916fa90bc..de6518023 100644 --- a/cw_core/lib/wownero_balance.dart +++ b/cw_core/lib/wownero_balance.dart @@ -3,26 +3,15 @@ import 'package:cw_core/wownero_amount_format.dart'; class WowneroBalance extends Balance { WowneroBalance({required this.fullBalance, required this.unlockedBalance, this.frozenBalance = 0}) - : formattedUnconfirmedBalance = wowneroAmountToString(amount: fullBalance - unlockedBalance), - formattedUnlockedBalance = wowneroAmountToString(amount: unlockedBalance), - formattedFrozenBalance = - wowneroAmountToString(amount: frozenBalance), - super(unlockedBalance, fullBalance); + : formattedFrozenBalance = wowneroAmountToString(amount: frozenBalance), + super.fromInt(unlockedBalance, fullBalance - unlockedBalance, frozen: frozenBalance); final int fullBalance; final int unlockedBalance; final int frozenBalance; - final String formattedUnconfirmedBalance; - final String formattedUnlockedBalance; final String formattedFrozenBalance; @override String get formattedUnAvailableBalance => formattedFrozenBalance == '0.0' ? '' : formattedFrozenBalance; - - @override - String get formattedAvailableBalance => formattedUnlockedBalance; - - @override - String get formattedAdditionalBalance => formattedUnconfirmedBalance; -} \ No newline at end of file +} diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index 3f74f77d6..2d0919c33 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -46,6 +46,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + bech32: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "05755063b593aa6cca0a4820a318e0ce17de6192" + url: "https://github.com/cake-tech/bech32.git" + source: git + version: "0.2.2" blockchain_utils: dependency: "direct main" description: diff --git a/cw_core/pubspec.yaml b/cw_core/pubspec.yaml index 949791eb3..3f9a6e521 100644 --- a/cw_core/pubspec.yaml +++ b/cw_core/pubspec.yaml @@ -2,11 +2,10 @@ name: cw_core description: A new Flutter package project. version: 0.0.1 publish_to: none -author: Cake Wallet homepage: https://cakewallet.com environment: - sdk: ">=2.17.5 <3.0.0" + sdk: '>=3.0.6 <4.0.0' flutter: ">=1.20.0" dependencies: @@ -46,6 +45,9 @@ dependencies: ref: cake-update-v2 sqflite: ^2.4.1 sqflite_common_ffi: ^2.3.4+4 + bech32: + git: + url: https://github.com/cake-tech/bech32.git sqlite3_flutter_libs: 0.5.40 dev_dependencies: diff --git a/cw_core/test/crypto_amount_format.dart b/cw_core/test/crypto_amount_format.dart index 0a66f3d73..2e4df51ee 100644 --- a/cw_core/test/crypto_amount_format.dart +++ b/cw_core/test/crypto_amount_format.dart @@ -1,34 +1,154 @@ -import 'package:flutter_test/flutter_test.dart'; import 'package:cw_core/crypto_amount_format.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { group('String.withMaxDecimals', () { test('should return the original string when it has fewer decimal places than the max', () { final input = '123.45'; final result = input.withMaxDecimals(3); - + expect(result, equals(input)); }); test('should truncate decimal places when the string has more than the max', () { final input = '123.4567'; final result = input.withMaxDecimals(2); - + expect(result, equals('123.45')); }); test('should handle strings with no decimal places', () { final input = '123'; final result = input.withMaxDecimals(2); - + expect(result, equals(input)); }); test('should handle strings with multiple decimal points', () { final input = '123.45.67'; final result = input.withMaxDecimals(4); - + expect(result, equals('123.4567')); }); }); + + group('String.withLocalSeperator', () { + group('Locale en_US', () { + const locale = 'en_US'; + + test('should not have a grouping separator', () { + final input = '123.45'; + final result = input.withLocalSeperator(locale); + + expect(result, equals(input)); + }); + + test('should have a grouping seperator', () { + final input = '1123.4567'; + final result = input.withLocalSeperator(locale); + + expect(result, equals('1,123.4567')); + }); + + test('should have multiple grouping seperator', () { + final input = '1000000.4567'; + final result = input.withLocalSeperator(locale); + + expect(result, equals('1,000,000.4567')); + }); + }); + + group('Locale de_DE', () { + const locale = 'de_DE'; + + test('should not have a grouping separator', () { + final input = '123.45'; + final result = input.withLocalSeperator(locale); + + expect(result, equals('123,45')); + }); + + test('should have a grouping seperator', () { + final input = '1123.4567'; + final result = input.withLocalSeperator(locale); + + expect(result, equals('1.123,4567')); + }); + + test('should have multiple grouping seperator', () { + final input = '1000000.4567'; + final result = input.withLocalSeperator(locale); + + expect(result, equals('1.000.000,4567')); + }); + }); + + group('Locale de_CH', () { + const locale = 'de_CH'; + + test('should not have a grouping separator', () { + final input = '123.45'; + final result = input.withLocalSeperator(locale); + + expect(result, equals('123.45')); + }); + + test('should have a grouping seperator', () { + final input = '1123.4567'; + final result = input.withLocalSeperator(locale); + + expect(result, equals('1’123.4567')); + }); + + test('should have multiple grouping seperator', () { + final input = '1000000.4567'; + final result = input.withLocalSeperator(locale); + + expect(result, equals('1’000’000.4567')); + }); + }); + + group('With XMR Suffix', () { + const locale = 'de_CH'; + + test('should handle suffix expected', () { + final input = '1123.4567 XMR'; + final result = input.withLocalSeperator(locale); + + expect(result, equals('1’123.4567 XMR')); + }); + + test('should handle suffix odd spacing', () { + final input = '1123.4567 XMR '; + final result = input.withLocalSeperator(locale); + + expect(result, equals('1’123.4567 XMR ')); + }); + }); + + group('With > < prefix', () { + const locale = 'de_CH'; + + test('should handle prefix and suffix', () { + final input = '> 1123.4567 XMR'; + final result = input.withLocalSeperator(locale); + + expect(result, equals('> 1’123.4567 XMR')); + }); + + test('should handle prefix and suffix odd spacing', () { + final input = '< 1123.4567 XMR '; + final result = input.withLocalSeperator(locale); + + expect(result, equals('< 1’123.4567 XMR ')); + }); + + test('should handle just prefix', () { + final input = '< 1123.4567'; + final result = input.withLocalSeperator(locale); + + expect(result, equals('< 1’123.4567')); + }); + }); + }); } diff --git a/cw_core/test/lnurl_test.dart b/cw_core/test/lnurl_test.dart new file mode 100644 index 000000000..1df503b9e --- /dev/null +++ b/cw_core/test/lnurl_test.dart @@ -0,0 +1,85 @@ +import 'package:cw_core/lnurl.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; +import 'package:cw_core/utils/tor/abstract.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('lnurl', () { + test('decode lnurl', () { + final content = LNURL.decode( + "lnurl1dp68gurn8ghj7cmpddjjucmpwd5z7tnhv4kxctttdehhwm30d3h82unvwqhkkmmwwd6xj9vpzq4"); + expect(content, Uri.parse("https://cake.cash/.well-known/lnurlp/konsti")); + }); + + test('get amount', () async { + CakeTor.instance = await CakeTorInstance.getInstance(); + + final content = await LNURL.getPayRequestAmount( + "LNURL1DP68GURN8GHJ7CTSWQHXY6T5WFJHZAT9WD6ZU6T09AUZ7URJDAU8JTMKXYHKCM308A5N6MNHVVNXJEPAXGMNWWPEX3JRGDTPXVNXZ0FCX5CRQVPSXQNX602VFE24YNP9XGC8W6T5DQJNYVRPD4HH2MN5GTRNLZ"); + expect(content, 8500); + }); + + test('get no amount', () async { + CakeTor.instance = await CakeTorInstance.getInstance(); + + final content = await LNURL.getPayRequestAmount( + "lnurl1dp68gurn8ghj7cmpddjjucmpwd5z7tnhv4kxctttdehhwm30d3h82unvwqhkkmmwwd6xj9vpzq4"); + expect(content, null); + }); + + test('encode lnurl', () { + final content = LNURL.encode("https://cake.cash/.well-known/lnurlp/konsti"); + expect(content, + "lnurl1dp68gurn8ghj7cmpddjjucmpwd5z7tnhv4kxctttdehhwm30d3h82unvwqhkkmmwwd6xj9vpzq4"); + }); + }); + + group("isBolt11ZeroInvoice", () { + test("should identify a zero amount invoice", () { + final invoice = "lnbc1p5564y8pp5vm48jp3w82yjssrtdhvrjacwwzp59hqjm2lq6fud5thfznyuswgqsp54mykkptx9mlqhpe93f9pcgt9p5ml5xhclyn7qsltqy9ldf25j3qqxqyz5vqnp4qvyndeaqzman7h898jxm98dzkm0mlrsx36s93smrur7h0azyyuxc5rzjqwghf7zxvfkxq5a6sr65g0gdkv768p83mhsnt0msszapamzx2qvuxqqqqrt49lmtcqqqqqqqqqqq86qq9qrzjq25carzepgd4vqsyn44jrk85ezrpju92xyrk9apw4cdjh6yrwt5jgqqqqrt49lmtcqqqqqqqqqqq86qq9qrzjqfvmpdwfesckajzfcf7ff2vqaz9jdgpcsa9xc0fq8ny749dy8y5geapyqr6zgqqqq8hxk2qqae4jsqyugqcqzpgdqq9qyyssqycdyx8r09wgm8vny3n8pf2e0crcdq9mgx3ncxvzsrkljxyrcuwx8zjtje7du40nzdy7x2he67v9asp6ac6ed75r33yyt9pzeepygcmsptc4ve4"; + expect(isBolt11ZeroInvoice(invoice), true); + }); + + test("should identify a non zero amount invoice", () { + final invoice = "lnbc1u1p5564nxpp5mrklx2pjaggkcfs9r5tfk84wszzy9gypcnrt9yjq7n6uf95s70xqsp59fph78twjra598n2mll2arw5enluy7a4uf4y9a94ddd30hm98njsxq9z0rgqnp4qvyndeaqzman7h898jxm98dzkm0mlrsx36s93smrur7h0azyyuxc5rzjqwghf7zxvfkxq5a6sr65g0gdkv768p83mhsnt0msszapamzx2qvuxqqqqrt49lmtcqqqqqqqqqqq86qq9qrzjq25carzepgd4vqsyn44jrk85ezrpju92xyrk9apw4cdjh6yrwt5jgqqqqrt49lmtcqqqqqqqqqqq86qq9qcqzpgdql2djkuepqw3hjqsmpddjjq4mpd3kx2aq9qyyssq3nf60ey9fgkf0elu2y8j96rx5pm4gx2a976h8yadx83dyg0ltnysrxhep8e2p3yvz4kf597qk3rttgdy72wwqq0mwr8hdht3pqpchyqq0aa9v0"; + expect(isBolt11ZeroInvoice(invoice), false); + }); + + test("should identify a zero amount prefixed invoice", () { + final invoice = "lightning:lnbc1p5564y8pp5vm48jp3w82yjssrtdhvrjacwwzp59hqjm2lq6fud5thfznyuswgqsp54mykkptx9mlqhpe93f9pcgt9p5ml5xhclyn7qsltqy9ldf25j3qqxqyz5vqnp4qvyndeaqzman7h898jxm98dzkm0mlrsx36s93smrur7h0azyyuxc5rzjqwghf7zxvfkxq5a6sr65g0gdkv768p83mhsnt0msszapamzx2qvuxqqqqrt49lmtcqqqqqqqqqqq86qq9qrzjq25carzepgd4vqsyn44jrk85ezrpju92xyrk9apw4cdjh6yrwt5jgqqqqrt49lmtcqqqqqqqqqqq86qq9qrzjqfvmpdwfesckajzfcf7ff2vqaz9jdgpcsa9xc0fq8ny749dy8y5geapyqr6zgqqqq8hxk2qqae4jsqyugqcqzpgdqq9qyyssqycdyx8r09wgm8vny3n8pf2e0crcdq9mgx3ncxvzsrkljxyrcuwx8zjtje7du40nzdy7x2he67v9asp6ac6ed75r33yyt9pzeepygcmsptc4ve4"; + expect(isBolt11ZeroInvoice(invoice), true); + }); + + test("should identify a non zero amount prefixed invoice", () { + final invoice = "lightning:lnbc1u1p5564nxpp5mrklx2pjaggkcfs9r5tfk84wszzy9gypcnrt9yjq7n6uf95s70xqsp59fph78twjra598n2mll2arw5enluy7a4uf4y9a94ddd30hm98njsxq9z0rgqnp4qvyndeaqzman7h898jxm98dzkm0mlrsx36s93smrur7h0azyyuxc5rzjqwghf7zxvfkxq5a6sr65g0gdkv768p83mhsnt0msszapamzx2qvuxqqqqrt49lmtcqqqqqqqqqqq86qq9qrzjq25carzepgd4vqsyn44jrk85ezrpju92xyrk9apw4cdjh6yrwt5jgqqqqrt49lmtcqqqqqqqqqqq86qq9qcqzpgdql2djkuepqw3hjqsmpddjjq4mpd3kx2aq9qyyssq3nf60ey9fgkf0elu2y8j96rx5pm4gx2a976h8yadx83dyg0ltnysrxhep8e2p3yvz4kf597qk3rttgdy72wwqq0mwr8hdht3pqpchyqq0aa9v0"; + expect(isBolt11ZeroInvoice(invoice), false); + }); + }); + + group("getBolt11Amount", () { + test("should identify a zero amount invoice", () { + final invoice = "lnbc1p5564y8pp5vm48jp3w82yjssrtdhvrjacwwzp59hqjm2lq6fud5thfznyuswgqsp54mykkptx9mlqhpe93f9pcgt9p5ml5xhclyn7qsltqy9ldf25j3qqxqyz5vqnp4qvyndeaqzman7h898jxm98dzkm0mlrsx36s93smrur7h0azyyuxc5rzjqwghf7zxvfkxq5a6sr65g0gdkv768p83mhsnt0msszapamzx2qvuxqqqqrt49lmtcqqqqqqqqqqq86qq9qrzjq25carzepgd4vqsyn44jrk85ezrpju92xyrk9apw4cdjh6yrwt5jgqqqqrt49lmtcqqqqqqqqqqq86qq9qrzjqfvmpdwfesckajzfcf7ff2vqaz9jdgpcsa9xc0fq8ny749dy8y5geapyqr6zgqqqq8hxk2qqae4jsqyugqcqzpgdqq9qyyssqycdyx8r09wgm8vny3n8pf2e0crcdq9mgx3ncxvzsrkljxyrcuwx8zjtje7du40nzdy7x2he67v9asp6ac6ed75r33yyt9pzeepygcmsptc4ve4"; + expect(getBolt11Amount(invoice), null); + }); + + test("should get the amount 1 BTC encoded as bitcoin", () { + final invoice = "lnbc11p5exaxspp5u6e6lx90rw2vqr2e5axt8ur3v98qnmqfsmh8r0l0mcm7ry70vyvsdqqcqzzsxqrrsssp5eqydmrufk39dwhuxzajwx4xyffn0mlcu3qa8vjwry6qev5gsepkq9qxpqysgqt0zldve9uxt3txjt8wp0gev52lquljgugy26nneue85pjwk5yak4hs2d8533tus07pq5ka6zrnd0ruaeepwres8p7ws5mx63xlwff5sqlxcy8a"; + expect(getBolt11Amount(invoice), 100000000); + }); + + test("should get the amount 0,1 BTC encoded as millibitcoin", () { + final invoice = "lnbc100m1p5exagmpp5n4c5tml3h8h7k4gthc9papkyeymceqqvcldkng4nvxwzj7nu9jnsdqqcqzzsxqrrsssp58kgzrh7y3pevantduc4yg33aa6r79ed6xgmhr0dy40zc8pxrz5ms9qxpqysgqcq27km3v3wcz6pntmsfnz259zy6tff0uwjz9kamdrvmxmdv09gp44mh0ygn7gweqf4zdev4x8e67p5kcvahethchdkkjexhtk7emuqcpasz0lp"; + expect(getBolt11Amount(invoice), 10000000); + }); + + test("should get the amount 1000 SATS encoded as microbitcoin", () { + final invoice = "lnbc10u1p5exfgvpp5n3s9dsw9ddax5c3h8437ya7y4g582uyufyna626yr9vqdfjrhwzqdqqcqzzsxqrrsssp5mmkyp5h35fpaxm4063f3kc7d4nmgd4gd8fv8086wxd784m76kmpq9qxpqysgqye9r0vahpjy9l2je6vakxzk3cjnsyx76r29c4amkz4cmapq5tqt5l7rl0emkd003jjvz2d8jqrw6wc4cduvapuyeseh7a855l82wutcpeq30kg"; + expect(getBolt11Amount(invoice), 1000); + }); + + test("should get the amount 1 SAT encoded as nanobitcoin", () { + final invoice = "lnbc10n1p5exazppp5szh273r6nzvwydzsdlg0ws0vhxwnc6nqtqt4sjt0vu6x6l8t5csqdqqcqzzsxqrrsssp5nkup6uht7xlfqaj8lxpe5kx0ejum9p4pvv3nlnndr9ykzge3393s9qxpqysgqufh05a7qs0ezeckvmh7tjkr9ngng96nq274twg20z45ysp7afpdrttm2ky73cpjwpjqsmtzzd79w6lyvszgmkanwn8mj0jcfunrwv3qprg3z3j"; + expect(getBolt11Amount(invoice), 1); + }); + }); +} diff --git a/cw_decred/lib/balance.dart b/cw_decred/lib/balance.dart index a88098a9f..97889c9af 100644 --- a/cw_decred/lib/balance.dart +++ b/cw_decred/lib/balance.dart @@ -2,24 +2,25 @@ import 'package:cw_decred/amount_format.dart'; import 'package:cw_core/balance.dart'; class DecredBalance extends Balance { - const DecredBalance({required this.confirmed, required this.unconfirmed, required this.frozen}) - : super(confirmed, unconfirmed); + DecredBalance({required this.confirmed, required this.unconfirmed, required int frozen}) + : _frozen = frozen, super.fromInt(confirmed, unconfirmed); factory DecredBalance.zero() => DecredBalance(confirmed: 0, unconfirmed: 0, frozen: 0); final int confirmed; final int unconfirmed; - final int frozen; + final int _frozen; + BigInt get frozen => BigInt.from(_frozen); @override - String get formattedAvailableBalance => decredAmountToString(amount: confirmed - frozen); + String get formattedAvailableBalance => decredAmountToString(amount: confirmed - _frozen); @override String get formattedAdditionalBalance => decredAmountToString(amount: unconfirmed); @override String get formattedUnAvailableBalance { - final frozenFormatted = decredAmountToString(amount: frozen); + final frozenFormatted = decredAmountToString(amount: _frozen); return frozenFormatted == '0.0' ? '' : frozenFormatted; } } diff --git a/cw_decred/lib/wallet.dart b/cw_decred/lib/wallet.dart index 432edb4bc..97c118331 100644 --- a/cw_decred/lib/wallet.dart +++ b/cw_decred/lib/wallet.dart @@ -54,7 +54,7 @@ abstract class DecredWalletBase derivationInfo.derivationPath == DecredWalletService.pubkeyRestorePathTestnet, super(walletInfo, derivationInfo) { - walletAddresses = DecredWalletAddresses(walletInfo, libwallet); + walletAddresses = DecredWalletAddresses(walletInfo, libwallet, isTestnet); transactionHistory = DecredTransactionHistory(); reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) { diff --git a/cw_decred/lib/wallet_addresses.dart b/cw_decred/lib/wallet_addresses.dart index e4af108b9..7bdff8e77 100644 --- a/cw_decred/lib/wallet_addresses.dart +++ b/cw_decred/lib/wallet_addresses.dart @@ -1,8 +1,9 @@ import 'dart:convert'; +import 'package:cw_core/payment_uris.dart'; +import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:mobx/mobx.dart'; -import 'package:cw_core/address_info.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_decred/api/libdcrwallet.dart'; @@ -12,11 +13,10 @@ part 'wallet_addresses.g.dart'; class DecredWalletAddresses = DecredWalletAddressesBase with _$DecredWalletAddresses; abstract class DecredWalletAddressesBase extends WalletAddresses with Store { - DecredWalletAddressesBase(WalletInfo walletInfo, Libwallet libwallet) - : _libwallet = libwallet, - super(walletInfo); + DecredWalletAddressesBase(super.walletInfo, this._libwallet, super.isTestnet); + final Libwallet _libwallet; - String currentAddr = ''; + String _currentAddr = ''; @observable bool isEnabledAutoGenerateSubaddress = true; @@ -26,14 +26,10 @@ abstract class DecredWalletAddressesBase extends WalletAddresses with Store { @override @computed - String get address { - return selectedAddr; - } + String get address => selectedAddr; @override - set address(value) { - selectedAddr = value; - } + set address(value) => selectedAddr = value; @override Future init() async { @@ -47,14 +43,13 @@ abstract class DecredWalletAddressesBase extends WalletAddresses with Store { @override Future updateAddressesInBox() async { - final addrs = await libAddresses(); - final allAddrs = new List.from(addrs.usedAddrs)..addAll(addrs.unusedAddrs); + final addrs = await _libAddresses(); + final allAddrs = List.from(addrs.usedAddrs)..addAll(addrs.unusedAddrs); // Add all addresses. allAddrs.forEach((addr) { - if (addressesMap.containsKey(addr)) { - return; - } + if (addressesMap.containsKey(addr)) return; + addressesMap[addr] = ""; addressInfos[0] ??= []; addressInfos[0]?.add( @@ -70,44 +65,37 @@ abstract class DecredWalletAddressesBase extends WalletAddresses with Store { // Add used addresses. addrs.usedAddrs.forEach((addr) { - if (!usedAddresses.contains(addr)) { - usedAddresses.add(addr); - } + if (!usedAddresses.contains(addr)) usedAddresses.add(addr); }); - if (addrs.unusedAddrs.length > 0 && addrs.unusedAddrs[0] != currentAddr) { - currentAddr = addrs.unusedAddrs[0]; - selectedAddr = currentAddr; + if (addrs.unusedAddrs.length > 0 && addrs.unusedAddrs[0] != _currentAddr) { + _currentAddr = addrs.unusedAddrs[0]; + selectedAddr = _currentAddr; } await saveAddressesInBox(); } List getAddressInfos() { - if (addressInfos.containsKey(0)) { - return addressInfos[0]!; - } + if (addressInfos.containsKey(0)) return addressInfos[0]!; + return []; } Future updateAddress(String address, String label) async { - if (!addressInfos.containsKey(0)) { - return; - } + if (!addressInfos.containsKey(0)) return; + addressInfos[0]!.forEach((info) { - if (info.address == address) { - info.label = label; - } + if (info.address == address) info.label = label; }); await saveAddressesInBox(); } - Future libAddresses() async { + Future<_LibAddresses> _libAddresses() async { final nUsed = "10"; var nUnused = "1"; - if (this.isEnabledAutoGenerateSubaddress) { - nUnused = "3"; - } + if (this.isEnabledAutoGenerateSubaddress) nUnused = "3"; + try { final res = await _libwallet.addresses(walletInfo.name, nUsed, nUnused); final decoded = json.decode(res); @@ -115,10 +103,10 @@ abstract class DecredWalletAddressesBase extends WalletAddresses with Store { final unusedAddrs = List.from(decoded["unused"] ?? []); // index is the index of the first unused address. final index = decoded["index"] ?? 0; - return new LibAddresses(usedAddrs, unusedAddrs, index); + return _LibAddresses(usedAddrs, unusedAddrs, index); } catch (e) { printV(e); - return LibAddresses([], [], 0); + return _LibAddresses([], [], 0); } } @@ -126,9 +114,8 @@ abstract class DecredWalletAddressesBase extends WalletAddresses with Store { // NOTE: This will ignore the gap limit and may cause problems when restoring from seed if too // many addresses are taken and not used. final addr = await _libwallet.newExternalAddress(walletInfo.name) ?? ''; - if (addr == "") { - return; - } + if (addr == "") return; + if (!addressesMap.containsKey(addr)) { addressesMap[addr] = ""; addressInfos[0] ??= []; @@ -145,11 +132,22 @@ abstract class DecredWalletAddressesBase extends WalletAddresses with Store { selectedAddr = addr; await saveAddressesInBox(); } + + @override + List get receivePageOptions => isTestnet + ? [ + ReceivePageOption.testnet, + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ] + : ReceivePageOptions; + + @override + PaymentURI getPaymentUri(String amount) => DecredURI(address: address, amount: amount); } -class LibAddresses { +class _LibAddresses { final List usedAddrs, unusedAddrs; final int firstUnusedAddrIndex; - LibAddresses(this.usedAddrs, this.unusedAddrs, this.firstUnusedAddrIndex); + _LibAddresses(this.usedAddrs, this.unusedAddrs, this.firstUnusedAddrIndex); } diff --git a/cw_decred/pubspec.lock b/cw_decred/pubspec.lock index 035874d93..6600652b8 100644 --- a/cw_decred/pubspec.lock +++ b/cw_decred/pubspec.lock @@ -46,6 +46,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + bech32: + dependency: transitive + description: + path: "." + ref: HEAD + resolved-ref: "05755063b593aa6cca0a4820a318e0ce17de6192" + url: "https://github.com/cake-tech/bech32.git" + source: git + version: "0.2.2" blockchain_utils: dependency: transitive description: diff --git a/cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart b/cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart index 75d06c148..ed3e56f8b 100644 --- a/cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart +++ b/cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart @@ -2,6 +2,7 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/utils.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; @@ -10,7 +11,8 @@ part 'dogecoin_wallet_addresses.g.dart'; class DogeCoinWalletAddresses = DogeCoinWalletAddressesBase with _$DogeCoinWalletAddresses; abstract class DogeCoinWalletAddressesBase extends ElectrumWalletAddresses with Store { - DogeCoinWalletAddressesBase(WalletInfo walletInfo, { + DogeCoinWalletAddressesBase( + WalletInfo walletInfo, { required super.mainHd, required super.sideHd, required super.network, @@ -18,12 +20,18 @@ abstract class DogeCoinWalletAddressesBase extends ElectrumWalletAddresses with super.initialAddresses, super.initialRegularAddressIndex, super.initialChangeAddressIndex, - super.initialAddressPageType + super.initialAddressPageType, }) : super(walletInfo); @override - String getAddress({required int index, + String getAddress({ + required int index, required Bip32Slip10Secp256k1 hd, - BitcoinAddressType? addressType}) => + BitcoinAddressType? addressType, + }) => generateP2PKHAddress(hd: hd, index: index, network: network); + + @override + PaymentURI getPaymentUri(String amount) => + DogeURI(address: address, amount: amount); } diff --git a/cw_evm/lib/clients/evm_chain_client.dart b/cw_evm/lib/clients/evm_chain_client.dart index c1676c5de..a39e4becf 100644 --- a/cw_evm/lib/clients/evm_chain_client.dart +++ b/cw_evm/lib/clients/evm_chain_client.dart @@ -74,12 +74,40 @@ class EVMChainClient { }); } + // Merge split transfers (same hash + same token) + final Map> mergedMap = {}; + for (var tx in res) { + final hash = tx['hash']; + final key = '${hash}_${tx['contractAddress'] ?? ''}'; + + if (mergedMap.containsKey(key)) { + try { + final currentNet = getNetFlow(mergedMap[key]!, address); + final newNet = getNetFlow(tx, address); + final totalNet = currentNet + newNet; + + mergedMap[key]!['value'] = totalNet.abs().toString(); + if (totalNet < BigInt.zero) { + mergedMap[key]!['from'] = address; + } else { + + mergedMap[key]!['to'] = address; + mergedMap[key]!['from'] = ''; + } + } catch (e) { + printV('Error merging transaction values: $e'); + } + } else { + mergedMap[key] = Map.from(tx); + } + } + + final mergedList = mergedMap.values.toList(); + final symbol = EVMChainUtils.getFeeCurrency(chainId); - return res - .map( - (e) => EVMChainTransactionModel.fromJson(e as Map, symbol, chainId), - ) + return mergedList + .map((e) => EVMChainTransactionModel.fromJson(e, symbol, chainId)) .toList(); } @@ -90,6 +118,18 @@ class EVMChainClient { } } + + BigInt getNetFlow(Map txData, String address) { + final val = BigInt.parse(txData['value'] ?? '0'); + final isIncoming = txData['to']?.toLowerCase() == address.toLowerCase(); + final isOutgoing = txData['from']?.toLowerCase() == address.toLowerCase(); + + if (isIncoming && !isOutgoing) return val; + if (isOutgoing && !isIncoming) return -val; + return BigInt.zero; + } + + Future> fetchInternalTransactions(String address) async { try { if (secrets.etherScanApiKey.isEmpty) { diff --git a/cw_evm/lib/evm_chain_transaction_info.dart b/cw_evm/lib/evm_chain_transaction_info.dart index 47da8a24c..128c00eab 100644 --- a/cw_evm/lib/evm_chain_transaction_info.dart +++ b/cw_evm/lib/evm_chain_transaction_info.dart @@ -1,5 +1,4 @@ // ignore_for_file: overridden_fields, annotate_overrides - import 'dart:math'; import 'package:cw_core/format_amount.dart'; @@ -69,10 +68,7 @@ class EVMChainTransactionInfo extends TransactionInfo { void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); @override - String feeFormatted() { - final amount = formatFixed(ethFee, 18); - return '$amount $feeCurrency'; - } + String feeFormatted() => '${formatFixed(ethFee, 18)} $feeCurrency'; factory EVMChainTransactionInfo.fromJson(Map data, int chainId) { return EVMChainTransactionInfo( diff --git a/cw_evm/lib/evm_chain_transaction_priority.dart b/cw_evm/lib/evm_chain_transaction_priority.dart index 721c7e477..19e8697b9 100644 --- a/cw_evm/lib/evm_chain_transaction_priority.dart +++ b/cw_evm/lib/evm_chain_transaction_priority.dart @@ -9,7 +9,7 @@ class EVMChainTransactionPriority extends TransactionPriority { static const List all = [fast, medium, slow]; static const EVMChainTransactionPriority slow = - EVMChainTransactionPriority(title: 'slow', raw: 0, tip: 1); + EVMChainTransactionPriority(title: 'Slow', raw: 0, tip: 1); static const EVMChainTransactionPriority medium = EVMChainTransactionPriority(title: 'Medium', raw: 1, tip: 2); static const EVMChainTransactionPriority fast = diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index f60cd64e9..c0453d817 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -53,9 +53,18 @@ import 'evm_erc20_balance.dart'; part 'evm_chain_wallet.g.dart'; const Map methodSignatureToType = { + // ERC20 '0x095ea7b3': 'approval', '0xa9059cbb': 'transfer', '0x23b872dd': 'transferFrom', + + // Aggregator / Router (Swaps.xyz paths) + '0x9be111d1': 'smartSwap', + '0x5327a3d2': 'crossChainSwap', + '0x205030b2': 'routerExecution', + '0x5ad29efa': 'relayedExecution', + + // Misc contracts '0x574da717': 'transferOut', '0x2e1a7d4d': 'withdraw', '0x7ff36ab5': 'swapExactETHForTokens', @@ -90,7 +99,7 @@ abstract class EVMChainWalletBase _isTransactionUpdating = false, _client = client, selectedChainId = initialChainId ?? _getInitialChainId(walletInfo.type), - walletAddresses = EVMChainWalletAddresses(walletInfo), + walletAddresses = EVMChainWalletAddresses(walletInfo, initialChainId ?? _getInitialChainId(walletInfo.type)), balance = ObservableMap.of( { nativeCurrency: initialBalance ?? EVMChainERC20Balance(BigInt.zero), @@ -320,7 +329,7 @@ abstract class EVMChainWalletBase if (prefs.getBool(migrationKey) ?? false) return; final box = evmChainErc20TokensBox; - final keys = box.keys.cast().toList(growable: false); + final keys = box.keys.toList(); if (keys.isEmpty) { await prefs.setBool(migrationKey, true); @@ -338,7 +347,8 @@ abstract class EVMChainWalletBase continue; } - final lowerKey = key.toLowerCase(); + final lowerKey = key is String ? key.toLowerCase() : token.contractAddress.toLowerCase(); + if (key is int) needsRewrite = true; final Erc20Token normalizedToken = token.contractAddress == token.contractAddress.toLowerCase() @@ -1038,21 +1048,16 @@ abstract class EVMChainWalletBase } Future createCallDataTransaction( - String to, - String dataHex, - BigInt valueWei, - EVMChainTransactionPriority? priority, { - bool useBlinkProtection = true, - }) async { - // Estimate gas with the SAME call (sender, to, value, data) - final gas = await calculateActualEstimatedFeeForCreateTransaction( - amount: valueWei, // native value (usually 0 for ERC20 transfer) - receivingAddressHex: to, - priority: priority, - contractAddress: null, - data: _client.hexToBytes(dataHex), - ); + String to, + String dataHex, + BigInt valueWei, + EVMChainTransactionPriority? priority, + String? sourceTokenAddress, + BigInt? sourceTokenAmount, { + bool useBlinkProtection = true, + }) async { + // Define Native Currency final nativeCurrency = switch (selectedChainId) { 137 => CryptoCurrency.maticpoly, 56 => CryptoCurrency.bnb, @@ -1061,26 +1066,83 @@ abstract class EVMChainWalletBase _ => CryptoCurrency.eth, }; - // Fallback for nodes that fail estimate (non-zero) - final gasUnits = gas.estimatedGasUnits == 0 ? 65000 : gas.estimatedGasUnits; + // Gas Estimation + GasParamsHandler gas; + try { + gas = await calculateActualEstimatedFeeForCreateTransaction( + amount: valueWei, + receivingAddressHex: to, + priority: priority, + contractAddress: null, + data: _client.hexToBytes(dataHex), + ); + } catch (_) { + // If estimation fails, we proceed but will use a safe gas limit below. + // This is common for complex swaps that depend on block state. + gas = GasParamsHandler.zero(); + } - // Sign raw (native) tx with callData - return _client.signTransaction( - privateKey: _evmChainPrivateKey, - toAddress: to, - amount: valueWei, - gasFee: BigInt.from(gas.estimatedGasFee), - estimatedGasUnits: gasUnits, - maxFeePerGas: gas.maxFeePerGas, - priority: priority, - currency: nativeCurrency, - feeCurrency: nativeCurrency.title, - exponent: 18, - contractAddress: null, - data: dataHex, - gasPrice: gas.gasPrice, - useBlinkProtection: useBlinkProtection, - ); + // Validate NATIVE Balance (for Gas + Value) + final nativeBal = balance[nativeCurrency]?.balance ?? BigInt.zero; + final requiredNative = valueWei + BigInt.from(gas.estimatedGasFee); + + if (requiredNative > nativeBal) { + throw Exception('Not enough ${nativeCurrency.title} to cover value and fees.'); + } + + // Validate ERC20 Balance (Only if source is NOT native) + final cleanAddress = sourceTokenAddress?.toLowerCase() ?? ''; + + // Check for both 0xeeee... AND 0x0000... (Zero Address) + bool isNativeSource = sourceTokenAddress == null || + cleanAddress == '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' || + cleanAddress == '0x0000000000000000000000000000000000000000'; + + if (!isNativeSource && sourceTokenAmount != null && sourceTokenAmount > BigInt.zero) { + + // Filter list to find match. + final matchingTokens = balance.keys.where((k) => + k is Erc20Token && + k.contractAddress.toLowerCase() == cleanAddress + ); + + if (matchingTokens.isEmpty) { + // Token is not in the wallet balance map -> Balance is 0 + throw Exception('Insufficient token balance (Token not found in wallet).'); + } + + final tokenKey = matchingTokens.first; + final tokenBalance = balance[tokenKey]?.balance ?? BigInt.zero; + + if (tokenBalance < sourceTokenAmount) { + throw Exception('Insufficient ${tokenKey.title} balance to cover the transaction amount.'); + } + } + + // Final Safe Gas Limit + // If estimation failed (0), use 300,000 as a safe default for swaps. + final gasUnits = gas.estimatedGasUnits == 0 ? 300000 : gas.estimatedGasUnits; + + try { + return _client.signTransaction( + privateKey: _evmChainPrivateKey, + toAddress: to, + amount: valueWei, + gasFee: BigInt.from(gas.estimatedGasFee), + estimatedGasUnits: gasUnits, + maxFeePerGas: gas.maxFeePerGas, + priority: priority, + currency: nativeCurrency, + feeCurrency: nativeCurrency.title, + exponent: 18, + contractAddress: null, + data: dataHex, + gasPrice: gas.gasPrice, + useBlinkProtection: useBlinkProtection, + ); + } catch (_) { + throw Exception('Failed to create the transaction.'); + } } Future createApprovalTransaction(BigInt amount, String spender, @@ -1096,14 +1158,20 @@ abstract class EVMChainWalletBase toAddress: EthereumAddress.fromHex(spender), ); + final tokenContract = (transactionCurrency as Erc20Token).contractAddress; + final gasFeesModel = await calculateActualEstimatedFeeForCreateTransaction( - amount: amount, - receivingAddressHex: spender, + amount: BigInt.zero, + receivingAddressHex: tokenContract, priority: priority, - contractAddress: transactionCurrency.contractAddress, + contractAddress: tokenContract, data: data, ); + final int safeGasUnits = gasFeesModel.estimatedGasUnits == 0 + ? 65000 + : (gasFeesModel.estimatedGasUnits < 65000 ? 65000 : gasFeesModel.estimatedGasUnits); + return _client.signApprovalTransaction( privateKey: _evmChainPrivateKey, spender: spender, @@ -1112,9 +1180,9 @@ abstract class EVMChainWalletBase gasFee: BigInt.from(gasFeesModel.estimatedGasFee), maxFeePerGas: gasFeesModel.maxFeePerGas, feeCurrency: feeCurrency, - estimatedGasUnits: gasFeesModel.estimatedGasUnits, + estimatedGasUnits: safeGasUnits, exponent: transactionCurrency.decimal, - contractAddress: transactionCurrency.contractAddress, + contractAddress: tokenContract, gasPrice: gasFeesModel.gasPrice, useBlinkProtection: useBlinkProtection, ); @@ -1180,7 +1248,26 @@ abstract class EVMChainWalletBase continue; } - result[transactionModel.hash] = getTransactionInfo(transactionModel, address); + final newTxInfo = getTransactionInfo(transactionModel, address); + final existingTxInfo = result[transactionModel.hash]; + final savedTxInfo = transactionHistory.transactions[transactionModel.hash]; + + // Prioritize saved incoming transactions + if (savedTxInfo != null && + savedTxInfo.direction == TransactionDirection.incoming && + newTxInfo.direction == TransactionDirection.outgoing) { + result[transactionModel.hash] = savedTxInfo; + continue; + } + + if (existingTxInfo == null) { + result[transactionModel.hash] = newTxInfo; + } + + else if (newTxInfo.direction == TransactionDirection.incoming && + existingTxInfo.direction == TransactionDirection.outgoing) { + result[transactionModel.hash] = newTxInfo; + } } return result; @@ -1188,7 +1275,7 @@ abstract class EVMChainWalletBase String? analyzeTransaction(String? transactionInput) { if (transactionInput == '0x' || transactionInput == null || transactionInput.isEmpty) { - return 'simpleTransfer'; + return ''; } final methodSignature = @@ -1330,7 +1417,20 @@ abstract class EVMChainWalletBase final token = tokenContract.toLowerCase(); if (token == zero || token == evmNative.toLowerCase()) return false; if (requiredAmount <= BigInt.zero) return false; + try { + final allowance = await getAllowance(tokenContract, spender); + if (allowance == null) { + printV('Could not fetch allowance for $tokenContract, assuming approval is required'); + return true; + } + return allowance < requiredAmount; + } catch (e) { + printV('approval-check error: $e'); + return true; + } + } + Future getAllowance(String tokenContract, String spender) async { try { final owner = _evmChainPrivateKey.address; final erc20 = ERC20( @@ -1340,11 +1440,10 @@ abstract class EVMChainWalletBase ); final allowance = await erc20.allowance(owner, EthereumAddress.fromHex(spender)); - - return allowance < requiredAmount; + return allowance; } catch (e) { - printV('approval-check error: $e'); - return true; + printV('getAllowance error: $e'); + return null; } } diff --git a/cw_evm/lib/evm_chain_wallet_addresses.dart b/cw_evm/lib/evm_chain_wallet_addresses.dart index 7dd501cc5..2d577f418 100644 --- a/cw_evm/lib/evm_chain_wallet_addresses.dart +++ b/cw_evm/lib/evm_chain_wallet_addresses.dart @@ -1,5 +1,6 @@ import 'dart:developer'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; @@ -9,7 +10,7 @@ part 'evm_chain_wallet_addresses.g.dart'; class EVMChainWalletAddresses = EVMChainWalletAddressesBase with _$EVMChainWalletAddresses; abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store { - EVMChainWalletAddressesBase(WalletInfo walletInfo) + EVMChainWalletAddressesBase(WalletInfo walletInfo, this._selectedChainId) : address = '', super(walletInfo); @@ -20,6 +21,9 @@ abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store { @override String get primaryAddress => address; + final int _selectedChainId; + + @override Future init() async { address = walletInfo.address; @@ -36,4 +40,22 @@ abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store { log(e.toString()); } } + + @override + PaymentURI getPaymentUri(String amount) { + switch (_selectedChainId) { + case 1: + return EthereumURI(amount: amount, address: address); + case 137: + return PolygonURI(amount: amount, address: address); + case 8453: + return BaseURI(amount: amount, address: address); + case 42161: + return ArbitrumURI(amount: amount, address: address); + case 56: + return BSCURI(amount: amount, address: address); + default: + return EthereumURI(amount: amount, address: address); + } + } } diff --git a/cw_evm/lib/evm_erc20_balance.dart b/cw_evm/lib/evm_erc20_balance.dart index a29b74188..13ae310c8 100644 --- a/cw_evm/lib/evm_erc20_balance.dart +++ b/cw_evm/lib/evm_erc20_balance.dart @@ -1,21 +1,14 @@ import 'dart:convert'; -import 'package:cw_core/format_fixed.dart'; import 'package:cw_core/balance.dart'; class EVMChainERC20Balance extends Balance { EVMChainERC20Balance(this.balance, {this.exponent = 18}) - : super(balance.toInt(), balance.toInt()); + : super(balance, balance); final BigInt balance; final int exponent; - @override - String get formattedAdditionalBalance => formatFixed(balance, exponent, fractionalDigits: 12); - - @override - String get formattedAvailableBalance => formatFixed(balance, exponent, fractionalDigits: 12); - String toJSON() => json.encode({ 'balanceInWei': balance.toString(), 'exponent': exponent, diff --git a/cw_evm/lib/tokens/ethereum_tokens.dart b/cw_evm/lib/tokens/ethereum_tokens.dart index 5f1f4b273..d1395d761 100644 --- a/cw_evm/lib/tokens/ethereum_tokens.dart +++ b/cw_evm/lib/tokens/ethereum_tokens.dart @@ -88,7 +88,7 @@ class EthereumTokens { contractAddress: "0x68749665ff8d2d112fa859aa293f07a622782f38", decimal: 6, enabled: false, - iconPath: "assets/images/xaut_icon.png", + iconPath: "assets/images/xaut.webp", ), ]; diff --git a/cw_evm/lib/utils/evm_chain_utils.dart b/cw_evm/lib/utils/evm_chain_utils.dart index 634d2a1fc..59b6f6560 100644 --- a/cw_evm/lib/utils/evm_chain_utils.dart +++ b/cw_evm/lib/utils/evm_chain_utils.dart @@ -72,7 +72,7 @@ class EVMChainUtils { static String getFeeCurrency(int chainId) { return switch (chainId) { 1 => 'ETH', - 137 => 'MATIC', + 137 => 'POL', 8453 => 'ETH', 42161 => 'ETH', 56 => 'BNB', @@ -83,7 +83,7 @@ class EVMChainUtils { static String getDefaultTokenSymbol(int chainId) { return switch (chainId) { 1 => 'ETH', - 137 => 'MATIC', + 137 => 'POL', 8453 => 'BASE', 42161 => 'ARBITRUM', 56 => 'BSC', diff --git a/cw_monero/lib/monero_transaction_info.dart b/cw_monero/lib/monero_transaction_info.dart index 0ac48dcba..b7ddd0ca6 100644 --- a/cw_monero/lib/monero_transaction_info.dart +++ b/cw_monero/lib/monero_transaction_info.dart @@ -1,13 +1,12 @@ -import 'package:cw_core/transaction_info.dart'; -import 'package:cw_core/monero_amount_format.dart'; -import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_info.dart'; class MoneroTransactionInfo extends TransactionInfo { - MoneroTransactionInfo(this.txHash, this.height, this.direction, this.date, - this.isPending, this.amount, this.accountIndex, this.addressIndex, this.fee, - this.confirmations) : - id = "${txHash}_${amount}_${accountIndex}_${addressIndex}"; + MoneroTransactionInfo(this.txHash, this.height, this.direction, this.date, this.isPending, + this.amount, this.accountIndex, this.addressIndex, this.fee, this.confirmations) + : id = "${txHash}_${amount}_${accountIndex}_${addressIndex}"; final String id; final String txHash; @@ -25,8 +24,7 @@ class MoneroTransactionInfo extends TransactionInfo { String? _fiatAmount; @override - String amountFormatted() => - '${formatAmount(moneroAmountToString(amount: amount))} XMR'; + String amountFormatted() => '${CryptoCurrency.xmr.formatAmount(BigInt.from(amount))} XMR'; @override String fiatAmount() => _fiatAmount ?? ''; @@ -35,6 +33,5 @@ class MoneroTransactionInfo extends TransactionInfo { void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); @override - String feeFormatted() => - '${formatAmount(moneroAmountToString(amount: fee))} XMR'; + String feeFormatted() => '${CryptoCurrency.xmr.formatAmount(BigInt.from(fee))} XMR'; } diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 9f5986e26..81771193b 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -3,6 +3,7 @@ import 'dart:ffi'; import 'dart:io'; import 'dart:isolate'; +import 'package:cw_core/format_fixed.dart'; import 'package:cw_core/monero_amount_format.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/transaction_priority.dart'; diff --git a/cw_monero/lib/monero_wallet_addresses.dart b/cw_monero/lib/monero_wallet_addresses.dart index 0b38ac5fd..9a7264c03 100644 --- a/cw_monero/lib/monero_wallet_addresses.dart +++ b/cw_monero/lib/monero_wallet_addresses.dart @@ -1,5 +1,5 @@ import 'package:cw_core/account.dart'; -import 'package:cw_core/address_info.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/subaddress.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; @@ -155,4 +155,7 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { @override bool containsAddress(String address) => addressInfos[account?.id ?? 0]?.any((it) => it.address == address) ?? false; + + @override + PaymentURI getPaymentUri(String amount) => MoneroURI(address: address, amount: amount); } diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 9de3df0bf..2d4bb12ee 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -46,6 +46,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + bech32: + dependency: transitive + description: + path: "." + ref: HEAD + resolved-ref: "05755063b593aa6cca0a4820a318e0ce17de6192" + url: "https://github.com/cake-tech/bech32.git" + source: git + version: "0.2.2" bip32: dependency: "direct main" description: diff --git a/cw_nano/lib/banano_balance.dart b/cw_nano/lib/banano_balance.dart index d766077fc..408b9dddb 100644 --- a/cw_nano/lib/banano_balance.dart +++ b/cw_nano/lib/banano_balance.dart @@ -9,19 +9,19 @@ class BananoBalance extends Balance { final BigInt currentBalance; final BigInt receivableBalance; - BananoBalance({required this.currentBalance, required this.receivableBalance}) : super(0, 0); + BananoBalance({required this.currentBalance, required this.receivableBalance}) : super(BigInt.zero, BigInt.zero); BananoBalance.fromFormattedString( {required String formattedCurrentBalance, required String formattedReceivableBalance}) : currentBalance = stringAmountToBigIntBanano(formattedCurrentBalance), receivableBalance = stringAmountToBigIntBanano(formattedReceivableBalance), - super(0, 0); + super(BigInt.zero, BigInt.zero); BananoBalance.fromRawString( {required String currentBalance, required String receivableBalance}) : currentBalance = BigInt.parse(currentBalance), receivableBalance = BigInt.parse(receivableBalance), - super(0, 0); + super(BigInt.zero, BigInt.zero); @override String get formattedAvailableBalance { diff --git a/cw_nano/lib/nano_balance.dart b/cw_nano/lib/nano_balance.dart index 691b3a32d..ce9464696 100644 --- a/cw_nano/lib/nano_balance.dart +++ b/cw_nano/lib/nano_balance.dart @@ -1,4 +1,5 @@ import 'package:cw_core/balance.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:nanoutil/nanoutil.dart'; BigInt stringAmountToBigIntNano(String amount) { @@ -9,19 +10,19 @@ class NanoBalance extends Balance { final BigInt currentBalance; final BigInt receivableBalance; - NanoBalance({required this.currentBalance, required this.receivableBalance}) : super(0, 0); + NanoBalance({required this.currentBalance, required this.receivableBalance}) : super(currentBalance, receivableBalance); NanoBalance.fromFormattedString( {required String formattedCurrentBalance, required String formattedReceivableBalance}) : currentBalance = stringAmountToBigIntNano(formattedCurrentBalance), receivableBalance = stringAmountToBigIntNano(formattedReceivableBalance), - super(0, 0); + super(stringAmountToBigIntNano(formattedCurrentBalance), stringAmountToBigIntNano(formattedReceivableBalance)); NanoBalance.fromRawString( {required String currentBalance, required String receivableBalance}) : currentBalance = BigInt.parse(currentBalance), receivableBalance = BigInt.parse(receivableBalance), - super(0, 0); + super(BigInt.parse(currentBalance), BigInt.parse(receivableBalance)); @override String get formattedAvailableBalance { diff --git a/cw_nano/lib/nano_wallet_addresses.dart b/cw_nano/lib/nano_wallet_addresses.dart index f1ff14a85..f52cf4ca1 100644 --- a/cw_nano/lib/nano_wallet_addresses.dart +++ b/cw_nano/lib/nano_wallet_addresses.dart @@ -1,4 +1,5 @@ import 'package:cw_core/cake_hive.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; @@ -15,6 +16,7 @@ abstract class NanoWalletAddressesBase extends WalletAddresses with Store { : accountList = NanoAccountList(walletInfo.address), address = '', super(walletInfo); + @override @observable String address; @@ -51,4 +53,7 @@ abstract class NanoWalletAddressesBase extends WalletAddresses with Store { printV(e.toString()); } } + + @override + PaymentURI getPaymentUri(String amount) => NanoURI(amount: amount, address: address); } diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock index f139a4f1a..72684a8fc 100644 --- a/cw_nano/pubspec.lock +++ b/cw_nano/pubspec.lock @@ -46,6 +46,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + bech32: + dependency: transitive + description: + path: "." + ref: HEAD + resolved-ref: "05755063b593aa6cca0a4820a318e0ce17de6192" + url: "https://github.com/cake-tech/bech32.git" + source: git + version: "0.2.2" bip32: dependency: "direct main" description: diff --git a/cw_solana/lib/solana_balance.dart b/cw_solana/lib/solana_balance.dart index e88a510dd..5b8a533e8 100644 --- a/cw_solana/lib/solana_balance.dart +++ b/cw_solana/lib/solana_balance.dart @@ -3,7 +3,9 @@ import 'dart:convert'; import 'package:cw_core/balance.dart'; class SolanaBalance extends Balance { - SolanaBalance(this.balance) : super(balance.toInt(), balance.toInt()); + SolanaBalance(this.balance, bool isToken) : super( + BigInt.from(int.tryParse(balance.toStringAsFixed(isToken ? 6 : 9).replaceFirst(".", "")) ?? 0), + BigInt.from(int.tryParse(balance.toStringAsFixed(isToken ? 6 : 9).replaceFirst(".", "")) ?? 0)); final double balance; @@ -21,7 +23,7 @@ class SolanaBalance extends Balance { return stringBalance; } - static SolanaBalance? fromJSON(String? jsonSource) { + static SolanaBalance? fromJSON(String? jsonSource, bool isToken) { if (jsonSource == null) { return null; } @@ -29,9 +31,9 @@ class SolanaBalance extends Balance { final decoded = json.decode(jsonSource) as Map; try { - return SolanaBalance(decoded['balance']); + return SolanaBalance(decoded['balance'], isToken); } catch (e) { - return SolanaBalance(0.0); + return SolanaBalance(0.0, isToken); } } diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart index 2f4eff10b..5ceb7f756 100644 --- a/cw_solana/lib/solana_client.dart +++ b/cw_solana/lib/solana_client.dart @@ -126,7 +126,7 @@ class SolanaWalletClient { totalBalance += balanceAsDouble; } - return SolanaBalance(totalBalance); + return SolanaBalance(totalBalance, true); } catch (_) { if (throwOnError) { rethrow; @@ -293,83 +293,105 @@ class SolanaWalletClient { return null; } - /// Detects if a transaction is a swap by checking for both native SOL - /// and SPL token balance changes involving the wallet address + /// Detects if a transaction is a swap by checking that the wallet both sends one asset and receives another. + /// + /// A simple token transfer is NOT a swap (wallet only sends or receives, not both). bool _isSwapTransaction( ConfirmedTransactionMeta meta, VersionedMessage message, String walletAddress, ) { - bool hasNativeBalanceChange = false; - bool hasTokenBalanceChange = false; - - // First we check if there are any native SOL balance changes for the wallet + final fee = meta.fee; final preBalances = meta.preBalances; final postBalances = meta.postBalances; final accountKeys = message.accountKeys; + bool walletSentSol = false; + bool walletReceivedSol = false; + if (preBalances.isNotEmpty && postBalances.isNotEmpty) { - final maxLength = - accountKeys.length < preBalances.length ? accountKeys.length : preBalances.length; + final maxLength = [ + accountKeys.length, + preBalances.length, + postBalances.length, + ].reduce((a, b) => a < b ? a : b); - for (int i = 0; i < maxLength && i < postBalances.length; i++) { - final accountKey = accountKeys[i]; - final accountAddress = accountKey.address; + for (int i = 0; i < maxLength; i++) { + if (accountKeys[i].address != walletAddress) { + continue; + } - if (accountAddress == walletAddress) { - final preBalance = preBalances[i]; - final postBalance = postBalances[i]; - final balanceChange = postBalance - preBalance; + final change = postBalances[i] - preBalances[i]; - if (balanceChange != BigInt.zero) { - hasNativeBalanceChange = true; - break; + if (change > BigInt.zero) { + walletReceivedSol = true; + } else if (change < BigInt.zero) { + // Only count as sent if the decrease + // exceeds the fee (otherwise it's just fees). + final netDecrease = change.abs() - BigInt.from(fee); + if (netDecrease > BigInt.zero) { + walletSentSol = true; } } + break; } } - // Next, we check if there are any SPL token balance changes + bool walletSentToken = false; + bool walletReceivedToken = false; - // There is a caveat though, Jupiter swaps token accounts might be intermediate accounts, so we need to check for that, otherwise we might miss some transactions final preTokenBalances = meta.preTokenBalances; final postTokenBalances = meta.postTokenBalances; if (preTokenBalances != null && postTokenBalances != null) { - bool hasTokenDecrease = false; - bool hasTokenIncrease = false; + // Check wallet-owned balances that exist in pre + for (final preBal in preTokenBalances) { + if (preBal.owner?.address != walletAddress) { + continue; + } - for (final preTokenBal in preTokenBalances) { - final mint = preTokenBal.mint.address; - final preAmount = preTokenBal.uiTokenAmount.uiAmount ?? 0.0; + final mint = preBal.mint.address; + final preAmt = preBal.uiTokenAmount.uiAmount ?? 0.0; - // We find the corresponding post balance by matching mint and owner - for (final postTokenBal in postTokenBalances) { - final postMint = postTokenBal.mint.address; - final postOwner = postTokenBal.owner?.address ?? ''; - final preOwner = preTokenBal.owner?.address ?? ''; - final postAmount = postTokenBal.uiTokenAmount.uiAmount ?? 0.0; - - if (postMint == mint && postOwner == preOwner) { - final diff = postAmount - preAmount; - if (diff < 0) { - hasTokenDecrease = true; - } else if (diff > 0) { - hasTokenIncrease = true; - } + double postAmt = preAmt; + for (final postBal in postTokenBalances) { + if (postBal.owner?.address == walletAddress && postBal.mint.address == mint) { + postAmt = postBal.uiTokenAmount.uiAmount ?? 0.0; break; } } + + final diff = postAmt - preAmt; + if (diff < 0) { + walletSentToken = true; + } else if (diff > 0) { + walletReceivedToken = true; + } } - // If we have both token decreases and increases, or if wallet sent SOL and there are token changes, it's likely a swap - if (hasNativeBalanceChange && (hasTokenDecrease || hasTokenIncrease)) { - hasTokenBalanceChange = true; + // Check for tokens the wallet received into + // a newly created ATA (no pre-balance entry). + for (final postBal in postTokenBalances) { + if (postBal.owner?.address != walletAddress) { + continue; + } + final postAmt = postBal.uiTokenAmount.uiAmount ?? 0.0; + if (postAmt <= 0) continue; + + final mint = postBal.mint.address; + final existsInPre = preTokenBalances.any( + (p) => p.owner?.address == walletAddress && p.mint.address == mint, + ); + if (!existsInPre) { + walletReceivedToken = true; + } } } - // It's a swap if both native and token balances changed - return hasNativeBalanceChange && hasTokenBalanceChange; + // A swap requires the wallet to both send and receive across different assets. + final walletSent = walletSentSol || walletSentToken; + final walletReceived = walletReceivedSol || walletReceivedToken; + return walletSent && walletReceived; } /// Parses a swap transaction and creates dual entries (outgoing and incoming) @@ -422,8 +444,7 @@ class SolanaWalletClient { } } - final bool isSplToSplSwap = - decreasedMintForWallet != null && + final bool isSplToSplSwap = decreasedMintForWallet != null && increasedMintForWallet != null && decreasedMintForWallet != increasedMintForWallet; @@ -698,57 +719,67 @@ class SolanaWalletClient { required String signature, required BigInt? blockTime, }) async { - // Calculate total balance changes across all accounts - BigInt totalBalanceChange = BigInt.zero; - String? sender; - String? receiver; + final accountKeys = message.accountKeys; + final preBalances = meta.preBalances; + final postBalances = meta.postBalances; - final accountKeysLength = message.accountKeys.length; - final balancesLength = meta.preBalances.length; - final maxLength = accountKeysLength < balancesLength ? accountKeysLength : balancesLength; + final maxLen = [ + accountKeys.length, + preBalances.length, + postBalances.length, + ].reduce((a, b) => a < b ? a : b); - for (int i = 0; i < maxLength; i++) { - final preBalance = meta.preBalances[i]; - final postBalance = meta.postBalances[i]; - final balanceChange = preBalance - postBalance; - - if (balanceChange > BigInt.zero) { - // This account sent funds - if (i < accountKeysLength) { - sender = message.accountKeys[i].address; - totalBalanceChange += balanceChange; - } - } else if (balanceChange < BigInt.zero) { - // This account received funds - if (i < accountKeysLength) { - receiver = message.accountKeys[i].address; - } + // Find the wallet's own balance change. + int walletIndex = -1; + for (int i = 0; i < maxLen; i++) { + if (accountKeys[i].address == walletAddress) { + walletIndex = i; + break; } } - // We subtract the fee from total balance change if the fee payer is the sender - if (sender != null && - feePayerIndex < message.accountKeys.length && - sender == message.accountKeys[feePayerIndex].address) { - totalBalanceChange -= BigInt.from(fee); + if (walletIndex < 0) return null; + + final walletPre = preBalances[walletIndex]; + final walletPost = postBalances[walletIndex]; + // Positive = wallet lost SOL, negative = wallet gained. + final walletChange = walletPre - walletPost; + + final bool walletPaidFee = + feePayerIndex < accountKeys.length && accountKeys[feePayerIndex].address == walletAddress; + + // Net transfer amount excluding the fee. + final netChange = walletPaidFee ? walletChange - BigInt.from(fee) : walletChange; + + final isOutgoing = netChange > BigInt.zero; + final amountLamports = netChange.abs(); + final amountInSol = amountLamports.toDouble() / SolanaUtils.lamportsPerSol; + + if (amountInSol < minValidAmount) return null; + + // Find the most likely receiver, the account that has the largest opposite balance change. + String? receiver; + BigInt bestChange = BigInt.zero; + + for (int i = 0; i < maxLen; i++) { + if (i == walletIndex) continue; + final change = postBalances[i] - preBalances[i]; + + if (isOutgoing && change > bestChange) { + bestChange = change; + receiver = accountKeys[i].address; + } else if (!isOutgoing && change < BigInt.zero && change.abs() > bestChange) { + bestChange = change.abs(); + receiver = accountKeys[i].address; + } } - if (sender == null || receiver == null) { - return null; - } - - final amount = totalBalanceChange / BigInt.from(1e9); - final amountInSol = amount.abs().toDouble(); - - // Skip transactions with very small amounts (likely spam) - if (amountInSol < minValidAmount) { - return null; - } + if (receiver == null) return null; return SolanaTransactionModel( - isOutgoingTx: sender == walletAddress, - from: sender, - to: receiver, + isOutgoingTx: isOutgoing, + from: isOutgoing ? walletAddress : receiver, + to: isOutgoing ? receiver : walletAddress, id: signature, amount: amountInSol, programId: SystemProgramConst.programId.address, @@ -769,33 +800,57 @@ class SolanaWalletClient { required BigInt? blockTime, String? splTokenSymbol, }) async { - final preBalances = meta.preTokenBalances; - final postBalances = meta.postTokenBalances; + final preTokenBalances = meta.preTokenBalances; + final postTokenBalances = meta.postTokenBalances; + + final accountKeys = message.accountKeys; + final accounts = instruction.accounts; + + // TransferChecked has 4 accounts: + // [0] source, [1] mint, [2] destination, [3] owner + // Transfer has 3 accounts: + // [0] source, [1] destination, [2] owner + final isTransferChecked = accounts.length >= 4; + + final sourceAccountIndex = accounts[0]; + final destinationAccountIndex = isTransferChecked ? accounts[2] : accounts[1]; - double amount = 0.0; - bool isOutgoing = false; String? mintAddress; + if (isTransferChecked) { + mintAddress = accountKeys[accounts[1]].address; + } double userPreAmount = 0.0; - if (preBalances != null && preBalances.isNotEmpty) { - for (final preBal in preBalances) { - if (preBal.owner?.address == walletAddress) { - userPreAmount = preBal.uiTokenAmount.uiAmount ?? 0.0; + double userPostAmount = 0.0; - mintAddress = preBal.mint.address; - break; + if (preTokenBalances != null) { + for (final preBal in preTokenBalances) { + final idx = preBal.accountIndex; + if (idx == sourceAccountIndex || idx == destinationAccountIndex) { + if (preBal.owner?.address == walletAddress) { + if (mintAddress != null && preBal.mint.address != mintAddress) { + continue; + } + mintAddress ??= preBal.mint.address; + userPreAmount = preBal.uiTokenAmount.uiAmount ?? 0.0; + break; + } } } } - double userPostAmount = 0.0; - if (postBalances != null && postBalances.isNotEmpty) { - for (final postBal in postBalances) { - if (postBal.owner?.address == walletAddress) { - userPostAmount = postBal.uiTokenAmount.uiAmount ?? 0.0; - - mintAddress ??= postBal.mint.address; - break; + if (postTokenBalances != null) { + for (final postBal in postTokenBalances) { + final idx = postBal.accountIndex; + if (idx == sourceAccountIndex || idx == destinationAccountIndex) { + if (postBal.owner?.address == walletAddress) { + if (mintAddress != null && postBal.mint.address != mintAddress) { + continue; + } + mintAddress ??= postBal.mint.address; + userPostAmount = postBal.uiTokenAmount.uiAmount ?? 0.0; + break; + } } } } @@ -804,17 +859,35 @@ class SolanaWalletClient { final rawAmount = diff.abs(); final amountInString = rawAmount.toStringAsFixed(6); - amount = double.parse(amountInString); + final amount = double.parse(amountInString); + final isOutgoing = diff > 0; - isOutgoing = diff > 0; + // Resolve sender/receiver from token balance owners + String? senderOwner; + String? receiverOwner; - if (mintAddress == null && instruction.accounts.length >= 4) { - final mintIndex = instruction.accounts[3]; - mintAddress = message.accountKeys[mintIndex].address; + final allBalances = [ + ...?preTokenBalances, + ...?postTokenBalances, + ]; + + for (final bal in allBalances) { + if (mintAddress != null && bal.mint.address != mintAddress) { + continue; + } + if (bal.accountIndex == sourceAccountIndex) { + senderOwner ??= bal.owner?.address; + } + if (bal.accountIndex == destinationAccountIndex) { + receiverOwner ??= bal.owner?.address; + } + if (senderOwner != null && receiverOwner != null) { + break; + } } - final sender = message.accountKeys[instruction.accounts[0]].address; - final receiver = message.accountKeys[instruction.accounts[1]].address; + final sender = senderOwner ?? accountKeys[sourceAccountIndex].address; + final receiver = receiverOwner ?? accountKeys[destinationAccountIndex].address; String? tokenSymbol = splTokenSymbol; @@ -1261,14 +1334,16 @@ class SolanaWalletClient { commitment, ); - bool hasSufficientFundsLeft = await hasSufficientFundsLeftForRent( - inputAmount: inputAmount, - fee: fee, - solBalance: solBalance, - ); + if (!isSendAll) { + bool hasSufficientFundsLeft = await hasSufficientFundsLeftForRent( + inputAmount: inputAmount, + fee: fee, + solBalance: solBalance, + ); - if (!hasSufficientFundsLeft) { - throw SolanaSignNativeTokenTransactionRentException(); + if (!hasSufficientFundsLeft) { + throw SolanaSignNativeTokenTransactionRentException(); + } } String serializedTransaction; @@ -1434,7 +1509,7 @@ class SolanaWalletClient { // For transaction history loading (shouldCreateATA: false), try standard token program first // to avoid unnecessary RPC call. Only fetch token program ID when creating accounts. SolAddress tokenProgramId = SPLTokenProgramConst.tokenProgramId; - + if (shouldCreateATA) { // Only fetch token program ID when we need to create an account tokenProgramId = await _getTokenProgramId(mintAddress); @@ -1472,7 +1547,7 @@ class SolanaWalletClient { owner: ownerAddress, tokenProgramId: token2022ProgramId, ); - + try { accountInfo = await _provider!.request( SolanaRPCGetAccountInfo( diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index e9d89d0f6..7ffc1b24f 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -57,7 +57,7 @@ abstract class SolanaWalletBase _client = SolanaWalletClient(), walletAddresses = SolanaWalletAddresses(walletInfo), balance = ObservableMap.of( - {CryptoCurrency.sol: initialBalance ?? SolanaBalance(BigInt.zero.toDouble())}), + {CryptoCurrency.sol: initialBalance ?? SolanaBalance(BigInt.zero.toDouble(), false)}), super(walletInfo, derivationInfo) { this.walletInfo = walletInfo; transactionHistory = SolanaTransactionHistory( @@ -497,7 +497,7 @@ abstract class SolanaWalletBase if (!hasKeysFile) rethrow; } - final balance = SolanaBalance.fromJSON(data?['balance'] as String?) ?? SolanaBalance(0.0); + final balance = SolanaBalance.fromJSON(data?['balance'] as String?, false) ?? SolanaBalance(0.0, false); final WalletKeysData keysData; // Migrate wallet from the old scheme to then new .keys file scheme @@ -545,7 +545,7 @@ abstract class SolanaWalletBase Future _fetchSOLBalance() async { final balance = await _client.getBalance(solanaAddress); - return SolanaBalance(balance); + return SolanaBalance(balance, false); } /// Internal helper to update SPL token balances. @@ -588,7 +588,7 @@ abstract class SolanaWalletBase for (final entry in results) { final token = entry.key; final fetchedBalance = entry.value; - final currentBalance = balance[token] ?? SolanaBalance(0.0); + final currentBalance = balance[token] ?? SolanaBalance(0.0, true); balance[token] = fetchedBalance ?? currentBalance; } } @@ -636,7 +636,7 @@ abstract class SolanaWalletBase if (token.enabled) { final tokenBalance = await _client.getSplTokenBalance(token.mintAddress, solanaAddress) ?? balance[token] ?? - SolanaBalance(0.0); + SolanaBalance(0.0, true); balance[token] = tokenBalance; } else { diff --git a/cw_solana/lib/solana_wallet_addresses.dart b/cw_solana/lib/solana_wallet_addresses.dart index 7e9bd9008..1431f8cc8 100644 --- a/cw_solana/lib/solana_wallet_addresses.dart +++ b/cw_solana/lib/solana_wallet_addresses.dart @@ -1,3 +1,4 @@ +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; @@ -34,4 +35,7 @@ abstract class SolanaWalletAddressesBase extends WalletAddresses with Store { printV(e.toString()); } } + + @override + PaymentURI getPaymentUri(String amount) => SolanaURI(address: address, amount: amount); } diff --git a/cw_tron/lib/tron_balance.dart b/cw_tron/lib/tron_balance.dart index 5b2ba3fa7..6c28ad337 100644 --- a/cw_tron/lib/tron_balance.dart +++ b/cw_tron/lib/tron_balance.dart @@ -1,22 +1,13 @@ import 'dart:convert'; import 'package:cw_core/balance.dart'; -import 'package:on_chain/on_chain.dart'; class TronBalance extends Balance { - TronBalance(this.balance) : super(balance.toInt(), balance.toInt()); + TronBalance(this.balance) : super(balance, balance); final BigInt balance; - @override - String get formattedAdditionalBalance => TronHelper.fromSun(balance); - - @override - String get formattedAvailableBalance => TronHelper.fromSun(balance); - - String toJSON() => json.encode({ - 'balance': balance.toString(), - }); + String toJSON() => json.encode({ 'balance': balance.toString() }); static TronBalance? fromJSON(String? jsonSource) { if (jsonSource == null) { diff --git a/cw_tron/lib/tron_wallet_addresses.dart b/cw_tron/lib/tron_wallet_addresses.dart index 095f97fa9..99767e965 100644 --- a/cw_tron/lib/tron_wallet_addresses.dart +++ b/cw_tron/lib/tron_wallet_addresses.dart @@ -1,5 +1,6 @@ import 'dart:developer'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; @@ -36,4 +37,7 @@ abstract class TronWalletAddressesBase extends WalletAddresses with Store { log(e.toString()); } } + + @override + PaymentURI getPaymentUri(String amount) => TronURI(amount: amount, address: address); } diff --git a/cw_wownero/lib/wownero_wallet_addresses.dart b/cw_wownero/lib/wownero_wallet_addresses.dart index 936c18724..e6c6b2b2c 100644 --- a/cw_wownero/lib/wownero_wallet_addresses.dart +++ b/cw_wownero/lib/wownero_wallet_addresses.dart @@ -1,10 +1,9 @@ import 'package:cw_core/account.dart'; -import 'package:cw_core/address_info.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/subaddress.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:cw_wownero/api/transaction_history.dart'; import 'package:cw_wownero/api/subaddress_list.dart' as subaddress_list; import 'package:cw_wownero/api/wallet.dart'; import 'package:cw_wownero/wownero_account_list.dart'; @@ -63,7 +62,7 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { WowneroSubaddressList subaddressList; WowneroAccountList accountList; - + @override Set usedAddresses = Set(); @@ -151,4 +150,8 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { @override bool containsAddress(String address) => addressInfos[account?.id ?? 0]?.any((it) => it.address == address) ?? false; + + @override + PaymentURI getPaymentUri(String amount) => + WowneroURI(address: address, amount: amount); } diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index fdc254a1b..e95c3b6c6 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -46,6 +46,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + bech32: + dependency: transitive + description: + path: "." + ref: HEAD + resolved-ref: "05755063b593aa6cca0a4820a318e0ce17de6192" + url: "https://github.com/cake-tech/bech32.git" + source: git + version: "0.2.2" blockchain_utils: dependency: transitive description: diff --git a/cw_zano/lib/model/zano_balance.dart b/cw_zano/lib/model/zano_balance.dart index 882c0e11b..0f1e5c81a 100644 --- a/cw_zano/lib/model/zano_balance.dart +++ b/cw_zano/lib/model/zano_balance.dart @@ -5,9 +5,9 @@ class ZanoBalance extends Balance { final BigInt total; final BigInt unlocked; final int decimalPoint; - ZanoBalance({required this.total, required this.unlocked, this.decimalPoint = ZanoFormatter.defaultDecimalPoint}) : super(unlocked.isValidInt ? unlocked.toInt() : 0, (total - unlocked).isValidInt ? (total - unlocked).toInt() : 0); + ZanoBalance({required this.total, required this.unlocked, this.decimalPoint = ZanoFormatter.defaultDecimalPoint}) : super(unlocked, (total - unlocked)); - ZanoBalance.empty({this.decimalPoint = ZanoFormatter.defaultDecimalPoint}): total = BigInt.zero, unlocked = BigInt.zero, super(0, 0); + ZanoBalance.empty({this.decimalPoint = ZanoFormatter.defaultDecimalPoint}): total = BigInt.zero, unlocked = BigInt.zero, super(BigInt.zero, BigInt.zero); @override String get formattedAdditionalBalance => ZanoFormatter.bigIntAmountToString(total - unlocked, decimalPoint); diff --git a/cw_zano/lib/zano_wallet_addresses.dart b/cw_zano/lib/zano_wallet_addresses.dart index 39e61be7f..1562ea8ee 100644 --- a/cw_zano/lib/zano_wallet_addresses.dart +++ b/cw_zano/lib/zano_wallet_addresses.dart @@ -1,7 +1,7 @@ +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:cw_zano/zano_wallet_api.dart'; import 'package:mobx/mobx.dart'; part 'zano_wallet_addresses.g.dart'; @@ -38,4 +38,7 @@ abstract class ZanoWalletAddressesBase extends WalletAddresses with Store { printV(e.toString()); } } + + @override + PaymentURI getPaymentUri(String amount) => ZanoURI(amount: amount, address: address); } diff --git a/cw_zano/pubspec.lock b/cw_zano/pubspec.lock index 152451312..b18f0cfc4 100644 --- a/cw_zano/pubspec.lock +++ b/cw_zano/pubspec.lock @@ -46,6 +46,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + bech32: + dependency: transitive + description: + path: "." + ref: HEAD + resolved-ref: "05755063b593aa6cca0a4820a318e0ce17de6192" + url: "https://github.com/cake-tech/bech32.git" + source: git + version: "0.2.2" blockchain_utils: dependency: transitive description: diff --git a/cw_zcash/lib/src/zcash_balance.dart b/cw_zcash/lib/src/zcash_balance.dart index 5247ce9aa..ab5ec84d2 100644 --- a/cw_zcash/lib/src/zcash_balance.dart +++ b/cw_zcash/lib/src/zcash_balance.dart @@ -2,14 +2,16 @@ import 'package:cw_core/balance.dart'; import 'package:cw_core/crypto_currency.dart'; class ZcashBalance extends Balance { - ZcashBalance({required this.confirmed, required this.unconfirmed, required this.frozen}) - : super(confirmed, unconfirmed); + ZcashBalance({required this.confirmed, required this.unconfirmed, required final int frozen}) + : _frozen = frozen, + super.fromInt(confirmed, unconfirmed); factory ZcashBalance.zero() => ZcashBalance(confirmed: 0, unconfirmed: 0, frozen: 0); final int confirmed; final int unconfirmed; - final int frozen; + final int _frozen; + BigInt get frozen => BigInt.from(_frozen); @override String get formattedAvailableBalance { @@ -25,6 +27,6 @@ class ZcashBalance extends Balance { @override String get formattedUnAvailableBalance { if (frozen == 0) return ''; - return CryptoCurrency.zec.formatAmount(BigInt.from(frozen)); + return CryptoCurrency.zec.formatAmount(BigInt.from(_frozen)); } } diff --git a/cw_zcash/lib/src/zcash_receive_page_options.dart b/cw_zcash/lib/src/zcash_receive_page_options.dart index 400ade3bc..8a9d05766 100644 --- a/cw_zcash/lib/src/zcash_receive_page_options.dart +++ b/cw_zcash/lib/src/zcash_receive_page_options.dart @@ -10,8 +10,6 @@ enum ZcashAddressType { } class ZcashReceivePageOption implements ReceivePageOption { - const ZcashReceivePageOption._(this.type); - factory ZcashReceivePageOption.fromType(final ZcashAddressType type) { switch (type) { case ZcashAddressType.transparent: @@ -26,22 +24,56 @@ class ZcashReceivePageOption implements ReceivePageOption { return unified; } } - static const transparent = ZcashReceivePageOption._(ZcashAddressType.transparent); - static const transparentRotated = ZcashReceivePageOption._(ZcashAddressType.transparentRotated); - static const shieldedSapling = ZcashReceivePageOption._(ZcashAddressType.shieldedSapling); - static const shieldedOrchard = ZcashReceivePageOption._(ZcashAddressType.shieldedOrchard); - static const unified = ZcashReceivePageOption._(ZcashAddressType.unifiedType); + const ZcashReceivePageOption._( + this.type, + this.value, { + this.iconPath, + this.description, + this.isCommon = false, + }); + + final String value; + final String? iconPath; + final String? description; + final bool isCommon; + final bool addAddressWord = false; + + static const transparent = ZcashReceivePageOption._( + ZcashAddressType.transparent, + "Public", + description: "Static & Transparent", + iconPath: "assets/new-ui/address-type-picker-icons/zec/public.svg", + ); + static const transparentRotated = ZcashReceivePageOption._( + ZcashAddressType.transparentRotated, + "Transparent", + description: "Disposable", + iconPath: "assets/new-ui/address-type-picker-icons/zec/transparent.svg", + isCommon: true, + ); + static const shieldedSapling = ZcashReceivePageOption._( + ZcashAddressType.shieldedSapling, + "Legacy Shielded", + description: "Sapling", + iconPath: "assets/new-ui/address-type-picker-icons/zec/sapling.svg", + ); + static const shieldedOrchard = ZcashReceivePageOption._( + ZcashAddressType.shieldedOrchard, + "Shielded", + description: "Default (Orchard)", + iconPath: "assets/new-ui/address-type-picker-icons/zec/shielded.svg", + isCommon: true, + ); + static const unified = ZcashReceivePageOption._( + ZcashAddressType.unifiedType, + "Unified", + description: "Compatible Shielded", + iconPath: "assets/new-ui/address-type-picker-icons/zec/unified.svg", + isCommon: true, + ); final ZcashAddressType type; - String get value => switch (type) { - ZcashAddressType.transparent => "Transparent (Static)", - ZcashAddressType.transparentRotated => "Transparent (Disposable)", - ZcashAddressType.shieldedSapling => "Shielded (Sapling)", - ZcashAddressType.shieldedOrchard => "Shielded (Orchard)", - ZcashAddressType.unifiedType => "Unified", - }; - String toString() { return value; } diff --git a/cw_zcash/lib/src/zcash_wallet.dart b/cw_zcash/lib/src/zcash_wallet.dart index 386e69f49..0ade54d54 100644 --- a/cw_zcash/lib/src/zcash_wallet.dart +++ b/cw_zcash/lib/src/zcash_wallet.dart @@ -175,8 +175,8 @@ abstract class ZcashWalletBase } final paymentUri = WarpApi.decodePaymentURI(coin, address); - // String memo = output.note ?? ''; - String memo = ''; + String memo = output.memo ?? ''; + // String memo = ''; if (paymentUri != null && paymentUri.address != null) { address = paymentUri.address!; if (memo.isEmpty && paymentUri.memo != null) { @@ -395,7 +395,6 @@ abstract class ZcashWalletBase } catch (e) { printV("Rescan error: $e"); syncStatus = FailedSyncStatus(error: e.toString()); - rethrow; } } @@ -1161,7 +1160,7 @@ abstract class ZcashWalletBase final bal = balance[CryptoCurrency.zec]!.confirmed + balance[CryptoCurrency.zec]!.unconfirmed + - balance[CryptoCurrency.zec]!.frozen; + balance[CryptoCurrency.zec]!.frozen.toInt(); final osCacheDir = await getApplicationCacheDirectory(); final cacheDir = osCacheDir.createTempSync("zkool-import"); zkoolSweep = ZkoolSweep( diff --git a/cw_zcash/lib/src/zcash_wallet_addresses.dart b/cw_zcash/lib/src/zcash_wallet_addresses.dart index 943646a39..a9fd207ae 100644 --- a/cw_zcash/lib/src/zcash_wallet_addresses.dart +++ b/cw_zcash/lib/src/zcash_wallet_addresses.dart @@ -1,3 +1,5 @@ +import 'package:cw_core/payment_uris.dart'; +import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; @@ -123,6 +125,10 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store { @computed String get addressForExchange => transparentAddressRotated ?? transparentAddress; + @override + @computed + String get addressForBuy => transparentAddressRotated ?? transparentAddress; + @override bool containsAddress(final String address) { return this.address == address || addressesMap.values.contains(address); @@ -227,4 +233,15 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store { } return allInfos; } + + @override + List get receivePageOptions { + return [ + ...ZcashReceivePageOption.all, + ...ReceivePageOptions.where((final element) => element != ReceivePageOption.mainnet), + ]; + } + + @override + PaymentURI getPaymentUri(final String amount) => ZcashURI(amount: amount, address: address); } diff --git a/cw_zcash/pubspec.yaml b/cw_zcash/pubspec.yaml index 37dbb8d69..835981eb2 100644 --- a/cw_zcash/pubspec.yaml +++ b/cw_zcash/pubspec.yaml @@ -24,6 +24,7 @@ dependencies: url: https://github.com/cake-tech/zkool2 ref: b1642f260da4fa00dd2f8d4dd689073d0b56960a + path_provider: any dependency_overrides: decimal: ^2.3.3 diff --git a/integration_test/robots/wallet_group_description_page_robot.dart b/integration_test/robots/wallet_group_description_page_robot.dart index 5f3a8d541..79c228f94 100644 --- a/integration_test/robots/wallet_group_description_page_robot.dart +++ b/integration_test/robots/wallet_group_description_page_robot.dart @@ -1,6 +1,5 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/new_wallet/wallet_group_description_page.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import '../components/common_test_cases.dart'; diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ab0689541..071f654b6 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,6 +1,8 @@ PODS: - bitbox_flutter (0.0.1): - Flutter + - breez_sdk_spark_flutter (0.9.1): + - Flutter - connectivity_plus (0.0.1): - Flutter - CryptoSwift (1.8.4) @@ -156,6 +158,7 @@ PODS: DEPENDENCIES: - bitbox_flutter (from `.symlinks/plugins/bitbox_flutter/ios`) + - breez_sdk_spark_flutter (from `.symlinks/plugins/breez_sdk_spark_flutter/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - CryptoSwift - cw_decred (from `.symlinks/plugins/cw_decred/ios`) @@ -213,6 +216,8 @@ SPEC REPOS: EXTERNAL SOURCES: bitbox_flutter: :path: ".symlinks/plugins/bitbox_flutter/ios" + breez_sdk_spark_flutter: + :path: ".symlinks/plugins/breez_sdk_spark_flutter/ios" connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" cw_decred: @@ -298,6 +303,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: bitbox_flutter: 506f80b961ddf646b0d80cef9f6eadaab96d91b0 + breez_sdk_spark_flutter: e96b24d1c0ca1ef7dec122dd7bbf63eb921f97f4 connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d CryptoSwift: e64e11850ede528a02a0f3e768cec8e9d92ecb90 cw_decred: 9c0e1df74745b51a1289ec5e91fb9e24b68fa14a diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 0899da3a7..6df8e02bf 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -496,6 +496,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = 32J6BB6VUS; DISABLED_ARCHS = x86_64; @@ -519,6 +521,7 @@ MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -644,6 +647,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = 32J6BB6VUS; DISABLED_ARCHS = x86_64; @@ -668,6 +673,7 @@ ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -685,6 +691,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = 32J6BB6VUS; DISABLED_ARCHS = x86_64; @@ -708,6 +716,7 @@ MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/ios/Runner/InfoBase.plist b/ios/Runner/InfoBase.plist index a7f443f22..d82fec087 100644 --- a/ios/Runner/InfoBase.plist +++ b/ios/Runner/InfoBase.plist @@ -382,6 +382,26 @@ zcash-wallet + + CFBundleTypeRole + Viewer + CFBundleURLName + lightning + CFBundleURLSchemes + + lightning + + + + CFBundleTypeRole + Viewer + CFBundleURLName + lightning-wallet + CFBundleURLSchemes + + lightning-wallet + + CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 90ca4f791..5fa7a7620 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -153,16 +153,17 @@ class CWBitcoin extends Bitcoin { return BitcoinTransactionCredentials( outputs .map((out) => OutputInfo( - fiatAmount: out.fiatAmount, - cryptoAmount: out.cryptoAmount, - address: out.address, - note: out.note, - sendAll: out.sendAll, - extractedAddress: out.extractedAddress, - isParsedAddress: out.isParsedAddress, - formattedCryptoAmount: out.formattedCryptoAmount, - memo: out.memo, - extra: out.extra)) + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmount, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + formattedCryptoAmount: out.formattedCryptoAmount, + memo: out.memo.isNotEmpty ? out.memo : null, + extra: out.extra, + )) .toList(), priority: priority as BitcoinTransactionPriority, feeRate: bitcoinFeeRate, @@ -264,6 +265,7 @@ class CWBitcoin extends Bitcoin { return element.bitcoinAddressRecord.type == SegwitAddresType.mweb; case UnspentCoinType.nonMweb: return element.bitcoinAddressRecord.type != SegwitAddresType.mweb; + case UnspentCoinType.lightning: case UnspentCoinType.any: return true; } @@ -319,30 +321,6 @@ class CWBitcoin extends Bitcoin { return bitcoinWallet.walletAddresses.addressPageType == SilentPaymentsAddresType.p2sp; } - @override - List getBitcoinReceivePageOptions(Object wallet) { - final bitcoinWallet = wallet as ElectrumWallet; - final keys = bitcoinWallet.keys; - if (keys.privateKey.isEmpty) { - return BitcoinReceivePageOption.allViewOnly; - } - return BitcoinReceivePageOption.all; - } - - @override - List getLitecoinReceivePageOptions(Object wallet) { - final litecoinWallet = wallet as ElectrumWallet; - if (Platform.isLinux || - Platform.isMacOS || - Platform.isWindows || - litecoinWallet.isHardwareWallet) { - return BitcoinReceivePageOption.allLitecoin - .where((element) => element != BitcoinReceivePageOption.mweb) - .toList(); - } - return BitcoinReceivePageOption.allLitecoin; - } - @override BitcoinAddressType getBitcoinAddressType(ReceivePageOption option) { switch (option) { @@ -362,6 +340,13 @@ class CWBitcoin extends Bitcoin { } } + @override + BitcoinReceivePageOption getBitcoinLightningReceivePageOption() => BitcoinReceivePageOption.lightning; + @override + BitcoinReceivePageOption getBitcoinSegwitPageOption() => BitcoinReceivePageOption.p2wpkh; + @override + BitcoinReceivePageOption getLitecoinMwebReceivePageOption() => BitcoinReceivePageOption.mweb; + @override Future> compareDerivationMethods( {required String mnemonic, required Node node}) async { @@ -703,6 +688,8 @@ class CWBitcoin extends Bitcoin { } List updateOutputs(PendingTransaction pendingTransaction, List outputs) { + if (pendingTransaction is PendingLightningTransaction) return outputs; + final pendingTx = pendingTransaction as PendingBitcoinTransaction; if (!pendingTx.hasSilentPayment) { @@ -780,6 +767,15 @@ class CWBitcoin extends Bitcoin { } } + Future getUnusedSpakDepositAddress(Object wallet) async { + try { + final bitcoinWallet = wallet as BitcoinWallet; + return wallet.lightningWallet?.getDepositAddress(); + } catch (_) { + return null; + } + } + @override Future commitPsbtUR(Object wallet, List urCodes) { if (wallet is LitecoinWallet) return wallet.commitPsbtUR(urCodes); @@ -804,6 +800,22 @@ class CWBitcoin extends Bitcoin { } } + @override + bool useLightning(Object wallet) { + final _wallet = wallet as ElectrumWallet; + if (_wallet is BitcoinWallet) return _wallet.useLightning; + + return false; + } + + @override + void updateUseLightning(Object wallet, bool value) { + final _wallet = wallet as ElectrumWallet; + if (_wallet is BitcoinWallet) { + _wallet.useLightning = value; + } + } + @override void resumePayjoinSessions(Object wallet) { final _wallet = wallet as ElectrumWallet; @@ -839,4 +851,66 @@ class CWBitcoin extends Bitcoin { return addresses; } + + @override + String getNetworkName(Object wallet) { + return (wallet as ElectrumWallet).network.value; + } + + @override + Future setLightningUsername(Object wallet, String username) async { + final electrumWallet = wallet as ElectrumWallet; + await electrumWallet.walletAddresses.setLightningAddress(wallet.name, newAddress: username); + } + + @override + Future getLightningUsername(Object wallet) async { + final electrumWallet = wallet as ElectrumWallet; + + if (electrumWallet.walletAddresses.lightningWallet == null) { + printV("lightning wallet is null"); + return null; + } + return (await electrumWallet.walletAddresses.lightningWallet!.getAddress()) + ?.replaceFirst("@cake.cash", ""); + } + + @override + Future getLightningInvoice(Object wallet, BigInt amount) async { + final electrumWallet = wallet as ElectrumWallet; + + if (electrumWallet is BitcoinWallet && electrumWallet.lightningWallet != null) { + return electrumWallet.lightningWallet!.getBolt11Invoice(amount, "Send to CakeWallet"); + } + return null; + } + + @override + String? getBreezSdkError(Object exception) { + if (exception is SdkError_SparkError) { + return (exception as SdkError_SparkError).field0.toString(); + } + if (exception is SdkError_InvalidUuid) { + return (exception as SdkError_InvalidUuid).field0.toString(); + } + if (exception is SdkError_InvalidInput) { + return (exception as SdkError_InvalidInput).field0.toString(); + } + if (exception is SdkError_NetworkError) { + return (exception as SdkError_NetworkError).field0.toString(); + } + if (exception is SdkError_StorageError) { + return (exception as SdkError_StorageError).field0.toString(); + } + if (exception is SdkError_ChainServiceError) { + return (exception as SdkError_ChainServiceError).field0.toString(); + } + if (exception is SdkError_LnurlError) { + return (exception as SdkError_LnurlError).field0.toString(); + } + if (exception is SdkError_Generic) { + return (exception as SdkError_Generic).field0.toString(); + } + return null; + } } diff --git a/lib/buy/moonpay/moonpay_provider.dart b/lib/buy/moonpay/moonpay_provider.dart index 6ff720fcd..dfd09e130 100644 --- a/lib/buy/moonpay/moonpay_provider.dart +++ b/lib/buy/moonpay/moonpay_provider.dart @@ -21,7 +21,6 @@ import 'package:cw_core/currency_for_wallet_type.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; diff --git a/lib/buy/onramper/onramper_buy_provider.dart b/lib/buy/onramper/onramper_buy_provider.dart index 353a01397..23da5a9aa 100644 --- a/lib/buy/onramper/onramper_buy_provider.dart +++ b/lib/buy/onramper/onramper_buy_provider.dart @@ -8,7 +8,6 @@ import 'package:cake_wallet/buy/pairs_utils.dart'; import 'package:cake_wallet/buy/payment_method.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/themes/core/theme_store.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -17,7 +16,7 @@ import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; class OnRamperBuyProvider extends BuyProvider { - OnRamperBuyProvider(this._themeStore, + OnRamperBuyProvider( {required WalletBase wallet, bool isTestEnvironment = false}) : super(wallet: wallet, isTestEnvironment: isTestEnvironment, @@ -39,8 +38,6 @@ class OnRamperBuyProvider extends BuyProvider { static const List _notSupportedFiat = []; static Map _onrampMetadata = {}; - final ThemeStore _themeStore; - String? recommendedPaymentType; String get _apiKey => secrets.onramperApiKey; diff --git a/lib/buy/payment_method.dart b/lib/buy/payment_method.dart index f23d6fbce..06f1cfe34 100644 --- a/lib/buy/payment_method.dart +++ b/lib/buy/payment_method.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:cake_wallet/core/selectable_option.dart'; enum PaymentType { diff --git a/lib/cake_pay/src/auth/cake_pay_account_page.dart b/lib/cake_pay/src/auth/cake_pay_account_page.dart index c0cb28f0e..527cc703f 100644 --- a/lib/cake_pay/src/auth/cake_pay_account_page.dart +++ b/lib/cake_pay/src/auth/cake_pay_account_page.dart @@ -3,7 +3,7 @@ import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/cake_pay/src/widgets/cake_pay_tile.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; import 'package:cake_wallet/view_model/cake_pay/cake_pay_account_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; diff --git a/lib/cake_pay/src/auth/cake_pay_verify_otp_page.dart b/lib/cake_pay/src/auth/cake_pay_verify_otp_page.dart index 4647809cb..f8691b459 100644 --- a/lib/cake_pay/src/auth/cake_pay_verify_otp_page.dart +++ b/lib/cake_pay/src/auth/cake_pay_verify_otp_page.dart @@ -1,12 +1,11 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/cake_pay/src/cake_pay_states.dart'; -import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; import 'package:cake_wallet/typography.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/cake_pay/cake_pay_auth_view_model.dart'; diff --git a/lib/cake_pay/src/auth/cake_pay_welcome_page.dart b/lib/cake_pay/src/auth/cake_pay_welcome_page.dart index e8a99b18c..7313c7bf9 100644 --- a/lib/cake_pay/src/auth/cake_pay_welcome_page.dart +++ b/lib/cake_pay/src/auth/cake_pay_welcome_page.dart @@ -6,7 +6,7 @@ import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; import 'package:cake_wallet/typography.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/cake_pay/cake_pay_auth_view_model.dart'; diff --git a/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart b/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart index bf6dc1fd9..5c06d2e60 100644 --- a/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart +++ b/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart @@ -619,11 +619,16 @@ class CakePayBuyCardPage extends BasePage { titleIconPath: _sendViewModel.selectedCryptoCurrency.iconPath, currency: _sendViewModel.selectedCryptoCurrency, amount: S.of(bottomSheetContext).send_amount, - amountValue: _sendViewModel.pendingTransaction!.amountFormatted, + amountValue: _sendViewModel.amountParsingProxy.getDisplayCryptoAmount( + _sendViewModel.pendingTransaction!.amountFormatted, + _sendViewModel.selectedCryptoCurrency), quantity: 'QTY: ${cakePayBuyCardViewModel.quantity}', fiatAmountValue: _sendViewModel.pendingTransactionFiatAmountFormatted, fee: S.of(bottomSheetContext).send_fee, - feeValue: _sendViewModel.pendingTransaction!.feeFormatted, + feeValue: "${_sendViewModel.amountParsingProxy.getDisplayCryptoAmount( + _sendViewModel.pendingTransaction!.feeFormattedValue, + _sendViewModel.selectedCryptoCurrency)} ${_sendViewModel.amountParsingProxy + .getCryptoSymbol(_sendViewModel.wallet.currency)}", feeFiatAmount: _sendViewModel.pendingTransactionFeeFiatAmountFormatted, outputs: displayingOutputs, footerType: FooterType.slideActionButton, @@ -640,6 +645,7 @@ class CakePayBuyCardPage extends BasePage { }, change: _sendViewModel.pendingTransaction!.change, isOpenCryptoPay: _sendViewModel.ocpRequest != null, + amountParsingProxy: _sendViewModel.amountParsingProxy, ); }, ); diff --git a/lib/cake_pay/src/models/cake_pay_order.dart b/lib/cake_pay/src/models/cake_pay_order.dart index d4cb37c6b..7d6b945bb 100644 --- a/lib/cake_pay/src/models/cake_pay_order.dart +++ b/lib/cake_pay/src/models/cake_pay_order.dart @@ -65,8 +65,7 @@ class CakePayOrder { CakePayPaymentMethod.LTC => order.paymentData.ltc, CakePayPaymentMethod.LTC_MWEB => order.paymentData.ltc_mweb, CakePayPaymentMethod.ZEC => order.paymentData.zec, - // TODO: Handle this case. - CakePayPaymentMethod.BTC_LN => throw UnimplementedError(), + CakePayPaymentMethod.BTC_LN => order.paymentData.btc_ln, }; if (data == null) return null; diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 70b7dc956..0d44bad67 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -12,21 +12,29 @@ const AFTER_REGEX = '(\$|\\s)'; class AddressValidator extends TextValidator { AddressValidator({required CryptoCurrency type, bool isTestnet = false}) : super( - errorMessage: S.current.error_text_address, - useAdditionalValidation: type == CryptoCurrency.btc || type == CryptoCurrency.ltc - ? (String txt) => BitcoinAddressUtils.validateAddress( - address: txt, - network: type == CryptoCurrency.btc - ? isTestnet - ? BitcoinNetwork.testnet - : BitcoinNetwork.mainnet - : LitecoinNetwork.mainnet, - ) - : type == CryptoCurrency.zano - ? zano?.validateAddress - : null, - pattern: getPattern(type, isTestnet: isTestnet), - length: getLength(type)); + errorMessage: S.current.error_text_address, + useAdditionalValidation: [CryptoCurrency.btc, CryptoCurrency.ltc].contains(type) + ? (String txt) { + final RegExp lightningInvoiceRegex = RegExp( + r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt|lnurl)[a-z0-9]+$', + caseSensitive: false); + if (lightningInvoiceRegex.hasMatch(txt)) return true; + + return BitcoinAddressUtils.validateAddress( + address: txt, + network: type == CryptoCurrency.btc + ? isTestnet + ? BitcoinNetwork.testnet + : BitcoinNetwork.mainnet + : LitecoinNetwork.mainnet, + ); + } + : type == CryptoCurrency.zano + ? zano?.validateAddress + : null, + pattern: getPattern(type, isTestnet: isTestnet), + length: getLength(type), + ); static String getPattern(CryptoCurrency type, {bool isTestnet = false}) { var pattern = ""; @@ -53,6 +61,7 @@ class AddressValidator extends TextValidator { '|(bc1q[ac-hj-np-z02-9]{25,39})' '|(bc1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))' '|(bc1q[ac-hj-np-z02-9]{40,80})' + '|(lightning:)?(lnbc|lntb|lnbs|lnbcrt|lnurl)[a-z0-9]+' '|(${silentPaymentAddressPatternMainnet})(\$|\s)'; } case CryptoCurrency.ltc: @@ -161,7 +170,7 @@ class AddressValidator extends TextValidator { case CryptoCurrency.pivx: pattern = 'D([1-9a-km-zA-HJ-NP-Z]){33}'; case CryptoCurrency.btcln: - pattern = '(lnbc|LNBC)([0-9]{1,}[a-zA-Z0-9]+)'; + pattern = r'(lightning:)?(lnbc|lntb|lnbs|lnbcrt|lnurl|LNBC|LNTB|LNBS|LNBCRT|LNURL)[a-zA-Z0-9]+'; case CryptoCurrency.zano: pattern = r'([1-9A-HJ-NP-Za-km-z]{90,200})|(@[\w\d.-]+)'; case CryptoCurrency.doge: @@ -315,6 +324,10 @@ class AddressValidator extends TextValidator { static String get silentPaymentAddressPatternTestnet => '(tsp|sprt)1[0-9a-zA-Z]{113}'; static String get mWebAddressPattern => MwebAddress.regex.pattern; + static const String bolt11InvoiceMatcher = r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]+$'; + static const String bolt12OfferMatcher = r'^(lightning:)?(lno1)[a-z0-9]+$'; + static const String lnurlMatcher = r'^(lightning:)?(lnurl)[a-z0-9]+$'; + // NOTE: not needed to check for network here as it's a general address catcher, validation is separate static String? getAddressFromStringPattern(CryptoCurrency type) { String? pattern = null; diff --git a/lib/core/amount.dart b/lib/core/amount.dart deleted file mode 100644 index 3d64f1d66..000000000 --- a/lib/core/amount.dart +++ /dev/null @@ -1,37 +0,0 @@ -// abstract class Amount { -// Amount(this.value); - -// int value; - -// int minorDigits; - -// String code; - -// String formatted(); -// } - -// class MoneroAmount extends Amount { -// MoneroAmount(int value) : super(value) { -// minorDigits = 12; -// code = 'XMR'; -// } - -// // const moneroAmountLength = 12; -// // const moneroAmountDivider = 1000000000000; -// // final moneroAmountFormat = NumberFormat() -// // ..maximumFractionDigits = moneroAmountLength -// // ..minimumFractionDigits = 1; - -// // String moneroAmountToString({int amount}) => -// // moneroAmountFormat.format(cryptoAmountToDouble(amount: amount, divider: moneroAmountDivider)); - -// // double moneroAmountToDouble({int amount}) => cryptoAmountToDouble(amount: amount, divider: moneroAmountDivider); - -// // int moneroParseAmount({String amount}) => moneroAmountFormat.parse(amount).toInt(); - -// @override -// String formatted() { -// // TODO: implement formatted -// throw UnimplementedError(); -// } -// } diff --git a/lib/core/amount_parsing_proxy.dart b/lib/core/amount_parsing_proxy.dart new file mode 100644 index 000000000..58fb1056c --- /dev/null +++ b/lib/core/amount_parsing_proxy.dart @@ -0,0 +1,82 @@ +import 'package:cake_wallet/entities/bitcoin_amount_display_mode.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/utils/string_parsing.dart'; +import 'package:cw_core/crypto_amount_format.dart'; +import 'package:cw_core/crypto_currency.dart'; + +class AmountParsingProxy { + final BitcoinAmountDisplayMode displayMode; + + const AmountParsingProxy(this.displayMode); + + /// [getCryptoInputAmount] turns the input [amount] into the canonical representation of [cryptoCurrency] + String getCanonicalCryptoAmount(String amount, CryptoCurrency cryptoCurrency) { + if (useSatoshi(cryptoCurrency) && amount.isNotEmpty) { + return cryptoCurrency.formatAmount(BigInt.tryParse(amount) ?? BigInt.zero); + } + + return amount; + } + + /// [getCryptoOutputAmount] turns the input [amount] into the preferred representation of [cryptoCurrency] + String getDisplayCryptoAmount(String amount, CryptoCurrency cryptoCurrency) { + if (useSatoshi(cryptoCurrency) && amount.isNotEmpty) { + return cryptoCurrency.parseAmount(amount.withMaxDecimals(cryptoCurrency.decimals)).toString(); + } + + return amount.withMaxDecimals(cryptoCurrency.decimals); + } + + /// [getCryptoStringRepresentation] turns the input [amount] into the preferred representation of [cryptoCurrency] + String getDisplayCryptoString(int amount, CryptoCurrency cryptoCurrency) { + if (useSatoshi(cryptoCurrency)) { + return "$amount"; + } + + return cryptoCurrency.formatAmount(BigInt.from(amount)); + } + + String getDisplayCryptoStringFromBigInt(BigInt amount, CryptoCurrency cryptoCurrency) { + if (useSatoshi(cryptoCurrency)) { + return "$amount"; + } + + return cryptoCurrency.formatAmount(amount); + } + + /// [getCryptoStringFromDouble] turns the input [amount] into the preferred representation of [cryptoCurrency] and + String getDisplayCryptoStringFromDouble(double amount, CryptoCurrency cryptoCurrency) { + if (useSatoshi(cryptoCurrency)) { + return "$amount"; + } + + return cryptoCurrency.formatAmount(BigInt.from(amount)); + } + + /// [parseCryptoString] turns the input [string] into a `BigInt` presentation of the [cryptoCurrency] + BigInt parseCryptoString(String amount, CryptoCurrency cryptoCurrency) { + if (useSatoshi(cryptoCurrency)) { + return BigInt.parse(amount); + } + + return cryptoCurrency.parseAmount(amount); + } + + /// [tryParseCryptoString] tries to turn the display representation [string] into a `BigInt` presentation of the [cryptoCurrency] + BigInt? tryParseCryptoString(String amount, CryptoCurrency cryptoCurrency) { + if (useSatoshi(cryptoCurrency)) { + return BigInt.tryParse(amount); + } + + return cryptoCurrency.tryParseAmount(amount); + } + + /// [getCryptoSymbol] returns the correct Symbol related to the presentation + String getCryptoSymbol(CryptoCurrency cryptoCurrency) => + useSatoshi(cryptoCurrency) ? "sats" : cryptoCurrency.title.safeSubString(0, 8); + + bool useSatoshi(CryptoCurrency cryptoCurrency) => + ([CryptoCurrency.btc, CryptoCurrency.btcln].contains(cryptoCurrency) && + displayMode == BitcoinAmountDisplayMode.satoshi) || + (CryptoCurrency.btcln == cryptoCurrency && + displayMode == BitcoinAmountDisplayMode.satoshiForLightning); +} diff --git a/lib/core/amount_validator.dart b/lib/core/amount_validator.dart index b6c8abda5..d056ad0c5 100644 --- a/lib/core/amount_validator.dart +++ b/lib/core/amount_validator.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/core/amount_parsing_proxy.dart'; import 'package:cake_wallet/core/validator.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -6,6 +7,7 @@ import 'package:cw_core/currency.dart'; class AmountValidator extends TextValidator { AmountValidator({ required CryptoCurrency currency, + required AmountParsingProxy amountParsingProxy, bool isAutovalidate = false, String? minValue, String? maxValue, @@ -17,11 +19,15 @@ class AmountValidator extends TextValidator { amountMinValidator = AmountMinValidator( minValue: minValue, isAutovalidate: isAutovalidate, + amountParsingProxy: amountParsingProxy, + cryptoCurrency: currency, ); amountMaxValidator = AmountMaxValidator( maxValue: maxValue, isAutovalidate: isAutovalidate, + amountParsingProxy: amountParsingProxy, + cryptoCurrency: currency, ); } @@ -89,10 +95,14 @@ class AllAmountValidator extends TextValidator { class AmountMinValidator extends Validator { final String? minValue; final bool isAutovalidate; + final AmountParsingProxy amountParsingProxy; + final CryptoCurrency cryptoCurrency; AmountMinValidator({ this.minValue, required this.isAutovalidate, + required this.amountParsingProxy, + required this.cryptoCurrency, }) : super(errorMessage: S.current.error_text_input_below_minimum_limit); @override @@ -105,6 +115,7 @@ class AmountMinValidator extends Validator { return true; } + value = amountParsingProxy.getCanonicalCryptoAmount(value, cryptoCurrency); final valueInDouble = parseToDouble(value); final minInDouble = parseToDouble(minValue ?? ''); @@ -124,10 +135,14 @@ class AmountMinValidator extends Validator { class AmountMaxValidator extends Validator { final String? maxValue; final bool isAutovalidate; + final AmountParsingProxy amountParsingProxy; + final CryptoCurrency cryptoCurrency; AmountMaxValidator({ this.maxValue, required this.isAutovalidate, + required this.amountParsingProxy, + required this.cryptoCurrency, }) : super(errorMessage: S.current.error_text_input_above_maximum_limit); @override @@ -140,6 +155,7 @@ class AmountMaxValidator extends Validator { return true; } + value = amountParsingProxy.getCanonicalCryptoAmount(value, cryptoCurrency); final valueInDouble = parseToDouble(value); final maxInDouble = parseToDouble(maxValue ?? ''); diff --git a/lib/core/auth_service.dart b/lib/core/auth_service.dart index 899721fc2..39fc53071 100644 --- a/lib/core/auth_service.dart +++ b/lib/core/auth_service.dart @@ -260,7 +260,7 @@ Future _handleDuressLogin( // Wipe wallet-related database tables try { - await db.transaction((txn) async { + await db!.transaction((txn) async { await txn.delete(WalletInfoAddressInfo.tableName); await txn.delete(WalletInfoAddressMap.tableName); await txn.delete(WalletInfoAddress.tableName); diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 6a0f66879..2db105c13 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -12,7 +12,7 @@ import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:cryptography/cryptography.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:archive/archive_io.dart'; +import 'package:archive/archive_io.dart' hide Mac; import 'package:cw_core/cake_hive.dart'; import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/entities/encrypt.dart'; diff --git a/lib/core/backup_service_v3.dart b/lib/core/backup_service_v3.dart index ac73f9b3a..9e9b5b667 100644 --- a/lib/core/backup_service_v3.dart +++ b/lib/core/backup_service_v3.dart @@ -8,6 +8,7 @@ import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_backup/backup.dart' as cake_backup; import 'package:cake_wallet/utils/package_info.dart'; import 'package:crypto/crypto.dart'; +import 'package:cw_core/db/sqlite.dart'; import 'package:cw_core/root_dir.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; @@ -314,6 +315,7 @@ class BackupServiceV3 extends $BackupService { // Delete decrypted data file decryptedData.deleteSync(); + await initDb(); } Future verifyHardwareWallets(String password, diff --git a/lib/core/open_crypto_pay/lnurl.dart b/lib/core/open_crypto_pay/lnurl.dart deleted file mode 100644 index 0087bab51..000000000 --- a/lib/core/open_crypto_pay/lnurl.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'dart:convert'; - -import 'package:bech32/bech32.dart'; - -Uri decodeLNURL(String encodedUrl) { - Uri decodedUri; - - /// The URL doesn't have to be encoded at all as per LUD-17: Protocol schemes and raw (non bech32-encoded) URLs. - /// https://github.com/lnurl/luds/blob/luds/17.md - /// Handle non bech32-encoded LNURL - final lud17prefixes = ['lnurlw', 'lnurlc', 'lnurlp', 'keyauth']; - decodedUri = Uri.parse(encodedUrl); - for (final prefix in lud17prefixes) { - if (decodedUri.scheme.contains(prefix)) { - decodedUri = decodedUri.replace(scheme: prefix); - } - } - if (lud17prefixes.contains(decodedUri.scheme)) { - /// If the non-bech32 LNURL is a Tor address, the port has to be http instead of https for the clearnet LNURL so check if the host ends with '.onion' or '.onion.' - decodedUri = decodedUri.replace( - scheme: decodedUri.host.endsWith('onion') || - decodedUri.host.endsWith('onion.') - ? 'http' - : 'https'); - } else { - /// Try to parse the input as a lnUrl. Will throw an error if it fails. - final lnUrl = _findLnUrl(encodedUrl); - - /// Decode the lnurl using bech32 - final bech32 = const Bech32Codec().decode(lnUrl, lnUrl.length); - decodedUri = Uri.parse(utf8.decode(_convert(bech32.data, 5, 8, false))); - } - return decodedUri; -} - -/// Parse and return a given lnurl string if it's valid. Will remove -/// `lightning:` from the beginning of it if present. -String _findLnUrl(String input) { - final res = RegExp( - r',*?((lnurl)([0-9]+[a-z0-9]+))', - ).allMatches(input.toLowerCase()); - - if (res.length == 1) { - return res.first.group(0)!; - } else { - throw ArgumentError('Not a valid lnurl string'); - } -} - -/// Taken from bech32 (bitcoinjs): https://github.com/bitcoinjs/bech32 -List _convert(List data, int inBits, int outBits, bool pad) { - var value = 0; - var bits = 0; - final maxV = (1 << outBits) - 1; - - final result = []; - for (final dataValue in data) { - value = (value << inBits) | dataValue; - bits += inBits; - - while (bits >= outBits) { - bits -= outBits; - result.add((value >> bits) & maxV); - } - } - - if (pad) { - if (bits > 0) result.add((value << (outBits - bits)) & maxV); - } else { - if (bits >= inBits) throw Exception('Excess padding'); - - if ((value << (outBits - bits)) & maxV > 0) { - throw Exception('Non-zero padding'); - } - } - - return result; -} diff --git a/lib/core/open_crypto_pay/open_cryptopay_service.dart b/lib/core/open_crypto_pay/open_cryptopay_service.dart index cc94740f0..714094e7b 100644 --- a/lib/core/open_crypto_pay/open_cryptopay_service.dart +++ b/lib/core/open_crypto_pay/open_cryptopay_service.dart @@ -2,9 +2,9 @@ import 'dart:convert'; import 'dart:developer'; import 'package:cake_wallet/core/open_crypto_pay/exceptions.dart'; -import 'package:cake_wallet/core/open_crypto_pay/lnurl.dart'; import 'package:cake_wallet/core/open_crypto_pay/models.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/lnurl.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; class OpenCryptoPayService { @@ -59,7 +59,7 @@ class OpenCryptoPayService { lnUrl = params["lightning"] as String; } - final url = decodeLNURL(lnUrl); + final url = LNURL.decode(lnUrl); final params = await _getOpenCryptoPayParams(url); return OpenCryptoPayRequest( diff --git a/lib/core/sync_status_title.dart b/lib/core/sync_status_title.dart index 1cfc66137..b013bab17 100644 --- a/lib/core/sync_status_title.dart +++ b/lib/core/sync_status_title.dart @@ -30,7 +30,7 @@ String syncStatusTitle(SyncStatus syncStatus, SyncStatusDisplayMode syncStatusDi if (eta.isEmpty) { return S.current.Blocks_remaining('${syncStatus.blocksLeft}'); } else { - return "${syncStatus.formattedProgress()} - ${S.current.eta} $eta"; + return "$eta"; } } else { return S.current.Blocks_remaining('${syncStatus.blocksLeft}'); @@ -42,54 +42,54 @@ String syncStatusTitle(SyncStatus syncStatus, SyncStatusDisplayMode syncStatusDi } if (syncStatus is SyncedSyncStatus) { - return S.current.sync_status_syncronized; + return ""; } if (syncStatus is FailedSyncStatus) { if (syncStatus.error != null) { return syncStatus.error!; } - return S.current.sync_status_failed_connect; - } - - if (syncStatus is NotConnectedSyncStatus) { - return S.current.sync_status_not_connected; - } - - if (syncStatus is AttemptingSyncStatus) { - return S.current.sync_status_attempting_sync; - } - - if (syncStatus is ConnectingSyncStatus) { - return S.current.sync_status_connecting; - } - - if (syncStatus is ConnectedSyncStatus) { - return S.current.sync_status_connected; + return S.current.sync_offline; } if (syncStatus is LostConnectionSyncStatus) { - return S.current.sync_status_failed_connect; + return S.current.sync_offline; + } + + if (syncStatus is NotConnectedSyncStatus) { + return S.current.sync_not_connected; + } + + if (syncStatus is AttemptingSyncStatus) { + return S.current.sync_attempting; + } + + if (syncStatus is ConnectingSyncStatus) { + return S.current.sync_connecting; + } + + if (syncStatus is ConnectedSyncStatus) { + return S.current.connected; } if (syncStatus is UnsupportedSyncStatus) { - return S.current.sync_status_unsupported; + return S.current.sync_unsupported_node; } if (syncStatus is TimedOutSyncStatus) { - return S.current.sync_status_timed_out; + return S.current.sync_timed_out; } if (syncStatus is SyncronizingSyncStatus) { - return S.current.sync_status_syncronizing; + return S.current.sync_syncing; } if (syncStatus is StartingScanSyncStatus) { - return S.current.sync_status_starting_scan(syncStatus.beginHeight.toString()); + return "${S.current.sync_starting_scan} (${(syncStatus.beginHeight.toString())})"; } if (syncStatus is AttemptingScanSyncStatus) { - return S.current.sync_status_attempting_scan; + return "Attempting scan"; } if (syncStatus is ProcessingSyncStatus) { diff --git a/lib/core/universal_address_detector.dart b/lib/core/universal_address_detector.dart index 4c3933844..397ee73f1 100644 --- a/lib/core/universal_address_detector.dart +++ b/lib/core/universal_address_detector.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/core/address_validator.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/lnurl.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; @@ -73,7 +74,7 @@ class UniversalAddressDetector { // Determine currency from scheme final currency = CryptoCurrency.fromString(uri.scheme.toLowerCase()); - final walletType = cryptoCurrencyToWalletType(currency); + final walletType = cryptoCurrencyOrTokenToWalletType(currency); final chainId = getChainIdByCryptoCurrency(currency); return AddressDetectionResult( @@ -105,21 +106,27 @@ class UniversalAddressDetector { // Detection patterns ordered by specificity (most specific first) final detectionPatterns = [ - // Lightning Network + // Lightning Network (Bolt11 Invoice format) _DetectionPattern( - pattern: RegExp(r'^(lnbc|LNBC)[a-km-zA-HJ-NP-Z1-9]{1,}[a-zA-Z0-9]+$'), + pattern: RegExp(r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]+$', caseSensitive: false), currency: CryptoCurrency.btcln, ), - + + // Lightning Network (Bolt12 Offer format) + _DetectionPattern( + pattern: RegExp(r'^(lightning:)?(lno1)[a-z0-9]+$', caseSensitive: false), + currency: CryptoCurrency.btcln, + ), + // Lightning Address (email format) _DetectionPattern( pattern: RegExp(r'^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'), currency: CryptoCurrency.btcln, ), - // LNURL format + // Lightning Address (LNURL format) _DetectionPattern( - pattern: RegExp(r'^(LNURL|lnurl)[a-zA-Z0-9]+$', caseSensitive: false), + pattern: RegExp(r'^(lightning:)?(lnurl)[a-z0-9]+$', caseSensitive: false), currency: CryptoCurrency.btcln, ), @@ -152,7 +159,7 @@ class UniversalAddressDetector { // Bitcoin Cash _DetectionPattern( - pattern: RegExp(r'^(q|p)[a-z0-9]{41}$'), + pattern: RegExp(r'^([qp])[a-z0-9]{41}$'), currency: CryptoCurrency.bch, ), @@ -212,7 +219,7 @@ class UniversalAddressDetector { // Decred _DetectionPattern( - pattern: RegExp(r'^(D|T|S)[ksecS][a-km-zA-HJ-NP-Z1-9]+$'), + pattern: RegExp(r'^([DTS])[ksecS][a-km-zA-HJ-NP-Z1-9]+$'), currency: CryptoCurrency.dcr, ), @@ -232,8 +239,9 @@ class UniversalAddressDetector { // Test each pattern in order of specificity for (final pattern in detectionPatterns) { if (pattern.pattern.hasMatch(cleanInput)) { - final walletType = cryptoCurrencyToWalletType(pattern.currency); + final walletType = cryptoCurrencyOrTokenToWalletType(pattern.currency); final chainId = getChainIdByCryptoCurrency(pattern.currency); + final amount = _getAmountFromInvoice(cleanInput, pattern.currency); return AddressDetectionResult( address: cleanInput, @@ -241,6 +249,7 @@ class UniversalAddressDetector { detectedWalletType: walletType, isValid: true, chainId: chainId, + amount: amount, ); } } @@ -251,6 +260,15 @@ class UniversalAddressDetector { isValid: false, ); } + + static String? _getAmountFromInvoice(String input, CryptoCurrency currency) { + if (currency != CryptoCurrency.btcln) return null; + try { + return getBolt11Amount(input).toString(); + } catch (e) { + return null; + } + } } class _DetectionPattern { diff --git a/lib/core/utilities.dart b/lib/core/utilities.dart index 1278dfc44..1384c35fe 100644 --- a/lib/core/utilities.dart +++ b/lib/core/utilities.dart @@ -6,4 +6,10 @@ extension EnhancedList on Iterable { return null; } } +} + + +String middleTruncate(String s, int head, int tail) { + if (s.length <= head + tail + 3) return s; + return s.substring(0, head) + '...' + s.substring(s.length - tail); } \ No newline at end of file diff --git a/lib/di.dart b/lib/di.dart index 776f52b2d..a6b5c740a 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -6,92 +6,63 @@ import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/anypay/anypay_api.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; +import 'package:cake_wallet/entities/bitcoin_amount_display_mode.dart'; import 'package:cake_wallet/evm/evm.dart'; import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart'; import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; -import 'package:cake_wallet/order/order.dart'; -import 'package:cake_wallet/core/backup_service_v3.dart'; -import 'package:cake_wallet/core/new_wallet_arguments.dart'; import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart'; +import 'package:cake_wallet/cake_pay/cake_pay.dart'; +import 'package:cake_wallet/cake_pay/src/models/cake_pay_vendor.dart'; +import 'package:cake_wallet/cake_pay/src/services/cake_pay_api.dart'; +import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart'; import 'package:cake_wallet/core/auth_service.dart'; +import 'package:cake_wallet/core/backup_service_v3.dart'; import 'package:cake_wallet/core/key_service.dart'; +import 'package:cake_wallet/core/new_wallet_arguments.dart'; import 'package:cake_wallet/core/new_wallet_type_arguments.dart'; +import 'package:cake_wallet/core/node_switching_service.dart'; +import 'package:cake_wallet/core/reset_service.dart'; import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/core/selectable_option.dart'; import 'package:cake_wallet/core/totp_request_details.dart'; +import 'package:cake_wallet/core/trade_monitor.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart'; import 'package:cake_wallet/core/yat_service.dart'; -import 'package:cake_wallet/core/node_switching_service.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/entities/biometric_auth.dart'; import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/hardware_wallet/require_hardware_wallet_connection.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; -import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; -import 'package:cake_wallet/haven/cw_haven.dart'; -import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart'; -import 'package:cake_wallet/src/screens/dev/moneroc_cache_debug.dart'; -import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart'; -import 'package:cake_wallet/src/screens/dev/network_requests.dart'; -import 'package:cake_wallet/src/screens/dev/qr_tools_page.dart'; -import 'package:cake_wallet/src/screens/dev/exchange_provider_logs_page.dart'; -import 'package:cake_wallet/src/screens/dev/secure_preferences_page.dart'; -import 'package:cake_wallet/src/screens/dev/shared_preferences_page.dart'; -import 'package:cake_wallet/src/screens/integrations/deuro/savings_page.dart'; -import 'package:cake_wallet/src/screens/settings/background_sync_page.dart'; -import 'package:cake_wallet/src/screens/start_tor/start_tor_page.dart'; -import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart'; -import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart'; -import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart'; -import 'package:cake_wallet/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart'; -import 'package:cake_wallet/src/widgets/bottom_sheet/swap_details_bottom_sheet.dart'; - -import 'package:cake_wallet/store/dashboard/order_filter_store.dart'; -import 'package:cake_wallet/themes/core/theme_store.dart'; -import 'package:cake_wallet/view_model/dev/monero_background_sync.dart'; -import 'package:cake_wallet/view_model/dev/secure_preferences.dart'; -import 'package:cake_wallet/view_model/dev/shared_preferences.dart'; -import 'package:cake_wallet/view_model/hardware_wallet/bitbox_view_model.dart'; -import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart'; -import 'package:cake_wallet/view_model/hardware_wallet/trezor_view_model.dart'; -import 'package:cake_wallet/view_model/integrations/deuro_view_model.dart'; -import 'package:cake_wallet/view_model/link_view_model.dart'; -import 'package:cake_wallet/tron/tron.dart'; -import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart'; -import 'package:cake_wallet/view_model/start_tor_view_model.dart'; -import 'package:cake_wallet/zcash/zcash.dart'; -import 'package:cw_core/receive_page_option.dart'; -import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart'; -import 'package:cake_wallet/entities/wallet_manager.dart'; -import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart'; -import 'package:cake_wallet/src/screens/buy/payment_method_options_page.dart'; -import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_external_send_page.dart'; -import 'package:cake_wallet/src/screens/payjoin_details/payjoin_details_page.dart'; -import 'package:cake_wallet/src/screens/receive/address_list_page.dart'; -import 'package:cake_wallet/src/screens/seed/seed_verification/seed_verification_page.dart'; -import 'package:cake_wallet/src/screens/send/transaction_success_info_page.dart'; -import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart'; -import 'package:cake_wallet/src/screens/settings/mweb_logs_page.dart'; -import 'package:cake_wallet/src/screens/settings/mweb_node_page.dart'; -import 'package:cake_wallet/src/screens/welcome/welcome_page.dart'; -import 'package:cake_wallet/store/dashboard/payjoin_transactions_store.dart'; -import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart'; -import 'package:cake_wallet/view_model/payjoin_details_view_model.dart'; -import 'package:cw_core/payjoin_session.dart'; -import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; -import 'package:cake_wallet/view_model/send/fees_view_model.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/entities/template.dart'; import 'package:cake_wallet/entities/transaction_description.dart'; +import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart'; +import 'package:cake_wallet/entities/wallet_manager.dart'; import 'package:cake_wallet/exchange/exchange_template.dart'; +import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; import 'package:cake_wallet/exchange/trade.dart'; +import 'package:cake_wallet/haven/cw_haven.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/nano/nano.dart'; -import 'package:cake_wallet/decred/decred.dart'; +import 'package:cake_wallet/new-ui/new_dashboard.dart'; +import 'package:cake_wallet/new-ui/pages/about_page.dart'; +import 'package:cake_wallet/new-ui/pages/account_customizer.dart'; +import 'package:cake_wallet/new-ui/pages/coin_control_page.dart'; +import 'package:cake_wallet/new-ui/pages/addresses_page.dart'; +import 'package:cake_wallet/new-ui/pages/home_page.dart'; +import 'package:cake_wallet/new-ui/pages/send_page.dart'; +import 'package:cake_wallet/new-ui/pages/lightning_username_page.dart'; +import 'package:cake_wallet/new-ui/pages/receive_page.dart'; +import 'package:cake_wallet/new-ui/viewmodels/lightning_username/lightning_username_bloc.dart'; +import 'package:cake_wallet/new-ui/widgets/addresses_page/address_label_input.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_label_modal.dart'; +import 'package:cake_wallet/new-ui/pages/swap_page.dart'; +import 'package:cake_wallet/order/order.dart'; import 'package:cake_wallet/reactions/on_authentication_state_change.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/solana/solana.dart'; @@ -99,7 +70,9 @@ import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dar import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/src/screens/backup/backup_page.dart'; import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart'; +import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart'; import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart'; +import 'package:cake_wallet/src/screens/buy/payment_method_options_page.dart'; import 'package:cake_wallet/src/screens/buy/webview_page.dart'; import 'package:cake_wallet/src/screens/contact/contact_list_page.dart'; import 'package:cake_wallet/src/screens/contact/contact_page.dart'; @@ -111,12 +84,25 @@ import 'package:cake_wallet/src/screens/dashboard/edit_token_page.dart'; import 'package:cake_wallet/src/screens/dashboard/home_settings_page.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/address_page.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/balance/balance_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/cake_features_page.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart'; +import 'package:cake_wallet/src/screens/dev/background_sync_logs_page.dart'; +import 'package:cake_wallet/src/screens/dev/exchange_provider_logs_page.dart'; +import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart'; +import 'package:cake_wallet/src/screens/dev/moneroc_cache_debug.dart'; +import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart'; +import 'package:cake_wallet/src/screens/dev/network_requests.dart'; +import 'package:cake_wallet/src/screens/dev/qr_tools_page.dart'; +import 'package:cake_wallet/src/screens/dev/secure_preferences_page.dart'; +import 'package:cake_wallet/src/screens/dev/shared_preferences_page.dart'; +import 'package:cake_wallet/src/screens/dev/socket_health_logs_page.dart'; import 'package:cake_wallet/src/screens/exchange/exchange_page.dart'; import 'package:cake_wallet/src/screens/exchange/exchange_template_page.dart'; import 'package:cake_wallet/src/screens/exchange_trade/exchange_confirm_page.dart'; +import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_external_send_page.dart'; import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_page.dart'; import 'package:cake_wallet/src/screens/faq/faq_page.dart'; +import 'package:cake_wallet/src/screens/integrations/deuro/savings_page.dart'; import 'package:cake_wallet/src/screens/monero_accounts/monero_account_edit_or_create_page.dart'; import 'package:cake_wallet/src/screens/monero_accounts/monero_account_list_page.dart'; import 'package:cake_wallet/src/screens/nano/nano_change_rep_page.dart'; @@ -126,7 +112,9 @@ import 'package:cake_wallet/src/screens/new_wallet/new_wallet_type_page.dart'; import 'package:cake_wallet/src/screens/nodes/node_create_or_edit_page.dart'; import 'package:cake_wallet/src/screens/nodes/pow_node_create_or_edit_page.dart'; import 'package:cake_wallet/src/screens/order_details/order_details_page.dart'; +import 'package:cake_wallet/src/screens/payjoin_details/payjoin_details_page.dart'; import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; +import 'package:cake_wallet/src/screens/receive/address_list_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart'; import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart'; @@ -137,20 +125,25 @@ import 'package:cake_wallet/src/screens/restore/restore_options_page.dart'; import 'package:cake_wallet/src/screens/restore/wallet_restore_choose_derivation.dart'; import 'package:cake_wallet/src/screens/restore/wallet_restore_page.dart'; import 'package:cake_wallet/src/screens/seed/pre_seed_page.dart'; +import 'package:cake_wallet/src/screens/seed/seed_verification/seed_verification_page.dart'; import 'package:cake_wallet/src/screens/seed/wallet_seed_page.dart'; import 'package:cake_wallet/src/screens/send/send_page.dart'; import 'package:cake_wallet/src/screens/send/send_template_page.dart'; +import 'package:cake_wallet/src/screens/send/transaction_success_info_page.dart'; +import 'package:cake_wallet/src/screens/settings/background_sync_page.dart'; import 'package:cake_wallet/src/screens/settings/connection_sync_page.dart'; import 'package:cake_wallet/src/screens/settings/desktop_settings/desktop_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/display_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/domain_lookups_page.dart'; import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart'; +import 'package:cake_wallet/src/screens/settings/mweb_logs_page.dart'; +import 'package:cake_wallet/src/screens/settings/mweb_node_page.dart'; import 'package:cake_wallet/src/screens/settings/mweb_settings.dart'; import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/privacy_page.dart'; import 'package:cake_wallet/src/screens/settings/security_backup_page.dart'; -import 'package:cake_wallet/src/screens/settings/silent_payments_settings.dart'; import 'package:cake_wallet/src/screens/settings/silent_payments_logs_page.dart'; +import 'package:cake_wallet/src/screens/settings/silent_payments_settings.dart'; import 'package:cake_wallet/src/screens/settings/trocador_providers_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/modify_2fa_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa.dart'; @@ -158,65 +151,36 @@ import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_enter_code_page.dart import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_info_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_qr_page.dart'; import 'package:cake_wallet/src/screens/setup_pin_code/setup_pin_code.dart'; +import 'package:cake_wallet/src/screens/start_tor/start_tor_page.dart'; import 'package:cake_wallet/src/screens/subaddress/address_edit_or_create_page.dart'; import 'package:cake_wallet/src/screens/support/support_page.dart'; import 'package:cake_wallet/src/screens/support_chat/support_chat_page.dart'; import 'package:cake_wallet/src/screens/support_other_links/support_other_links_page.dart'; -import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart'; -import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart'; -import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart'; -import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart'; -import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_page.dart'; -import 'package:cake_wallet/utils/device_info.dart'; -import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; -import 'package:cake_wallet/utils/payment_request.dart'; -import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart'; -import 'package:cake_wallet/view_model/animated_ur_model.dart'; -import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart'; -import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart'; -import 'package:cake_wallet/view_model/anonpay_details_view_model.dart'; -import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart'; -import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; -import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart'; -import 'package:cake_wallet/view_model/cake_pay/cake_pay_auth_view_model.dart'; -import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart'; -import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart'; -import 'package:cake_wallet/cake_pay/src/services/cake_pay_api.dart'; -import 'package:cake_wallet/cake_pay/src/models/cake_pay_vendor.dart'; -import 'package:cake_wallet/cake_pay/cake_pay.dart'; -import 'package:cake_wallet/view_model/cake_pay/cake_pay_account_view_model.dart'; -import 'package:cake_wallet/view_model/cake_pay/cake_pay_cards_list_view_model.dart'; -import 'package:cake_wallet/view_model/nano_account_list/nano_account_edit_or_create_view_model.dart'; -import 'package:cake_wallet/view_model/nano_account_list/nano_account_list_view_model.dart'; -import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart'; -import 'package:cake_wallet/view_model/wallet_groups_display_view_model.dart'; -import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; -import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart'; -import 'package:cake_wallet/view_model/settings/display_settings_view_model.dart'; -import 'package:cake_wallet/view_model/settings/mweb_settings_view_model.dart'; -import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart'; -import 'package:cake_wallet/view_model/settings/privacy_settings_view_model.dart'; -import 'package:cake_wallet/view_model/settings/security_settings_view_model.dart'; -import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart'; -import 'package:cake_wallet/view_model/settings/trocador_providers_view_model.dart'; -import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; -import 'package:cake_wallet/view_model/wallet_list/wallet_edit_view_model.dart'; -import 'package:cake_wallet/view_model/wallet_restore_choose_derivation_view_model.dart'; -import 'package:cw_core/nano_account.dart'; -import 'package:cw_core/unspent_coin_type.dart'; -import 'package:cw_core/unspent_coins_info.dart'; -import 'package:cw_core/wallet_service.dart'; -import 'package:cw_core/transaction_info.dart'; -import 'package:cw_core/node.dart'; import 'package:cake_wallet/src/screens/trade_details/trade_details_page.dart'; +import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart'; import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart'; +import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart'; +import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart'; import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart'; +import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart'; +import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart'; +import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_page.dart'; +import 'package:cake_wallet/src/screens/welcome/welcome_page.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/swap_details_bottom_sheet.dart'; +import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/authentication_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; +import 'package:cake_wallet/store/dashboard/order_filter_store.dart'; import 'package:cake_wallet/store/dashboard/orders_store.dart'; +import 'package:cake_wallet/store/dashboard/payjoin_transactions_store.dart'; import 'package:cake_wallet/store/dashboard/trade_filter_store.dart'; import 'package:cake_wallet/store/dashboard/trades_store.dart'; import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart'; @@ -227,31 +191,76 @@ import 'package:cake_wallet/store/templates/exchange_template_store.dart'; import 'package:cake_wallet/store/templates/send_template_store.dart'; import 'package:cake_wallet/store/wallet_list_store.dart'; import 'package:cake_wallet/store/yat/yat_store.dart'; +import 'package:cake_wallet/themes/core/theme_store.dart'; +import 'package:cake_wallet/tron/tron.dart'; +import 'package:cake_wallet/utils/device_info.dart'; +import 'package:cake_wallet/utils/payment_request.dart'; +import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart'; +import 'package:cake_wallet/view_model/animated_ur_model.dart'; +import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart'; +import 'package:cake_wallet/view_model/anonpay_details_view_model.dart'; import 'package:cake_wallet/view_model/auth_view_model.dart'; import 'package:cake_wallet/view_model/backup_view_model.dart'; import 'package:cake_wallet/view_model/buy/buy_amount_view_model.dart'; +import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart'; import 'package:cake_wallet/view_model/buy/buy_view_model.dart'; +import 'package:cake_wallet/view_model/cake_pay/cake_pay_account_view_model.dart'; +import 'package:cake_wallet/view_model/cake_pay/cake_pay_auth_view_model.dart'; +import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart'; +import 'package:cake_wallet/view_model/cake_pay/cake_pay_cards_list_view_model.dart'; import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/cake_features_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart'; +import 'package:cake_wallet/view_model/dev/background_sync_logs_view_model.dart'; +import 'package:cake_wallet/view_model/dev/exchange_provider_logs_view_model.dart'; +import 'package:cake_wallet/view_model/dev/monero_background_sync.dart'; +import 'package:cake_wallet/view_model/dev/secure_preferences.dart'; +import 'package:cake_wallet/view_model/dev/shared_preferences.dart'; +import 'package:cake_wallet/view_model/dev/socket_health_logs_view_model.dart'; import 'package:cake_wallet/view_model/edit_backup_password_view_model.dart'; import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart'; import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart'; +import 'package:cake_wallet/view_model/hardware_wallet/bitbox_view_model.dart'; +import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; +import 'package:cake_wallet/view_model/hardware_wallet/trezor_view_model.dart'; +import 'package:cake_wallet/view_model/integrations/deuro_view_model.dart'; +import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart'; +import 'package:cake_wallet/view_model/nano_account_list/nano_account_edit_or_create_view_model.dart'; +import 'package:cake_wallet/view_model/nano_account_list/nano_account_list_view_model.dart'; import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart'; +import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart'; import 'package:cake_wallet/view_model/order_details_view_model.dart'; +import 'package:cake_wallet/view_model/payjoin_details_view_model.dart'; +import 'package:cake_wallet/view_model/payment/payment_view_model.dart'; import 'package:cake_wallet/view_model/rescan_view_model.dart'; +import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:cake_wallet/view_model/restore_from_backup_view_model.dart'; +import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; +import 'package:cake_wallet/view_model/send/fees_view_model.dart'; import 'package:cake_wallet/view_model/send/send_template_view_model.dart'; import 'package:cake_wallet/view_model/send/send_view_model.dart'; +import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart'; +import 'package:cake_wallet/view_model/settings/display_settings_view_model.dart'; +import 'package:cake_wallet/view_model/settings/mweb_settings_view_model.dart'; +import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart'; +import 'package:cake_wallet/view_model/settings/privacy_settings_view_model.dart'; +import 'package:cake_wallet/view_model/settings/security_settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/silent_payments_settings_view_model.dart'; +import 'package:cake_wallet/view_model/settings/trocador_providers_view_model.dart'; import 'package:cake_wallet/view_model/setup_pin_code_view_model.dart'; +import 'package:cake_wallet/view_model/start_tor_view_model.dart'; import 'package:cake_wallet/view_model/support_view_model.dart'; import 'package:cake_wallet/view_model/trade_details_view_model.dart'; import 'package:cake_wallet/view_model/transaction_details_view_model.dart'; @@ -259,19 +268,33 @@ import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_details_view_ import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_groups_display_view_model.dart'; import 'package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart'; import 'package:cake_wallet/view_model/wallet_keys_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_edit_view_model.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:cake_wallet/view_model/wallet_new_vm.dart'; +import 'package:cake_wallet/view_model/wallet_restore_choose_derivation_view_model.dart'; import 'package:cake_wallet/view_model/wallet_restore_view_model.dart'; import 'package:cake_wallet/view_model/wallet_seed_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart'; import 'package:cake_wallet/view_model/wallet_unlock_loadable_view_model.dart'; import 'package:cake_wallet/view_model/wallet_unlock_verifiable_view_model.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; +import 'package:cake_wallet/zcash/zcash.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/nano_account.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/payjoin_session.dart'; +import 'package:cw_core/receive_page_option.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/unspent_coin_type.dart'; +import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -280,19 +303,12 @@ import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:trezor_connect/trezor_connect.dart'; + import 'buy/kryptonim/kryptonim.dart'; import 'buy/meld/meld_buy_provider.dart'; import 'dogecoin/dogecoin.dart'; +import 'new-ui/viewmodels/card_customizer/card_customizer_bloc.dart'; import 'src/screens/buy/buy_sell_page.dart'; -import 'package:cake_wallet/view_model/dev/background_sync_logs_view_model.dart'; -import 'package:cake_wallet/src/screens/dev/background_sync_logs_page.dart'; -import 'package:cake_wallet/core/trade_monitor.dart'; -import 'package:cake_wallet/core/reset_service.dart'; -import 'package:cake_wallet/view_model/dev/socket_health_logs_view_model.dart'; -import 'package:cake_wallet/src/screens/dev/socket_health_logs_page.dart'; -import 'package:cake_wallet/view_model/dev/exchange_provider_logs_view_model.dart'; -import 'package:cake_wallet/view_model/payment/payment_view_model.dart'; -import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart'; final getIt = GetIt.instance; @@ -374,7 +390,7 @@ Future setup({ nodeListStore: getIt.get(), themeStore: getIt.get())); getIt.registerSingleton( - TradesStore(tradesSource: _tradesSource, settingsStore: getIt.get())); + TradesStore(tradesSource: _tradesSource, appStore: getIt.get())); getIt.registerSingleton( OrdersStore(ordersSource: _ordersSource, settingsStore: getIt.get())); getIt.registerFactory(() => @@ -441,6 +457,9 @@ Future setup({ newWalletArguments: newWalletArgs, )); + + final walletList = await WalletInfo.getAll(); + getIt.registerFactory( () => WalletManager( getIt.get(), @@ -532,7 +551,7 @@ Future setup({ getIt.registerFactory(() => BalanceViewModel( appStore: getIt.get(), settingsStore: getIt.get(), - fiatConvertationStore: getIt.get())); + fiatConversionStore: getIt.get())); getIt.registerFactory( () => ExchangeViewModel( @@ -540,7 +559,6 @@ Future setup({ _tradesSource, getIt.get(), getIt.get(), - getIt.get().settingsStore, getIt.get(), getIt.get(), getIt.get(), @@ -574,8 +592,21 @@ Future setup({ sharedPreferences: getIt.get(), keyService: getIt.get())); - final walletList = await WalletInfo.getAll(); - + getIt.registerFactoryParam( + (lightningMode, displayMode) { + final wallet = getIt.get().wallet!; + return CardCustomizerBloc(wallet, + lightningMode: lightningMode, + displaySats: wallet.type == WalletType.bitcoin && (displayMode == BitcoinAmountDisplayMode.satoshi || + (displayMode == BitcoinAmountDisplayMode.satoshiForLightning && lightningMode))); + }); + + getIt.registerFactory(() => AccountCreationModal( + accountEditOrCreateViewModel: getIt.get())); + + getIt.registerFactory( + () => LightningUsernameBloc(getIt.get().wallet!)); + getIt.registerFactory( () => AuthService( secureStorage: getIt.get(), @@ -626,6 +657,13 @@ Future setup({ ); }); + getIt.registerFactoryParam( + (isSetup, _) => LightningUsernamePage( + isSetup: isSetup ?? false, + themeStore: getIt.get(), + lightningUsernameBloc: getIt.get(), + )); + getIt.registerFactory(instanceName: 'login', () { return AuthPage(getIt.get(), closable: false, onAuthenticationFinished: (isAuthenticated, AuthPageState authPageState) async { @@ -742,6 +780,13 @@ Future setup({ addressListViewModel: getIt.get(), )); + getIt.registerFactory(() => NewDashboard( + dashboardViewModel: getIt.get(), + bottomSheetService: getIt.get(), + )); + + getIt.registerFactory(()=>NewHomePage(dashboardViewModel: getIt.get(),nftViewModel: getIt.get(),)); + getIt.registerFactory(() { final GlobalKey _navigatorKey = GlobalKey(); return DesktopSidebarWrapper( @@ -802,10 +847,22 @@ Future setup({ getIt.registerFactory( () => ReceivePage(addressListViewModel: getIt.get())); - getIt.registerFactory(() => AddressPage( - addressListViewModel: getIt.get(), - dashboardViewModel: getIt.get(), - receiveOptionViewModel: getIt.get())); + + getIt.registerFactory( + () => AddressPage( + addressListViewModel: getIt(), + dashboardViewModel: getIt(), + receiveOptionViewModel: getIt(), + ), + ); + + getIt.registerFactoryParam((param1, param2) => + NewReceivePage( + addressListViewModel: getIt.get(), + receiveOptionViewModel: getIt.get(), + dashboardViewModel: getIt.get(), + lightningMode: param1 ?? false, + initialCurrency: param2)); getIt.registerFactoryParam( (WalletAddressListItem? item, _) => @@ -816,9 +873,19 @@ Future setup({ addressEditOrCreateViewModel: getIt.get(param1: item))); + getIt.registerFactoryParam((dynamic item, _) => + AddressLabelInputPopup( + walletAddressEditOrCreateViewModel: + getIt.get(param1: item))); + + getIt.registerFactoryParam((dynamic item, _) => + ReceiveLabelModal( + walletAddressEditOrCreateViewModel: + getIt.get(param1: item))); + getIt.registerFactory(() => SendTemplateViewModel( getIt.get().wallet!, - getIt.get().settingsStore, + getIt.get(), getIt.get(), getIt.get())); @@ -849,6 +916,18 @@ Future setup({ walletSwitcherViewModel: getIt.get(), )); + getIt.registerFactoryParam((params, _) { + params ??= SendPageParams(); + return NewSendPage( + sendViewModel: getIt.get(param1: params.unspentCoinType), + authService: getIt.get(), + params: params, + contactListViewModel: getIt.get(), + paymentViewModel: getIt.get(), + walletSwitcherViewModel: getIt.get(), + ); + }); + getIt.registerFactory( () => SendTemplatePage(sendTemplateViewModel: getIt.get())); @@ -970,7 +1049,7 @@ Future setup({ getIt.get(param1: account))); getIt.registerFactory(() => - DisplaySettingsViewModel(getIt.get(), getIt.get())); + DisplaySettingsViewModel(getIt.get(), getIt.get())); getIt.registerFactory(() => SilentPaymentsSettingsViewModel(getIt.get(), getIt.get().wallet!)); @@ -1009,7 +1088,7 @@ Future setup({ getIt.registerFactory(() => WalletKeysViewModel(getIt.get())); getIt.registerFactory(() => WalletKeysPage(getIt.get())); - + getIt.registerFactory(() => AnimatedURModel(getIt.get())); getIt.registerFactoryParam, void>((Map urQr, _) => @@ -1022,14 +1101,26 @@ Future setup({ (CryptoCurrency? cur, _) => ContactListViewModel(_contactSource, walletList, cur, getIt.get())); - getIt.registerFactoryParam((CryptoCurrency? cur, _) => - ContactListPage(getIt.get(param1: cur), getIt.get())); + getIt.registerFactoryParam( + (CryptoCurrency? cur, bool? showAddContact) => ContactListPage( + getIt.get(param1: cur), + getIt.get(), + showAddButton: showAddContact ?? true, + )); getIt.registerFactoryParam( (ContactRecord? contact, _) => ContactPage(getIt.get(param1: contact))); getIt.registerFactory(() => AddressListPage(getIt.get())); + getIt.registerFactoryParam( + (showHidden, _) => NewAddressesPage( + showHidden: showHidden, + addressListViewModel: getIt(), + dashboardViewModel: getIt(), + ), + ); + getIt.registerFactory(() { final appStore = getIt.get(); return NodeListViewModel(_nodeSource, appStore); @@ -1066,27 +1157,40 @@ Future setup({ getIt.registerFactory(() => OtherSettingsPage(getIt.get())); + getIt.registerFactory(()=> AboutPage(appVersion: getIt.get().appVersion)); + getIt.registerFactory(() => NanoChangeRepPage( settingsStore: getIt.get().settingsStore, wallet: getIt.get().wallet!, )); - getIt.registerFactoryParam( - (WalletType? type, bool? isPow) => NodeCreateOrEditViewModel( - (isPow ?? false) ? _powNodeSource : _nodeSource, - type ?? getIt.get().wallet!.type, - getIt.get())); + getIt.registerFactoryParam, void>( + (Map args, _) { + final WalletType type = args['type'] as WalletType? ?? getIt.get().wallet!.type; + final bool isPow = args['isPow'] as bool? ?? false; + final Node? editingNode = args['editingNode'] as Node?; + final nodeSourceArgs = isPow ? _powNodeSource : _nodeSource; + return NodeCreateOrEditViewModel( + nodeSourceArgs, + type, + editingNode, + getIt.get()); + } + ); getIt.registerFactoryParam( - (Node? editingNode, bool? isSelected) => NodeCreateOrEditPage( - nodeCreateOrEditViewModel: getIt.get(param2: false), + (Node? editingNode, bool? isSelected) { + final vm = getIt.get(param1: {'isPow' : false,'editingNode': editingNode}); + return NodeCreateOrEditPage( + nodeCreateOrEditViewModel: vm, editingNode: editingNode, isSelected: isSelected, - type: getIt.get().wallet!.type)); + type: getIt.get().wallet!.type); + }); getIt.registerFactoryParam( (Node? editingNode, bool? isSelected) => PowNodeCreateOrEditPage( - nodeCreateOrEditViewModel: getIt.get(param2: true), + nodeCreateOrEditViewModel: getIt.get(param1: {'isPow' : true, 'editingNode': editingNode}), editingNode: editingNode, isSelected: isSelected)); @@ -1111,7 +1215,6 @@ Future setup({ )); getIt.registerFactory(() => OnRamperBuyProvider( - getIt.get(), wallet: getIt.get().wallet!, )); @@ -1148,6 +1251,17 @@ Future setup({ return ExchangePage(getIt.get(), getIt.get(), paymentRequest); }); + getIt.registerFactoryParam( + (PaymentRequest? paymentRequest, CryptoCurrency? initialCurrency) { + return NewSwapPage( + getIt.get(), + getIt.get(), + paymentRequest, + walletSwitcherViewModel: getIt.get(), + initialCurrency: initialCurrency, + ); + }); + getIt.registerFactory(() => ExchangeConfirmPage(tradesStore: getIt.get())); getIt.registerFactory( @@ -1295,7 +1409,7 @@ Future setup({ transactionInfo: transactionInfo, transactionDescriptionBox: _transactionDescriptionBox, wallet: wallet, - settingsStore: getIt.get(), + appStore: getIt.get(), sendViewModel: getIt.get(), canReplaceByFee: canReplaceByFee, ); @@ -1340,6 +1454,11 @@ Future setup({ getIt.registerFactory(() => CakeFeaturesViewModel(getIt.get())); + + getIt.registerFactory(() => CakeFeaturesPage( + dashboardViewModel: getIt.get(), + cakeFeaturesViewModel: getIt.get())); + getIt.registerFactory(() => BackupServiceV3(getIt.get(), _transactionDescriptionBox, getIt.get(), getIt.get())); @@ -1435,7 +1554,7 @@ Future setup({ wallet: wallet!, unspentCoinsInfo: _unspentCoinsInfoSource, fiatConversationStore: getIt.get(), - settingsStore: getIt.get(), + appStore: getIt.get(), coinTypeToSpendFrom: coinTypeToSpendFrom ?? UnspentCoinType.any, ); }); @@ -1445,6 +1564,13 @@ Future setup({ unspentCoinsListViewModel: getIt.get(param1: coinTypeToSpendFrom))); + getIt.registerFactoryParam( + (coinTypeToSpendFrom, _) => NewCoinControlPage( + unspentCoinsListViewModel: + getIt.get(param1: coinTypeToSpendFrom)) + + ); + getIt.registerFactoryParam( (item, model) => @@ -1589,17 +1715,17 @@ Future setup({ getIt.registerFactory(() => DevSharedPreferencesPage(getIt.get())); getIt.registerFactory(() => DevSecurePreferencesPage(getIt.get())); - + getIt.registerFactory(() => BackgroundSyncLogsViewModel()); - + getIt.registerFactory(() => DevBackgroundSyncLogsPage(getIt.get())); - + getIt.registerFactory(() => SocketHealthLogsViewModel()); getIt.registerFactory(() => DevSocketHealthLogsPage(getIt.get())); - + getIt.registerFactory(() => DevNetworkRequests()); - + getIt.registerFactory(() => DevQRToolsPage()); getIt.registerFactory(() => ExchangeProviderLogsViewModel()); diff --git a/lib/entities/bitcoin_amount_display_mode.dart b/lib/entities/bitcoin_amount_display_mode.dart new file mode 100644 index 000000000..9c4ce52cc --- /dev/null +++ b/lib/entities/bitcoin_amount_display_mode.dart @@ -0,0 +1,32 @@ +import 'package:cw_core/enumerable_item.dart'; + +class BitcoinAmountDisplayMode extends EnumerableItem with Serializable { + const BitcoinAmountDisplayMode({required String title, required int raw}) + : super(title: title, raw: raw); + + static const all = [ + BitcoinAmountDisplayMode.satoshi, + BitcoinAmountDisplayMode.satoshiForLightning, + BitcoinAmountDisplayMode.bitcoin, + ]; + static const satoshiForLightning = + BitcoinAmountDisplayMode(raw: 0, title: 'sats (LN)'); + static const bitcoin = BitcoinAmountDisplayMode(raw: 1, title: 'BTC'); + static const satoshi = BitcoinAmountDisplayMode(raw: 2, title: 'sats'); + + static BitcoinAmountDisplayMode deserialize({required int raw}) { + switch (raw) { + case 0: + return satoshiForLightning; + case 1: + return bitcoin; + case 2: + return satoshi; + default: + throw Exception('Unexpected token: $raw for BalanceDisplayMode deserialize'); + } + } + + @override + String toString() => title; +} diff --git a/lib/entities/calculate_fiat_amount.dart b/lib/entities/calculate_fiat_amount.dart index 236d548cd..574600bd2 100644 --- a/lib/entities/calculate_fiat_amount.dart +++ b/lib/entities/calculate_fiat_amount.dart @@ -1,4 +1,4 @@ -String calculateFiatAmount({double? price, String? cryptoAmount}) { +String calculateFiatAmount({double? price, String? cryptoAmount, bool raw = false}) { if (price == null || cryptoAmount == null) { return '0.00'; } @@ -13,6 +13,10 @@ String calculateFiatAmount({double? price, String? cryptoAmount}) { return '0.00'; } + if(raw) { + return result.toStringAsFixed(2); + } + var formatted = ''; final parts = result.toString().split('.'); @@ -34,12 +38,16 @@ String formatWithCommas(String? number) { final parts = number!.split('.'); final integerPart = parts[0]; - final decimalPart = parts.length > 1 ? parts[1] : ''; + var decimalPart = parts.length > 1 ? parts[1] : ''; final formattedInteger = integerPart.replaceAllMapped( RegExp(r'\B(?=(\d{3})+(?!\d))'), (Match match) => ',', ); + if(decimalPart.length == 1) { + decimalPart = "${decimalPart}0"; + } + return decimalPart.isNotEmpty ? '$formattedInteger.$decimalPart' : formattedInteger; } diff --git a/lib/entities/contact.dart b/lib/entities/contact.dart index 901993f43..bd7fcf9a3 100644 --- a/lib/entities/contact.dart +++ b/lib/entities/contact.dart @@ -7,7 +7,7 @@ part 'contact.g.dart'; @HiveType(typeId: Contact.typeId) class Contact extends HiveObject with Keyable { - Contact({required this.name, required this.address, CryptoCurrency? type, DateTime? lastChange}) + Contact({required this.name, required this.address, CryptoCurrency? type, DateTime? lastChange, this.displayName = ""}) : lastChange = lastChange ?? DateTime.now() { if (type != null) { raw = type.raw; @@ -29,6 +29,9 @@ class Contact extends HiveObject with Keyable { @HiveField(3) DateTime lastChange; + @HiveField(4, defaultValue: "") + String displayName; + CryptoCurrency get type => CryptoCurrency.deserialize(raw: raw); @override diff --git a/lib/entities/contact_base.dart b/lib/entities/contact_base.dart index 89d08708d..a3270fd92 100644 --- a/lib/entities/contact_base.dart +++ b/lib/entities/contact_base.dart @@ -1,11 +1,13 @@ import 'package:cw_core/crypto_currency.dart'; abstract class ContactBase { - ContactBase(this.name, this.address, this.type); + ContactBase(this.name, this.address, this.type, {this.displayName = ""}); String name; String address; + String displayName; + CryptoCurrency type; } \ No newline at end of file diff --git a/lib/entities/contact_record.dart b/lib/entities/contact_record.dart index 4b0e120ba..ff522528e 100644 --- a/lib/entities/contact_record.dart +++ b/lib/entities/contact_record.dart @@ -14,6 +14,7 @@ abstract class ContactRecordBase extends Record with Store implements C : name = original.name, address = original.address, type = original.type, + displayName = original.displayName, lastChange = original.lastChange, super(source, original); @@ -29,6 +30,10 @@ abstract class ContactRecordBase extends Record with Store implements C @observable CryptoCurrency type; + @override + @observable + String displayName; + DateTime? lastChange; @override diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 0efe09906..59e072a1f 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -1,30 +1,35 @@ import 'dart:convert'; import 'dart:io' show Directory, File, Platform; + import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/secure_storage.dart'; +import 'package:cake_wallet/entities/balance_display_mode.dart'; +import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cake_wallet/entities/fs_migration.dart'; import 'package:cake_wallet/entities/haven_seed_store.dart'; +import 'package:cake_wallet/entities/node_list.dart'; +import 'package:cake_wallet/entities/preferences_key.dart'; +import 'package:cake_wallet/entities/secret_store_key.dart'; +import 'package:cake_wallet/exchange/trade.dart'; +import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:collection/collection.dart'; +import 'package:cw_core/db/sqlite.dart'; +import 'package:cw_core/node.dart'; import 'package:cake_wallet/entities/sync_status_display_mode.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/pathForWallet.dart'; -import 'package:cake_wallet/entities/secret_store_key.dart'; import 'package:cw_core/root_dir.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:hive/hive.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:cake_wallet/entities/preferences_key.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:cw_core/node.dart'; -import 'package:cake_wallet/entities/balance_display_mode.dart'; -import 'package:cake_wallet/entities/fiat_currency.dart'; -import 'package:cake_wallet/entities/node_list.dart'; -import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/entities/contact.dart'; -import 'package:cake_wallet/entities/fs_migration.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:encrypt/encrypt.dart' as encrypt; +import 'package:hive/hive.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:collection/collection.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/erc20_token.dart'; @@ -570,7 +575,6 @@ Future defaultSettingsMigration( type: WalletType.zcash, currentNodePreferenceKey: PreferencesKey.currentZcashNodeIdKey, ); - break; case 56: await sharedPreferences.setString( PreferencesKey.syncStatusDisplayMode, SyncStatusDisplayMode.blocksRemaining.name); @@ -586,6 +590,12 @@ Future defaultSettingsMigration( currentNodePreferenceKey: PreferencesKey.currentBscNodeIdKey, ); break; + case 58: // BalanceCardStyleSettings no-op (handled in sqlite.dart) + case 59: // WalletInfo.receiveInfoboxDismissed no-op (handled in sqlite.dart) + case 60: // BalanceCardStyleSettings.cardOrder no-op (handled in sqlite.dart) + // Do not migrate SQLite here, do that in sqlite.dart in order to prevent runtime + // errors, missing row and missing tables. + break; default: break; } diff --git a/lib/entities/generate_name.dart b/lib/entities/generate_name.dart deleted file mode 100644 index 9376542d4..000000000 --- a/lib/entities/generate_name.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'dart:convert'; -import 'dart:math'; - -import 'package:flutter/services.dart'; - -extension StringExtension on String { - String capitalized() { - return "${this[0].toUpperCase()}${this.substring(1)}"; - } -} - -Future generateName() async { - final randomThing = Random(); - final adjectiveStringRaw = - await rootBundle.loadString('assets/text/Wallet_Adjectives.txt'); - final nounStringRaw = - await rootBundle.loadString('assets/text/Wallet_Nouns.txt'); - - final ls = LineSplitter(); - final adjectives = ls.convert(adjectiveStringRaw); - final nouns = ls.convert(nounStringRaw); - - final chosenAdjective = adjectives[randomThing.nextInt(adjectives.length)]; - final chosenNoun = nouns[randomThing.nextInt(nouns.length)]; - final returnString = - chosenAdjective.capitalized() + ' ' + chosenNoun.capitalized(); - return returnString; -} diff --git a/lib/entities/language_service.dart b/lib/entities/language_service.dart index 44a5575ec..30c4f1a38 100644 --- a/lib/entities/language_service.dart +++ b/lib/entities/language_service.dart @@ -9,7 +9,7 @@ class LanguageService { 'es': 'Español (Spanish)', 'fa': 'فارسی (Persian)', 'fr': 'Français (French)', - 'gn': 'Guarani (Guarani)', + // 'gn': 'Guarani (Guarani)', 'hi': 'हिंदी (Hindi)', 'ja': '日本 (Japanese)', 'ko': '한국어 (Korean)', diff --git a/lib/entities/lnurlpay_record.dart b/lib/entities/lnurlpay_record.dart new file mode 100644 index 000000000..c6e9788a5 --- /dev/null +++ b/lib/entities/lnurlpay_record.dart @@ -0,0 +1,74 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/lnurl.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; + +class LNUrlPayRecord { + LNUrlPayRecord({ + required this.address, + required this.name, + }); + + final String name; + final String address; + + static Future checkWellKnownUsername(String username, CryptoCurrency currency) async { + if (![CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency)) return null; + + // split the string by the @ symbol: + try { + final List splitStrs = username.split("@"); + String name = splitStrs.first.toLowerCase(); + final String domain = splitStrs.last; + + if (splitStrs.length == 3) { + // for username like @alice@domain.org instead of alice@domain.org + name = splitStrs[1]; + } + + if (name.isEmpty) { + name = "_"; + } + + final expectedUrl = "https://$domain/.well-known/lnurlp/$name"; + final response = await ProxyWrapper().get( + clearnetUri: Uri.parse(expectedUrl), + headers: {"Accept": "application/json"}, + ); + + if (response.statusCode == 200) { + return LNURL.encode(expectedUrl); + } + } catch (e) { + printV("error checking well-known username: $e"); + } + return null; + } + + static String formatDomainName(String name) { + String formattedName = name; + + if (name.contains("@")) { + formattedName = name.replaceAll("@", "."); + } + + return formattedName; + } + + static Future fetchAddressAndName({ + required String formattedName, + required CryptoCurrency currency, + }) async { + final name = formattedName; + + printV("formattedName: $formattedName"); + + final address = await checkWellKnownUsername(formattedName, currency); + + if (address == null) { + return null; + } + + return LNUrlPayRecord(address: address, name: name); + } +} diff --git a/lib/entities/new_main_actions.dart b/lib/entities/new_main_actions.dart new file mode 100644 index 000000000..519b914ff --- /dev/null +++ b/lib/entities/new_main_actions.dart @@ -0,0 +1,65 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:flutter/material.dart'; + +class NewMainActions { + final Key key; + final String Function(BuildContext context) name; + final String image; + final VoidCallback onTap; + final bool Function(DashboardViewModel viewModel)? isEnabled; + final bool Function(DashboardViewModel viewModel)? canShow; + + NewMainActions._({ + required this.key, + required this.name, + required this.image, + required this.onTap, + this.isEnabled, + this.canShow, + }); + + static List all = [ + homeAction, + walletsAction, + contactsAction, + appsAction, + //chartsAction, + ]; + + static NewMainActions homeAction = NewMainActions._( + name: (context) => S.of(context).home, + image: 'assets/new-ui/navbar/home.svg', + key: ValueKey('dashboard_page_home_action_button_key'), + onTap: () {}, + ); + + static NewMainActions walletsAction = NewMainActions._( + name: (context) => S.of(context).wallets, + image: 'assets/new-ui/navbar/wallets.svg', + key: ValueKey('dashboard_page_wallets_action_button_key'), + onTap: () {}, + ); + + static NewMainActions contactsAction = NewMainActions._( + name: (context) => S.of(context).contacts, + image: 'assets/new-ui/navbar/contacts.svg', + key: ValueKey('dashboard_page_contacts_action_button_key'), + onTap: () {}, + ); + + static NewMainActions appsAction = NewMainActions._( + name: (context) => S.of(context).apps, + image: 'assets/new-ui/navbar/apps.svg', + key: ValueKey('dashboard_page_apps_action_button_key'), + canShow: (dashboardVM) => dashboardVM.showApps, + onTap: () {}, + ); + + // static NewMainActions chartsAction = NewMainActions._( + // name: (context) => 'Charts', //TODO S.of(context).charts, + // image: 'assets/new-ui/navbar/charts.svg', + // key: ValueKey('dashboard_page_charts_action_button_key'), + // onTap: () {}, + // ); +} diff --git a/lib/entities/new_ui_entities/list_item/list_Item_checkbox.dart b/lib/entities/new_ui_entities/list_item/list_Item_checkbox.dart new file mode 100644 index 000000000..c02f076e0 --- /dev/null +++ b/lib/entities/new_ui_entities/list_item/list_Item_checkbox.dart @@ -0,0 +1,22 @@ +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart'; +import 'package:flutter/material.dart'; + +class ListItemCheckbox extends ListItem { + const ListItemCheckbox({ + required super.keyValue, + required super.label, + this.subtitle, + this.iconPath, + this.onTap, + this.showArrow = false, + required this.value, + required this.onChanged, + }); + + final ValueChanged onChanged; + final VoidCallback? onTap; + final bool value; + final String? subtitle; + final String? iconPath; + final bool showArrow; +} diff --git a/lib/entities/new_ui_entities/list_item/list_item.dart b/lib/entities/new_ui_entities/list_item/list_item.dart new file mode 100644 index 000000000..0d0a4d2c8 --- /dev/null +++ b/lib/entities/new_ui_entities/list_item/list_item.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +abstract class ListItem { + const ListItem({ + required this.keyValue, + required this.label, + this.isFirstInSection = false, + this.isLastInSection = false, + }); + + final String keyValue; + final String label; + + final bool isFirstInSection; + final bool isLastInSection; + + BorderRadius get radius => BorderRadius.vertical( + top: Radius.circular(isFirstInSection ? 16 : 0), + bottom: Radius.circular(isLastInSection ? 16 : 0), + ); +} diff --git a/lib/entities/new_ui_entities/list_item/list_item_dropdown.dart b/lib/entities/new_ui_entities/list_item/list_item_dropdown.dart new file mode 100644 index 000000000..f1f1a83ed --- /dev/null +++ b/lib/entities/new_ui_entities/list_item/list_item_dropdown.dart @@ -0,0 +1,14 @@ +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart'; +import 'package:flutter/material.dart'; + +class ListItemDropdown extends ListItem { + const ListItemDropdown({ + required super.keyValue, + required super.label, + this.trailingText, + required this.onTap, + }); + + final String? trailingText; + final VoidCallback onTap; +} diff --git a/lib/entities/new_ui_entities/list_item/list_item_regular_row.dart b/lib/entities/new_ui_entities/list_item/list_item_regular_row.dart new file mode 100644 index 000000000..1f23e2b00 --- /dev/null +++ b/lib/entities/new_ui_entities/list_item/list_item_regular_row.dart @@ -0,0 +1,28 @@ +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart'; +import 'package:flutter/material.dart'; + +class ListItemRegularRow extends ListItem { + const ListItemRegularRow({ + required super.keyValue, + required super.label, + this.subtitle, + this.trailingText, + this.iconPath, + this.onTap, + this.trailingIconPath, + this.showArrow = true, + this.truncateTrailingText = false, + this.foregroundColor, + this.trailingIconSize + }); + + final String? subtitle; + final String? trailingText; + final String? iconPath; + final String? trailingIconPath; + final VoidCallback? onTap; + final bool showArrow; + final bool truncateTrailingText; + final Color? foregroundColor; + final double? trailingIconSize; +} diff --git a/lib/entities/new_ui_entities/list_item/list_item_selector.dart b/lib/entities/new_ui_entities/list_item/list_item_selector.dart new file mode 100644 index 000000000..9012a4c7b --- /dev/null +++ b/lib/entities/new_ui_entities/list_item/list_item_selector.dart @@ -0,0 +1,16 @@ +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart'; +import 'package:flutter/material.dart'; + +class ListItemSelector extends ListItem { + const ListItemSelector({ + required this.options, + required super.keyValue, + required super.label, + this.trailingText, + required this.onTap, + }); + + final String? trailingText; + final VoidCallback onTap; + final List options; +} \ No newline at end of file diff --git a/lib/entities/new_ui_entities/list_item/list_item_text_field.dart b/lib/entities/new_ui_entities/list_item/list_item_text_field.dart new file mode 100644 index 000000000..7206da30e --- /dev/null +++ b/lib/entities/new_ui_entities/list_item/list_item_text_field.dart @@ -0,0 +1,22 @@ +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart'; +import 'package:flutter/material.dart'; + +class ListItemTextField extends ListItem { + const ListItemTextField({ + required super.keyValue, + required super.label, + this.initialValue, + this.validator, + this.onChanged, + this.onFieldSubmitted, + this.focusNode + }); + + final String? initialValue; + final FormFieldValidator? validator; + final ValueChanged? onChanged; + final ValueChanged? onFieldSubmitted; + final FocusNode? focusNode; + + +} diff --git a/lib/entities/new_ui_entities/list_item/list_item_toggle.dart b/lib/entities/new_ui_entities/list_item/list_item_toggle.dart new file mode 100644 index 000000000..004ef3db0 --- /dev/null +++ b/lib/entities/new_ui_entities/list_item/list_item_toggle.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart'; + +class ListItemToggle extends ListItem { + const ListItemToggle({ + required super.keyValue, + required super.label, + required this.value, + required this.onChanged, + this.leadingEndWidget, + }); + + final ValueChanged onChanged; + final Widget? leadingEndWidget; + final bool value; +} diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index eba883be3..36fc3d65f 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -2,6 +2,7 @@ import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/core/yat_service.dart'; import 'package:cake_wallet/entities/emoji_string_extension.dart'; import 'package:cake_wallet/entities/ens_record.dart'; +import 'package:cake_wallet/entities/lnurlpay_record.dart'; import 'package:cake_wallet/entities/fio_address_provider.dart'; import 'package:cake_wallet/entities/openalias_record.dart'; import 'package:cake_wallet/entities/parsed_address.dart'; @@ -386,6 +387,14 @@ class AddressResolver { return ParsedAddress.fetchWellKnownAddress(address: record.address, name: text); } } + + if (walletType == WalletType.bitcoin) { + final record = + await LNUrlPayRecord.fetchAddressAndName(formattedName: text, currency: currency); + if (record != null) { + return ParsedAddress.fetchLNUrlPayAddress(address: record.address, name: text); + } + } } if (!text.startsWith('@') && text.contains('@') && !text.contains('.')) { diff --git a/lib/entities/parsed_address.dart b/lib/entities/parsed_address.dart index 9343718d8..329e83536 100644 --- a/lib/entities/parsed_address.dart +++ b/lib/entities/parsed_address.dart @@ -16,7 +16,8 @@ enum ParseFrom { wellKnown, zanoAlias, zcashAddress, - bip353 + bip353, + lnurlpay, } class ParsedAddress { @@ -184,6 +185,14 @@ class ParsedAddress { ); } + factory ParsedAddress.fetchLNUrlPayAddress({required String address, required String name}) { + return ParsedAddress( + addresses: [address], + name: name, + parseFrom: ParseFrom.lnurlpay, + ); + } + final List addresses; final String name; final String description; diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index a33c5a7e8..5abda9da8 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -26,6 +26,7 @@ class PreferencesKey { static const currentZcashNodeIdKey = 'current_node_id_zec'; static const currentTransactionPriorityKeyLegacy = 'current_fee_priority'; static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; + static const displayAmountsInSatoshi = 'display_amounts_in_satoshi'; static const shouldSaveRecipientAddressKey = 'save_recipient_address'; static const isAppSecureKey = 'is_app_secure'; static const disableTradeOption = 'disable_buy'; @@ -62,6 +63,7 @@ class PreferencesKey { static const zanoTransactionPriority = 'current_fee_priority_zano'; static const wowneroTransactionPriority = 'current_fee_priority_wownero'; static const decredTransactionPriority = 'current_fee_priority_decred'; + static const zcashTransactionPriority = 'current_fee_priority_decred'; static const customBitcoinFeeRate = 'custom_electrum_fee_rate'; static const silentPaymentsCardDisplay = 'silentPaymentsCardDisplay'; static const mwebCardDisplay = 'mwebCardDisplay'; @@ -105,6 +107,8 @@ class PreferencesKey { static const showPayjoinCard = 'show_payjoin_card'; static const showCameraConsent = 'show_camera_consent'; static const showDecredInfoCard = 'show_decred_info_card'; + static const forceDecentralizedExchanges = 'force_decentralized_exchanges'; + static const decentralizedExchangesPromptDismissed = 'decentralized_exchanges_prompt_dismissed'; static String moneroWalletUpdateV1Key(String name) => '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}'; @@ -131,4 +135,5 @@ class PreferencesKey { static const enableAutomaticNodeSwitching = 'enable_automatic_node_switching'; static const syncStatusDisplayMode = 'sync_status_display_mode'; static const backgroundImage = 'background_image'; + static const mwebAdDismissed = "mweb_ad_dismissed"; } diff --git a/lib/entities/wallet_contact.dart b/lib/entities/wallet_contact.dart index 641dedb80..114a047a2 100644 --- a/lib/entities/wallet_contact.dart +++ b/lib/entities/wallet_contact.dart @@ -17,4 +17,8 @@ class WalletContact implements ContactBase { /// Wallet type of the wallet this contact belongs to /// Used for EVM chain filtering final WalletType? walletType; + + String get displayName => ""; + + set displayName(String _) {} } diff --git a/lib/evm/cw_evm.dart b/lib/evm/cw_evm.dart index 705013779..723310c54 100644 --- a/lib/evm/cw_evm.dart +++ b/lib/evm/cw_evm.dart @@ -238,6 +238,13 @@ class CWEVM extends EVM { ) => (wallet as EVMChainWallet).isApprovalRequired(tokenContract, spender, requiredAmount); + @override + Future getAllowance( + WalletBase wallet, + String tokenContract, + String spender) => + (wallet as EVMChainWallet).getAllowance(tokenContract, spender); + @override Future createTokenApproval( WalletBase wallet, @@ -265,14 +272,18 @@ class CWEVM extends EVM { String to, String dataHex, BigInt valueWei, - TransactionPriority? priority, { + TransactionPriority? priority,{ bool useBlinkProtection = true, + String? sourceTokenAddress, + BigInt? sourceTokenAmount, }) => (wallet as EVMChainWallet).createCallDataTransaction( to, dataHex, valueWei, priority as EVMChainTransactionPriority?, + sourceTokenAddress, + sourceTokenAmount, useBlinkProtection: useBlinkProtection, ); @@ -468,6 +479,14 @@ class CWEVM extends EVM { return _registry.getWalletTypeByChainId(chainId); } + @override + BigInt? getERC20AvailableBalance(Object balance) { + if(balance is EVMChainERC20Balance) { + return balance.balance; + } + return null; + } + @override List getAllChains() { final allChains = _registry.getAllChains(); diff --git a/lib/exchange/exchange_provider_description.dart b/lib/exchange/exchange_provider_description.dart index 14d2b5e28..3e71d79f9 100644 --- a/lib/exchange/exchange_provider_description.dart +++ b/lib/exchange/exchange_provider_description.dart @@ -2,46 +2,47 @@ import 'package:cw_core/enumerable_item.dart'; class ExchangeProviderDescription extends EnumerableItem with Serializable { const ExchangeProviderDescription( - {required String title, required int raw, required this.image, this.horizontalLogo = false}) + {required String title, required int raw, required this.image, this.horizontalLogo = false, required this.isCentralized}) : super(title: title, raw: raw); final bool horizontalLogo; final String image; + final bool isCentralized; static const xmrto = - ExchangeProviderDescription(title: 'XMR.TO', raw: 0, image: 'assets/images/xmrto.png'); + ExchangeProviderDescription(title: 'XMR.TO', raw: 0, image: 'assets/new-ui/trade_providers/xmrto.svg', isCentralized: true); static const changeNow = - ExchangeProviderDescription(title: 'ChangeNOW', raw: 1, image: 'assets/images/changenow.png'); + ExchangeProviderDescription(title: 'ChangeNOW', raw: 1, image: 'assets/new-ui/trade_providers/changenow.svg', isCentralized: true); static const morphToken = - ExchangeProviderDescription(title: 'MorphToken', raw: 2, image: 'assets/images/morph.png'); + ExchangeProviderDescription(title: 'MorphToken', raw: 2, image: 'assets/new-ui/trade_providers/morph.svg', isCentralized: true); static const sideShift = - ExchangeProviderDescription(title: 'SideShift', raw: 3, image: 'assets/images/sideshift.png'); + ExchangeProviderDescription(title: 'SideShift', raw: 3, image: 'assets/new-ui/trade_providers/sideshift.svg', isCentralized: true); static const simpleSwap = - ExchangeProviderDescription(title: 'SimpleSwap', raw: 4, image: 'assets/images/simpleSwap.png'); + ExchangeProviderDescription(title: 'SimpleSwap', raw: 4, image: 'assets/new-ui/trade_providers/simpleswap.svg', isCentralized: true); static const trocador = - ExchangeProviderDescription(title: 'Trocador', raw: 5, image: 'assets/images/trocador.png'); + ExchangeProviderDescription(title: 'Trocador', raw: 5, image: 'assets/new-ui/trade_providers/trocador.svg', isCentralized: true); static const exolix = - ExchangeProviderDescription(title: 'Exolix', raw: 6, image: 'assets/images/exolix.png'); + ExchangeProviderDescription(title: 'Exolix', raw: 6, image: 'assets/new-ui/trade_providers/exolix.svg', isCentralized: true); static const all = - ExchangeProviderDescription(title: 'All trades', raw: 7, image: ''); + ExchangeProviderDescription(title: 'All trades', raw: 7, image: '', isCentralized: true); static const thorChain = - ExchangeProviderDescription(title: 'ThorChain', raw: 8, image: 'assets/images/thorchain.png'); + ExchangeProviderDescription(title: 'ThorChain', raw: 8, image: 'assets/new-ui/trade_providers/thorchain.svg', isCentralized: true); static const swapTrade = - ExchangeProviderDescription(title: 'SwapTrade', raw: 9, image: 'assets/images/swap_trade.png'); + ExchangeProviderDescription(title: 'SwapTrade', raw: 9, image: 'assets/new-ui/trade_providers/swaptrade.svg', isCentralized: true); static const letsExchange = - ExchangeProviderDescription(title: 'LetsExchange', raw: 10, image: 'assets/images/letsexchange_icon.svg'); + ExchangeProviderDescription(title: 'LetsExchange', raw: 10, image: 'assets/new-ui/trade_providers/letsexchange.svg', isCentralized: true); static const stealthEx = - ExchangeProviderDescription(title: 'StealthEx', raw: 11, image: 'assets/images/stealthex.png'); + ExchangeProviderDescription(title: 'StealthEx', raw: 11, image: 'assets/new-ui/trade_providers/stealthex.svg', isCentralized: true); static const chainflip = - ExchangeProviderDescription(title: 'Chainflip', raw: 12, image: 'assets/images/chainflip.png'); + ExchangeProviderDescription(title: 'Chainflip', raw: 12, image: 'assets/new-ui/trade_providers/chainflip.svg', isCentralized: false); static const xoSwap = - ExchangeProviderDescription(title: 'XOSwap', raw: 13, image: 'assets/images/xoswap.svg'); + ExchangeProviderDescription(title: 'XOSwap', raw: 13, image: 'assets/new-ui/trade_providers/xoswap.svg', isCentralized: true); static const swapsXyz = - ExchangeProviderDescription(title: 'Swaps.XYZ', raw: 14, image: 'assets/images/swaps_xyz.svg'); + ExchangeProviderDescription(title: 'Swaps.XYZ', raw: 14, image: 'assets/new-ui/trade_providers/swaps_xyz.svg', isCentralized: false); static const nearIntents = - ExchangeProviderDescription(title: 'Near Intents', raw: 15, image: 'assets/images/near.png'); + ExchangeProviderDescription(title: 'Near Intents', raw: 15, image: 'assets/new-ui/trade_providers/near-intents.svg', isCentralized: false); static const jupiter = - ExchangeProviderDescription(title: 'Jupiter', raw: 16, image: 'assets/images/jupiter.png'); + ExchangeProviderDescription(title: 'Jupiter', raw: 16, image: 'assets/new-ui/trade_providers/jupiter.svg', isCentralized: false); static ExchangeProviderDescription deserialize({required int raw}) { switch (raw) { diff --git a/lib/exchange/provider/exolix_exchange_provider.dart b/lib/exchange/provider/exolix_exchange_provider.dart index b7e8df135..4c36f40a3 100644 --- a/lib/exchange/provider/exolix_exchange_provider.dart +++ b/lib/exchange/provider/exolix_exchange_provider.dart @@ -8,7 +8,6 @@ import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/exchange/trade_not_found_exception.dart'; import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; -import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -55,11 +54,11 @@ class ExolixExchangeProvider extends ExchangeProvider { if (isFixedRateMode) { params['coinFrom'] = _normalizeCurrency(to); - params['coinTo'] = _normalizeCurrency(from); + params['coinTo'] = _normalizeCurrency(_overrideFromCryptoCurrency(from)); params['networkFrom'] = _networkFor(to); params['networkTo'] = _networkFor(from); } else { - params['coinFrom'] = _normalizeCurrency(from); + params['coinFrom'] = _normalizeCurrency(_overrideFromCryptoCurrency(from)); params['coinTo'] = _normalizeCurrency(to); params['networkFrom'] = _networkFor(from); params['networkTo'] = _networkFor(to); @@ -103,7 +102,7 @@ class ExolixExchangeProvider extends ExchangeProvider { if (amount == 0) return 0.0; final params = { - 'coinFrom': _normalizeCurrency(from), + 'coinFrom': _normalizeCurrency(_overrideFromCryptoCurrency(from)), 'coinTo': _normalizeCurrency(to), 'networkFrom': _networkFor(from), 'networkTo': _networkFor(to), @@ -180,6 +179,7 @@ class ExolixExchangeProvider extends ExchangeProvider { }, ); printV(e.toString()); + printV(s.toString()); return 0.0; } } @@ -192,8 +192,8 @@ class ExolixExchangeProvider extends ExchangeProvider { }) async { final headers = {'Content-Type': 'application/json'}; final body = { - 'coinFrom': _normalizeCurrency(request.fromCurrency), - 'coinTo': _normalizeCurrency(request.toCurrency), + 'coinFrom': _normalizeCurrency(_overrideFromCryptoCurrency(request.fromCurrency)), + 'coinTo': _normalizeCurrency(_overrideToCryptoCurrency(request.toCurrency, request.toAddress)), 'networkFrom': _networkFor(request.fromCurrency), 'networkTo': _networkFor(request.toCurrency), 'withdrawalAddress': _normalizeAddress(request.toAddress), @@ -217,7 +217,7 @@ class ExolixExchangeProvider extends ExchangeProvider { if (response.statusCode == 400) { final responseJSON = json.decode(response.body) as Map; - final errors = responseJSON['errors'] as Map; + final errors = responseJSON['error'] as Map; final errorMessage = errors.values.join(', '); ExchangeProviderLogger.logError( @@ -394,6 +394,8 @@ class ExolixExchangeProvider extends ExchangeProvider { switch (currency) { case CryptoCurrency.arb: return 'ARBITRUM'; + case CryptoCurrency.btcln: + return 'LIGHTNING'; default: return currency.tag != null ? _normalizeTag(currency.tag!) : currency.title; } @@ -406,6 +408,18 @@ class ExolixExchangeProvider extends ExchangeProvider { }; } + CryptoCurrency _overrideFromCryptoCurrency(CryptoCurrency currency) { + if (currency == CryptoCurrency.zec) + return CryptoCurrency.zaddr; // Sending is always shielded zcash + return currency; + } + + CryptoCurrency _overrideToCryptoCurrency(CryptoCurrency currency, String address) { + if (RegExp(r'u1[a-zA-Z0-9]{100,300}').hasMatch(address) && currency == CryptoCurrency.zec) + return CryptoCurrency.zaddr; // If the user pastes a unified address use shielded zcash + return currency; + } + String _normalizeCurrency(CryptoCurrency currency) { switch (currency) { case CryptoCurrency.nano: @@ -414,6 +428,8 @@ class ExolixExchangeProvider extends ExchangeProvider { return 'BTT'; case CryptoCurrency.zec: return 'ZEC'; + case CryptoCurrency.zaddr: + return 'ZEC-SHIELDED'; default: return currency.title; } diff --git a/lib/exchange/provider/swapsxyz_exchange_provider.dart b/lib/exchange/provider/swapsxyz_exchange_provider.dart index b93b23621..5f933946e 100644 --- a/lib/exchange/provider/swapsxyz_exchange_provider.dart +++ b/lib/exchange/provider/swapsxyz_exchange_provider.dart @@ -17,6 +17,16 @@ import 'package:cw_core/crypto_currency.dart'; class SwapsXyzExchangeProvider extends ExchangeProvider { SwapsXyzExchangeProvider(); + static final List _notSupportedAsSourceToken = [ + CryptoCurrency.sol, + ...CryptoCurrency.all.where( + (c) => (c.tag ?? '').toUpperCase() == 'SOL', + ), + ]; + + static const _transferSig = '0xa9059cbb'; + static const _swapAndExecuteSig = '0x9be111d1'; + static final _apiKey = secrets.swapsXyzApiKey; static const _baseUrl = 'api-v2.swaps.xyz'; static const _getChainList = 'api/getChainList'; @@ -40,8 +50,10 @@ class SwapsXyzExchangeProvider extends ExchangeProvider { @override bool get isEnabled => true; + // There is an issue with fixed-rate swaps on Swaps.XYZ + // when they don't guarantee that the trade will be created at the requested amount. @override - bool get supportsFixedRate => true; + bool get supportsFixedRate => false; @override ExchangeProviderDescription get description => @@ -60,22 +72,23 @@ class SwapsXyzExchangeProvider extends ExchangeProvider { final chains = await _geSupportedChain(); if (chains.isEmpty) throw Exception('Failed to fetch supported chains'); - final srcChain = _findChainByCurrency(from, chains); - final dstChain = _findChainByCurrency(to, chains); + final fromToUse = isFixedRateMode ? to : from; + final toToUse = isFixedRateMode ? from : to; + + final srcChain = _findChainByCurrency(fromToUse, chains); + final dstChain = _findChainByCurrency(toToUse, chains); await _ensureTokensCached( - fromChain: srcChain, toChain: dstChain, from: from, to: to); + fromChain: srcChain, toChain: dstChain, from: fromToUse, to: toToUse); - final srcToken = _getTokenAddress(currency: from, chain: srcChain); - final dstToken = _getTokenAddress(currency: to, chain: dstChain); + final srcToken = _getTokenAddress(currency: fromToUse, chain: srcChain); + final dstToken = _getTokenAddress(currency: toToUse, chain: dstChain); - // parameters to and from are swapped in this endpoint - // i.e. to get limits for FROM -> TO, we request paths for TO -> FROM final params = { - 'srcChainId': '${dstChain.chainId}', - 'srcToken': dstToken, - 'dstChainId': '${srcChain.chainId}', - 'dstToken': srcToken, + 'srcChainId': '${srcChain.chainId}', + 'srcToken': srcToken, + 'dstChainId': '${dstChain.chainId}', + 'dstToken': dstToken, }; final uri = Uri.https(_baseUrl, _getPaths, params); @@ -86,37 +99,75 @@ class SwapsXyzExchangeProvider extends ExchangeProvider { final body = json.decode(res.body) as Map; - final paths = (body['paths'] as List?) ?? const []; + final paths = + (body['paths'] as List? ?? const []).cast>(); if (paths.isEmpty) { - throw Exception('No paths for ${from.title} -> ${to.title}'); + throw Exception('No paths for ${fromToUse.title} -> ${toToUse.title}'); } - final path0 = paths.first as Map; - final tokens = path0['tokens']; + final int requestedDstId = dstChain.chainId; - // If tokens == "all", use path-level amountLimits - if (tokens is String && tokens == 'all') { - final amountLimits = path0['amountLimits'] as Map?; + Map path = paths.firstWhere( + (p) => p['chainId'] == requestedDstId, + orElse: () => {}, + ); - final min = - double.tryParse(amountLimits?['minAmount']?.toString() ?? ''); - final max = - double.tryParse(amountLimits?['maxAmount']?.toString() ?? ''); - - return Limits(min: min, max: max); + if (path.isEmpty) { + path = paths.firstWhere( + (p) => (p['tokens'] is List) || p['amountLimits'] != null, + orElse: () => paths.first, + ); } - // Otherwise tokens is a list -> find the specific token entry - final tokenList = (tokens as List?) ?? const []; - final token = findTokenBySymbol(title: from.title, tokens: tokenList); + final supportsExactAmountIn = + path['supportsExactAmountIn'] as bool? ?? false; + final supportsExactAmountOut = + path['supportsExactAmountOut'] as bool? ?? false; - if (token == null) { - throw Exception('Token info not found for ${from.title}'); + if (isFixedRateMode && !supportsExactAmountOut) { + throw Exception( + 'This route does not support fixed receive (exact-amount-out)'); + } + if (!isFixedRateMode && !supportsExactAmountIn) { + throw Exception( + 'This route does not support exact send (exact-amount-in)'); } - final min = double.tryParse(token['minAmount']?.toString() ?? ''); - final max = double.tryParse(token['maxAmount']?.toString() ?? ''); + Map? useLimits; + if (isFixedRateMode) { + final tokensField = path['tokens']; + useLimits = null; + + if (tokensField is List && tokensField.isNotEmpty) { + final tokens = tokensField.cast>(); + String norm(String s) => s.toUpperCase(); + final wantSym = norm(_normalizeCakeNativeTokenName(toToUse.title)); + final wantAddr = (dstToken).toLowerCase(); + + final match = tokens.firstWhere( + (t) { + final sym = norm(t['symbol']?.toString() ?? ''); + final addr = (t['address']?.toString() ?? '').toLowerCase(); + return sym == wantSym || (addr.isNotEmpty && addr == wantAddr); + }, + orElse: () => const {}, + ); + + if (match.isNotEmpty) { + useLimits = { + 'minAmount': match['minAmount'], + 'maxAmount': match['maxAmount'], + }; + } + } + } else { + // Floating/Exact-in: use the route-level limits + useLimits = path['amountLimits'] as Map?; + } + + final min = double.tryParse((useLimits?['minAmount'])?.toString() ?? ''); + final max = double.tryParse((useLimits?['maxAmount'])?.toString() ?? ''); return Limits(min: min, max: max); } catch (e) { printV('fetchLimits error: $e'); @@ -157,6 +208,15 @@ class SwapsXyzExchangeProvider extends ExchangeProvider { required bool isFixedRateMode, required bool isReceiveAmount}) async { try { + + if(_notSupportedAsSourceToken.contains(from)) { + printV( + 'fetchRate: source token ${from.title} is not supported as source token'); + return 0.0; + } + + + final chains = await _geSupportedChain(); if (chains.isEmpty) return 0.0; @@ -248,20 +308,6 @@ class SwapsXyzExchangeProvider extends ExchangeProvider { final dstToken = _getTokenAddress(currency: request.toCurrency, chain: dstChain); - // Optional: ensure path supports exact-out before attempting fixed rate. - if (isFixedRateMode) { - final path = await _pickPath( - srcChainId: srcChain.chainId, - srcToken: srcToken, - dstChainId: dstChain.chainId, - dstToken: dstToken, - ); - if (path == null || !path.supportsExactOut) { - throw Exception( - 'This route does not support fixed receive (exact-amount-out)'); - } - } - final amountStr = isFixedRateMode ? request.toAmount : request.fromAmount; final rawAmount = double.tryParse(amountStr) ?? 0.0; if (rawAmount <= 0) throw Exception('Invalid amount'); @@ -297,12 +343,28 @@ class SwapsXyzExchangeProvider extends ExchangeProvider { final data = json.decode(res.body) as Map; final txId = data['txId'] as String? ?? ''; + final vmId = data['vmId'] as String? ?? ''; final txObj = (data['tx'] as Map?) ?? const {}; final txTo = txObj['to']?.toString(); final chainId = txObj['chainId']?.toString(); final routerData = txObj['data']?.toString(); + + // Allow only: + // - null (native / deposit-address flow) + // - '0x' (no call data) + // - ERC20 transfer(0xa9059cbb) selector + // - swapAndExecute(0x9be111d1) selector + final isAllowed = routerData == null || + routerData == '0x' || + _decodeMethodSelector(routerData) == _transferSig || + _decodeMethodSelector(routerData) == _swapAndExecuteSig; + + if (!isAllowed) { + throw Exception('Does not support that method selector'); + } + final txValue = txObj['value']?.toString() ?? '0'; final bridgeIds = (data['bridgeIds'] as List?) ?? const []; @@ -537,57 +599,36 @@ class SwapsXyzExchangeProvider extends ExchangeProvider { } } - // Ensure tokens for this pair are cached (fills both src & dst chain entries) - // Replace your _ensureTokensCached with this one Future _ensureTokensCached({ required Chain fromChain, required Chain toChain, required CryptoCurrency from, required CryptoCurrency to, }) async { - bool needSrc = !_tokensCache.containsKey(fromChain.chainId) || + final needSrc = !_tokensCache.containsKey(fromChain.chainId) || (_tokensCache[fromChain.chainId]?.isEmpty ?? true); - bool needDst = !_tokensCache.containsKey(toChain.chainId) || + + final needDst = !_tokensCache.containsKey(toChain.chainId) || (_tokensCache[toChain.chainId]?.isEmpty ?? true); if (!needSrc && !needDst) return; - // First try: from -> to - await _fetchAndCacheTokens( - srcChainId: fromChain.chainId, - srcToken: from.title.toUpperCase(), - dstChainId: toChain.chainId, - dstToken: to.title.toUpperCase(), - ); - - // Re-check after first call - needSrc = !_tokensCache.containsKey(fromChain.chainId) || - (_tokensCache[fromChain.chainId]?.isEmpty ?? true); - needDst = !_tokensCache.containsKey(toChain.chainId) || - (_tokensCache[toChain.chainId]?.isEmpty ?? true); - - // If any still missing, try swapped: to -> from - if (needSrc || needDst) { - await _fetchAndCacheTokens( - srcChainId: toChain.chainId, - srcToken: to.title.toUpperCase(), - dstChainId: fromChain.chainId, - dstToken: from.title.toUpperCase(), - ); + if (needSrc) { + await _fetchAndCacheTokens(srcChainId: fromChain.chainId); + } + if (needDst) { + await _fetchAndCacheTokens(srcChainId: toChain.chainId); } } // call getPaths and merge tokens into cache keyed by the chainId Future _fetchAndCacheTokens({ required int srcChainId, - required String srcToken, - required int dstChainId, - required String dstToken, }) async { final params = { 'srcChainId': '$srcChainId', - 'srcToken': srcToken, - 'dstChainId': '$dstChainId', + 'srcToken': '0x0000000000000000000000000000000000000000', + // Native placeholder }; final uri = Uri.https(_baseUrl, _getPaths, params); @@ -597,21 +638,71 @@ class SwapsXyzExchangeProvider extends ExchangeProvider { return; } - final body = json.decode(res.body) as Map; + Map body; + try { + body = json.decode(res.body) as Map; + } catch (e) { + printV('getPaths JSON decode error: $e'); + return; + } + + // Always cache the source chain's native token (from body['srcToken']) + final srcTokenJson = body['srcToken'] as Map?; + if (srcTokenJson != null) { + final symbol = (srcTokenJson['symbol'] as String? ?? '').toUpperCase(); + if (symbol.isNotEmpty) { + final isNative = srcTokenJson['isNative'] == true; + final decimals = (srcTokenJson['decimals'] as num?)?.toInt(); + // Treat native token as address = null so _getTokenAddress() emits zero-address + final addr = isNative ? null : (srcTokenJson['address'] as String?); + _mergeCache(srcChainId, [ + TokenPathInfo( + symbol: symbol, + address: addr, + decimals: decimals, + minAmount: srcTokenJson['minAmount']?.toString(), + maxAmount: srcTokenJson['maxAmount']?.toString(), + ), + ]); + } + } + final paths = (body['paths'] as List?) ?? const []; if (paths.isEmpty) return; - final path0 = paths.first as Map; - final pathChainId = (path0['chainId'] as num?)?.toInt(); - final tokens = (path0['tokens'] as List?) ?? const []; + for (final path in paths) { + final map = path as Map; + final pathChainId = (map['chainId'] as num?)?.toInt(); + if (pathChainId == null) continue; - if (pathChainId == null || tokens.isEmpty) return; + final tokensField = map['tokens']; - final parsed = tokens - .map((t) => TokenPathInfo.fromJson(t as Map)) - .toList(); + // Case 1: String "all" -> cache empty list to indicate all tokens supported + if (tokensField is String) { + if (tokensField.toLowerCase() == 'all') { + _tokensCache[pathChainId] = + _tokensCache[pathChainId] ?? []; + } + continue; + } - _mergeCache(pathChainId, parsed); + // Case 2: List -> parse and merge + if (tokensField is List) { + final parsed = []; + for (final token in tokensField) { + if (token is Map) { + try { + parsed.add(TokenPathInfo.fromJson(token)); + } catch (e) { + printV('Token parse error on chain $pathChainId: $e : $token'); + } + } + } + if (parsed.isNotEmpty) { + _mergeCache(pathChainId, parsed); + } + } + } } // Merge by symbol, prefer entries that have a non-empty address/decimals @@ -639,52 +730,19 @@ class SwapsXyzExchangeProvider extends ExchangeProvider { _tokensCache[chainId] = bySymbol.values.toList(); } - String _nativeSymbolForChain(Chain chain) { - final network = chain.name.toUpperCase(); - switch (network) { - case 'BTC': - case 'BITCOIN': - return 'BTC'; - case 'XMR': - case 'MONERO': - return 'XMR'; - case 'TRX': - case 'TRON': - return 'TRX'; - case 'ETH': - case 'ETHEREUM': - return 'ETH'; - case 'LTC': - case 'LITECOIN': - return 'LTC'; - case 'DOGE': - case 'DOGECOIN': - return 'DOGE'; - case 'XRP': - case 'RIPPLE': - return 'XRP'; - case 'XLM': - case 'STELLAR': - return 'XLM'; - case 'SOL': - case 'SOLANA': - return 'SOL'; - case 'CARDANO': - return 'ADA'; - case 'Bitcoin Cash': - return 'BCH'; - case 'POLYGON': - return 'POL'; - default: - return network; - } + String _normalizeCakeNativeTokenName(String title) { + final name = title.toUpperCase(); + return switch (name) { + 'ZZEC' => 'ZEC', + _ => name, + }; } String _getTokenAddress({ required CryptoCurrency currency, required Chain chain, }) { - final symbol = currency.title.toUpperCase(); + final symbol = _normalizeCakeNativeTokenName(currency.title); final list = _tokensCache[chain.chainId]; // Try cache hit @@ -692,31 +750,17 @@ class SwapsXyzExchangeProvider extends ExchangeProvider { for (final t in list) { if (t.symbol == symbol && t.address != null && t.address!.isNotEmpty) { return t.address!; + } else if (t.symbol == symbol && (t.address == null)) { + // Native token on this chain + return '0x0000000000000000000000000000000000000000'; } } } - // If it's the native coin for this chain, return native placeholder - final nativeSym = _nativeSymbolForChain(chain); - if (nativeSym == symbol) { - return _nativePlaceholderForVm(chain.vmId); - } - // May fail for non-native Alt-VM assets return symbol; } - String _nativePlaceholderForVm(String vmId) { - switch (vmId.toLowerCase()) { - case 'evm': - return '0x0000000000000000000000000000000000000000'; - case 'alt-vm': - return '0x0000000000000000000000000000000000000000'; - default: - return '0x0000000000000000000000000000000000000000'; - } - } - Map? findTokenBySymbol( {required String title, required List tokens}) { final reqSymbol = title.toUpperCase(); @@ -731,7 +775,9 @@ class SwapsXyzExchangeProvider extends ExchangeProvider { Chain _findChainByCurrency(CryptoCurrency cur, List chains) { final network = _normalizeCakeNetwork(cur.tag ?? cur.title); return chains.firstWhere( - (c) => c.name.toUpperCase() == network, + (c) { + return c.name.toUpperCase() == network; + }, orElse: () => throw Exception('Unsupported chain for ${cur.title}'), ); } @@ -768,6 +814,9 @@ class SwapsXyzExchangeProvider extends ExchangeProvider { final s = str ?? '0'; return s.endsWith('n') ? s.substring(0, s.length - 1) : s; } + + String _decodeMethodSelector(String s) => + (s.startsWith('0x') && s.length >= 10) ? s.substring(0, 10) : ''; } class TokenPathInfo { diff --git a/lib/exchange/provider/swaptrade_exchange_provider.dart b/lib/exchange/provider/swaptrade_exchange_provider.dart index ff37165a0..65f296812 100644 --- a/lib/exchange/provider/swaptrade_exchange_provider.dart +++ b/lib/exchange/provider/swaptrade_exchange_provider.dart @@ -91,6 +91,7 @@ class SwapTradeExchangeProvider extends ExchangeProvider { }) async { try { if (amount == 0) return 0.0; + if(from == CryptoCurrency.btcln || to == CryptoCurrency.btcln) return 0; final params = {}; final body = { diff --git a/lib/exchange/provider/xoswap_exchange_provider.dart b/lib/exchange/provider/xoswap_exchange_provider.dart index 80559a7cb..7e7eeebed 100644 --- a/lib/exchange/provider/xoswap_exchange_provider.dart +++ b/lib/exchange/provider/xoswap_exchange_provider.dart @@ -358,7 +358,13 @@ class XOSwapExchangeProvider extends ExchangeProvider { final payoutAddress = responseJSON['toAddress'] as String; final depositAddress = responseJSON['payInAddress'] as String; final refundAddress = responseJSON['fromAddress'] as String; - final depositAmount = _toDouble(amount['value']); + final depositAmountStr = amount['value'].toString(); + final parsedAmount = double.tryParse(depositAmountStr); + + if (parsedAmount == null || parsedAmount <= 0) { + throw Exception('Invalid deposit amount received from API'); + } + final receiveAmount = toAmount['value'] as String; final status = responseJSON['status'] as String; final createdAtString = responseJSON['createdAt'] as String; @@ -386,7 +392,7 @@ class XOSwapExchangeProvider extends ExchangeProvider { 'depositAddress': depositAddress, 'payoutAddress': payoutAddress, 'refundAddress': refundAddress, - 'depositAmount': depositAmount, + 'depositAmount': depositAmountStr, 'receiveAmount': receiveAmount, 'status': status, 'createdAt': createdAtString, @@ -405,7 +411,7 @@ class XOSwapExchangeProvider extends ExchangeProvider { refundAddress: refundAddress, state: TradeState.deserialize(raw: status), createdAt: createdAt, - amount: depositAmount.toString(), + amount: depositAmountStr, receiveAmount: receiveAmount.toString(), payoutAddress: payoutAddress, extraId: extraId, @@ -499,7 +505,13 @@ class XOSwapExchangeProvider extends ExchangeProvider { final amount = responseJSON['amount'] as Map; final toAmount = responseJSON['toAmount'] as Map; final orderId = responseJSON['id'] as String; - final depositAmount = amount['value'] as String; + final depositAmountStr = amount['value'].toString(); + final parsedAmount = double.tryParse(depositAmountStr); + + if (parsedAmount == null || parsedAmount <= 0) { + throw Exception('Invalid deposit amount received from API'); + } + final receiveAmount = toAmount['value'] as String; final depositAddress = responseJSON['payInAddress'] as String; final payoutAddress = responseJSON['toAddress'] as String; @@ -526,7 +538,7 @@ class XOSwapExchangeProvider extends ExchangeProvider { refundAddress: refundAddress, state: TradeState.deserialize(raw: status), createdAt: createdAt, - amount: depositAmount, + amount: depositAmountStr, receiveAmount: receiveAmount, payoutAddress: payoutAddress, extraId: extraId, @@ -554,14 +566,4 @@ class XOSwapExchangeProvider extends ExchangeProvider { } return null; } - - double _toDouble(dynamic value) { - if (value is int) { - return value.toDouble(); - } else if (value is double) { - return value; - } else { - return 0.0; - } - } } diff --git a/lib/exchange/trade.dart b/lib/exchange/trade.dart index 4147eddb0..2dde57c8b 100644 --- a/lib/exchange/trade.dart +++ b/lib/exchange/trade.dart @@ -1,9 +1,9 @@ -import 'package:cake_wallet/entities/generate_name.dart'; import 'package:cake_wallet/evm/evm.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/generate_name.dart'; import 'package:cw_core/hive_type_ids.dart'; import 'package:hive/hive.dart'; diff --git a/lib/main.dart b/lib/main.dart index dd10adcd5..77e92e63a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,17 +1,19 @@ import 'dart:async'; import 'dart:io'; import 'dart:ui'; + import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/app_scroll_behavior.dart'; -import 'package:cake_wallet/order/order.dart'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/background_sync.dart'; import 'package:cake_wallet/core/node_switching_service.dart'; +import 'package:cake_wallet/core/reset_service.dart'; +import 'package:cake_wallet/core/secure_storage.dart'; +import 'package:cake_wallet/core/trade_monitor.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/default_settings_migration.dart'; import 'package:cake_wallet/entities/get_encryption_key.dart'; -import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/entities/haven_seed_store.dart'; import 'package:cake_wallet/entities/language_service.dart'; import 'package:cake_wallet/entities/template.dart'; @@ -20,39 +22,43 @@ import 'package:cake_wallet/exchange/exchange_template.dart'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/locales/locale.dart'; +import 'package:cake_wallet/order/order.dart'; import 'package:cake_wallet/reactions/bootstrap.dart'; import 'package:cake_wallet/router.dart' as Router; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/root/root.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/authentication_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/test_asset_bundles.dart'; import 'package:cake_wallet/themes/utils/theme_provider.dart'; -import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; import 'package:cake_wallet/utils/feature_flag.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/zcash/zcash.dart'; import 'package:cw_core/address_info.dart'; import 'package:cw_core/cake_hive.dart'; +import 'package:cw_core/db/sqlite.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/hive_type_ids.dart'; import 'package:cw_core/key.dart'; import 'package:cw_core/mweb_utxo.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/payjoin_session.dart'; +import 'package:cw_core/root_dir.dart'; import 'package:cw_core/spl_token.dart'; import 'package:cw_core/tron_token.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/utils/proxy_logger/memory_proxy_logger.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; -import 'package:cw_core/db/sqlite.dart'; -import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/utils/tor/abstract.dart'; +import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:cw_core/window_size.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -61,11 +67,8 @@ import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:hive/hive.dart'; import 'package:cw_core/root_dir.dart'; import 'package:quick_actions/quick_actions.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:cw_core/window_size.dart'; import 'package:logging/logging.dart'; -import 'package:cake_wallet/core/trade_monitor.dart'; -import 'package:cake_wallet/core/reset_service.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:trezor_connect/trezor_connect.dart'; final navigatorKey = GlobalKey(); @@ -321,7 +324,7 @@ Future initializeAppConfigs({bool loadWallet = true}) async { payjoinSessionSource: payjoinSessionSource, anonpayInvoiceInfo: anonpayInvoiceInfo, havenSeedStore: havenSeedStore, - initialMigrationVersion: 57, + initialMigrationVersion: 60, ); } @@ -437,6 +440,9 @@ class AppState extends State with SingleTickerProviderStateMixin { navigatorObservers: [routeObserver, appRouteObserver], navigatorKey: navigatorKey, debugShowCheckedModeBanner: false, + builder: (context, child) => MediaQuery( + data: MediaQuery.of(context).copyWith(textScaler: TextScaler.noScaling), + child: child!), theme: theme, darkTheme: darkTheme, themeMode: themeMode, diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index 643eb304d..bb2f50824 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -48,6 +48,10 @@ class CWMoneroAccountList extends MoneroAccountList { final moneroWallet = wallet as MoneroWallet; moneroWallet.walletAddresses.accountList .setLabelAccount(accountIndex: accountIndex, label: label); + if (accountIndex == moneroWallet.walletAddresses.account?.id) { + moneroWallet.walletAddresses.account = monero_account.Account( + id: accountIndex, label: label, balance: moneroWallet.walletAddresses.account!.balance); + } } } @@ -383,7 +387,7 @@ class CWMonero extends Monero { Future getCurrentHeight() async { return monero_wallet_api.getCurrentHeight(); } - + @override bool importKeyImagesUR(Object wallet, String ur) { final moneroWallet = wallet as MoneroWallet; diff --git a/lib/new-ui/long_press_popup.dart b/lib/new-ui/long_press_popup.dart new file mode 100644 index 000000000..08ae861dd --- /dev/null +++ b/lib/new-ui/long_press_popup.dart @@ -0,0 +1,58 @@ +import 'dart:ui'; + +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:flutter/material.dart'; + +class LongPressPopupBuilder extends StatelessWidget { + const LongPressPopupBuilder( + {super.key, required this.child, required this.popup, this.spacing = 8}); + + final Widget child; + final Widget popup; + final double spacing; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onLongPress: () { + final RenderBox renderBox = context.findRenderObject() as RenderBox; + final offset = renderBox.localToGlobal(Offset.zero); + final size = renderBox.size; + + showPopUp( + context: context, + builder: (context) => _buildPopup(context, offset, size), + ); + }, + child: child, + ); + } + + Widget _buildPopup(BuildContext context, Offset offset, Size size) { + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), + child: Stack( + children: [ + Positioned( + left: offset.dx, + top: offset.dy, + width: size.width, + height: size.height, + child: Material( + color: Colors.transparent, + child: child, + ), + ), + Positioned( + left: offset.dx * 2, + top: offset.dy + size.height + spacing, + child: Material( + color: Colors.transparent, + child: popup, + ), + ), + ], + ), + ); + } +} diff --git a/lib/new-ui/modal_navigator.dart b/lib/new-ui/modal_navigator.dart new file mode 100644 index 000000000..400a54549 --- /dev/null +++ b/lib/new-ui/modal_navigator.dart @@ -0,0 +1,111 @@ +import 'package:cake_wallet/router.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:flutter/material.dart'; + +/// allows for full navigation with pages and routes inside a single modal sheet. +/// call this in your modal sheet's builder, passing whatever the modal's first page should be to rootPage. +/// afterwards you can call pushNamed to push other pages, they'll be created inside the modal. +/// you can use the back button (android) or the swipe back gesture (both android and iphone) to go back. +/// calling Navigator.of(context).pop() will pop the page INSIDE the modal sheet. +/// if you want to pop the whole sheet, use Navigator.of(context, rootNavigator: true).pop(). +class ModalNavigator extends StatefulWidget { + const ModalNavigator({super.key, required this.rootPage, required this.parentContext, this.heightMode = ModalHeightModes.fullScreen}); + + final Widget rootPage; + final BuildContext parentContext; + final ModalHeightModes heightMode; + + @override + State createState() => _ModalNavigatorState(); +} + +enum ModalHeightModes { + /// just render as big a modal as possible + fullScreen, + /// after first frame, read height and lock to that height + autoLock, + /// let the content size itself. jumps around when pushing different-sized pages! + natural +} + +class _ModalNavigatorState extends State { + final GlobalKey _sheetKey = GlobalKey(); + final GlobalKey _navigatorKey = GlobalKey(); + double? _sheetHeight; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + final context = _sheetKey.currentContext; + if (context == null) return; + if(_sheetHeight != null) return; + + final box = context.findRenderObject() as RenderBox; + setState(() { + _sheetHeight = box.size.height; + }); + }); + } + + @override + Widget build(BuildContext context) { + late final double? height; + switch(widget.heightMode) { + case ModalHeightModes.fullScreen: + height = MediaQuery.of(context).size.height; break; + case ModalHeightModes.autoLock: + height = _sheetHeight; break; + case ModalHeightModes.natural: + height = null; break; + } + return Container( + key:_sheetKey, + height: height, + child: Theme( + data: Theme.of(context).copyWith( + pageTransitionsTheme: PageTransitionsTheme( + builders: { + // requested by ui - iphone-style back anim on every platform + TargetPlatform.android: CupertinoPageTransitionsBuilder(), + TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), + TargetPlatform.linux: CupertinoPageTransitionsBuilder(), + TargetPlatform.macOS: CupertinoPageTransitionsBuilder(), + TargetPlatform.windows: CupertinoPageTransitionsBuilder(), + }, + ), + ), + child: PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) async { + if (didPop) return; + + final navigator = _navigatorKey.currentState; + if (navigator != null && await navigator.maybePop()) { + return; + } + + Navigator.of(widget.parentContext).pop(); + }, + child: Navigator( + key: _navigatorKey, + onGenerateRoute: (settings) { + + printV(settings.name); + + if (settings.name == "/") + return handleRouteWithPlatformAwareness((context) => PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) async { + Navigator.of(widget.parentContext).pop(); + }, + child: widget.rootPage), + fullscreenDialog: false); + else + return createRoute(settings); + }), + ), + ), + ); + } +} diff --git a/lib/new-ui/new_dashboard.dart b/lib/new-ui/new_dashboard.dart new file mode 100644 index 000000000..a285e9cac --- /dev/null +++ b/lib/new-ui/new_dashboard.dart @@ -0,0 +1,169 @@ +import 'dart:io'; + +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/preferences_key.dart'; +import 'package:cake_wallet/new-ui/pages/home_page.dart'; +import 'package:cake_wallet/new-ui/widgets/changelog_modal.dart'; +import 'package:cake_wallet/src/screens/contact/contact_list_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/cake_features_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/widgets/new_main_navbar_widget.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_listener_widget.dart'; +import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart'; +import 'package:cake_wallet/src/widgets/vulnerable_seeds_popup.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/utils/version_comparator.dart'; +import 'package:flutter/material.dart'; +import 'package:mobx/mobx.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../view_model/dashboard/dashboard_view_model.dart'; + +class NewDashboard extends StatefulWidget { + NewDashboard({ + super.key, + required this.dashboardViewModel, + required this.bottomSheetService, + }); + + final DashboardViewModel dashboardViewModel; + final BottomSheetService bottomSheetService; + + final List dashboardPageWidgets = [ + getIt.get(), + getIt.get(), + getIt.get(), + getIt.get(), + Placeholder(), + ]; + + @override + State createState() => _NewDashboardState(); +} + +class _NewDashboardState extends State { + int _selectedPage = 0; + + @override + void initState() { + super.initState(); + reaction((_) => widget.dashboardViewModel.appStore.wallet, (_) { + setState(() { + _selectedPage = 0; + }); + }); + + Future.delayed(Duration(milliseconds: 300)).then((_) => _showChangelog(context)); + _showVulnerableSeedsPopup(context); + } + + @override + Widget build(BuildContext context) { + return BottomSheetListener( + bottomSheetService: widget.bottomSheetService, + child: CupertinoScaffold( + body: Material( + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + // widget.dashboardPageWidgets[_selectedPage], + IndexedStack( + index: _selectedPage, + children: widget.dashboardPageWidgets, + ), + IgnorePointer( + child: Container( + height: 150, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Theme.of(context).colorScheme.surfaceDim.withAlpha(5), + Theme.of(context).colorScheme.surfaceDim.withAlpha(50), + Theme.of(context).colorScheme.surfaceDim.withAlpha(100), + Theme.of(context).colorScheme.surfaceDim.withAlpha(150), + Theme.of(context).colorScheme.surfaceDim.withAlpha(200), + Theme.of(context).colorScheme.surfaceDim.withAlpha(200), + ], + ), + ), + ), + ), + SafeArea( + bottom: !(Platform.isIOS), + child: SizedBox( + width: double.infinity, + height: NewMainNavBar.barHeight + NewMainNavBar.barBottomPadding, + child: AbsorbPointer( + absorbing: true, + child: Container(color: Colors.transparent), + ), + ), + ), + NewMainNavBar( + dashboardViewModel: widget.dashboardViewModel, + selectedIndex: _selectedPage, + onItemTap: (index) { + setState(() { + _selectedPage = index; + }); + }, + ) + ], + ), + ), + ), + ); + } + + void _showChangelog(BuildContext context) async { + final sharedPrefs = await SharedPreferences.getInstance(); + final currentAppVersion = VersionComparator.getExtendedVersionNumber( + widget.dashboardViewModel.settingsStore.appVersion); + final lastSeenAppVersion = sharedPrefs.getInt(PreferencesKey.lastSeenAppVersion); + final isNewInstall = sharedPrefs.getBool(PreferencesKey.isNewInstall) ?? true; + + if (currentAppVersion != lastSeenAppVersion && !isNewInstall) { + Future.delayed( + Duration(seconds: 1), + () { + if (context.mounted) { + showModalBottomSheet( + isScrollControlled: true, + context: context, + builder: (context) { + return ChangelogModal( + version: widget.dashboardViewModel.settingsStore.appVersion, + ); + }); + } + }, + ); + + sharedPrefs.setInt(PreferencesKey.lastSeenAppVersion, currentAppVersion); + } else if (isNewInstall) { + sharedPrefs.setInt(PreferencesKey.lastSeenAppVersion, currentAppVersion); + } + } + + void _showVulnerableSeedsPopup(BuildContext context) async { + final List affectedWalletNames = await widget.dashboardViewModel.checkAffectedWallets(); + + if (affectedWalletNames.isNotEmpty) { + Future.delayed( + Duration(seconds: 1), + () { + if (context.mounted) { + showPopUp( + context: context, + builder: (BuildContext context) { + return VulnerableSeedsPopup(affectedWalletNames); + }, + ); + } + }, + ); + } + } +} diff --git a/lib/new-ui/pages/about_page.dart b/lib/new-ui/pages/about_page.dart new file mode 100644 index 000000000..991b513c5 --- /dev/null +++ b/lib/new-ui/pages/about_page.dart @@ -0,0 +1,154 @@ +import 'dart:math'; + +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:url_launcher/url_launcher.dart'; + +// written by people who happened to read my slack message in 2025 +// whoever works on this codebase in the future, feel free to add your own mark here +const List aboutPageEasterEggs = [ + "Designed in (S)pain", + "Proudly managing over 🤷‍♂️ XMR", + "The cake is not a lie 🍰", + "I don’t play soccer because I enjoy the sport. I’m just doing it for kicks.", + "Markets in red? Big deal.\nWhat color is the grass outside?", + "Conquered Web3, now working on Web6-7", + "Proud owner of none of your funds\n(we are not impressed)", + "*writing down my seedphrase*\ncake cake cake cake cake cake cake ca...", + "Don't forget to actually use your crypto to pay for stuff in the real world 🙂", + "A chain of blocks? That's preposterous!", + "IOU a hug <3", + "Warning: up to 4.8% programmed by cats", + "We love collecting your data <3\nWe're just really incompetent at it" +]; + +class AboutPage extends StatefulWidget { + const AboutPage({super.key, required this.appVersion}); + + final String appVersion; + + @override + State createState() => _AboutPageState(); +} + +class _AboutPageState extends State { + static const int easterEggTreshold = 5; + String _bottomText = S.current.payment_made_easy; + int _easterEggCounter = 0; + + void _easterEgg() { + _easterEggCounter++; + if(_easterEggCounter == easterEggTreshold) { + setState(() { + _bottomText = aboutPageEasterEggs.elementAt(Random().nextInt(aboutPageEasterEggs.length)); + }); + } + } + + @override + Widget build(BuildContext context) { + return Container( + color: Theme.of(context).colorScheme.surface, + child: Column(children: [ + ModalTopBar( + title: S.of(context).about, + leadingIcon: Icon(Icons.arrow_back_ios_new), + onLeadingPressed: Navigator.of(context).pop, + ), + Column( + children: [ + Column( + spacing: 16, + children: [ + SizedBox(), + GestureDetector( + onTap: _easterEgg, + child: SvgPicture.asset( + "assets/new-ui/cake_squircle_icon.svg", + width: 128, + height: 128, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 8, + children: [ + Text( + "Cake Wallet", + style: TextStyle(fontSize: 32, fontWeight: FontWeight.w500), + ), + Text(widget.appVersion, + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurfaceVariant)) + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Wrap( + children: [ + Text( + _bottomText, + textAlign: TextAlign.center, + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + ], + ), + ), + ], + ), + SizedBox(height: 32), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: NewListSections(sections: { + "": [ + ListItemRegularRow( + keyValue: "official website", + label: "Official Website", + onTap: () => launchUrl(Uri.https("cakewallet.com")), + trailingIconPath: "assets/new-ui/link_arrow.svg", + foregroundColor: Theme.of(context).colorScheme.primary, + trailingIconSize: 10), + ListItemRegularRow( + keyValue: "docs", + label: "Cake Docs", + onTap: () => launchUrl(Uri.https("docs.cakewallet.com")), + trailingIconPath: "assets/new-ui/link_arrow.svg", + foregroundColor: Theme.of(context).colorScheme.primary, + trailingIconSize: 10) + ], + "2": [ + ListItemRegularRow( + keyValue: "gh", + label: "GitHub", + onTap: () => launchUrl(Uri.https("github.com", "cake-tech")), + trailingIconPath: "assets/new-ui/link_arrow.svg", + foregroundColor: Theme.of(context).colorScheme.primary, + trailingIconSize: 10), + ListItemRegularRow( + keyValue: "twitter", + label: "X (Twitter)", + onTap: () => launchUrl(Uri.https("twitter.com", "cakewallet")), + trailingIconPath: "assets/new-ui/link_arrow.svg", + foregroundColor: Theme.of(context).colorScheme.primary, + trailingIconSize: 10), + ListItemRegularRow( + keyValue: "tg", + label: "Telegram", + onTap: () => launchUrl(Uri.https("t.me", "cakewallet")), + trailingIconPath: "assets/new-ui/link_arrow.svg", + foregroundColor: Theme.of(context).colorScheme.primary, + trailingIconSize: 10) + ] + }), + ) + ], + ) + ])); + } +} diff --git a/lib/new-ui/pages/account_customizer.dart b/lib/new-ui/pages/account_customizer.dart new file mode 100644 index 000000000..4884f5a7f --- /dev/null +++ b/lib/new-ui/pages/account_customizer.dart @@ -0,0 +1,485 @@ +import 'dart:ui'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/new-ui/pages/card_customizer.dart'; +import 'package:cake_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart'; +import 'package:cake_wallet/new-ui/widgets/modal_grab_handle.dart'; +import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; +import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart'; +import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart'; +import 'package:cw_core/balance_card_style_settings.dart'; +import 'package:cw_core/generate_name.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; + +class AccountCustomizerListItem { + final BalanceCard card; + final int order; + final AccountListItem accountListItem; + + AccountCustomizerListItem( + {required this.card, required this.order, required this.accountListItem}); +} + +class AccountCustomizer extends StatefulWidget { + const AccountCustomizer( + {super.key, + required this.accountListViewModel, + required this.accountEditOrCreateViewModel, + required this.dashboardViewModel}); + + final MoneroAccountListViewModel accountListViewModel; + final MoneroAccountEditOrCreateViewModel accountEditOrCreateViewModel; + final DashboardViewModel dashboardViewModel; + + @override + State createState() => _AccountCustomizerState(); +} + +class _AccountCustomizerState extends State { + static const double _kStackVisibleFactor = 0.2; + late final double cardWidth = MediaQuery.of(context).size.width * 0.9; + + final List _items = []; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + loadCards(); + final activeId = monero!.getCurrentAccount(widget.dashboardViewModel.wallet).id; + for (int i = 0; i < _items.length-1; i++) { + if(_items[i].accountListItem.id == activeId) { + final lastIndex = _items.length - 1; + final temp = _items[i]; + _items[i] = _items[lastIndex]; + _items[lastIndex] = temp; + saveCardOrder(); + widget.dashboardViewModel.loadCardDesigns(); + break; + } + } + + }); + + + } + + @override + void dispose() { + saveCardOrder().then((value) => widget.dashboardViewModel.loadCardDesigns()); + super.dispose(); + } + + void loadCards() { + _items.clear(); + + final accounts = widget.accountListViewModel.accounts; + for (int i = 0; i < accounts.length; i++) { + final index = widget.dashboardViewModel.cardOrder[i]; + + if(index == null || index >= accounts.length) { + // db order broken. + reset(); + break; + } + + _items.add(AccountCustomizerListItem( + card: BalanceCard( + accountName: accounts[index].label, + accountIndex: accounts[index].id, + balance: accounts[index].balance ?? "0.00", + accountBalance: accounts[index].balance ?? "0.00", + designSwitchDuration: Duration.zero, + assetName: widget.accountListViewModel.currency.title, + onCustomizeTapped: (i == accounts.length - 1) ? _openCardCustomizer : null, + selected: i == accounts.length - 1, + width: cardWidth, + design: widget.dashboardViewModel.cardDesigns[index], + ), + order: index, + accountListItem: accounts[index])); + } + setState(() {}); + } + + @override + Widget build(BuildContext context) { + if (_items.isEmpty) return SizedBox.shrink(); + + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(24))), + child: Column( + children: [ + ModalTopBar( + title: S.of(context).wallet_accounts, + leadingIcon: Icon(Icons.close), + onLeadingPressed: Navigator.of(context).maybePop, + trailingIcon: Icon(Icons.refresh), + onTrailingPressed: showResetDialog, + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 24.0), + child: Text( + S.of(context).account_customizer_desc, + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + ), + Expanded( + child: Stack( + children: [ + ReorderableListView.builder( + padding: EdgeInsets.only(bottom: 196), + scrollController: ModalScrollController.of(context), + onReorder: reorder, + proxyDecorator: (child, index, animation) { + return AnimatedBuilder( + animation: animation, + builder: (context, _) { + final animValue = Curves.easeOutCubic.transform(animation.value); + final scale = lerpDouble(1, 1.05, animValue)!; + + return Opacity( + opacity: 1 - animValue.clamp(0.0, 0.1), + child: Center( + child: SizedBox( + width: cardWidth, + child: Transform.scale( + scale: scale, + child: child, + ), + ), + ), + ); + }, + child: _items[index].card, + ); + }, + itemCount: _items.length, + itemBuilder: (BuildContext context, int index) { + final card = _items[index].card; + + return Container( + key: ValueKey(index), + child: GestureDetector( + onTap: () { + reorder(index, _items.length); + }, + child: Align( + alignment: Alignment.topCenter, + heightFactor: _kStackVisibleFactor, + child: card, + ), + ), + ); + }, + ), + SafeArea( + child: Padding( + padding: EdgeInsets.only(bottom: 50), + child: Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(999999), + onTap: _showAddAccountModal, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(999999)), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 18.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 8, + children: [ + Icon( + Icons.add, + size: 28, + color: Theme.of(context).colorScheme.primary, + ), + Text( + S.of(context).add_account, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.w500), + ) + ], + ), + ), + ), + ), + ), + )))), + ], + ), + ), + ], + ), + ); + } + + bool _checkReadyToManage() { + if (widget.dashboardViewModel.status is! SyncedSyncStatus) { + showDialog( + context: context, + builder: (context) => AlertWithOneAction( + alertTitle: S.of(context).wallet_is_syncing, + alertContent: S.of(context).cannot_manage_accounts_during_sync, + buttonText: S.of(context).ok, + buttonAction: Navigator.of(context).pop)); + return false; + } + return true; + } + + Future _showAddAccountModal() async { + if (!_checkReadyToManage()) { + return; + } + + final res = await showCupertinoModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + builder: (context) { + final modal = getIt.get(); + return Material(child: modal); + }); + if (res != null && res is bool && res == true) { + await widget.dashboardViewModel.loadCardDesigns(); + loadCards(); + await saveCardOrder(); + } + } + + void _openCardCustomizer() { + if (!_checkReadyToManage()) { + return; + } + + final bloc = getIt.get(param1: false); + + Navigator.of(context).push(CupertinoPageRoute( + builder: (context) { + return BlocProvider( + create: (context) => bloc, + child: Material( + child: CardCustomizer( + cryptoTitle: widget.dashboardViewModel.wallet.currency.fullName ?? + widget.dashboardViewModel.wallet.currency.name, + cryptoName: widget.dashboardViewModel.wallet.currency.name, + ), + ), + ); + }, + )).then((_) async { + bloc.add(DesignSaved()); + await bloc.stream.firstWhere((item) => item is CardCustomizerSaved); + await widget.dashboardViewModel.loadCardDesigns(); + loadCards(); + }); + } + + void reorder(int oldIndex, int newIndex) { + setState(() { + if (oldIndex < newIndex) { + newIndex -= 1; + } + final AccountCustomizerListItem item = _items.removeAt(oldIndex); + _items.insert(newIndex, item); + }); + + // necessary to copy all this to keep constant constructor for BalanceCard + for (int i = 0; i < _items.length; i++) { + _items[i] = AccountCustomizerListItem( + card: BalanceCard( + accountName: _items[i].card.accountName, + balance: _items[i].card.balance, + accountIndex: _items[i].card.accountIndex, + accountBalance: _items[i].card.accountBalance, + assetName: _items[i].card.assetName, + designSwitchDuration: _items[i].card.designSwitchDuration, + onCustomizeTapped: (i == _items.length - 1) ? _openCardCustomizer : null, + selected: i == _items.length - 1, + width: _items[i].card.width, + design: _items[i].card.design, + ), + order: i, + accountListItem: _items[i].accountListItem); + } + + if (newIndex == _items.length - 1 || oldIndex == _items.length - 1) { + widget.accountListViewModel.select(_items[_items.length - 1].accountListItem); + } + } + + Future saveCardOrder() async { + for (int i = 0; i < _items.length; i++) { + final idx = _items.indexWhere((element) => element.accountListItem.id == i); + printV("$i: $idx"); + + await BalanceCardStyleSettings.fromCardDesign( + widget.dashboardViewModel.wallet.walletInfo.internalId, + i, + idx, + _items[idx].card.design) + .insert(); + } + } + + Future showResetDialog() async { + final res = await showPopUp( + context: context, + builder: (context) { + return AlertWithTwoActions( + alertTitle: S.of(context).reset, + alertContent: S.of(context).card_order_reset_desc, + leftButtonText: S.of(context).yes, + rightButtonText: S.of(context).no, + actionLeftButton: () { + Navigator.of(context).pop(true); + }, + actionRightButton: Navigator.of(context).pop); + }); + if(res != null && res is bool && res) { + reset(); + } + } + + Future reset() async { + _items.clear(); + + final accounts = widget.accountListViewModel.accounts; + for (int i = 0; i < widget.accountListViewModel.accounts.length; i++) { + + _items.add(AccountCustomizerListItem( + card: BalanceCard( + accountName: accounts[i].label, + accountIndex: accounts[i].id, + balance: accounts[i].balance ?? "0.00", + accountBalance: accounts[i].balance ?? "0.00", + assetName: widget.accountListViewModel.currency.title, + selected: true, + designSwitchDuration: Duration(milliseconds: 200), + width: cardWidth, + design: widget.dashboardViewModel.cardDesigns[i], + ), + order: i, + accountListItem: accounts[i])); + } + + saveCardOrder(); + setState(() {}); + } +} + +class AccountCreationModal extends StatefulWidget { + const AccountCreationModal({super.key, required this.accountEditOrCreateViewModel}); + + final MoneroAccountEditOrCreateViewModel accountEditOrCreateViewModel; + + @override + State createState() => _AccountCreationModalState(); +} + +class _AccountCreationModalState extends State { + final TextEditingController _controller = TextEditingController(); + bool _loading = false; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(30))), + child: SafeArea( + top: false, + child: Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ModalGrabHandle(), + ModalTopBar(title: S.of(context).create_account), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + spacing: 50, + children: [ + SizedBox(), + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(16)), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _controller, + decoration: InputDecoration(hintText: S.of(context).account_name), + ), + ), + Padding( + padding: const EdgeInsets.all(12.0), + child: GestureDetector( + onTap: () async { + _controller.text = await generateName(); + }, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(5)), + child: SvgPicture.asset( + "assets/new-ui/randomize.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, BlendMode.srcIn), + ), + ), + ), + ) + ], + ), + ), + NewPrimaryButton( + onPressed: () async { + if (_loading) return; + setState(() { + _loading = true; + }); + widget.accountEditOrCreateViewModel.label = _controller.text; + await widget.accountEditOrCreateViewModel.save(); + Navigator.of(context).pop(true); + }, + text: S.of(context).continue_text, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + isLoading: _loading, + ), + SizedBox(), + ], + ), + ) + ], + ), + ), + )); + } +} diff --git a/lib/new-ui/pages/addresses_page.dart b/lib/new-ui/pages/addresses_page.dart new file mode 100644 index 000000000..6cc5f6d30 --- /dev/null +++ b/lib/new-ui/pages/addresses_page.dart @@ -0,0 +1,480 @@ +import 'dart:ui'; + +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/new-ui/long_press_popup.dart'; +import 'package:cake_wallet/new-ui/widgets/addresses_page/address_label_input.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart'; +import 'package:cake_wallet/new-ui/widgets/long_press_menu.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/utils/address_formatter.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; +import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cw_core/card_design.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:flutter_svg/svg.dart'; + +import 'package:cake_wallet/utils/list_item.dart'; +import 'package:mobx/mobx.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; + +class NewAddressesPage extends StatefulWidget { + const NewAddressesPage( + {super.key, + required this.addressListViewModel, + required this.dashboardViewModel, + required this.showHidden}); + + final WalletAddressListViewModel addressListViewModel; + final DashboardViewModel dashboardViewModel; + final bool showHidden; + + @override + State createState() => _NewAddressesPageState(); +} + +class _NewAddressesPageState extends State { + List getItems(List list) { + return list.where((element) { + if (element is WalletAddressListItem) { + if (!element.isHidden && !widget.showHidden) return true; + if (element.isHidden && widget.showHidden) return true; + return false; + } + return false; + }).toList(); + } + + List items = []; + + late final TextEditingController _searchController; + + CardDesign? design; + + void updateItems() { + setState(() { + items = getItems(widget.addressListViewModel.forceRecomputeItems); + }); + } + + @override + void initState() { + super.initState(); + + items = getItems(widget.addressListViewModel.items); + _searchController = TextEditingController() + ..addListener(() { + setState(() {}); + }); + + WidgetsBinding.instance.addPostFrameCallback((_) { + reaction((context) => widget.dashboardViewModel.cardDesigns.first, (value) { + setState(() { + design = value; + }); + }); + }); + } + + @override + Widget build(BuildContext context) { + final filteredItems = items.where((item) { + if (item is! WalletAddressListItem) return false; + if (_searchController.text.isEmpty) return true; + if (item.name == null || item.name!.isEmpty) { + return (item.address.contains(_searchController.text.toLowerCase())); + } + return (item.name ?? '').toLowerCase().contains(_searchController.text.toLowerCase()); + }).toList(); + + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(16))), + child: Column( + spacing: 12.0, + children: [ + ModalTopBar( + title: widget.showHidden ? S.of(context).hidden_addresses : S.of(context).addresses, + leadingIcon: Icon(Icons.arrow_back), + onLeadingPressed: Navigator.of(context).pop, + onTrailingPressed: () {}), + Expanded( + child: Stack( + children: [ + CustomScrollView( + controller: ModalScrollController.of(context), + slivers: [ + if (!widget.showHidden) + SliverToBoxAdapter( + child: Column( + spacing: 16, + children: [ + if (widget.dashboardViewModel.type == WalletType.monero || + widget.dashboardViewModel.type == WalletType.wownero) + Observer( + builder: (_) => AccountPreviewHeader( + dashboardViewModel: widget.dashboardViewModel, + design: design, + )), + Text( + S.of(context).long_press_edit_address, + style: TextStyle( + fontSize: 10, + color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + ShowHiddenButton() + ], + ), + ), + SliverPadding( + padding: EdgeInsets.only(bottom: 64), + sliver: SliverList.separated( + itemCount: filteredItems.length, + itemBuilder: (context, index) { + final item = filteredItems[index]; + if (item is WalletAddressListItem) { + return Observer( + builder: (_) => AddressRow( + selected: item.address == widget.addressListViewModel.address.address, + first: widget.showHidden && index == 0, + last: index == filteredItems.length - 1, + item: item, + onSelect: () { + widget.addressListViewModel.setAddress(item); + }, + onLabelChanged: updateItems, + onAddressHidden: () async { + await widget.addressListViewModel.toggleHideAddress(item); + updateItems(); + }, + walletType: widget.addressListViewModel.type, + ), + ); + } + return SizedBox.shrink(); + }, + separatorBuilder: (_, __) => Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Container( + height: 1, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + ), + ), + ), + SliverToBoxAdapter(child: SizedBox(height: 72)) + ], + ), + SafeArea( + child: Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: AddressSearchBox(controller: _searchController)), + ), + ], + ), + ) + ], + ), + ); + } +} + +class AddressSearchBox extends StatelessWidget { + const AddressSearchBox({super.key, required this.controller}); + + final TextEditingController controller; + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 30.0, vertical: 18.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(999999), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh.withAlpha(128), + border: Border.all( + color: Theme.of(context).colorScheme.surfaceContainerHighest, width: 1), + borderRadius: BorderRadius.circular(99999)), + child: TextField( + controller: controller, + decoration: InputDecoration( + hintText: S.of(context).search, + hintStyle: TextStyle( + fontWeight: FontWeight.w600, + ), + prefixIcon: Icon(Icons.search), + filled: false, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(99999), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(99999), + borderSide: BorderSide.none, + ), + contentPadding: EdgeInsets.symmetric(vertical: 8), + ), + ), + ), + ), + ), + ), + ); + } +} + +class AccountPreviewHeader extends StatelessWidget { + AccountPreviewHeader({super.key, required this.dashboardViewModel, required this.design}); + + final DashboardViewModel dashboardViewModel; + final CardDesign? design; + + @override + Widget build(BuildContext context) { + return Container( + height: 64, + width: MediaQuery.of(context).size.width * 0.9, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: EdgeInsets.all(12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + spacing: 10, + children: [ + Observer( + builder: (_) => BalanceCard( + borderRadius: 5, width: 50, design: design ?? CardDesign.genericDefault), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + monero?.getCurrentAccount(dashboardViewModel.wallet).label ?? + wownero?.getCurrentAccount(dashboardViewModel.wallet).label ?? + "", + style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.primary), + ), + Text( + dashboardViewModel.wallet.name, + style: TextStyle( + fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), + ) + ], + ), + ], + ), + Row( + spacing: 12, + children: [ + Container( + width: 1, + height: 40, + decoration: + BoxDecoration(color: Theme.of(context).colorScheme.surfaceContainerHigh), + ), + Text(dashboardViewModel.balanceViewModel.balances.values.first.availableBalance) + ], + ) + ], + ), + ), + ); + } +} + +class AddressRow extends StatelessWidget { + const AddressRow( + {super.key, + required this.selected, + required this.first, + required this.last, + required this.item, + required this.onSelect, + required this.walletType, + required this.onLabelChanged, + required this.onAddressHidden}); + + final bool selected; + final bool first; + final bool last; + final WalletAddressListItem item; + final VoidCallback onSelect; + final VoidCallback onLabelChanged; + final VoidCallback onAddressHidden; + final WalletType walletType; + + @override + Widget build(BuildContext context) { + final hasLabel = item.name != null && item.name!.isNotEmpty; + + return GestureDetector( + onTap: onSelect, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: LongPressPopupBuilder( + popup: LongPressMenu( + items: [ + LongPressMenuItem( + label: S.of(context).set_label, + iconPath: "assets/new-ui/address_set_label.svg", + onSelected: () async { + Navigator.of(context, rootNavigator: true).pop(); + final res = await showPopUp( + context: context, + builder: (context) => getIt.get(param1: item)); + + if (res != null) { + onLabelChanged(); + } + }), + LongPressMenuItem( + label: item.isHidden ? S.of(context).unhide_address : S.of(context).hide_address, + iconPath: "assets/new-ui/address_hide.svg", + onSelected: () { + Navigator.of(context, rootNavigator: true).pop(); + onAddressHidden(); + }, + color: Theme.of(context).colorScheme.error), + ], + ), + child: AnimatedContainer( + duration: Duration(milliseconds: 150), + decoration: BoxDecoration( + color: selected + ? Theme.of(context).colorScheme.surfaceContainerHigh + : Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.vertical( + top: Radius.circular(first ? 16 : 0), + bottom: Radius.circular(last ? 16 : 0), + ), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + spacing: 8, + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + children: [ + Row( + spacing: 4, + children: [ + if (hasLabel) + Text( + item.name!, + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.w500), + ), + Expanded( + child: AddressFormatter.buildSegmentedAddress( + address: item.address, + walletType: walletType, + textAlign: TextAlign.left, + evenTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 12, + fontWeight: FontWeight.w400, + color: hasLabel + ? Theme.of(context).colorScheme.onSurfaceVariant + : Theme.of(context).colorScheme.onSurface, + ), + shouldTruncate: (hasLabel)), + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${S.of(context).transactions}: ${item.txCount}", + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + Text("${S.of(context).balance}: ${item.balance}", + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant)), + ], + ) + ], + ), + ), + if (selected) SvgPicture.asset("assets/new-ui/checkmark.svg") + ], + ), + ), + ), + ), + ), + ); + } +} + +class ShowHiddenButton extends StatelessWidget { + const ShowHiddenButton({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + Material( + child: InkWell( + onTap: () { + Navigator.of(context).pushNamed(Routes.receiveAddresses, arguments: true); + }, + child: Container( + height: 48, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(S.of(context).show_hidden_addresses), + RotatedBox( + quarterTurns: 1, + child: SvgPicture.asset("assets/new-ui/dropdown_arrow.svg")) + ], + ), + ), + ), + ), + ), + Container( + height: 1, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + ], + ), + ); + } +} diff --git a/lib/new-ui/pages/card_customizer.dart b/lib/new-ui/pages/card_customizer.dart new file mode 100644 index 000000000..47d91c952 --- /dev/null +++ b/lib/new-ui/pages/card_customizer.dart @@ -0,0 +1,237 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class CardCustomizer extends StatefulWidget { + const CardCustomizer({super.key, required this.cryptoTitle, required this.cryptoName}); + + final String cryptoTitle; + final String cryptoName; + + @override + State createState() => _CardCustomizerState(); +} + +class _CardCustomizerState extends State { + final accountNameController = TextEditingController(); + bool editEnabled = false; + late final CardCustomizerBloc bloc; + + + @override + void initState() { + super.initState(); + + bloc = context.read(); + + if (bloc.state is! CardCustomizerNotLoaded) { + editEnabled = bloc.state.accountName.isNotEmpty; + accountNameController.text = bloc.state.accountName; + } else { + late final StreamSubscription sub; + sub = bloc.stream.listen((state) { + if (state is! CardCustomizerNotLoaded) { + setState(() { + editEnabled = state.accountName.isNotEmpty; + }); + sub.cancel(); + } + }); + } + } + + @override + Widget build(BuildContext context) { + return BlocListener( + listenWhen: (previous, current) => + previous.accountName != current.accountName, + listener: (context, state) { + if(accountNameController.text != state.accountName) { + accountNameController.text = state.accountName; + } + }, + child: BlocBuilder( + builder: (context, state) { + if (state is CardCustomizerNotLoaded) return SizedBox.shrink(); + return PopScope( + child: SingleChildScrollView( + child: SafeArea( + child: Column( + spacing: 25.0, + mainAxisSize: MainAxisSize.min, + children: [ + ModalTopBar( + title: editEnabled ? S.of(context).edit_account : S.of(context).edit_card, + leadingIcon: Icon(Icons.close), + // trailingIcon: editEnabled ? Icon(Icons.delete_forever) : null, + onLeadingPressed: () => Navigator.of(context).maybePop(), + // onTrailingPressed: () {}, + ), + if (editEnabled) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + spacing: 8.0, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(S.of(context).account_name), + TextField( + maxLength: 32, + decoration: InputDecoration(counterText: ""), + onChanged: (value) { + context.read().add(AccountNameChanged(value)); + }, + controller: accountNameController, + ) + ], + ), + ), + BalanceCard( + width: min(MediaQuery.of(context).size.width * 0.87, 768), + selected: true, + designSwitchDuration: Duration(milliseconds: 300), + accountName: + editEnabled ? state.accountName:"" , + balance: "0.00", + assetName: state.displaySats ? "sats" : widget.cryptoName, + capitalizeAssetName: !state.displaySats, + design: state.selectedDesign, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + spacing: 8.0, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(S.of(context).card_style), + Container( + height: 63, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: state.availableDesigns.length, + separatorBuilder: (context, index) { + return SizedBox(width: 8.0); + }, + itemBuilder: (context, index) { + return GestureDetector( + onTap: () { + context + .read() + .add(CardDesignSelected(index)); + }, + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + decoration: ShapeDecoration( + shape: RoundedSuperellipseBorder( + side: BorderSide(color: ((index == state.selectedDesignIndex )? Theme.of(context).colorScheme.onSurface : Colors.transparent), width: 1), + borderRadius: BorderRadiusGeometry.circular(12), + ), + ), + child: AnimatedScale( + duration: Duration(milliseconds: 200), + scale: index == state.selectedDesignIndex ? 0.94 : 1, + child: BalanceCard( + width: 96, + borderRadius: 10, + selected: false, + designSwitchDuration: Duration(milliseconds: 300), + design: state.availableDesigns[index], + gradient: state.selectedDesign.gradient, + ), + ), + ), + ); + }), + ), + ], + )), + SizedBox(height: 8), + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + spacing: 8.0, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(S.of(context).color), + Container( + width: double.infinity, + child: Wrap( + direction: Axis.horizontal, + spacing: 4, // space between items in a row + runSpacing: 8, + children: + List.generate(state.availableColors.length, (index) { + return Material( + borderRadius: BorderRadius.circular(999999999), + child: InkWell( + borderRadius: BorderRadius.circular(999999999), + onTap: () { + context + .read() + .add(ColorSelected(index)); + }, + child: Stack( + children: [ + AnimatedOpacity( + duration: Duration(milliseconds: 200), + opacity: index == state.selectedColorIndex ? 1 : 0, + child: Container( + width:32,height:32,decoration: BoxDecoration(borderRadius: BorderRadius.circular(99999999),border: Border.all(color:Theme.of(context) + .colorScheme + .onSurface)) + ), + ), + AnimatedScale( + duration: Duration(milliseconds: 200), + scale: index == state.selectedColorIndex ? 0.8 : 1, + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(99999999), + gradient: state.availableColors[index]), + ), + ), + ], + ), + ), + ); + }), + )), + ], + ), + ), + ) + ], + ), + )), + ], + ), + ), + ), + ); + }, + ), +); + } +} diff --git a/lib/new-ui/pages/coin_control_page.dart b/lib/new-ui/pages/coin_control_page.dart new file mode 100644 index 000000000..54c7eefd5 --- /dev/null +++ b/lib/new-ui/pages/coin_control_page.dart @@ -0,0 +1,228 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/coin_control_page/coin_control_list_item.dart'; +import 'package:cake_wallet/new-ui/widgets/modal_header.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import "package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart"; + +class NewCoinControlPage extends StatefulWidget { + const NewCoinControlPage({super.key, required this.unspentCoinsListViewModel}); + + final UnspentCoinsListViewModel unspentCoinsListViewModel; + + @override + State createState() => _NewCoinControlPageState(); +} + +class _NewCoinControlPageState extends State { + late Future _initialization; + + @override + void initState() { + super.initState(); + _initialization = widget.unspentCoinsListViewModel.initialSetup(); + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: !widget.unspentCoinsListViewModel.isSavingItems, + child: Material( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + color: Colors.transparent, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(16))), + child: Column( + children: [ + ModalTopBar( + title: "", + onLeadingPressed: Navigator.of(context).pop, + leadingIcon: Icon(Icons.arrow_back_ios_new), + onTrailingPressed: () {}), + FutureBuilder( + future: _initialization, + builder: (context, asyncSnapshot) { + if (asyncSnapshot.connectionState == ConnectionState.waiting) { + return Expanded( + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 12, + children: [ + CupertinoActivityIndicator(), + Text("${S.of(context).loading}...") + ], + )), + ); + } + + if (asyncSnapshot.hasError) + return Center(child: Text(S.of(context).coin_control_load_failed)); + + return Expanded( + child: SingleChildScrollView( + child: SafeArea( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: ModalHeader( + iconPath: "assets/new-ui/settings_row_icons/coin-control.svg", + title: "Coin Control", + message: S.of(context).coin_control_desc), + ), + if (widget.unspentCoinsListViewModel.items.isNotEmpty) + Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + spacing: 20, + children: [ + GestureDetector( + onTap: () { + widget.unspentCoinsListViewModel.toggleSelectAll(true); + }, + child: Text(S.of(context).select_all, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontSize: 14, + fontWeight: FontWeight.w400)), + ), + GestureDetector( + onTap: () { + widget.unspentCoinsListViewModel.toggleSelectAll(false); + }, + child: Text(S.of(context).unselect_all, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontSize: 14, + fontWeight: FontWeight.w400)), + ) + ], + ), + ), + if (widget.unspentCoinsListViewModel.nonFrozenItems.isEmpty && + widget.unspentCoinsListViewModel.frozenItems.isEmpty) ...[ + SizedBox(height: 12), + Center( + child: Text( + S.of(context).no_unspent_coins, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + )), + ], + Observer( + builder: (_) => widget + .unspentCoinsListViewModel.nonFrozenItems.isEmpty + ? SizedBox.shrink() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: CoinControlListSection( + items: widget.unspentCoinsListViewModel.nonFrozenItems, + unspentCoinsListViewModel: + widget.unspentCoinsListViewModel), + ), + ), + Observer( + builder: (context) => + widget.unspentCoinsListViewModel.frozenItems.isEmpty + ? SizedBox.shrink() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + spacing: 10, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 12), + Text( + S.of(context).frozen, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context) + .colorScheme + .onSurfaceVariant), + ), + CoinControlListSection( + items: widget + .unspentCoinsListViewModel.frozenItems, + unspentCoinsListViewModel: + widget.unspentCoinsListViewModel), + ]), + ), + ), + SizedBox(height: 12) + ], + ), + ), + ), + ); + }) + ], + ), + ), + ), + ); + } +} + +class CoinControlListSection extends StatelessWidget { + const CoinControlListSection( + {super.key, required this.items, required this.unspentCoinsListViewModel}); + + final List items; + final UnspentCoinsListViewModel unspentCoinsListViewModel; + + @override + Widget build(BuildContext context) { + return ListView.separated( + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: NeverScrollableScrollPhysics(), + itemCount: items.length, + separatorBuilder: (_, __) => Container( + height: 1, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + itemBuilder: (_, int index) { + return Observer(builder: (_) { + final item = items[index]; + final fiatAmount = unspentCoinsListViewModel.fiatAmounts[item.amount] ?? ''; + return GestureDetector( + onTap: () => Navigator.of(context).pushNamed( + Routes.unspentCoinsDetails, + arguments: [item, unspentCoinsListViewModel], + ), + child: CoinControlListItem( + note: item.note, + amount: item.amount, + fiatAmount: fiatAmount, + address: item.address, + isSending: item.isSending, + isFrozen: item.isFrozen, + isChange: item.isChange, + isSilentPayment: item.isSilentPayment, + isLoading: item.isBeingSaved, + isFirst: index == 0, + isLast: index == items.length - 1, + onCheckBoxTap: item.isFrozen + ? null + : () async { + item.isSending = !item.isSending; + await unspentCoinsListViewModel.saveUnspentCoinInfo(item); + }, + ), + ); + }); + }, + ); + } +} diff --git a/lib/new-ui/pages/home_page.dart b/lib/new-ui/pages/home_page.dart new file mode 100644 index 000000000..8da84dc50 --- /dev/null +++ b/lib/new-ui/pages/home_page.dart @@ -0,0 +1,227 @@ +import 'package:cake_wallet/core/auth_service.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/new-ui/modal_navigator.dart'; +import 'package:cake_wallet/new-ui/pages/account_customizer.dart'; +import 'package:cake_wallet/new-ui/pages/card_customizer.dart'; +import 'package:cake_wallet/new-ui/pages/settings_page.dart'; +import 'package:cake_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/action_row/coin_action_row.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/assets_history_section.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/cards/cards_view.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/mweb_ad.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/wallet_info.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; +import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart'; +import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; + +class NewHomePage extends StatefulWidget { + NewHomePage({super.key, required this.dashboardViewModel, required this.nftViewModel}); + + final DashboardViewModel dashboardViewModel; + final NFTViewModel nftViewModel; + + @override + State createState() => _NewHomePageState(); +} + +class _NewHomePageState extends State { + MoneroAccountListViewModel? accountListViewModel; + bool _lightningMode = false; + + @override + void initState() { + super.initState(); + _setAccountViewModel(); + reaction((_)=>widget.dashboardViewModel.wallet, (_) { + _setAccountViewModel(); + setState(() { + _lightningMode = false; + }); + }); + } + + void _setAccountViewModel() { + accountListViewModel = widget.dashboardViewModel.balanceViewModel.hasAccounts + ? getIt.get() + : null; + } + + @override + Widget build(BuildContext context) { + return Container( + height: MediaQuery.of(context).size.height, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Theme.of(context).colorScheme.surface, + Theme.of(context).colorScheme.surfaceDim, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: Stack( + children: [ + CustomScrollView( + physics: BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), + slivers:[ + SliverPadding( + padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top), + sliver: CupertinoSliverRefreshControl( + refreshTriggerPullDistance: 160, + refreshIndicatorExtent: 90, + onRefresh: () => widget.dashboardViewModel.refreshDashboard(), + ), + ), + SliverToBoxAdapter( + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + spacing: 24.0, + children: [ + TopBar( + dashboardViewModel: widget.dashboardViewModel, + lightningMode: _lightningMode, + onLightningSwitchPress: () { + setState(() { + _lightningMode = !_lightningMode; + }); + }, + onSettingsButtonPress: () { + CupertinoScaffold.showCupertinoModalBottomSheet( + context: context, + barrierColor: Colors.black.withAlpha(85), + builder: (context) => FractionallySizedBox( + child: Material( + child: NewSettingsPage( + dashboardViewModel: widget.dashboardViewModel, + authService: getIt.get(), + ))), + ); + }, + ), + Observer( + builder: (_)=>WalletInfoBar( + lightningMode: _lightningMode, + hardwareWalletType: widget.dashboardViewModel.wallet.hardwareWalletType, + name: widget.dashboardViewModel.wallet.name, + hasCustomize: accountListViewModel != null, + onCustomizeButtonTap: openAccountCustomizer), + ), + Column( + children: [ + Observer( + builder: (_) => CardsView( + key: ValueKey(widget.dashboardViewModel.wallet.name), + onCustomizeTapped: openCardCustomizer, + dashboardViewModel: widget.dashboardViewModel, + accountListViewModel: accountListViewModel, + onCompactModeBackgroundCardsTapped: openAccountCustomizer, + lightningMode: _lightningMode, + ), + ), + UnconfirmedBalanceWidget(dashboardViewModel: widget.dashboardViewModel,), + ], + ), + Observer( + builder: (_) { + return Column( + children: [ + CoinActionRow( + lightningMode: _lightningMode, + showSwap: widget.dashboardViewModel.isEnabledSwapAction,), + MwebAd(dashboardViewModel: widget.dashboardViewModel,), + ], + ); + }, + ), + Observer( + builder: (_) => AssetsHistorySection( + nftViewModel: widget.nftViewModel, + dashboardViewModel: widget.dashboardViewModel, + ), + ), + SizedBox(height: 80.0) + ], + ), + ),] + ), + Container( + height: (MediaQuery.of(context).padding.top), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Theme.of(context).colorScheme.surface.withAlpha(5), + Theme.of(context).colorScheme.surface.withAlpha(25), + Theme.of(context).colorScheme.surface.withAlpha(50), + Theme.of(context).colorScheme.surface.withAlpha(100), + Theme.of(context).colorScheme.surface.withAlpha(150), + Theme.of(context).colorScheme.surface.withAlpha(175), + Theme.of(context).colorScheme.surface.withAlpha(200), + ], + ), + ), + ), + ], + ), + ); + } + + void openAccountCustomizer() async { + await CupertinoScaffold.showCupertinoModalBottomSheet( + barrierColor: Colors.black.withAlpha(60), + context: context, + builder: (context) { + return ModalNavigator( + parentContext: context, + heightMode: ModalHeightModes.fullScreen, + rootPage: Material( + child: AccountCustomizer( + accountListViewModel: accountListViewModel!, + accountEditOrCreateViewModel: getIt.get(), + dashboardViewModel: widget.dashboardViewModel, + )), + ); + }, + ); + widget.dashboardViewModel.loadCardDesigns(); + } + + void openCardCustomizer() async { + final bloc = getIt.get( + param1: _lightningMode, + param2: widget.dashboardViewModel.settingsStore.displayAmountsInSatoshi); + await CupertinoScaffold.showCupertinoModalBottomSheet( + barrierColor: Colors.black.withAlpha(60), + context: context, + builder: (context) { + return ModalNavigator( + parentContext: context, + heightMode: ModalHeightModes.fullScreen, + rootPage: BlocProvider( + create: (context) => bloc, + child: Material( + child: CardCustomizer( + cryptoTitle: widget.dashboardViewModel.wallet.currency.fullName ?? + widget.dashboardViewModel.wallet.currency.name, + cryptoName: widget.dashboardViewModel.wallet.currency.name, + )), + )); + }, + ); + bloc.add(DesignSaved()); + await bloc.stream.firstWhere((s) => s is CardCustomizerSaved); + widget.dashboardViewModel.loadCardDesigns(); + } +} diff --git a/lib/new-ui/pages/lightning_username_page.dart b/lib/new-ui/pages/lightning_username_page.dart new file mode 100644 index 000000000..341bbfc68 --- /dev/null +++ b/lib/new-ui/pages/lightning_username_page.dart @@ -0,0 +1,315 @@ +import 'dart:math'; + +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/viewmodels/lightning_username/lightning_username_bloc.dart'; +import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/themes/core/theme_store.dart'; +import 'package:cw_core/generate_name.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class LightningUsernamePage extends StatefulWidget { + LightningUsernamePage({super.key, required this.isSetup, required this.themeStore, required this.lightningUsernameBloc}); + + final bool isSetup; + + final ThemeStore themeStore; + late final bool isLightMode = !(themeStore.currentTheme.isDark); + final LightningUsernameBloc lightningUsernameBloc; + + @override + State createState() => _LightningUsernamePageState(); +} + +class _LightningUsernamePageState extends State { + late bool _editMode; + final TextEditingController _controller = TextEditingController(); + + @override + void initState() { + super.initState(); + _editMode = widget.isSetup; + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => widget.lightningUsernameBloc, + child: BlocConsumer( + listener: (context, state) { + if (state is LightningUsernameSaved) { + if (widget.isSetup) { + Navigator.of(context).popUntil((route) => route.isFirst); + } else { + Navigator.of(context).pop(); + } + } + if (state is LightningUsernameInitial) { + if (!widget.isSetup) { + _controller.text = state.username; + } + } + }, + builder: (context, state) { + return Material( + child: Container( + decoration: BoxDecoration(color: Theme.of(context).colorScheme.surface), + child: SafeArea( + child: Column( + children: [ + ModalTopBar( + title: "Lightning ${S.of(context).username}", + leadingIcon: Icon(Icons.arrow_back_ios_new), + onLeadingPressed: Navigator.of(context).pop, + ), + Expanded( + child: Column( + spacing: 24, + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: _editMode + ? LightningUsernameEditor( + isSetup: widget.isSetup, + controller: _controller, + isLightMode: widget.isLightMode, + onRandomizeButtonTap: () { + randomizeUsername(context); + }, + state: state, + ) + : LightningUsernameInfo( + username: state.username, + isLightMode: widget.isLightMode, + ), + ) + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + spacing: 12, + children: [ + if (widget.isSetup) ...[ + Text( + S.of(context).lightning_username_setup_later, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + SizedBox(), + NewPrimaryButton( + onPressed: () { + Navigator.of(context).popUntil((route) => route.isFirst); + }, + text: S.of(context).skip, + color: Theme.of(context).colorScheme.surfaceContainer, + textColor: Theme.of(context).colorScheme.primary), + ], + NewPrimaryButton( + onPressed: () { + if (_editMode) { + context.read().add(RequestUsernameSave()); + } else { + setState(() { + _editMode = true; + }); + } + }, + text: _editMode ? S.of(context).confirm : S.of(context).change_username, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + disabled: _editMode && (!(state is LightningUsernameReady)), + isLoading: (state is LightningUsernameSaving), + ), + SizedBox() + ], + ), + ) + ], + ), + ), + ), + ); + }, + ), + ); + } + + void randomizeUsername(BuildContext context) async { + final randomNumber = Random.secure().nextInt(9999); + final randomName = await generateName(); + final username = "${randomName.replaceAll(" ", "")}$randomNumber".toLowerCase(); + + context.read().add(ChangeUsername(username)); + _controller.text = username; + } +} + +class LightningUsernameInfo extends StatelessWidget { + const LightningUsernameInfo({super.key, required this.username, required this.isLightMode}); + + final String username; + final bool isLightMode; + + @override + Widget build(BuildContext context) { + return Column( + spacing: 24, + children: [ + SvgPicture.asset(isLightMode + ? "assets/new-ui/lightning_username_setup_light.svg" + : "assets/new-ui/lightning_username_setup.svg"), + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 2, + children: [ + Text( + username, + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500), + ), + Text( + usernameSuffix, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary), + textAlign: TextAlign.center, + ) + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32.0), + child: Text( + S.of(context).lightning_username_desc_completed, + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), + textAlign: TextAlign.center, + ), + ) + ], + ); + } +} + +class LightningUsernameEditor extends StatelessWidget { + const LightningUsernameEditor( + {super.key, + required this.controller, + required this.onRandomizeButtonTap, + required this.state, + required this.isSetup, + required this.isLightMode}); + + final TextEditingController controller; + final VoidCallback onRandomizeButtonTap; + final bool isLightMode; + final bool isSetup; + final LightningUsernameState state; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + spacing: 12, + children: [ + SvgPicture.asset(isLightMode + ? "assets/new-ui/lightning_username_setup_light.svg" + : "assets/new-ui/lightning_username_setup.svg"), + if (isSetup) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Text( + S.of(context).lightning_username_desc, + textAlign: TextAlign.center, + ), + ), + SizedBox(), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.horizontal(left: Radius.circular(16))), + child: Row( + spacing: 8, + children: [ + Expanded( + child: TextField( + onChanged: (val) { + context.read().add(ChangeUsername(val)); + }, + controller: controller, + )), + Material( + color: Colors.transparent, + child: InkWell( + onTap: onRandomizeButtonTap, + child: SvgPicture.asset("assets/new-ui/randomize.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, BlendMode.srcIn)), + ), + ), + SizedBox() + ], + ), + ), + ), + Container( + alignment: Alignment.center, + height: 56, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.horizontal(right: Radius.circular(16))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + usernameSuffix, + style: TextStyle(fontSize: 16), + ), + ), + ) + ], + ), + ), + if (state is LightningUsernameError) + Text( + (state as LightningUsernameError).error.message, + textAlign: TextAlign.center, + style: TextStyle( + color: (state as LightningUsernameError).error.isInfo + ? Colors.green + : Theme.of(context).colorScheme.error), + ) + else if (state is LightningUsernameReady) + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 8, + children: [ + Icon( + Icons.check, + color: Colors.green, + size: 12, + ), + Text( + S.of(context).username_available, + style: TextStyle(color: Colors.green), + ) + ], + ) + else + Text(""), + SizedBox(height: MediaQuery.of(context).viewInsets.bottom.clamp(0, 100)) + ], + ); + } +} diff --git a/lib/new-ui/pages/receive_page.dart b/lib/new-ui/pages/receive_page.dart new file mode 100644 index 000000000..1f6655701 --- /dev/null +++ b/lib/new-ui/pages/receive_page.dart @@ -0,0 +1,346 @@ +import 'package:cake_wallet/core/utilities.dart'; +import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/payjoin_copy_modal.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_address_type.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_address_widget.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_amount_display.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_amount_modal.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_bottom_buttons.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_info_box.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_label_modal.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_label_widget.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_large_amount_preview.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_qr_code.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_token_display.dart'; +import 'package:cake_wallet/utils/share_util.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; +import 'package:cake_wallet/zcash/zcash.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/payment_uris.dart'; +import 'package:cw_core/receive_page_option.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/anonpay/anonpay_donation_link_info.dart'; +import 'package:cake_wallet/entities/preferences_key.dart'; +import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; + +class NewReceivePage extends StatefulWidget { + NewReceivePage( + {super.key, + required this.addressListViewModel, + required this.receiveOptionViewModel, + required this.dashboardViewModel, + required this.lightningMode, + CryptoCurrency? initialCurrency}) { + if (initialCurrency != null && initialCurrency != addressListViewModel.selectedCurrency) { + addressListViewModel.setTokenCurrency(initialCurrency); + } + } + + final WalletAddressListViewModel addressListViewModel; + final ReceiveOptionViewModel receiveOptionViewModel; + final DashboardViewModel dashboardViewModel; + final bool lightningMode; + + @override + State createState() => _NewReceivePageState(); +} + +class _NewReceivePageState extends State { + bool _largeQrMode = false; + late WalletAddressListItem? _addressItemWithLabel; + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (widget.lightningMode) { + widget.receiveOptionViewModel.selectReceiveOption( + widget.receiveOptionViewModel.options + .firstWhereOrNull((item) => item.value.contains("Lightning")) ?? + ReceivePageOption.mainnet, + ); + widget.addressListViewModel.setTokenCurrency(CryptoCurrency.btcln); + } else if (widget.addressListViewModel.wallet.type == WalletType.bitcoin) { + widget.receiveOptionViewModel.selectReceiveOption( + widget.receiveOptionViewModel.options + .firstWhereOrNull((item) => item.value.contains("Standard")) ?? + ReceivePageOption.mainnet, + ); + } + }); + + reaction((_) => widget.addressListViewModel.uri, (newAddress) { + _reloadAddressWithLabel(newAddress); + }); + + _addressItemWithLabel = + widget.addressListViewModel.forceRecomputeItems.firstWhereOrNull((item) { + return (item is WalletAddressListItem && + item.address == widget.addressListViewModel.uri.address); + }) as WalletAddressListItem?; + + reaction((_) => widget.receiveOptionViewModel.selectedReceiveOption, + (ReceivePageOption option) { + if (widget.dashboardViewModel.type == WalletType.bitcoin && + bitcoin!.isBitcoinReceivePageOption(option)) { + widget.addressListViewModel.setAddressType(bitcoin!.getOptionToType(option)); + return; + } + if (widget.dashboardViewModel.type == WalletType.zcash) { + widget.addressListViewModel.setAddressType(zcash!.getOptionToType(option)); + return; + } + + switch (option) { + case ReceivePageOption.anonPayInvoice: + Navigator.pushNamed( + context, + Routes.anonPayInvoicePage, + arguments: [widget.addressListViewModel.address.address, option], + ); + break; + case ReceivePageOption.anonPayDonationLink: + final sharedPreferences = getIt.get(); + final clearnetUrl = sharedPreferences.getString(PreferencesKey.clearnetDonationLink); + final onionUrl = sharedPreferences.getString(PreferencesKey.onionDonationLink); + final donationWalletName = + sharedPreferences.getString(PreferencesKey.donationLinkWalletName); + + if (clearnetUrl != null && + onionUrl != null && + widget.addressListViewModel.wallet.name == donationWalletName) { + Navigator.pushNamed( + context, + Routes.anonPayReceivePage, + arguments: AnonPayReceivePageArgs( + invoiceInfo: AnonpayDonationLinkInfo( + clearnetUrl: clearnetUrl, + onionUrl: onionUrl, + address: widget.addressListViewModel.address.address, + ), + qrImage: widget.addressListViewModel.qrImage, + ), + ); + } else { + Navigator.pushNamed( + context, + Routes.anonPayInvoicePage, + arguments: [widget.addressListViewModel.address.address, option], + ); + } + break; + default: + if ([WalletType.bitcoin, WalletType.litecoin] + .contains(widget.addressListViewModel.type)) { + widget.addressListViewModel.setAddressType(bitcoin!.getBitcoinAddressType(option)); + } + if (widget.addressListViewModel.type == WalletType.zcash) { + printV("help me i'll kms if that wont work: ${zcash!.getZcashAddressType(option)}"); + widget.addressListViewModel.setAddressType(zcash!.getZcashAddressType(option)); + } + } + }); + } + + @override + Widget build(BuildContext context) { + final hasAddressTypeSelector = widget.receiveOptionViewModel.options.length > 1; + final hasLabel = _addressItemWithLabel?.name != null && _addressItemWithLabel!.name!.isNotEmpty; + final infoboxDismissed = widget.addressListViewModel.wallet.walletInfo.receiveInfoboxDismissed; + final infobox = ReceiveInfoBox.forWalletType(widget.addressListViewModel.type, + onDismissed: () { + widget.addressListViewModel.dismissInfobox(); + setState(() {}); + }, autoGenerateSubaddressStatus: widget.lightningMode ? AutoGenerateSubaddressStatus.disabled : widget.dashboardViewModel.settingsStore.autoGenerateSubaddressStatus); + + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Theme.of(context).colorScheme.surfaceBright, + Theme.of(context).colorScheme.surface, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + borderRadius: BorderRadius.vertical( + top: Radius.circular(24), + ), + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + ModalTopBar( + title: _largeQrMode ? "" : S.of(context).receive, + leadingIcon: Icon(Icons.close), + trailingIcon: _largeQrMode + ? Icon(Icons.share) + : widget.addressListViewModel.hasAddressRotation + /* TODO rotating is broken on mweb, disabling for now, fix after mvp*/ + && + !(widget.receiveOptionViewModel.selectedReceiveOption.description ?? "") + .toLowerCase() + .contains("mweb") + ? Icon(Icons.refresh) + : null, + onLeadingPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + }, + onTrailingPressed: () { + if (_largeQrMode) { + ShareUtil.share( + text: widget.addressListViewModel.uri.toString(), + context: context, + ); + } else if (widget.addressListViewModel.hasAddressRotation) { + widget.addressListViewModel.rotateAddress(); + } + }, + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + children: [ + ReceiveAmountDisplay( + walletAddressListViewModel: widget.addressListViewModel, + largeQrMode: _largeQrMode, + ), + ReceiveQrCode( + addressListViewModel: widget.addressListViewModel, + onTap: () { + setState(() { + _largeQrMode = !_largeQrMode; + // _infoboxDimissed = true; + widget.addressListViewModel.dismissInfobox(); + }); + }, + largeQrMode: _largeQrMode, + ), + if (widget.addressListViewModel.tokenCurrency != null) + ReceiveTokenDisplay(addressListViewModel: widget.addressListViewModel), + if (hasAddressTypeSelector) + ReceiveAddressTypeDisplay( + lightningMode: widget.lightningMode, + receiveOptionViewModel: widget.receiveOptionViewModel, + largeQrMode: _largeQrMode, + ), + ReceiveAddressWidget( + addressListViewModel: widget.addressListViewModel, + ), + GestureDetector( + onTap: _showLabelModal, + child: ReceiveLabelWidget( + name: _addressItemWithLabel?.name ?? "", + largeQrMode: _largeQrMode, + )), + Observer( + builder: (_) => ReceiveBottomButtons( + key: const ValueKey(0), + largeQrMode: _largeQrMode, + showAccountsButton: widget.addressListViewModel.hasAddressList, + showLabelButton: widget.addressListViewModel.hasAddressList && !hasLabel, + onCopyButtonPressed: () { + if (widget.addressListViewModel.hasPayjoin) { + showModalBottomSheet( + isScrollControlled: true, + context: context, + builder: (context) => + PayjoinCopyModal(uri: widget.addressListViewModel.uri)); + } else { + Clipboard.setData( + ClipboardData( + text: widget.addressListViewModel.displayAmount.isEmpty + ? widget.addressListViewModel.uri.address + : widget.addressListViewModel.uri.toString()), + ); + } + }, + onAmountButtonPressed: () { + showMaterialModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + barrierColor: Colors.black.withAlpha(80), + builder: (context) { + return ReceiveAmountModal( + walletAddressListViewModel: widget.addressListViewModel, + onSubmitted: (amount) {}, + ); + }, + ); + }, + onLabelButtonPressed: _showLabelModal, + onAccountsButtonPressed: () { + Navigator.of(context).pushNamed( + Routes.receiveAddresses, + arguments: false, + ); + }, + ), + ), + ReceiveLargeAmountPreview( + amount: widget.addressListViewModel.displayAmount, + currency: widget.addressListViewModel.cryptoCurrencySymbol, + largeQrMode: _largeQrMode), + if (infobox != null && !widget.addressListViewModel.isLightning) + ClipRect( + child: AnimatedAlign( + duration: const Duration(milliseconds: 200), + curve: Curves.easeOutCubic, + heightFactor: infoboxDismissed ? 0 : 1, + alignment: Alignment.center, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: infoboxDismissed ? 0 : 1, + curve: Curves.easeOutCubic, + child: infobox), + )) + ], + ), + ), + ], + ), + ), + ); + } + + void _showLabelModal() { + showMaterialModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + barrierColor: Colors.black.withAlpha(80), + builder: (context) { + return getIt.get(param1: _addressItemWithLabel); + }).then((value) { + _reloadAddressWithLabel(widget.addressListViewModel.uri); + }); + } + + void _reloadAddressWithLabel(PaymentURI newAddress) { + // FIXME: viewmodel doesn't want to load address name here, so we make it. investigate why later + setState(() { + _addressItemWithLabel = widget.addressListViewModel.forceRecomputeItems.firstWhereOrNull( + (item) => (item is WalletAddressListItem && item.address == newAddress.address)) + as WalletAddressListItem; + }); + } +} diff --git a/lib/new-ui/pages/scan_page.dart b/lib/new-ui/pages/scan_page.dart new file mode 100644 index 000000000..75bbddaa3 --- /dev/null +++ b/lib/new-ui/pages/scan_page.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class ScanPage extends StatelessWidget { + const ScanPage({super.key}); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/lib/new-ui/pages/send_page.dart b/lib/new-ui/pages/send_page.dart new file mode 100644 index 000000000..f62d2ded9 --- /dev/null +++ b/lib/new-ui/pages/send_page.dart @@ -0,0 +1,1372 @@ +import 'package:cake_wallet/core/address_validator.dart'; +import 'package:cake_wallet/core/auth_service.dart'; +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/core/open_crypto_pay/open_cryptopay_service.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/contact_record.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; +import 'package:cake_wallet/evm/evm.dart'; +import 'package:cake_wallet/exchange/trade.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/new-ui/modal_navigator.dart'; +import 'package:cake_wallet/new-ui/widgets/animated_dropdown.dart'; +import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart'; +import 'package:cake_wallet/new-ui/widgets/picker.dart'; +import 'package:cake_wallet/new-ui/widgets/send_page/fiat_amount_bar.dart'; +import 'package:cake_wallet/new-ui/widgets/send_page/send_confirm_sheet.dart'; +import 'package:cake_wallet/new-ui/widgets/send_page/send_memo_input.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/token_selection_bottom_sheet.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/list_item_regular_row_widget.dart'; +import 'package:cake_wallet/src/widgets/standard_checkbox.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cw_core/lnurl.dart'; +import 'package:cw_core/wallet_info.dart'; +import "package:cw_core/wallet_type.dart"; +import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/new-ui/widgets/send_page/directional_switcher.dart'; +import 'package:cake_wallet/new-ui/widgets/send_page/recipient_dot_row.dart'; +import 'package:cake_wallet/new-ui/widgets/send_page/send_address_input.dart'; +import 'package:cake_wallet/new-ui/widgets/send_page/send_amount_input.dart'; +import 'package:cake_wallet/new-ui/widgets/send_page/send_syncing_indicator.dart'; +import 'package:cake_wallet/routes.dart' show Routes; +import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; +import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/payment_confirmation_bottom_sheet.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/wallet_switcher_bottom_sheet.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/utils/payment_request.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/payment/payment_view_model.dart'; +import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cake_wallet/view_model/send/send_view_model.dart'; +import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; +import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/currency.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/utils/print_verbose.dart'; + +import 'package:cake_wallet/main.dart'; +import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; +import 'package:cake_wallet/new-ui/widgets/send_page/l2_action_wallet_selector.dart'; +import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; +import 'package:cw_core/unspent_coin_type.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:mobx/mobx.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; + +class SendPageHelpContent { + final String imagePath; + final String title; + final String description; + final String? disclaimer; + + const SendPageHelpContent( + {required this.title, required this.imagePath, required this.description, this.disclaimer}); +} + +class SendPageModes { + final bool showAddressField; + final String title; + final String? description; + final String? confirmSheetIconPath; + final SendPageHelpContent? helpContent; + final bool popOnConfirmation; + + const SendPageModes( + {required this.title, + this.description, + required this.showAddressField, + this.confirmSheetIconPath, + this.helpContent, + this.popOnConfirmation = true}); + + static final SendPageModes normal = SendPageModes(title: S.current.send, showAddressField: true); + + static final SendPageModes lightningDeposit = SendPageModes( + title: S.current.bitcoin_lightning_deposit, + description: S.current.to_lightning, + showAddressField: false, + helpContent: SendPageHelpContent( + title: S.current.bitcoin_lightning_deposit, + imagePath: "assets/new-ui/lightning_deposit_help.svg", + description: S.current.lightning_deposit_desc, + disclaimer: S.current.lightning_deposit_disclaimer), + popOnConfirmation: false); + + static final SendPageModes lightningWithdrawal = SendPageModes( + title: S.current.bitcoin_lightning_withdraw, + description: S.current.to_on_chain, + showAddressField: false, + helpContent: SendPageHelpContent( + title: S.current.bitcoin_lightning_withdraw, + imagePath: "assets/new-ui/lightning_withdraw_help.svg", + description: S.current.lightning_withdraw_desc, + disclaimer: S.current.lightning_withdraw_disclaimer), + popOnConfirmation: false); + + static final SendPageModes mwebDeposit = SendPageModes( + title: "${S.current.mask} Litecoin", + showAddressField: false, + confirmSheetIconPath: "assets/new-ui/mask.svg", + helpContent: SendPageHelpContent( + title: S.current.about_litecoin_privacy, + imagePath: "assets/new-ui/mweb_help.svg", + description: S.current.mweb_help_desc_1 + "\n\n" + S.current.mweb_help_desc_2, + disclaimer: S.current.mweb_help_disclaimer), + popOnConfirmation: false); + + static final SendPageModes mwebWithdrawal = SendPageModes( + title: "${S.current.unmask} Litecoin", + showAddressField: false, + confirmSheetIconPath: "assets/new-ui/unmask.svg", + helpContent: SendPageHelpContent( + title: S.current.about_litecoin_privacy, + imagePath: "assets/new-ui/mweb_help.svg", + description: S.current.mweb_help_desc_1 + "\n\n" + S.current.mweb_help_desc_2, + disclaimer: S.current.mweb_help_disclaimer), + popOnConfirmation: false); + + static final all = [normal, lightningDeposit, lightningWithdrawal, mwebDeposit, mwebWithdrawal]; +} + +class SendPageParams { + final PaymentRequest? initialPaymentRequest; + final SendPageModes mode; + final CryptoCurrency? initialCurrency; + final UnspentCoinType unspentCoinType; + + SendPageParams({ + this.initialPaymentRequest, + SendPageModes? mode, + this.unspentCoinType = UnspentCoinType.any, + this.initialCurrency, + }) : mode = mode ?? SendPageModes.normal; +} + +class NewSendPage extends StatefulWidget { + NewSendPage( + {super.key, + required this.sendViewModel, + required this.paymentViewModel, + required this.walletSwitcherViewModel, + required this.contactListViewModel, + required this.authService, + required SendPageParams params}) + : initialPaymentRequest = params.initialPaymentRequest, + mode = params.mode { + if (params.initialCurrency != null) { + sendViewModel.selectedCryptoCurrency = params.initialCurrency!; + } + } + + final SendViewModel sendViewModel; + final PaymentViewModel paymentViewModel; + final WalletSwitcherViewModel walletSwitcherViewModel; + final ContactListViewModel contactListViewModel; + final AuthService authService; + final PaymentRequest? initialPaymentRequest; + final SendPageModes mode; + + @override + State createState() => _NewSendPageState(); +} + +class _NewSendPageState extends State { + bool _fiatInputMode = false; + int _selectedOutput = 0; + + List _amountControllers = []; + List _addressControllers = []; + List _memoControllers = []; + final _formKey = GlobalKey(); + final _addressFocusNode = FocusNode(); + BuildContext? loadingBottomSheetContext; + BuildContext? dialogContext; + ContactRecord? newContactAddress; + + bool _justHandledPasteButton = false; + + @override + void initState() { + super.initState(); + _addInputControllers(); + + reaction((_) => widget.sendViewModel.outputs[_selectedOutput].sendAll, ((bool all) { + if (all) { + _fiatInputMode = false; + _amountControllers[_selectedOutput].text = S.current.all; + } + })); + + reaction((_) => widget.sendViewModel.outputs[_selectedOutput].address, ((address) { + if (_addressControllers[_selectedOutput].text != address) { + _addressControllers[_selectedOutput].text = address; + } + })); + + reaction((_) => widget.sendViewModel.outputs[_selectedOutput].memo, (String memo) { + if (memo != _memoControllers[_selectedOutput].text) { + _memoControllers[_selectedOutput].text = memo; + } + }); + + if (widget.initialPaymentRequest != null && + widget.sendViewModel.walletCurrencyName == + widget.initialPaymentRequest!.scheme.toLowerCase()) { + _addressControllers[0].text = widget.initialPaymentRequest!.address; + _amountControllers[0].text = widget.initialPaymentRequest!.amount; + _memoControllers[0].text = widget.initialPaymentRequest!.note; + } + + /// if the current wallet doesn't match the one in the qr code + if (widget.initialPaymentRequest != null && + widget.sendViewModel.walletCurrencyName != + widget.initialPaymentRequest!.scheme.toLowerCase()) { + WidgetsBinding.instance.addPostFrameCallback( + (timeStamp) { + if (mounted) { + final prefix = widget.initialPaymentRequest!.scheme.isNotEmpty + ? "${widget.initialPaymentRequest!.scheme}:" + : ""; + final amount = widget.initialPaymentRequest!.amount.isNotEmpty + ? "?amount=${widget.initialPaymentRequest!.amount}" + : ""; + final uri = prefix + widget.initialPaymentRequest!.address + amount; + _handlePaymentFlow(uri, widget.initialPaymentRequest!); + } + }, + ); + } + + _addressFocusNode.addListener(() async { + if (!_addressFocusNode.hasFocus && _addressControllers[_selectedOutput].text.isNotEmpty) { + final output = widget.sendViewModel.outputs[_selectedOutput]; + output.fetchParsedAddress(context).then((val) { + if (_addressControllers[_selectedOutput].text != output.extractedAddress) { + _addressControllers[_selectedOutput].text = output.extractedAddress; + } + }); + } + }); + } + + @override + Widget build(BuildContext context) { + return Observer(builder: (_) { + final output = widget.sendViewModel.outputs[_selectedOutput]; + return SafeArea( + bottom: false, + child: KeyboardHideOverlay( + unfocusOnTap: true, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(24))), + child: SafeArea( + child: Column( + spacing: 12, + mainAxisSize: MainAxisSize.max, + children: [ + ModalTopBar( + title: widget.mode.title, + subtitle: widget.mode.description, + leadingIcon: Icon(Icons.close), + onLeadingPressed: Navigator.of(context, rootNavigator: true).pop, + trailingWidget: Observer( + builder: (_) => Row( + spacing: 8, + children: [ + if (widget.sendViewModel.outputs.length > 1) + ModernButton( + size: 36, + icon: SvgPicture.asset( + "assets/new-ui/remove_recipient.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + onPressed: () { + final outputIndex = _selectedOutput; + if (_selectedOutput != 0) { + _setOutput(_selectedOutput - 1); + } else { + _setOutput(1); + } + _removeInputControllers(outputIndex); + widget.sendViewModel.removeOutput(output); + if (outputIndex == 0) _setOutput(0); + }), + if (widget.mode == SendPageModes.normal && + widget.sendViewModel.hasMultiRecipient) + ModernButton( + size: 36, + icon: Icon(Icons.add), + onPressed: () { + _addInputControllers(); + widget.sendViewModel.addOutput(); + _setOutput(widget.sendViewModel.outputs.length - 1); + }), + if (widget.mode.helpContent != null) + ModernButton( + size: 36, + icon: SvgPicture.asset( + "assets/new-ui/help.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, BlendMode.srcIn), + ), + onPressed: () { + Navigator.of(context).push(CupertinoPageRoute( + builder: (context) => Material( + child: SendHelpPage(content: widget.mode.helpContent!)))); + }) + ], + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + children: [ + Form( + key: _formKey, + child: DirectionalAnimatedSwitcher( + duration: Duration(milliseconds: 300), + child: Column( + key: ValueKey(_selectedOutput), + spacing: 24, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.mode.showAddressField) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 12, + children: [ + Text(S.of(context).address_or_alias), + NewSendAddressInput( + displayName: output.displayName, + validator: output.isParsedAddress + ? widget.sendViewModel.textValidator + : widget.sendViewModel.addressValidator, + addressController: _addressControllers[_selectedOutput], + focusNode: _addressFocusNode, + onURIScanned: (uri) async { + output.resetParsedAddress(); + await output.fetchParsedAddress(context); + + // Process the payment through the new flow + await _handlePaymentFlow( + uri.toString(), + PaymentRequest.fromString(uri.toString()), + ); + }, + onEditingComplete: () { + _addressFocusNode.unfocus(); + // output.fetchParsedAddress(context).then((val){ + // if(_addressControllers[_selectedOutput].text != output.extractedAddress) { + // _addressControllers[_selectedOutput].text = output.extractedAddress; + // } + // }); + }, + onPushAddressBookButton: (context) async { + output.resetParsedAddress(); + }, + onSelectedContact: (contact) { + output.loadContact(contact); + }, + onPushPasteButton: (context) async { + if(_justHandledPasteButton) return; + _justHandledPasteButton = true; + try { + output.resetParsedAddress(); + await output.fetchParsedAddress(context); + + final address = output.isParsedAddress + ? output.extractedAddress + : output.address; + + await _handlePaymentFlow( + address, + PaymentRequest( + address, + _amountControllers[_selectedOutput].text, + _memoControllers[_selectedOutput].text, + "", + null, + ), + ); + } finally { + _justHandledPasteButton = false; + } + + _handleLightningInvoicePaste(); + }, + selectedCurrency: + widget.sendViewModel.selectedCryptoCurrency, + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 12, + children: [ + Text(S.of(context).amount), + NewSendAmountInput( + validator: output.sendAll + ? widget.sendViewModel.allAmountValidator + : widget.sendViewModel.amountValidator(output), + amountController: _amountControllers[_selectedOutput], + currency: _fiatInputMode + ? widget.sendViewModel.fiatCurrency.title + : widget.sendViewModel.selectedCryptoCurrencySymbol, + currencyIconPath: _fiatInputMode + ? "" + : widget.sendViewModel.selectedCryptoCurrency + .iconPath ?? + "", + hasPicker: (_fiatInputMode || + widget.sendViewModel.hasMultipleTokens), + onPickerClicked: () { + _presentCurrencyPicker(context); + }, + ), + FiatAmountBar( + fiatInputMode: _fiatInputMode, + onSwitchButtonPressed: () { + setState(() { + _fiatInputMode = !_fiatInputMode; + _amountControllers[_selectedOutput].text = + _fiatInputMode + ? output.fiatAmount + : output.displayCryptoAmount; + }); + }, + fiatAmount: _wrapAmount(output.roundedFiatAmount(6), 20), + cryptoAmount: + _wrapAmount(output.roundedCryptoAmount(6), 20), + allAmount: widget.sendViewModel.balance, + cryptoCurrency: + widget.sendViewModel.selectedCryptoCurrencySymbol, + fiatCurrency: widget.sendViewModel.fiatCurrency.title, + onAllButtonPressed: () async { + output.setSendAll( + await widget.sendViewModel.sendingBalance); + }, + ), + ], + ), + if (widget.sendViewModel.isMwebAvailable && widget.mode == SendPageModes.normal) + StandardCheckbox( + caption: S.of(context).litecoin_mweb_allow_coins, + captionColor: Theme.of(context).colorScheme.onSurface, + borderColor: Theme.of(context).colorScheme.primary, + iconColor: Theme.of(context).colorScheme.primary, + value: [UnspentCoinType.any, UnspentCoinType.mweb] + .contains(widget.sendViewModel.coinTypeToSpendFrom), + onChanged: (value) => + widget.sendViewModel.setAllowMwebCoins(value), + ), + if(widget.sendViewModel.hasMemos) + Observer( + builder: (_) => NewSendMemoInput( + memoController: _memoControllers[_selectedOutput], + maxMemoLength: widget.sendViewModel.maxMemoLength, + memoLength: output.memo.length, + )), + if (widget.sendViewModel.hasCoinControl || + widget.sendViewModel.hasFees) + AnimatedDropdown( + dropdownText: S.of(context).advanced_settings, + content: Column(children: [ + if (widget.sendViewModel.hasFees) + ListItemRegularRowWidget( + keyValue: "", + label: S.of(context).fees, + subtitle: + "~${output.estimatedFee} ${widget.sendViewModel.currencySymbol} (${output.estimatedFeeFiatAmount} ${widget.sendViewModel.fiatCurrency})", + onTap: () { + if (widget + .sendViewModel.feesViewModel.hasFeesPriority) + pickTransactionPriority(context, output); + }, + ), + if (widget.sendViewModel.hasCoinControl) + ListItemRegularRowWidget( + keyValue: "", + label: "Coin Control", + onTap: () => Navigator.of(context) + .pushNamed(Routes.unspentCoinsList), + ), + ]), + ) + ], + ), + ), + ), + Observer( + builder: (_) => Column( + spacing: 12, + children: [ + if (!widget.sendViewModel.isReadyForSend) + SendSyncingIndicator( + status: widget.sendViewModel.wallet.syncStatus), + if (widget.sendViewModel.outputs.length > 1) + RecipientDotRow( + numDots: widget.sendViewModel.outputs.length, + onSelected: _setOutput, + selectedDot: _selectedOutput, + ), + Observer( + builder: (_) { + return LoadingPrimaryButton( + key: ValueKey('send_page_send_button_key'), + onPressed: () async { + //Request dummy node to get the focus out of the text fields + FocusScope.of(context).requestFocus(FocusNode()); + + if (widget.sendViewModel.state is IsExecutingState) return; + + if (widget.mode == SendPageModes.normal) { + _handleSend(); + } else if (widget.mode == SendPageModes.lightningDeposit || + widget.mode == SendPageModes.mwebDeposit) { + Navigator.of(context).push(CupertinoPageRoute( + builder: (context) => Material( + child: L2ActionWalletSelector( + showOtherWallets: false, + action: l2actions.deposit, + sendViewModel: widget.sendViewModel, + contactListViewModel: + widget.contactListViewModel, + walletSwitcherViewModel: + widget.walletSwitcherViewModel, + onSendInitiated: _handleSend, + )))); + } else if (widget.mode == + SendPageModes.lightningWithdrawal || + widget.mode == SendPageModes.mwebWithdrawal) { + Navigator.of(context).push(CupertinoPageRoute( + builder: (context) => Material( + child: L2ActionWalletSelector( + showOtherWallets: false, + action: l2actions.withdraw, + sendViewModel: widget.sendViewModel, + contactListViewModel: + widget.contactListViewModel, + walletSwitcherViewModel: + widget.walletSwitcherViewModel, + onSendInitiated: _handleSend, + )))); + } + }, + text: S.of(context).continue_text, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + isLoading: widget.sendViewModel.state is IsExecutingState || + widget.sendViewModel.state is TransactionCommitting || + widget.sendViewModel.state + is IsAwaitingDeviceResponseState || + widget.sendViewModel.state + is LoadingTemplateExecutingState, + isDisabled: !widget.sendViewModel.isReadyForSend || + widget.sendViewModel.state is ExecutedSuccessfullyState, + ); + }, + ), + SizedBox(), + ], + ), + ) + ], + ), + ), + ) + ], + ), + ), + ), + ), + ); + }); + } + + void _setOutput(int index) { + setState(() { + _selectedOutput = index; + }); + // final output = widget.sendViewModel.outputs[index]; + // _amountController.text = _fiatInputMode ? output.fiatAmount : output.cryptoAmount; + // _addressController.text = output.address; + } + + void _addInputControllers() { + _amountControllers.add(TextEditingController()); + _addressControllers.add(TextEditingController()); + _memoControllers.add(TextEditingController()); + + _amountControllers[_amountControllers.length - 1].addListener(() { + if (_selectedOutput > widget.sendViewModel.outputs.length - 1) { + printV( + "_selectedOutput > widget.sendViewModel.outputs.length - 1! this should NOT happen!"); + return; + } + + final amount = _amountControllers[_selectedOutput].text.replaceAll(',', '.'); + final output = widget.sendViewModel.outputs[_selectedOutput]; + + if (_fiatInputMode) { + if (amount != output.fiatAmount) { + output.sendAll = false; + output.setFiatAmount(amount); + } + } else { + final isAll = context.mounted && amount != S.of(context).all; + if (output.sendAll && isAll) { + output.sendAll = false; + } + + if (S.current.all.contains(amount)) return; + + final cAmount = widget.sendViewModel.amountParsingProxy.getDisplayCryptoAmount( + output.cryptoAmount, widget.sendViewModel.selectedCryptoCurrency); + if (amount != cAmount) { + final newAmount = widget.sendViewModel.amountParsingProxy + .getCanonicalCryptoAmount(amount, widget.sendViewModel.selectedCryptoCurrency); + output.setCryptoAmount(newAmount); + } + } + }); + + _addressControllers[_amountControllers.length - 1].addListener(() { + if (_selectedOutput > widget.sendViewModel.outputs.length - 1) { + printV( + "_selectedOutput > widget.sendViewModel.outputs.length - 1! this should NOT happen!"); + return; + } + + final address = _addressControllers[_selectedOutput].text; + final output = widget.sendViewModel.outputs[_selectedOutput]; + + if (output.address != address && output.extractedAddress != address) { + output.resetParsedAddress(); + output.address = address; + } + }); + + _memoControllers[_amountControllers.length - 1].addListener(() { + if (_selectedOutput > widget.sendViewModel.outputs.length - 1) { + printV( + "_selectedOutput > widget.sendViewModel.outputs.length - 1! this should NOT happen!"); + return; + } + final memo = _memoControllers[_selectedOutput].text; + final output = widget.sendViewModel.outputs[_selectedOutput]; + + if (memo != output.memo && memo.length <= widget.sendViewModel.maxMemoLength) { + output.memo = memo; + } + + }); + } + + void _handleSend() async { + //TODO refactor this action. code was copied over from old ui. i don't like it. + + if (_formKey.currentState != null && !_formKey.currentState!.validate()) { + if (widget.sendViewModel.outputs.length > 1) { + showErrorValidationAlert(context); + } + + return; + } + + final notValidItems = widget.sendViewModel.outputs + .where((item) => item.address.isEmpty || (!item.sendAll && item.cryptoAmount.isEmpty)) + .toList(); + + if (notValidItems.isNotEmpty) { + showErrorValidationAlert(context); + return; + } + + if (widget.sendViewModel.wallet.isHardwareWallet) { + if (!widget.sendViewModel.hardwareWalletViewModel!.isConnected) { + await Navigator.of(context).pushNamed(Routes.connectDevices, + arguments: ConnectDevicePageParams( + walletType: widget.sendViewModel.walletType, + hardwareWalletType: widget.sendViewModel.wallet.walletInfo.hardwareWalletType!, + onConnectDevice: (BuildContext context, _) { + widget.sendViewModel.hardwareWalletViewModel! + .initWallet(widget.sendViewModel.wallet); + Navigator.of(context).pop(); + }, + )); + } else { + widget.sendViewModel.hardwareWalletViewModel!.initWallet(widget.sendViewModel.wallet); + } + } + + if (widget.sendViewModel.wallet.type == WalletType.monero) { + int amount = 0; + for (var item in widget.sendViewModel.outputs) { + amount += item.formattedCryptoAmount; + } + if (monero!.needExportOutputs(widget.sendViewModel.wallet, amount)) { + await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, + arguments: monero!.exportOutputsUR(widget.sendViewModel.wallet)); + await Future.delayed(Duration(seconds: 1)); // wait for monero to refresh the state + } + if (monero!.needExportOutputs(widget.sendViewModel.wallet, amount)) { + return; + } + } + + final check = widget.sendViewModel.shouldDisplayTotp(); + widget.authService.authenticateAction( + navigatorKey.currentContext ?? context, + conditionToDetermineIfToUse2FA: check, + onAuthSuccess: (value) async { + if (value) { + if (!widget.mode.popOnConfirmation) { + Navigator.of(context, rootNavigator: true).pop(); + } + showModalBottomSheet( + isScrollControlled: true, + context: navigatorKey.currentContext ?? context, + backgroundColor: Colors.transparent, + builder: (context) { + return SendConfirmSheet( + title: widget.mode.title, + iconPath: widget.mode.helpContent?.imagePath, + sendViewModel: widget.sendViewModel, + ); + }).then((value) async { + if (widget.sendViewModel.state is TransactionCommitted && + widget.mode.popOnConfirmation) { + if (!mounted) return; + Navigator.of(context, rootNavigator: true).pop(); + } + widget.sendViewModel.dismissTransaction(); + }); + + await widget.sendViewModel.createTransaction(); + } + }, + ); + } + + void _removeInputControllers(int index) { + _amountControllers.removeAt(index); + _addressControllers.removeAt(index); + _memoControllers.removeAt(index); + } + + void _presentCurrencyPicker(BuildContext context) { + if (!_fiatInputMode && !widget.sendViewModel.hasMultipleTokens) { + return; + } + + final output = widget.sendViewModel.outputs[_selectedOutput]; + + showPopUp( + context: context, + builder: (_) => CurrencyPicker( + key: ValueKey('send_page_currency_picker_dialog_button_key'), + selectedAtIndex: _fiatInputMode + ? widget.sendViewModel.fiatCurrencies.indexOf(widget.sendViewModel.fiatCurrency) + : widget.sendViewModel.currencies.indexOf(widget.sendViewModel.selectedCryptoCurrency), + items: + _fiatInputMode ? widget.sendViewModel.fiatCurrencies : widget.sendViewModel.currencies, + hintText: S.of(context).search_currency, + onItemSelected: (Currency cur) async { + late final selectedCurrency; + if (_fiatInputMode) { + selectedCurrency = widget.sendViewModel.setFiatCurrency(cur as FiatCurrency); + } else { + selectedCurrency = + widget.sendViewModel.selectedCryptoCurrency = (cur as CryptoCurrency); + } + await output.calculateEstimatedFee(); + return selectedCurrency; + }, + ), + ); + } + + void _handleLightningInvoicePaste() { + try { + final lnAmount = CryptoCurrency.btcln.formatAmount( + BigInt.from(getBolt11Amount(_addressControllers[_selectedOutput].text) ?? 0)); + if (lnAmount != 0) { + _amountControllers[_selectedOutput].text = lnAmount; + } + } catch (_) {} + } + + Future _handlePaymentFlow(String uri, PaymentRequest paymentRequest) async { + if (uri.contains('@') || paymentRequest.address.contains('@')) return; + + if (OpenCryptoPayService.isOpenCryptoPayQR(uri) && + widget.sendViewModel.selectedCryptoCurrency != CryptoCurrency.btcln) { + widget.sendViewModel.createOpenCryptoPayTransaction(uri); + return; + } + + try { + final result = await widget.paymentViewModel.processAddress(uri); + + if (paymentRequest.contractAddress != null) { + await widget.sendViewModel.fetchTokenForContractAddress(paymentRequest.contractAddress!); + } + + switch (result.type) { + case PaymentFlowType.singleWallet: + case PaymentFlowType.multipleWallets: + case PaymentFlowType.noWallets: + await _showPaymentConfirmation( + widget.paymentViewModel, + widget.walletSwitcherViewModel, + paymentRequest, + result, + ); + break; + case PaymentFlowType.evmNetworkSelection: + await _showTokenSelectionFlow( + widget.paymentViewModel, + widget.walletSwitcherViewModel, + paymentRequest, + fixedNetwork: result.walletType, + ); + break; + case PaymentFlowType.solanaTokenSelection: + await _showTokenSelectionFlow( + widget.paymentViewModel, + widget.walletSwitcherViewModel, + paymentRequest, + fixedNetwork: WalletType.solana, + ); + break; + case PaymentFlowType.tronTokenSelection: + await _showTokenSelectionFlow( + widget.paymentViewModel, + widget.walletSwitcherViewModel, + paymentRequest, + fixedNetwork: WalletType.tron, + ); + + break; + case PaymentFlowType.currentWalletCompatible: + case PaymentFlowType.error: + case PaymentFlowType.incompatible: + _applyPaymentRequest(paymentRequest); + break; + } + } catch (e) { + printV('Payment flow error: $e'); + _applyPaymentRequest(paymentRequest); + } + } + + Future _showPaymentConfirmation( + PaymentViewModel paymentViewModel, + WalletSwitcherViewModel walletSwitcherViewModel, + PaymentRequest paymentRequest, + PaymentFlowResult result, + ) async { + if (!context.mounted) { + return; + } + + await showModalBottomSheet( + context: context, + isDismissible: true, + isScrollControlled: true, + builder: (BuildContext context) { + return PaymentConfirmationBottomSheet( + paymentFlowResult: result, + paymentViewModel: paymentViewModel, + walletSwitcherViewModel: walletSwitcherViewModel, + paymentRequest: paymentRequest, + onSelectWallet: () => _handleSelectWallet( + paymentViewModel, + walletSwitcherViewModel, + paymentRequest, + result, + ), + onChangeWallet: () => _handleChangeWallet( + paymentViewModel, + walletSwitcherViewModel, + paymentRequest, + result, + ), + onSwap: (bottomSheetContext) => + _handleSwapFlow(paymentViewModel, result, bottomSheetContext), + onSwitchNetwork: () => _handleSwitchNetwork( + paymentViewModel, + walletSwitcherViewModel, + paymentRequest, + result, + ), + ); + }, + ); + } + + Future _showTokenSelectionFlow( + PaymentViewModel paymentViewModel, + WalletSwitcherViewModel walletSwitcherViewModel, + PaymentRequest paymentRequest, { + WalletType? fixedNetwork, + }) async { + if (!context.mounted) { + return; + } + + await showModalBottomSheet( + context: context, + isDismissible: true, + isScrollControlled: true, + builder: (BuildContext context) { + return TokenSelectionBottomSheet( + paymentViewModel: paymentViewModel, + paymentRequest: paymentRequest, + fixedNetwork: fixedNetwork, + onNext: (PaymentFlowResult newResult) { + final selectedChainId = newResult.chainId; + final isCompatible = + selectedChainId == evm!.getSelectedChainId(widget.sendViewModel.wallet); + + if (isCompatible) { + widget.sendViewModel.setSelectedCryptoCurrency( + newResult.addressDetectionResult!.detectedCurrency!.title, + ); + _applyPaymentRequest(paymentRequest); + } else { + _showPaymentConfirmation( + paymentViewModel, + walletSwitcherViewModel, + paymentRequest, + newResult, + ); + } + }, + ); + }, + ); + } + + Future _handleSelectWallet( + PaymentViewModel paymentViewModel, + WalletSwitcherViewModel walletSwitcherViewModel, + PaymentRequest paymentRequest, + PaymentFlowResult result, + ) async { + Navigator.of(context).pop(); + + await showModalBottomSheet( + context: context, + isDismissible: true, + isScrollControlled: true, + builder: (BuildContext dialogContext) { + return WalletSwitcherBottomSheet( + viewModel: walletSwitcherViewModel, + filterWalletType: paymentViewModel.detectedWalletType, + ); + }, + ); + + final success = await walletSwitcherViewModel.switchToSelectedWallet(); + + if (success) { + if (isEVMCompatibleChain(widget.sendViewModel.wallet.type) && result.chainId != null) { + final appStore = getIt.get(); + final node = appStore.settingsStore.getCurrentNode( + widget.sendViewModel.wallet.type, + chainId: result.chainId, + ); + await evm!.selectChain(widget.sendViewModel.wallet, result.chainId!, node: node); + } + + await widget.sendViewModel.wallet.updateBalance(); + + final detectedCurrency = result.addressDetectionResult!.detectedCurrency; + if (detectedCurrency != null) { + widget.sendViewModel.setSelectedCryptoCurrency(detectedCurrency.title); + } + + _applyPaymentRequest(paymentRequest); + } + } + + Future _handleChangeWallet( + PaymentViewModel paymentViewModel, + WalletSwitcherViewModel walletSwitcherViewModel, + PaymentRequest paymentRequest, + PaymentFlowResult result, + ) async { + if (mounted && Navigator.of(context).canPop()) { + Navigator.of(context).pop(); + } + + if (result.type == PaymentFlowType.singleWallet && result.wallet != null) { + walletSwitcherViewModel.selectWallet(result.wallet!); + final success = await walletSwitcherViewModel.switchToSelectedWallet(); + if (success) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + showModalBottomSheet( + context: context, + isDismissible: false, + builder: (BuildContext context) { + loadingBottomSheetContext = context; + return LoadingBottomSheet( + titleText: S.of(context).loading_your_wallet, + ); + }, + ); + } + }); + + // If EVM wallet and chainId is specified, switch to that chain + if (isEVMCompatibleChain(widget.sendViewModel.wallet.type) && result.chainId != null) { + final appStore = getIt.get(); + final node = appStore.settingsStore.getCurrentNode( + widget.sendViewModel.wallet.type, + chainId: result.chainId, + ); + await evm!.selectChain(widget.sendViewModel.wallet, result.chainId!, node: node); + } + + await Future.delayed(const Duration(seconds: 2)); + if (loadingBottomSheetContext != null && + loadingBottomSheetContext!.mounted && + Navigator.canPop(loadingBottomSheetContext!)) { + Navigator.of(loadingBottomSheetContext!).pop(); + } + + await widget.sendViewModel.wallet.updateBalance(); + widget.sendViewModel + .setSelectedCryptoCurrency(result.addressDetectionResult!.detectedCurrency!.title); + _applyPaymentRequest(paymentRequest); + } + } else if (result.wallets.isNotEmpty && result.wallets.length == 1) { + walletSwitcherViewModel.selectWallet(result.wallets.first); + final success = await walletSwitcherViewModel.switchToSelectedWallet(); + if (success) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (context.mounted) { + showModalBottomSheet( + context: context, + isDismissible: false, + builder: (BuildContext context) { + loadingBottomSheetContext = context; + return LoadingBottomSheet( + titleText: S.of(context).loading_your_wallet, + ); + }, + ); + } + }); + + // If EVM wallet and chainId is specified, switch to that chain + if (isEVMCompatibleChain(widget.sendViewModel.wallet.type) && result.chainId != null) { + final appStore = getIt.get(); + final node = appStore.settingsStore.getCurrentNode( + widget.sendViewModel.wallet.type, + chainId: result.chainId, + ); + await evm!.selectChain(widget.sendViewModel.wallet, result.chainId!, node: node); + } + + await Future.delayed(const Duration(seconds: 2)); + if (loadingBottomSheetContext != null && loadingBottomSheetContext!.mounted) { + Navigator.of(loadingBottomSheetContext!).pop(); + } + + await widget.sendViewModel.wallet.updateBalance(); + widget.sendViewModel + .setSelectedCryptoCurrency(result.addressDetectionResult!.detectedCurrency!.title); + _applyPaymentRequest(paymentRequest); + } + } + } + + Future _handleSwitchNetwork( + PaymentViewModel paymentViewModel, + WalletSwitcherViewModel walletSwitcherViewModel, + PaymentRequest paymentRequest, + PaymentFlowResult result, + ) async { + if (result.type != PaymentFlowType.evmNetworkSelection || result.wallet == null) return; + + if (context.mounted && Navigator.of(context).canPop()) { + Navigator.of(context).pop(); + } + + try { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (context.mounted) { + showModalBottomSheet( + context: context, + isDismissible: false, + builder: (BuildContext context) { + loadingBottomSheetContext = context; + return LoadingBottomSheet( + titleText: S.of(context).loading_your_wallet, + ); + }, + ); + } + }); + + await paymentViewModel.selectChain(); + + await Future.delayed(const Duration(seconds: 2)); + if (loadingBottomSheetContext != null && loadingBottomSheetContext!.mounted) { + Navigator.of(loadingBottomSheetContext!).pop(); + } + + await widget.sendViewModel.wallet.updateBalance(); + final detectedCurrency = result.addressDetectionResult?.detectedCurrency; + if (detectedCurrency != null) { + widget.sendViewModel.setSelectedCryptoCurrency(detectedCurrency.title); + } + _applyPaymentRequest(paymentRequest); + } catch (e) { + if (loadingBottomSheetContext != null && loadingBottomSheetContext!.mounted) { + Navigator.of(loadingBottomSheetContext!).pop(); + } + printV('Switch network error: $e'); + } + } + + /// Apply payment request to current form + void _applyPaymentRequest(PaymentRequest paymentRequest) { + if (widget.sendViewModel.usePayjoin) { + widget.sendViewModel.payjoinUri = paymentRequest.pjUri; + } + _addressControllers[_selectedOutput].text = paymentRequest.address; + if (paymentRequest.amount.isNotEmpty) { + try{ + _amountControllers[_selectedOutput].text = widget.sendViewModel.amountParsingProxy + .getDisplayCryptoAmount( + paymentRequest.amount, widget.sendViewModel.selectedCryptoCurrency); + } catch(e) { + + } + } + _memoControllers[_selectedOutput].text = paymentRequest.note; + } + + Future _handleSwapFlow( + PaymentViewModel paymentViewModel, + PaymentFlowResult result, + BuildContext bottomSheetContext, + ) async { + Navigator.of(bottomSheetContext).pop(); + + await Future.delayed(const Duration(milliseconds: 100)); + + if (!mounted) return; + + final bottomSheet = getIt.get(param1: result); + await showModalBottomSheet( + context: context, + isDismissible: true, + isScrollControlled: true, + builder: (BuildContext context) => bottomSheet, + ); + } + + void showErrorValidationAlert(BuildContext context) { + int emptyAddressIndex = -1; + for (int i = 0; i < widget.sendViewModel.outputs.length; i++) { + if (widget.sendViewModel.outputs[i].address.isEmpty) { + emptyAddressIndex = i; + break; + } + } + + showPopUp( + context: context, + builder: (context) => AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: emptyAddressIndex == -1 + ? S.of(context).check_receiver_forms + : S.of(context).enter_recipient_address, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(), + ), + ); + if (emptyAddressIndex != -1) { + _setOutput(emptyAddressIndex); + } + } + + bool isRegularElectrumAddress(String address) { + final supportedTypes = [CryptoCurrency.btc, CryptoCurrency.ltc, CryptoCurrency.bch]; + final excludedPatterns = [ + RegExp(AddressValidator.silentPaymentAddressPatternMainnet), + RegExp(AddressValidator.silentPaymentAddressPatternTestnet), + RegExp(AddressValidator.mWebAddressPattern), + RegExp(AddressValidator.bolt11InvoiceMatcher), + ]; + + final trimmed = address.trim(); + + bool isValid = false; + for (final type in supportedTypes) { + final addressPattern = AddressValidator.getAddressFromStringPattern(type); + if (addressPattern != null) { + final regex = RegExp('^$addressPattern\$'); + if (regex.hasMatch(trimmed)) { + isValid = true; + break; + } + } + } + + for (final pattern in excludedPatterns) { + if (pattern.hasMatch(trimmed)) return false; + } + + return isValid; + } + + Future pickTransactionPriority(BuildContext pageContext, Output output) async { + final items = priorityForWalletType(widget.sendViewModel.walletType); + final selectedItem = items.indexOf(widget.sendViewModel.feesViewModel.transactionPriority); + final customItemIndex = widget.sendViewModel.feesViewModel.getCustomPriorityIndex(items); + final isBitcoinWallet = widget.sendViewModel.walletType == WalletType.bitcoin; + final maxCustomFeeRate = widget.sendViewModel.feesViewModel.maxCustomFeeRate?.toDouble(); + + FocusManager.instance.primaryFocus?.unfocus(); + + await showCupertinoModalBottomSheet( + context: pageContext, + expand: false, + builder: (BuildContext modalContext) { + int selectedIdx = selectedItem; + return Observer(builder: (context) { + double? customFeeRate = isBitcoinWallet + ? widget.sendViewModel.feesViewModel.customBitcoinFeeRate.toDouble() + : null; + return StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return IntrinsicHeight( + // height: MediaQuery.of(context).size.height*0.4, + child: ModalNavigator( + parentContext: modalContext, + heightMode: ModalHeightModes.autoLock, + rootPage: Material( + child: NewPicker( + title: S.of(context).set_fees, + description: S.of(context).set_fees_desc, + sliderPageTitle: S.of(context).custom_fee, + sliderInitialValue: customFeeRate, + sliderMaxValue: maxCustomFeeRate, + sliderValueDescription: "sat/byte", + items: items + .map((item) => PickerItem( + title: item.title, + subtitle: item.description, + hint: item.hint, + value: item, + isSliderItem: items.indexOf(item) == customItemIndex, + )) + .toList(), + onItemSelected: (TransactionPriority priority) async { + widget.sendViewModel.feesViewModel.setTransactionPriority(priority); + setState(() => selectedIdx = items.indexOf(priority)); + await output.calculateEstimatedFee(); + }, + onSliderChanged: (double value) { + widget.sendViewModel.feesViewModel.customBitcoinFeeRate = value.round(); + }, + selectedIndex: selectedIdx), + )), + ); + }, + ); + }); + }, + ); + } + + String _wrapAmount(String amount, int maxChars) { + return amount.length <= maxChars ? amount : "${amount.substring(0, maxChars - 3)}..."; + } +} + +class SendHelpPage extends StatelessWidget { + const SendHelpPage({super.key, required this.content}); + + final SendPageHelpContent content; + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ModalTopBar( + title: content.title, + leadingIcon: Icon(Icons.arrow_back_ios_new), + onLeadingPressed: Navigator.of(context).pop, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + spacing: 12, + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset(content.imagePath), + Text( + content.description, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface), + ), + if (content.disclaimer != null) ...[ + SizedBox(), + SizedBox(), + Text(content.disclaimer!, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant)), + ] + ], + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 18), + child: NewPrimaryButton( + onPressed: Navigator.of(context).pop, + text: S.of(context).i_understand, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary)) + ], + ), + ); + } +} diff --git a/lib/new-ui/pages/settings_page.dart b/lib/new-ui/pages/settings_page.dart new file mode 100644 index 000000000..470d745b2 --- /dev/null +++ b/lib/new-ui/pages/settings_page.dart @@ -0,0 +1,147 @@ +import 'package:cake_wallet/core/auth_service.dart'; +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart'; +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/modal_navigator.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:flutter/material.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; +import "package:cw_core/wallet_type.dart"; + +bool _trueFunc(DashboardViewModel _) => true; + +bool _falseFunc(DashboardViewModel _) => false; + +bool _isBtc(DashboardViewModel vm) => vm.wallet.type == WalletType.bitcoin; + +bool _hasMweb(DashboardViewModel vm) => vm.hasMweb; + +bool _isCupcake(DashboardViewModel vm) => vm.wallet.hardwareWalletType == HardwareWalletType.cupcake; + +class SettingsListItem { + final String iconPath; + final String title; + final String route; + final Object? routeArgs; + final bool requireAuth; + final bool Function(DashboardViewModel) use2fa; + final bool Function(DashboardViewModel) condition; + + const SettingsListItem(this.iconPath, this.title, this.route, {this.routeArgs = null, this.requireAuth = false, this.use2fa = _falseFunc, this.condition = _trueFunc}); +} + +class SettingsSectionData { + final String title; + final String titleIconPath; + final List items; + + const SettingsSectionData(this.title, this.titleIconPath, this.items); + + static SettingsSectionData walletSettings = + SettingsSectionData(S.current.wallet_settings, "assets/new-ui/wallet_settings.svg", [ + SettingsListItem("assets/new-ui/settings_row_icons/nodes.svg", S.current.nodes, Routes.manageNodes), + SettingsListItem("assets/new-ui/settings_row_icons/privacy.svg", S.current.privacy, Routes.privacyPage), + SettingsListItem("assets/new-ui/settings_row_icons/seed.svg", S.current.seed_and_keys, Routes.showKeys, + routeArgs: true, requireAuth: true, use2fa: (vm)=>vm.settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings), + SettingsListItem("assets/new-ui/settings_row_icons/lightning_username.svg", + "Lightning ${S.current.username}", Routes.lightningUsernamePage, condition: _isBtc), + SettingsListItem("assets/new-ui/settings_row_icons/silent-payments.svg", S.current.silent_payments_settings, Routes.silentPaymentsSettings, condition: _isBtc), + SettingsListItem("assets/new-ui/settings_row_icons/mweb.svg", S.current.litecoin_mweb_settings, Routes.mwebSettings, condition: _hasMweb), + SettingsListItem("assets/new-ui/settings_row_icons/cupcake.svg", S.current.export_outputs, Routes.urqrAnimatedPage, routeArgs: {'export-outputs': 'export-outputs'}, condition: _isCupcake), + SettingsListItem("assets/new-ui/settings_row_icons/other.svg", S.current.other, Routes.otherSettingsPage), + ]); + + static SettingsSectionData appSettings = + SettingsSectionData(S.current.app_settings, "assets/new-ui/app_settings.svg", [ + SettingsListItem("assets/new-ui/settings_row_icons/connections.svg", S.current.connections, Routes.connectionSync), + // SettingsListItem("assets/new-ui/settings_row_icons/defaults.svg", "Defaults", ""), + SettingsListItem("assets/new-ui/settings_row_icons/display.svg", S.current.display, Routes.displaySettingsPage), + SettingsListItem("assets/new-ui/settings_row_icons/security.svg", S.current.security, Routes.securityBackupPage), + SettingsListItem("assets/new-ui/settings_row_icons/backup.svg", S.current.backup, Routes.backup, requireAuth: true, use2fa: (vm)=>vm.settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings), + ]); + + static SettingsSectionData otherSettings = SettingsSectionData("", "", [ + SettingsListItem("assets/new-ui/settings_row_icons/support.svg", S.current.settings_support, Routes.support), + SettingsListItem("assets/new-ui/settings_row_icons/info.svg", S.current.about, Routes.aboutPage), + ]); + + static List all = [walletSettings, appSettings, otherSettings]; +} + +class NewSettingsPage extends StatelessWidget { + const NewSettingsPage({super.key, required this.dashboardViewModel, required this.authService}); + + final DashboardViewModel dashboardViewModel; + final AuthService authService; + + @override + Widget build(BuildContext context) { + return ModalNavigator( + parentContext: context, + rootPage: SettingsMainPage( + dashboardViewModel: dashboardViewModel, + authService: authService, + )); + } +} + +class SettingsMainPage extends StatelessWidget { + const SettingsMainPage({super.key, required this.dashboardViewModel, required this.authService}); + + final DashboardViewModel dashboardViewModel; + final AuthService authService; + + @override + Widget build(BuildContext context) { + Map> sections = + Map.fromEntries(SettingsSectionData.all.map((section) => MapEntry( + section.title, + section.items + .map((item) => item.condition(dashboardViewModel) + ? ListItemRegularRow( + keyValue: item.title, + label: item.title, + iconPath: item.iconPath, + onTap: () { + if (item.route.isNotEmpty) { + if(item.requireAuth) { + authService.authenticateAction(context, + conditionToDetermineIfToUse2FA: item.use2fa(dashboardViewModel), + route: item.route); + } else { + Navigator.of(context).pushNamed(item.route, arguments: item.routeArgs); + } + } + }) + : null) + .whereType() + .toList()))); + + return Container( + color: Theme.of(context).colorScheme.surface, + child: Column(children: [ + ModalTopBar( + title: "Settings", + leadingIcon: Icon(Icons.close), + onLeadingPressed: Navigator.of(context, rootNavigator: true).pop, + onTrailingPressed: () {}, + ), + Expanded( + child: ListView( + controller: ModalScrollController.of(context), + children: [Padding( + padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 20.0), + child: NewListSections( + sections: sections, + ), + ),] + ), + ), + ]), + ); + } +} \ No newline at end of file diff --git a/lib/new-ui/pages/swap_page.dart b/lib/new-ui/pages/swap_page.dart new file mode 100644 index 000000000..20b45a4c3 --- /dev/null +++ b/lib/new-ui/pages/swap_page.dart @@ -0,0 +1,1243 @@ +import 'package:cake_wallet/core/address_validator.dart'; +import 'package:cake_wallet/core/amount_validator.dart'; +import 'package:cake_wallet/core/auth_service.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/parse_address_from_domain.dart'; +import 'package:cake_wallet/entities/qr_scanner.dart'; +import 'package:cake_wallet/exchange/exchange_trade_state.dart'; +import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart'; +import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/new-ui/widgets/send_page/fiat_amount_bar.dart'; +import 'package:cake_wallet/new-ui/widgets/send_page/send_syncing_indicator.dart'; +import 'package:cake_wallet/new-ui/widgets/swap_page/provider_selector_page.dart'; +import 'package:cake_wallet/new-ui/widgets/swap_page/refund_address_modal.dart'; +import 'package:cake_wallet/new-ui/widgets/swap_page/swap_address_selection_modal.dart'; +import 'package:cake_wallet/new-ui/widgets/swap_page/swap_confirm_sheet.dart'; +import 'package:cake_wallet/new-ui/widgets/swap_page/swap_limit_popup.dart'; +import 'package:cake_wallet/new-ui/widgets/swap_page/swap_options_page.dart'; +import 'package:cake_wallet/new-ui/widgets/swap_page/swap_provider_initial_preference_modal.dart'; +import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; +import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker.dart'; +import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/utils/debounce.dart'; +import 'package:cake_wallet/utils/payment_request.dart'; +import 'package:cake_wallet/utils/permission_handler.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:flutter/services.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart'; +import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/currency.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:mobx/mobx.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; + +class NewSwapPage extends StatefulWidget { + NewSwapPage(this.exchangeViewModel, this.authService, this.initialPaymentRequest, + {required this.walletSwitcherViewModel, CryptoCurrency? initialCurrency}) { + depositWalletName = exchangeViewModel.depositCurrency == CryptoCurrency.xmr + ? exchangeViewModel.wallet.name + : null; + receiveWalletName = exchangeViewModel.receiveCurrency == CryptoCurrency.xmr + ? exchangeViewModel.wallet.name + : null; + if (initialCurrency != null) { + exchangeViewModel.changeDepositCurrency(currency: initialCurrency); + } + } + + final ExchangeViewModel exchangeViewModel; + final WalletSwitcherViewModel walletSwitcherViewModel; + final AuthService authService; + final PaymentRequest? initialPaymentRequest; + late final String? depositWalletName; + late final String? receiveWalletName; + + @override + State createState() => _NewSwapPageState(); +} + +class _NewSwapPageState extends State { + final depositKey = GlobalKey(); + final receiveKey = GlobalKey(); + final formKey = GlobalKey(); + final _depositAmountFocus = FocusNode(); + final _depositAddressFocus = FocusNode(); + final _receiveAmountFocus = FocusNode(); + final _receiveAddressFocus = FocusNode(); + final _receiveAmountDebounce = Debounce(Duration(milliseconds: 500)); + Debounce _depositAmountDebounce = Debounce(Duration(milliseconds: 500)); + + bool get _shouldWaitTillSynced => + [CryptoCurrency.xmr, CryptoCurrency.btc, CryptoCurrency.ltc] + .contains(widget.exchangeViewModel.depositCurrency) && + !(widget.exchangeViewModel.status is SyncedSyncStatus); + + @override + void initState() { + super.initState(); + if (widget.exchangeViewModel.feesViewModel.isLowFee) { + _showFeeAlert(context); + } + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!widget.exchangeViewModel.decentralizedExchangesPromptDismissed) { + showMaterialModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isDismissible: false, + builder: (context) { + return SwapProviderInitialPreferenceModal(); + }).then((val) { + widget.exchangeViewModel.dismissDecentralizedExchangesPrompt(); + if (val is bool && val == true && !widget.exchangeViewModel.forceDecentralizedExchanges) { + widget.exchangeViewModel.toggleForceDecentralizedExchanges(); + } + }); + } + + final depositAddressController = depositKey.currentState!.addressController; + final depositAmountController = depositKey.currentState!.amountController; + final receiveAddressController = receiveKey.currentState!.addressController; + final receiveAmountController = receiveKey.currentState!.amountController; + final depositFiatAmountController = depositKey.currentState!.fiatAmountController; + final receiveFiatAmountController = receiveKey.currentState!.fiatAmountController; + + depositFiatAmountController.addListener(() { + if (!depositKey.currentState!.amountFocusNode.hasFocus) { + return; + } + widget.exchangeViewModel.isFixedRateMode = false; + Future.delayed(Duration(milliseconds: 200)).then((_) { + if (double.tryParse(depositFiatAmountController.text) != null) { + widget.exchangeViewModel + .setDepositAmountFromFiat(fiatAmount: depositFiatAmountController.text); + receiveKey.currentState!.updateFiatAmount(); + } + }); + }); + receiveFiatAmountController.addListener(() { + if (!receiveKey.currentState!.amountFocusNode.hasFocus) { + return; + } + widget.exchangeViewModel.enableFixedRateMode(); + Future.delayed(Duration(milliseconds: 200)).then((_) { + if (double.tryParse(receiveFiatAmountController.text) != null) { + String text = receiveFiatAmountController.text; + if (text.contains(".")) { + text = text.replaceAll(RegExp(r'0+$'), ''); + text = text.replaceAll(RegExp(r'\.$'), ''); + } + widget.exchangeViewModel + .setReceiveAmountFromFiat(fiatAmount: receiveFiatAmountController.text); + depositKey.currentState!.updateFiatAmount(); + } + }); + }); + reaction((_) => widget.exchangeViewModel.isFixedRateMode, (val) { + Future.delayed(Duration(seconds: 3)).then((_) { + if (val) { + if (depositKey.currentState?.mounted == true) + depositKey.currentState!.updateFiatAmount(); + } else { + if (receiveKey.currentState?.mounted == true) + receiveKey.currentState!.updateFiatAmount(); + } + }); + }); + + reaction((_) => widget.exchangeViewModel.depositAmount, (String amount) { + if (widget.exchangeViewModel.isSendAllEnabled) { + depositAmountController.text = S.of(context).all; + } else if (depositAmountController.text != amount && amount != S.of(context).all) { + depositAmountController.text = amount; + } + }); + + _onCurrencyChange( + widget.exchangeViewModel.receiveCurrency, widget.exchangeViewModel, receiveKey); + _onCurrencyChange( + widget.exchangeViewModel.depositCurrency, widget.exchangeViewModel, depositKey); + + reaction( + (_) => widget.exchangeViewModel.wallet.name, + (_) => _onWalletNameChange( + widget.exchangeViewModel, widget.exchangeViewModel.receiveCurrency, receiveKey)); + + reaction( + (_) => widget.exchangeViewModel.wallet.name, + (_) => _onWalletNameChange( + widget.exchangeViewModel, widget.exchangeViewModel.depositCurrency, depositKey)); + + reaction( + (_) => widget.exchangeViewModel.receiveCurrency, + (CryptoCurrency currency) => + _onCurrencyChange(currency, widget.exchangeViewModel, receiveKey)); + + reaction( + (_) => widget.exchangeViewModel.depositCurrency, + (CryptoCurrency currency) => + _onCurrencyChange(currency, widget.exchangeViewModel, depositKey)); + + reaction((_) => widget.exchangeViewModel.depositAddress, (String address) { + if (depositKey.currentState!.addressController.text != address) { + depositKey.currentState!.addressController.text = address; + } + }); + + reaction((_) => widget.exchangeViewModel.isDepositAddressEnabled, (bool isEnabled) {}); + + reaction((_) => widget.exchangeViewModel.receiveAmount, (String amount) { + if (receiveKey.currentState!.amountController.text != amount) { + receiveKey.currentState!.amountController.text = amount; + } + }); + + reaction((_) => widget.exchangeViewModel.receiveAddress, (String address) { + if (receiveKey.currentState!.addressController.text != address) { + receiveKey.currentState!.addressController.text = address; + } + }); + + reaction((_) => widget.exchangeViewModel.isReceiveAmountEditable, + (bool isReceiveAmountEditable) {}); + + reaction((_) => widget.exchangeViewModel.tradeState, (ExchangeTradeState state) { + if (state is TradeIsCreatedFailure) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + key: ValueKey('exchange_page_trade_creation_failure_dialog_key'), + buttonKey: + ValueKey('exchange_page_trade_creation_failure_dialog_button_key'), + alertTitle: S.of(context).provider_error(state.title), + alertContent: state.error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + } + }); + } + if (state is TradeIsCreatedSuccessfully) { + if (widget.exchangeViewModel.tradeStarted) return; + final receiveAmount = widget.exchangeViewModel.receiveAmount; + // FIXME we don't know why a reset is/was needed here, it messes up ui so i removed it + // widget.exchangeViewModel.reset(); + // (widget.exchangeViewModel.tradesStore.trade?.provider == + // ExchangeProviderDescription.thorChain || + // widget.exchangeViewModel.tradesStore.trade?.provider == + // ExchangeProviderDescription.chainflip) + // ? Navigator.of(context).pushReplacementNamed(Routes.exchangeTrade) + // : Navigator.of(context).pushReplacementNamed(Routes.exchangeConfirm); + widget.exchangeViewModel.tradeStarted = true; + final vm = getIt.get(); + final page = SwapConfirmSheet( + exchangeViewModel: widget.exchangeViewModel, + exchangeTradeViewModel: vm, + receiveAmount: receiveAmount, + ); + showMaterialModalBottomSheet( + context: context, builder: (context) => page, backgroundColor: Colors.transparent); + } + }); + + reaction((_) => widget.exchangeViewModel.bestRate, (double rate) { + if (widget.exchangeViewModel.isFixedRateMode) { + widget.exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text); + } else { + printV("bestrate"); + if (depositAmountController.text == S.current.all) + widget.exchangeViewModel + .changeDepositAmount(amount: widget.exchangeViewModel.depositAmount); + else + widget.exchangeViewModel.changeDepositAmount(amount: depositAmountController.text); + } + }); + + depositAddressController.addListener( + () => widget.exchangeViewModel.depositAddress = depositAddressController.text); + + depositAmountController.addListener(() { + if (depositAmountController.text != widget.exchangeViewModel.depositAmount && + depositAmountController.text != S.of(context).all) { + widget.exchangeViewModel.isSendAllEnabled = false; + final isThorChain = widget.exchangeViewModel.selectedProviders + .any((provider) => provider is ThorChainExchangeProvider); + final isChainflip = widget.exchangeViewModel.selectedProviders + .any((provider) => provider is ChainflipExchangeProvider); + + _depositAmountDebounce = isThorChain || isChainflip + ? Debounce(Duration(milliseconds: 1000)) + : Debounce(Duration(milliseconds: 500)); + + _depositAmountDebounce.run(() { + widget.exchangeViewModel.calculateBestRate(); + widget.exchangeViewModel.changeDepositAmount(amount: depositAmountController.text); + widget.exchangeViewModel.isReceiveAmountEntered = false; + widget.exchangeViewModel.isFixedRateMode = false; + if (!receiveKey.currentState!.amountFocusNode.hasFocus) { + receiveKey.currentState!.updateFiatAmount(); + } + }); + } + }); + + receiveAddressController.addListener( + () => widget.exchangeViewModel.receiveAddress = receiveAddressController.text); + + receiveAmountController.addListener(() { + if (receiveAmountController.text != widget.exchangeViewModel.receiveAmount) { + _receiveAmountDebounce.run(() { + widget.exchangeViewModel.calculateBestRate(); + widget.exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text); + widget.exchangeViewModel.isReceiveAmountEntered = true; + widget.exchangeViewModel.enableFixedRateMode(); + if (!depositKey.currentState!.amountFocusNode.hasFocus) { + depositKey.currentState!.updateFiatAmount(); + } + }); + } + }); + + reaction((_) => widget.exchangeViewModel.wallet.walletAddresses.addressForExchange, + (String address) { + if (widget.exchangeViewModel.depositCurrency == CryptoCurrency.xmr) { + depositKey.currentState!.changeAddress(address: address); + } + + // if (widget.exchangeViewModel.receiveCurrency == CryptoCurrency.xmr) { + // receiveKey.currentState!.changeAddress(address: address); + // } + }); + + _depositAddressFocus.addListener(() async { + if (!_depositAddressFocus.hasFocus && depositAddressController.text.isNotEmpty) { + final domain = depositAddressController.text; + widget.exchangeViewModel.depositAddress = + await fetchParsedAddress(context, domain, widget.exchangeViewModel.depositCurrency); + } + }); + + _receiveAddressFocus.addListener(() async { + if (!_receiveAddressFocus.hasFocus && receiveAddressController.text.isNotEmpty) { + final domain = receiveAddressController.text; + widget.exchangeViewModel.receiveAddress = + await fetchParsedAddress(context, domain, widget.exchangeViewModel.receiveCurrency); + } + }); + + _receiveAmountFocus.addListener(() { + if (_receiveAmountFocus.hasFocus) { + widget.exchangeViewModel.enableFixedRateMode(); + } + // exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text); + }); + + _depositAmountFocus.addListener(() { + widget.exchangeViewModel.isFixedRateMode = false; + // exchangeViewModel.changeDepositAmount( + // amount: depositAmountController.text); + }); + + if (widget.initialPaymentRequest != null) { + try { + widget.exchangeViewModel.receiveCurrency = + CryptoCurrency.fromString(widget.initialPaymentRequest!.scheme); + widget.exchangeViewModel.setCanonicalReceiveAmount(widget.initialPaymentRequest!.amount); + widget.exchangeViewModel.receiveAddress = widget.initialPaymentRequest!.address; + } catch (e) { + printV('error: ${e.toString()}'); + // TODO + } + } + }); + } + + @override + void dispose() { + super.dispose(); + _depositAmountFocus.dispose(); + _depositAddressFocus.dispose(); + _receiveAmountFocus.dispose(); + _receiveAddressFocus.dispose(); + } + + void _onCurrencyChange(CryptoCurrency currency, ExchangeViewModel exchangeViewModel, + GlobalKey key) { + final isCurrentTypeWallet = exchangeViewModel.useSameWalletAddress(currency); + + if (key == depositKey && !isCurrentTypeWallet) exchangeViewModel.isSendFromExternal = true; + + key.currentState!.changeSelectedCurrency(currency); + + if (key == depositKey) + key.currentState!.changeAddress( + address: isCurrentTypeWallet + ? exchangeViewModel.wallet.walletAddresses.addressForExchange + : ''); + + key.currentState!.changeAmount(amount: ''); + } + + void _onWalletNameChange(ExchangeViewModel exchangeViewModel, CryptoCurrency currency, + GlobalKey key) { + final isCurrentTypeWallet = exchangeViewModel.useSameWalletAddress(currency); + + if (isCurrentTypeWallet) { + key.currentState!.addressController.text = + exchangeViewModel.wallet.walletAddresses.addressForExchange; + } else if (key.currentState!.addressController.text == + exchangeViewModel.wallet.walletAddresses.addressForExchange) { + key.currentState!.addressController.text = ''; + } + } + + Future fetchParsedAddress( + BuildContext context, String domain, CryptoCurrency currency) async { + printV("$domain"); + final parsedAddress = await getIt.get().resolve(context, domain, currency); + return extractAddressFromParsed(context, parsedAddress); + } + + void _showFeeAlert(BuildContext context) async { + await Future.delayed(Duration(seconds: 1)); + if (!context.mounted) { + return; + } + + final confirmed = await showPopUp( + context: context, + builder: (dialogContext) { + return AlertWithTwoActions( + alertTitle: S.of(context).low_fee, + alertContent: S.of(context).low_fee_alert, + leftButtonText: S.of(context).ignor, + rightButtonText: S.of(context).use_suggested, + actionLeftButton: () => Navigator.of(dialogContext).pop(false), + actionRightButton: () => Navigator.of(dialogContext).pop(true)); + }) ?? + false; + if (confirmed) { + widget.exchangeViewModel.feesViewModel.setDefaultTransactionPriority(); + } + } + + void disposeBestRateSync() => widget.exchangeViewModel.bestRateSync.cancel(); + + @override + Widget build(BuildContext context) { + return KeyboardHideOverlay( + unfocusOnTap: true, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Column( + children: [ + ModalTopBar( + title: S.of(context).swap, + leadingIcon: Icon(Icons.close), + onLeadingPressed: Navigator.of(context).maybePop, + trailingIcon: SvgPicture.asset( + "assets/new-ui/options.svg", + colorFilter: + ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn), + ), + onTrailingPressed: () { + Navigator.of(context).push(CupertinoPageRoute( + builder: (context) => Material( + child: SwapOptionsPage(exchangeViewModel: widget.exchangeViewModel)))); + }, + ), + Expanded( + child: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + children: [ + Form( + key: formKey, + child: Column( + children: [ + Observer( + builder: (_) => SwapAmountBox( + isReceiverCard: false, + walletSwitcherViewModel: widget.walletSwitcherViewModel, + exchangeViewModel: widget.exchangeViewModel, + onDispose: disposeBestRateSync, + hasAllAmount: widget.exchangeViewModel.hasAllAmount, + allAmount: widget.exchangeViewModel.hasAllAmount + ? () => widget.exchangeViewModel.enableSendAllAmount() + : null, + key: depositKey, + title: S.of(context).send, + initialCurrency: widget.exchangeViewModel.depositCurrency, + hasRefundAddress: true, + currencies: widget.exchangeViewModel.depositCurrencies, + onCurrencySelected: (currency) { + if (currency is CryptoCurrency) { + widget.exchangeViewModel + .changeDepositCurrency(currency: currency); + } + }, + currencyValueValidator: (value) { + return !widget.exchangeViewModel.isFixedRateMode && + value != S.of(context).all + ? AmountValidator( + isAutovalidate: true, + currency: widget.exchangeViewModel.depositCurrency, + minValue: widget.exchangeViewModel.limits.min.toString(), + maxValue: widget.exchangeViewModel.limits.max.toString(), + amountParsingProxy: + widget.exchangeViewModel.amountParsingProxy, + ).call(value) + : null; + }, + addressTextFieldValidator: AddressValidator( + type: widget.exchangeViewModel.depositCurrency), + onPushPasteButton: (context) async { + final clipboard = await Clipboard.getData('text/plain'); + widget.exchangeViewModel.depositAddress = clipboard?.text ?? ''; + + final domain = widget.exchangeViewModel.depositAddress; + widget.exchangeViewModel.depositAddress = + await fetchParsedAddress(context, domain, + widget.exchangeViewModel.depositCurrency); + }, + onPushAddressBookButton: (context) async { + final domain = widget.exchangeViewModel.depositAddress; + widget.exchangeViewModel.depositAddress = + await fetchParsedAddress(context, domain, + widget.exchangeViewModel.depositCurrency); + }, + ), + ), + SwapLimitPopup(exchangeViewModel: widget.exchangeViewModel), + Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: Stack( + alignment: Alignment.center, + children: [ + Container( + height: 1, + width: double.infinity, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + ModernButton.svg( + size: 36, + iconSize: 24, + svgPath: "assets/new-ui/swap_amounts.svg", + onPressed: widget.exchangeViewModel.reverseSwapDirection, + ), + ], + ), + ), + Observer( + builder: (_) => SwapAmountBox( + isReceiverCard: true, + walletSwitcherViewModel: widget.walletSwitcherViewModel, + exchangeViewModel: widget.exchangeViewModel, + onDispose: disposeBestRateSync, + key: receiveKey, + title: S.of(context).receive, + initialCurrency: widget.exchangeViewModel.receiveCurrency, + currencies: widget.exchangeViewModel.receiveCurrencies, + onCurrencySelected: (currency) { + if (currency is CryptoCurrency) { + widget.exchangeViewModel + .changeReceiveCurrency(currency: currency); + } + }, + currencyValueValidator: (value) { + return widget.exchangeViewModel.isFixedRateMode + ? AmountValidator( + isAutovalidate: true, + currency: widget.exchangeViewModel.receiveCurrency, + minValue: widget.exchangeViewModel.limits.min.toString(), + maxValue: widget.exchangeViewModel.limits.max.toString(), + amountParsingProxy: + widget.exchangeViewModel.amountParsingProxy, + ).call(value) + : null; + }, + addressTextFieldValidator: AddressValidator( + type: widget.exchangeViewModel.receiveCurrency), + onPushPasteButton: (context) async { + final clipboard = await Clipboard.getData('text/plain'); + widget.exchangeViewModel.receiveAddress = clipboard?.text ?? ''; + + final domain = widget.exchangeViewModel.receiveAddress; + widget.exchangeViewModel.receiveAddress = + await fetchParsedAddress(context, domain, + widget.exchangeViewModel.receiveCurrency); + }, + onPushAddressBookButton: (context) async { + final domain = widget.exchangeViewModel.receiveAddress; + widget.exchangeViewModel.receiveAddress = + await fetchParsedAddress(context, domain, + widget.exchangeViewModel.receiveCurrency); + }, + ), + ) + ], + ), + ), + Observer( + builder: (_) => Column( + spacing: 12, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (widget.exchangeViewModel.status is! SyncedSyncStatus) + SendSyncingIndicator(status: widget.exchangeViewModel.status), + if (widget.exchangeViewModel.isFixedRateMode) + Text(S.of(context).exchange_rate_is_fixed, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 12)), + SwapProviderPreview(exchangeViewModel: widget.exchangeViewModel), + Observer( + builder: (_) => LoadingPrimaryButton( + key: ValueKey('exchange_page_exchange_button_key'), + text: widget.exchangeViewModel.isAvailableInSelected + ? S.of(context).swap + : S.of(context).change_selected_exchanges, + onPressed: widget.exchangeViewModel.isAvailableInSelected + ? () { + FocusScope.of(context).unfocus(); + if (formKey.currentState != null && + formKey.currentState!.validate()) { + if (_shouldWaitTillSynced) { + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).exchange, + alertContent: + S.of(context).exchange_sync_alert_content, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(), + ); + }, + ); + } else { + final check = + widget.exchangeViewModel.shouldDisplayTOTP(); + widget.authService.authenticateAction( + context, + conditionToDetermineIfToUse2FA: check, + onAuthSuccess: (value) { + if (value) { + widget.exchangeViewModel.createTrade(); + } + }, + ); + } + } + } + : () => PresentProviderPicker( + exchangeViewModel: widget.exchangeViewModel) + .presentProviderPicker(context), + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + isDisabled: _swapButtonDisabled(), + isLoading: widget.exchangeViewModel.tradeState is TradeIsCreating, + ), + ), + SizedBox() + ], + ), + ) + ], + ), + ), + ), + ) + ], + ), + ), + ); + } + + bool _swapButtonDisabled() { + if (widget.exchangeViewModel.selectedProviders.isEmpty) { + return true; + } + if (widget.exchangeViewModel.receiveAddress.isEmpty) { + return true; + } + if (widget.exchangeViewModel.status is! SyncedSyncStatus) { + return true; + } + if (widget.exchangeViewModel.depositAmount.isEmpty) { + return true; + } + return false; + } +} + +class SwapProviderPreview extends StatelessWidget { + const SwapProviderPreview({super.key, required this.exchangeViewModel}); + + final ExchangeViewModel exchangeViewModel; + + @override + Widget build(BuildContext context) { + return Observer(builder: (_) { + if (exchangeViewModel.depositAmount.isEmpty) { + return SizedBox.shrink(); + } + + final provider = exchangeViewModel.forcedProvider ?? exchangeViewModel.providerDisplay; + final rate = exchangeViewModel.forcedProvider == null + ? exchangeViewModel.bestRate + : exchangeViewModel.forcedProviderRate; + + return GestureDetector( + onTap: () { + if (provider != null) { + Navigator.of(context).push(CupertinoPageRoute( + builder: (context) => + Material(child: ProviderSelectorPage(exchangeViewModel: exchangeViewModel)))); + } + }, + child: Container( + height: 48, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + spacing: 12, + children: [ + if (provider != null) + provider.description.image.toLowerCase().endsWith("svg") + ? SvgPicture.asset(provider.description.image, width: 28, height: 28) + : Image.asset(provider.description.image, width: 28, height: 28), + if (provider == null) CupertinoActivityIndicator(), + Text( + provider?.title ?? "${S.of(context).finding_provider}...", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: provider == null + ? Theme.of(context).colorScheme.onSurfaceVariant + : Theme.of(context).colorScheme.onSurface), + ) + ], + ), + if (provider != null) + Row( + children: [ + Text( + "1 ${exchangeViewModel.depositCurrency} ≈ ${rate.toStringAsFixed(6)} ${exchangeViewModel.receiveCurrency}", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + SvgPicture.asset( + "assets/new-ui/chooser.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn), + ) + ], + ) + ], + ), + ), + ), + ); + }); + } +} + +class SwapAmountBox extends StatefulWidget { + SwapAmountBox({ + Key? key, + required this.initialCurrency, + required this.currencies, + required this.onCurrencySelected, + this.currencyValueValidator, + this.addressTextFieldValidator, + this.title = '', + this.hasRefundAddress = false, + this.hasAllAmount = false, + this.allAmount, + this.onPushPasteButton, + this.onPushAddressBookButton, + this.onDispose, + required this.exchangeViewModel, + required this.isReceiverCard, + required this.walletSwitcherViewModel, + }) : super(key: key); + + final List currencies; + final Function(Currency) onCurrencySelected; + final String title; + final bool isReceiverCard; + final Currency initialCurrency; + final bool hasRefundAddress; + final FormFieldValidator? currencyValueValidator; + final FormFieldValidator? addressTextFieldValidator; + final FormFieldValidator allAmountValidator = AllAmountValidator(); + final bool hasAllAmount; + final VoidCallback? allAmount; + final void Function(BuildContext context)? onPushPasteButton; + final void Function(BuildContext context)? onPushAddressBookButton; + final Function()? onDispose; + final ExchangeViewModel exchangeViewModel; + final WalletSwitcherViewModel walletSwitcherViewModel; + + @override + State createState() => SwapAmountBoxState(); +} + +class SwapAmountBoxState extends State { + final addressController = TextEditingController(); + final amountController = TextEditingController(); + final fiatAmountController = TextEditingController(); + final amountFocusNode = FocusNode(); + + @override + void initState() { + _selectedCurrency = widget.initialCurrency; + + super.initState(); + } + + late Currency _selectedCurrency; + bool _fiatInputMode = false; + + @override + Widget build(BuildContext context) { + final currencyToShow = (_selectedCurrency is CryptoCurrency) + ? widget.exchangeViewModel.amountParsingProxy + .getCryptoSymbol(_selectedCurrency as CryptoCurrency) + : _selectedCurrency.name.toUpperCase(); + + return Column( + spacing: 12, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.title, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + Container( + decoration: ShapeDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + shape: RoundedSuperellipseBorder(borderRadius: BorderRadius.circular(24))), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + spacing: 12, + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: amountFocusNode.requestFocus, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + spacing: 4, + children: [ + Expanded( + child: Row( + spacing: 4, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flexible( + child: IntrinsicWidth( + child: TextFormField( + keyboardType: + TextInputType.numberWithOptions(signed: false, decimal: true), + validator: _fiatInputMode ? null : widget.currencyValueValidator, + controller: + _fiatInputMode ? fiatAmountController : amountController, + focusNode: amountFocusNode, + style: TextStyle( + fontSize: 28, + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.w400), + decoration: InputDecoration( + contentPadding: EdgeInsets.zero, + isDense: true, + hintText: "0", + fillColor: Colors.transparent, + hoverColor: Colors.transparent, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + ), + )), + ), + if (_fiatInputMode) + Center( + child: Text( + widget.exchangeViewModel.fiat.title, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant), + )), + ], + ), + ), + GestureDetector( + onTap: _presentCurrencyPicker, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(999999)), + child: Padding( + padding: const EdgeInsets.only(top: 4.0, bottom: 4, left: 4, right: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset(_selectedCurrency.iconPath ?? "", + width: 28, height: 28), + SizedBox(width: 10), + Text( + currencyToShow, + textAlign: TextAlign.center, + ), + SizedBox(width: 10), + RotatedBox( + quarterTurns: 2, + child: SvgPicture.asset( + "assets/new-ui/dropdown_arrow.svg", + width: 4, + height: 4, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, BlendMode.srcIn), + )), + SizedBox(width: 4), + ], + ), + ), + ), + ) + ], + ), + ), + Observer( + builder: (_) { + final hasAllAmount = !(widget.isReceiverCard || + widget.exchangeViewModel.isSendFromExternal || + !widget.exchangeViewModel.hasAllAmount); + + return FiatAmountBar( + foregroundElementColor: Theme.of(context).colorScheme.surfaceContainerHigh, + textColor: Theme.of(context).colorScheme.onSurfaceVariant, + allAmountColor: hasAllAmount ? null : Colors.transparent, + allAmountTextColor: + hasAllAmount ? null : Theme.of(context).colorScheme.onSurfaceVariant, + fiatInputMode: _fiatInputMode, + allAmount: !hasAllAmount + ? widget.isReceiverCard + ? null + : widget.exchangeViewModel.balanceDisplay + : widget.exchangeViewModel.depositAvailableAmount, + onSwitchButtonPressed: () { + setState(() { + _fiatInputMode = !_fiatInputMode; + }); + if (_fiatInputMode) { + updateFiatAmount(); + } + }, + onAllButtonPressed: widget.allAmount, + cryptoAmount: widget.isReceiverCard + ? widget.exchangeViewModel.roundedReceiveAmount(6) + : widget.exchangeViewModel.roundedDepositAmount(6), + fiatAmount: widget.isReceiverCard + ? widget.exchangeViewModel.roundedReceiveAmountFiat(6) + : widget.exchangeViewModel.roundedDepositAmountFiat(6), + cryptoCurrency: widget.isReceiverCard + ? widget.exchangeViewModel.receiveCurrency.title + : widget.exchangeViewModel.depositCurrency.title, + fiatCurrency: widget.exchangeViewModel.fiat.name); + }, + ), + Observer( + builder: (_) { + final addressEmpty = (widget.isReceiverCard && + widget.exchangeViewModel.receiveAddress.isEmpty) || + (!widget.isReceiverCard && widget.exchangeViewModel.depositAddress.isEmpty); + final addressPickerText = widget.isReceiverCard + ? (addressEmpty ? S.of(context).select_receiver : S.of(context).to) + : S.of(context).from; + final addressDescription = widget.isReceiverCard + ? widget.exchangeViewModel.receiveAddressDisplayName ?? + _middleTruncate(widget.exchangeViewModel.receiveAddress, 8, 8) + : widget.exchangeViewModel.isSendFromExternal + ? S.of(context).external + : widget.exchangeViewModel.wallet.name; + return Row( + spacing: 8, + children: [ + Flexible( + child: GestureDetector( + onTap: _presentWalletPicker, + child: Container( + height: 36, + decoration: BoxDecoration( + color: (addressEmpty && widget.isReceiverCard) + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(9999)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Observer( + builder: (_) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + spacing: 8, + mainAxisSize: MainAxisSize.max, + children: [ + Text( + addressPickerText, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: (addressEmpty && widget.isReceiverCard) + ? Theme.of(context).colorScheme.onPrimary + : Theme.of(context).colorScheme.onSurface), + ), + Text(addressDescription, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary)) + ], + ), + RotatedBox( + quarterTurns: 2, + child: SvgPicture.asset( + "assets/new-ui/dropdown_arrow.svg", + colorFilter: ColorFilter.mode( + (addressEmpty && widget.isReceiverCard) + ? Theme.of(context).colorScheme.onPrimary + : Theme.of(context).colorScheme.onSurface, + BlendMode.srcIn), + ), + ) + ], + ), + ), + ), + ), + ), + ), + if (!widget.isReceiverCard && + widget.exchangeViewModel.isSendFromExternal && + widget.exchangeViewModel.depositAddress.isEmpty) + ModernButton.svg( + svgPath: "assets/new-ui/refund_address.svg", + onPressed: askForRefundAddress, + size: 36, + iconSize: 18, + backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, + ), + if (widget.isReceiverCard && + widget.exchangeViewModel.receiveAddress.isEmpty) ...[ + ModernButton.svg( + svgPath: "assets/new-ui/paste.svg", + onPressed: () { + widget.onPushPasteButton?.call(context); + }, + size: 36, + iconSize: 20, + backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, + ), + ModernButton.svg( + svgPath: "assets/new-ui/scan.svg", + onPressed: () { + _presentQRScanner(context); + }, + size: 36, + iconSize: 20, + backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, + ), + ] + ], + ); + }, + ) + ], + ), + ), + ), + ], + ); + } + + void changeSelectedCurrency(Currency currency) { + setState(() => _selectedCurrency = currency); + } + + void changeAddress({required String address}) { + setState(() => addressController.text = _normalizeAddressFormat(address)); + } + + void changeAmount({required String amount}) { + setState(() => amountController.text = amount); + } + + String _normalizeAddressFormat(String address) { + if (address.startsWith('bitcoincash:')) address = address.substring(12); + return address; + } + + void _presentCurrencyPicker() { + final currencies = widget.isReceiverCard + ? widget.exchangeViewModel.receiveCurrencies + : widget.exchangeViewModel.depositCurrencies; + if (widget.exchangeViewModel.wallet.type == WalletType.bitcoin) { + currencies.sort((a, b) { + if (a == CryptoCurrency.btcln) return -1; + if (b == CryptoCurrency.btcln) return 1; + return 0; + }); + } + + showPopUp( + context: context, + builder: (_) => CurrencyPicker( + key: ValueKey('send_page_currency_picker_dialog_button_key'), + selectedAtIndex: currencies.indexOf(widget.isReceiverCard + ? widget.exchangeViewModel.receiveCurrency + : widget.exchangeViewModel.depositCurrency), + items: currencies, + hintText: S.of(context).search_currency, + onItemSelected: (Currency cur) async { + if (cur is CryptoCurrency) { + widget.onCurrencySelected(cur); + } + }, + ), + ); + } + + Future _presentQRScanner(BuildContext context) async { + bool isCameraPermissionGranted = + await PermissionHandler.checkPermission(Permission.camera, context); + if (!isCameraPermissionGranted) return; + final code = await presentQRScanner(context); + if (code == null) return; + if (code.isEmpty) return; + + try { + final uri = Uri.parse(code); + widget.exchangeViewModel.receiveAddress = uri.path; + } catch (_) {} + } + + void _presentWalletPicker() async { + final res = await showMaterialModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + builder: (context) { + return ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.6, + ), + child: SwapAddressSelectionModal( + isSelectingReceiver: widget.isReceiverCard, + exchangeViewModel: widget.exchangeViewModel, + ), + ); + }); + if (res != null && res is SwapAddressSelectionResult) { + if (widget.isReceiverCard) { + widget.exchangeViewModel.receiveAddress = res.address!; + if (res.walletName != null) { + if (res.accountName != null) { + widget.exchangeViewModel.receiveAddressDisplayName = + "${res.walletName} → ${res.accountName}"; + } else { + widget.exchangeViewModel.receiveAddressDisplayName = res.walletName!; + } + } + } else if (res.address == null || res.address!.isEmpty) { + widget.exchangeViewModel.isSendFromExternal = true; + askForRefundAddress(); + } else { + widget.exchangeViewModel.isSendFromExternal = false; + switchToDepositWallet(res.walletName!); + } + } + } + + void switchToDepositWallet(String walletName) async { + final walletType = cryptoCurrencyOrTokenToWalletType(widget.exchangeViewModel.depositCurrency); + if (walletType == null) return; + final wallet = await WalletInfo.get(walletName, walletType); + if (wallet == null) return; + widget.walletSwitcherViewModel.selectWallet(wallet); + await widget.walletSwitcherViewModel.switchToSelectedWallet(); + } + + void askForRefundAddress() async { + final refundAddress = await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => RefundAddressModal( + selectedCurrency: widget.exchangeViewModel.depositCurrency, + isFromWalletSelection: true, + )); + if (refundAddress != null && refundAddress is String) { + widget.exchangeViewModel.depositAddress = refundAddress; + } else { + widget.exchangeViewModel.depositAddress = ""; + } + } + + void updateFiatAmount() { + final newText = widget.isReceiverCard + ? widget.exchangeViewModel.receiveAmountFiat + : widget.exchangeViewModel.depositAmountFiat; + + if (double.tryParse(fiatAmountController.text) != double.tryParse(newText)) { + if (newText == "0.00") { + fiatAmountController.text = ""; + } else { + fiatAmountController.text = + newText.replaceAll(RegExp(r'(?<=\.\d*)0+$'), '').replaceAll(RegExp(r'\.$'), ''); + } + } + } + + String _middleTruncate(String s, int head, int tail) { + if (s.length <= head + tail + 3) return s; + return s.substring(0, head) + '...' + s.substring(s.length - tail); + } +} diff --git a/lib/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart b/lib/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart new file mode 100644 index 000000000..24f75fea5 --- /dev/null +++ b/lib/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart @@ -0,0 +1,168 @@ +import 'package:bloc/bloc.dart'; +import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/wownero/wownero.dart'; +import "package:cw_core/balance_card_style_settings.dart"; +import 'package:cw_core/card_design.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/wallet_base.dart'; +import "package:cw_core/wallet_type.dart"; +import 'package:flutter/src/painting/gradient.dart'; +import 'package:meta/meta.dart'; + +part 'card_customizer_event.dart'; +part 'card_customizer_state.dart'; + +class CardCustomizerBloc extends Bloc { + final WalletBase _wallet; + final bool lightningMode; + final bool displaySats; + + CardCustomizerBloc(this._wallet, {this.lightningMode = false, this.displaySats = false}) + : super(CardCustomizerNotLoaded(0, 0, [CardDesign.genericDefault], [], "", -1, displaySats, 0)) { + + on<_Init>(_init); + on(_onDesignSelected); + on(_onColorSelected); + on(_onDesignSaved); + on(_onAccountNameChanged); + + + add(_Init()); + } + + List _updateAvailableColors(CardDesign currentDesign) { + final list = List.from(CardDesign.allGradients, growable: true); + if (CardDesign.specialDesignsForCurrencies[_wallet.currency] != null) { + list.add(CardDesign.specialDesignsForCurrencies[_wallet.currency]!.gradient); + } + return list; + } + + Future _loadCurrentDesignSettings(int accountIndex) async { + return (await BalanceCardStyleSettings.get(_wallet.walletInfo.internalId, accountIndex)); + } + + List _initAvailableDesigns({bool lightningMode = false}) { + final List ret = List.empty(growable: true); + final curr = lightningMode ? CryptoCurrency.btcln : _wallet.currency; + + ret.add(CardDesign.forCurrencyIcon(curr)); + + if (CardDesign.specialDesignsForCurrencies[curr] != null) + ret.add(CardDesign.forCurrencySpecial(curr)); + + return ret; + } + + int _initSelectedDesign(CardDesign currentDesign) { + if (currentDesign.backgroundType == CardDesignBackgroundTypes.svgIcon) + return 0; + else if (currentDesign.backgroundType == CardDesignBackgroundTypes.svgFull) + return 1; + else + return 0; + } + + int _initSelectedColor(CardDesign currentDesign) { + int ret = CardDesign.allGradients.indexOf(currentDesign.gradient); + if(ret == -1) { + // special design with its own color. select last color in list. + return CardDesign.allGradients.length; + } else if (ret == -1) { + // no color selected, select default. + return 0; + } else { + // select whatever's selected. + return ret; + } + } + + void _init(_Init event, Emitter emit) async { + late final account; + if (_wallet.type == WalletType.monero) { + account = monero!.getCurrentAccount(_wallet); + } else if (_wallet.type == WalletType.wownero) { + account = wownero!.getCurrentAccount(_wallet); + } else { + account = null; + } + final accountName = (account?.label ?? "") as String; + late final int accountIndex; + if(account != null) { + accountIndex = account.id as int; + } else if(lightningMode) { + accountIndex = 0; + } else { + accountIndex = -1; + } + final currentDesignSettings = await _loadCurrentDesignSettings(accountIndex); + final currentDesign = CardDesign.fromStyleSettings(currentDesignSettings, lightningMode ? CryptoCurrency.btcln : _wallet.currency); + final availableDesigns = _initAvailableDesigns(lightningMode: lightningMode); + final availableColors = _updateAvailableColors(currentDesign); + final selectedDesign = _initSelectedDesign(currentDesign); + final selectedColor = _initSelectedColor(currentDesign); + + emit(CardCustomizerInitial(selectedDesign, selectedColor, availableDesigns, availableColors, + accountName, accountIndex, displaySats, currentDesignSettings?.cardOrder ?? 0)); + } + + void _onDesignSelected(CardDesignSelected event, Emitter emit) { + final newColors = _updateAvailableColors(state.availableDesigns[event.newDesignIndex]); + late final int newColorIndex; + if (newColors.length < state.availableColors.length) { + newColorIndex = 0; + } else { + newColorIndex = state.selectedColorIndex; + } + + emit(state.copyWith( + selectedDesignIndex: event.newDesignIndex, + availableColors: newColors, + selectedColorIndex: newColorIndex)); + } + + void _onColorSelected(ColorSelected event, Emitter emit) { + emit(state.copyWith(selectedColorIndex: event.newColorIndex)); + } + + void _onAccountNameChanged(AccountNameChanged event, Emitter emit) { + emit(state.copyWith(accountName: event.newAccountName)); + } + + void _onDesignSaved(DesignSaved event, Emitter emit) { + BalanceCardStyleSettings.fromCardDesign( + _wallet.walletInfo.internalId, state.accountIndex, state.cardOrder, state.selectedDesign) + .insert() + .then((value) { + emit(CardCustomizerSaved(state.selectedDesignIndex, state.selectedColorIndex, + state.availableDesigns, state.availableColors, state.accountName, state.accountIndex, state.displaySats, state.cardOrder)); + }); + saveAccountName(); + } + + Future saveAccountName() async { + if (_wallet.type == WalletType.monero) { + await saveMoneroAccountName(); + } + + if (_wallet.type == WalletType.wownero) { + await saveWowneroAccountName(); + } + } + + Future saveMoneroAccountName() async { + final MoneroAccountList moneroAccountList = monero!.getAccountList(_wallet); + await moneroAccountList.setLabelAccount(_wallet, + accountIndex: state.accountIndex, label: state.accountName); + + await _wallet.save(); + } + + Future saveWowneroAccountName() async { + final WowneroAccountList wowneroAccountList = wownero!.getAccountList(_wallet); + await wowneroAccountList.setLabelAccount(_wallet, + accountIndex: state.accountIndex, label: state.accountName); + + await _wallet.save(); + } +} diff --git a/lib/new-ui/viewmodels/card_customizer/card_customizer_event.dart b/lib/new-ui/viewmodels/card_customizer/card_customizer_event.dart new file mode 100644 index 000000000..e6eba5518 --- /dev/null +++ b/lib/new-ui/viewmodels/card_customizer/card_customizer_event.dart @@ -0,0 +1,28 @@ +part of 'card_customizer_bloc.dart'; + +@immutable +sealed class CardCustomizerEvent {} + +class _Init extends CardCustomizerEvent {} + +class CardDesignSelected extends CardCustomizerEvent { + final int newDesignIndex; + + CardDesignSelected(this.newDesignIndex); +} + +class ColorSelected extends CardCustomizerEvent { + final int newColorIndex; + + ColorSelected(this.newColorIndex); +} + + +class AccountNameChanged extends CardCustomizerEvent { + final String newAccountName; + + AccountNameChanged(this.newAccountName); +} + + +class DesignSaved extends CardCustomizerEvent {} diff --git a/lib/new-ui/viewmodels/card_customizer/card_customizer_state.dart b/lib/new-ui/viewmodels/card_customizer/card_customizer_state.dart new file mode 100644 index 000000000..916c10ecc --- /dev/null +++ b/lib/new-ui/viewmodels/card_customizer/card_customizer_state.dart @@ -0,0 +1,112 @@ +part of 'card_customizer_bloc.dart'; + +sealed class CardCustomizerState { + final int selectedDesignIndex; + final int selectedColorIndex; + final String accountName; + final int accountIndex; + final int cardOrder; + final bool displaySats; + final List availableDesigns; + final List availableColors; + + CardCustomizerState( + this.selectedDesignIndex, + this.selectedColorIndex, + this.availableDesigns, + this.availableColors, + this.accountName, + this.accountIndex, + this.displaySats, + this.cardOrder + ); + + CardDesign get selectedDesign { + return availableDesigns[selectedDesignIndex].withGradient(selectedColor); + } + + CardCustomizerState copyWith({ + int? selectedDesignIndex, + int? selectedColorIndex, + List? availableDesigns, + List? availableColors, + String? accountName, + int? accountIndex, + int? cardOrder, + }); + + Gradient get selectedColor => availableColors[selectedColorIndex]; +} + +final class CardCustomizerNotLoaded extends CardCustomizerState { + CardCustomizerNotLoaded(super.selectedDesignIndex, super.selectedColorIndex, super.availableDesigns, super.availableColors, super.accountName, super.accountIndex, super.displaySats, super.cardOrder); + + @override + CardCustomizerState copyWith({int? selectedDesignIndex, int? selectedColorIndex, List? availableDesigns, List? availableColors, String? accountName, int? accountIndex, int? cardOrder}) { + // this is never gonna be copied. it's near-instantly replaced with initial + throw UnimplementedError(); + } +} + +final class CardCustomizerInitial extends CardCustomizerState { + CardCustomizerInitial( + int selectedDesignIndex, + int selectedColorIndex, + List availableDesigns, + List availableColors, + String accountName, + int accountIndex, + bool displaySats, + int cardOrder, + ) : super(selectedDesignIndex, selectedColorIndex, availableDesigns, availableColors, accountName, + accountIndex, displaySats, cardOrder); + + CardCustomizerInitial copyWith({ + int? selectedDesignIndex, + int? selectedColorIndex, + List? availableDesigns, + List? availableColors, + String? accountName, + int? accountIndex, + bool? displaySats, + int? cardOrder, + }) { + return CardCustomizerInitial( + selectedDesignIndex ?? this.selectedDesignIndex, + selectedColorIndex ?? this.selectedColorIndex, + availableDesigns ?? this.availableDesigns, + availableColors ?? this.availableColors, + accountName ?? this.accountName, + accountIndex ?? this.accountIndex, + displaySats ?? this.displaySats, + cardOrder ?? this.cardOrder, + ); + } +} + +final class CardCustomizerSaved extends CardCustomizerState { + CardCustomizerSaved(super.selectedDesignIndex, super.selectedColorIndex, super.availableDesigns, + super.availableColors, super.accountName, super.accountIndex, super.displaySats, super.cardOrder); + + @override + CardCustomizerState copyWith( + {int? selectedDesignIndex, + int? selectedColorIndex, + List? availableDesigns, + List? availableColors, + String? accountName, + int? accountIndex, + bool? displaySats, + int? cardOrder}) { + return CardCustomizerSaved( + selectedDesignIndex ?? this.selectedDesignIndex, + selectedColorIndex ?? this.selectedColorIndex, + availableDesigns ?? this.availableDesigns, + availableColors ?? this.availableColors, + accountName ?? this.accountName, + accountIndex ?? this.accountIndex, + displaySats ?? this.displaySats, + cardOrder ?? this.cardOrder, + ); + } +} diff --git a/lib/new-ui/viewmodels/lightning_username/lightning_username_bloc.dart b/lib/new-ui/viewmodels/lightning_username/lightning_username_bloc.dart new file mode 100644 index 000000000..4cf78de60 --- /dev/null +++ b/lib/new-ui/viewmodels/lightning_username/lightning_username_bloc.dart @@ -0,0 +1,92 @@ +import 'package:bloc/bloc.dart'; +import 'package:bloc_concurrency/bloc_concurrency.dart'; +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/entities/lnurlpay_record.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:meta/meta.dart'; + +part 'lightning_username_event.dart'; + +part 'lightning_username_state.dart'; + +class UsernameError { + String message; + bool isInfo; + + UsernameError(this.message, {this.isInfo = false}); +} + +// TODO find other errors that api returns +final Map errorPatterns = {"name already taken": S.current.username_not_available}; + +const usernameRegex = r"^[a-z0-9_.-]+$"; +const usernameSuffix = "@cake.cash"; + +class LightningUsernameBloc extends Bloc { + final WalletBase _wallet; + + LightningUsernameBloc(this._wallet) : super(LightningUsernameInitial("")) { + on(onUsernameChanged, transformer: restartable()); + + on(onUsernameSaveRequested); + + on<_Init>((event, emit) async { + final username = await bitcoin!.getLightningUsername(_wallet); + emit(LightningUsernameInitial(username ?? "")); + }); + + add(_Init()); + } + + Future onUsernameChanged(ChangeUsername event, Emitter emit) async { + emit(LightningUsernameChecking(event.newUsername)); + + final error = await getUsernameError(event.newUsername); + + if (error != null) { + emit(LightningUsernameError(event.newUsername, error)); + } else { + emit(LightningUsernameReady(event.newUsername)); + } + } + + Future onUsernameSaveRequested(RequestUsernameSave event, Emitter emit) async { + emit(LightningUsernameSaving(state.username)); + try { + await bitcoin!.setLightningUsername(_wallet, state.username); + emit(LightningUsernameSaved(state.username)); + } catch (e) { + for (final pattern in errorPatterns.keys) { + if (e.toString().contains(pattern)) { + emit(LightningUsernameError(state.username, UsernameError(errorPatterns[pattern]!))); + return; + } + } + emit(LightningUsernameError(state.username, UsernameError(bitcoin!.getBreezSdkError(e) ?? e.toString()))); + } + } + + Future getUsernameError(String username) async { + if (!(RegExp(usernameRegex).hasMatch(username))) { + return UsernameError("${S.current.username_character_error} a-z, 0-9, - _ ."); + } + + if (username.length < 1 || username.length > 64) { + return UsernameError(S.current.username_length_error_1_64); + } + + if (username == (await bitcoin!.getLightningUsername(_wallet))) { + return UsernameError(S.current.already_your_username, isInfo: true); + } + + if ((await LNUrlPayRecord.checkWellKnownUsername( + "${username}${usernameSuffix}", CryptoCurrency.btc)) != + null) { + return UsernameError(S.current.username_not_available); + } + + return null; + } +} diff --git a/lib/new-ui/viewmodels/lightning_username/lightning_username_event.dart b/lib/new-ui/viewmodels/lightning_username/lightning_username_event.dart new file mode 100644 index 000000000..7d781eb02 --- /dev/null +++ b/lib/new-ui/viewmodels/lightning_username/lightning_username_event.dart @@ -0,0 +1,14 @@ +part of 'lightning_username_bloc.dart'; + +@immutable +sealed class LightningUsernameEvent {} + +final class _Init extends LightningUsernameEvent {} + +final class ChangeUsername extends LightningUsernameEvent { + final String newUsername; + + ChangeUsername(this.newUsername); +} + +final class RequestUsernameSave extends LightningUsernameEvent {} \ No newline at end of file diff --git a/lib/new-ui/viewmodels/lightning_username/lightning_username_state.dart b/lib/new-ui/viewmodels/lightning_username/lightning_username_state.dart new file mode 100644 index 000000000..5be36bd22 --- /dev/null +++ b/lib/new-ui/viewmodels/lightning_username/lightning_username_state.dart @@ -0,0 +1,34 @@ +part of 'lightning_username_bloc.dart'; + +@immutable +sealed class LightningUsernameState { + final String username; + + LightningUsernameState(this.username); +} + +final class LightningUsernameInitial extends LightningUsernameState { + LightningUsernameInitial(super.username); +} + +final class LightningUsernameChecking extends LightningUsernameState { + LightningUsernameChecking(super.username); +} + +final class LightningUsernameError extends LightningUsernameState { + final UsernameError error; + + LightningUsernameError(super.username, this.error); +} + +final class LightningUsernameReady extends LightningUsernameState { + LightningUsernameReady(super.username); +} + +final class LightningUsernameSaving extends LightningUsernameState { + LightningUsernameSaving(super.username); +} + +final class LightningUsernameSaved extends LightningUsernameState { + LightningUsernameSaved(super.username); +} diff --git a/lib/new-ui/widgets/addresses_page/address_label_input.dart b/lib/new-ui/widgets/addresses_page/address_label_input.dart new file mode 100644 index 000000000..48a879a98 --- /dev/null +++ b/lib/new-ui/widgets/addresses_page/address_label_input.dart @@ -0,0 +1,85 @@ +import 'dart:ui'; + +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_text_field.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:mobx/mobx.dart'; + +class AddressLabelInputPopup extends StatefulWidget { + const AddressLabelInputPopup({ + super.key, + required this.walletAddressEditOrCreateViewModel, + }); + + final WalletAddressEditOrCreateViewModel walletAddressEditOrCreateViewModel; + + @override + State createState() => _AddressLabelInputPopupState(); +} + +class _AddressLabelInputPopupState extends State { + late final TextEditingController _controller; + late final FocusNode _focusNode; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(); + _focusNode = FocusNode(); + + reaction((_) => widget.walletAddressEditOrCreateViewModel.state, + (AddressEditOrCreateState state) { + if (state is AddressSavedSuccessfully) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) + Navigator.of(context).pop(widget.walletAddressEditOrCreateViewModel.label); + }); + } + }); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _focusNode.requestFocus(); + _controller.text = widget.walletAddressEditOrCreateViewModel.label; + }); + } + + @override + void dispose() { + _controller.dispose(); + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: NewListSections( + sections: { + "": [ + ListItemTextField( + keyValue: "label", + label: S.of(context).label, + focusNode: _focusNode, + onFieldSubmitted: (value) { + widget.walletAddressEditOrCreateViewModel.label = value; + widget.walletAddressEditOrCreateViewModel.save(); + // Navigator.of(context).pop(); + }) + ] + }, + controllers: {"label": _controller}, + ), + ), + ]), + ); + } +} diff --git a/lib/new-ui/widgets/animated_dropdown.dart b/lib/new-ui/widgets/animated_dropdown.dart new file mode 100644 index 000000000..2c4e537f5 --- /dev/null +++ b/lib/new-ui/widgets/animated_dropdown.dart @@ -0,0 +1,49 @@ +import 'package:cake_wallet/new-ui/widgets/dropdown_row.dart'; +import 'package:flutter/material.dart'; + +class AnimatedDropdown extends StatefulWidget { + const AnimatedDropdown({super.key,required this.content, required this.dropdownText}); + + final Widget content; + final String dropdownText; + + @override + State createState() => _AnimatedDropdownState(); +} + +class _AnimatedDropdownState extends State { + bool _expanded = false; + + @override + Widget build(BuildContext context) { + return ClipRSuperellipse( + borderRadius: BorderRadius.circular(20), + child: Container( + decoration: ShapeDecoration( + shape: RoundedSuperellipseBorder( + borderRadius: BorderRadius.circular(20), + ), + color: Theme.of(context).colorScheme.surfaceContainer,), + child: Column( + children: [ + DropdownRow(expanded: _expanded, text: widget.dropdownText,onTap: (){ + setState(() { + _expanded = !_expanded; + }); + },), + AnimatedOpacity(duration:Duration(milliseconds: 150),opacity:_expanded?1:0,child: Divider(height: 1,thickness: 1,)), + AnimatedCrossFade( + firstChild: Container(height:0), + secondChild: widget.content, + crossFadeState: _expanded + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + duration: const Duration(milliseconds: 300), + sizeCurve: Curves.easeOutCubic, + ) + ], + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/apps_widget.dart b/lib/new-ui/widgets/apps_widget.dart new file mode 100644 index 000000000..5e4deae78 --- /dev/null +++ b/lib/new-ui/widgets/apps_widget.dart @@ -0,0 +1,147 @@ +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; +import 'package:cake_wallet/themes/core/theme_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class AppsWidget extends StatelessWidget { + AppsWidget({ + this.onTap, + required this.title, + required this.subTitle, + this.hint, + this.svgPicture, + this.image, + this.icon, + this.onClose, + this.customBorder, + this.shadowSpread, + this.shadowBlur, + super.key, + this.marginV, + this.marginH, + this.isWide, + this.isLink, + this.isCake, + }); + + final VoidCallback? onTap; + final VoidCallback? onClose; + final String title; + final String subTitle; + final Widget? hint; + final SvgPicture? svgPicture; + final Widget? icon; + final String? image; + final double? customBorder; + final double? marginV; + final double? marginH; + final double? shadowSpread; + final double? shadowBlur; + final bool? isWide; + final bool? isLink; + final bool? isCake; + + @override + Widget build(BuildContext context) { + if (isWide == true) { + return Stack( + children: [ + Container( + margin: EdgeInsets.symmetric(horizontal: marginH ?? 20, vertical: marginV ?? 5), + width: double.infinity, + decoration: ShapeDecoration( + shape: RoundedSuperellipseBorder(borderRadius: BorderRadius.circular(18)), + gradient: LinearGradient( + colors: [ + context.customColors.cardGradientColorPrimary, + context.customColors.cardGradientColorSecondary, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: TextButton( + onPressed: onTap, + style: TextButton.styleFrom( + shape: RoundedSuperellipseBorder( + borderRadius: BorderRadius.circular(18), + side: BorderSide( + width: 1.25, color: Theme.of(context).colorScheme.surfaceContainerHigh), + ), + padding: EdgeInsets.all(24), + ), + child: Column( + children: [ + Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 20), + child: CakeImageWidget(imageUrl: image, height: 54, width: 54), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + spacing: 6.0, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.w800, + fontSize: 20, + ), + softWrap: true, + ), + isCake == true ? CakeImageWidget(imageUrl: "assets/new-ui/cakelabs-icon.svg", color: Theme.of(context).colorScheme.onSurfaceVariant) : SizedBox(), + ], + ), + SizedBox(height: 5), + Text( + subTitle, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w500, + ), + softWrap: true, + ), + ], + ), + ), + Icon( + isLink == true ? Icons.arrow_outward : Icons.arrow_forward_ios, + color: Theme.of(context).colorScheme.onSurfaceVariant, + size: 20, + ) + + ], + ), + if (hint != null) ...[ + SizedBox(height: 10), + hint!, + ] + ], + ), + ), + ), + if (onClose != null) + Positioned( + top: 10, + right: 10, + child: IconButton( + icon: Icon(Icons.close), + onPressed: onClose, + //color: Theme.of(context).colorScheme.onSurface, + ), + ), + ], + ); + } + else { + return Stack( + + ); + } + } +} diff --git a/lib/new-ui/widgets/changelog_modal.dart b/lib/new-ui/widgets/changelog_modal.dart new file mode 100644 index 000000000..079bac629 --- /dev/null +++ b/lib/new-ui/widgets/changelog_modal.dart @@ -0,0 +1,202 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:url_launcher/url_launcher.dart'; + +const changelogIconLocation = "assets/new-ui/changelog/icons"; +const changelogTextLocation = "assets/new-ui/changelog/text"; + +class ChangelogItem { + final String iconFilename; + final String title; + final String description; + + ChangelogItem(this.iconFilename, this.title, this.description); +} + +class ChangelogModal extends StatefulWidget { + const ChangelogModal({super.key, required this.version}); + + final String version; + + @override + State createState() => _ChangelogModalState(); +} + +class _ChangelogModalState extends State { + List _items = []; + + @override + void initState() { + super.initState(); + loadChangelog(); + } + + void loadChangelog() async { + String lang = WidgetsBinding.instance.platformDispatcher.locale.languageCode; + if (lang == "und" || + lang.isEmpty || + !(await File("$changelogTextLocation/changelog_$lang.json").exists())) { + lang = "en"; + } + + final List changelog = + jsonDecode(await rootBundle.loadString("$changelogTextLocation/changelog_$lang.json")) + as List; + + for (final item in changelog) { + final itemMap = item as Map; + _items.add(ChangelogItem( + itemMap["icon"] as String? ?? "", + itemMap["title"] as String? ?? "", + itemMap["description"] as String? ?? "", + )); + } + setState(() {}); + } + + @override + Widget build(BuildContext context) { + if (_items.isEmpty) return SizedBox.shrink(); + + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 12, + children: [ + SizedBox(), + ModalTopBar(title: S.of(context).whats_new), + SizedBox(), + Flexible( + child: SingleChildScrollView( + child: Column( + children: [ + VersionNumberHeader(version: widget.version), + SizedBox(height: 40), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 32, + children: _items.map((item) => ChangelogItemWidget(item: item)).toList(), + ), + SizedBox(height: 32), + Text( + S.of(context).and_much_more_to_come, + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), + ) + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + spacing: 12, + children: [ + NewPrimaryButton( + onPressed: () { + try { + launchUrl(Uri.https("blog.cakewallet.com")); + } catch (_) {} + }, + text: S.of(context).view_more_info, + color: Theme.of(context).colorScheme.surfaceContainer, + textColor: Theme.of(context).colorScheme.primary), + NewPrimaryButton( + onPressed: Navigator.of(context).pop, + text: S.of(context).continue_text, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary), + ], + ), + ) + ], + ), + ), + ); + } +} + +class VersionNumberHeader extends StatelessWidget { + VersionNumberHeader({super.key, required this.version}); + + final String version; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).colorScheme.primary, width: 2), + borderRadius: BorderRadius.circular(99999999)), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16), + child: Row( + spacing: 8, + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + "assets/images/cake_logo_dark.svg", + height: 32, + width: 32, + colorFilter: + ColorFilter.mode(Theme.of(context).colorScheme.onSurface, BlendMode.srcIn), + ), + Text( + version, + style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontSize: 36), + ) + ], + ), + ), + ); + } +} + +class ChangelogItemWidget extends StatelessWidget { + const ChangelogItemWidget({super.key, required this.item}); + + final ChangelogItem item; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + spacing: 12, + children: [ + if (item.iconFilename.isNotEmpty) + SvgPicture.asset( + "$changelogIconLocation/${item.iconFilename}.svg", + height: 36, + width: 36, + colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 4, + children: [ + if (item.title.isNotEmpty) + Text( + item.title, + style: TextStyle(fontWeight: FontWeight.w500), + ), + if (item.description.isNotEmpty) + Text(item.description, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + )) + ], + ) + ], + ); + } +} diff --git a/lib/new-ui/widgets/coin_control_page/coin_control_list_item.dart b/lib/new-ui/widgets/coin_control_page/coin_control_list_item.dart new file mode 100644 index 000000000..1ec608d97 --- /dev/null +++ b/lib/new-ui/widgets/coin_control_page/coin_control_list_item.dart @@ -0,0 +1,128 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/new_simple_checkbox.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class CoinControlListItem extends StatelessWidget { + CoinControlListItem({ + required this.note, + required this.amount, + required this.fiatAmount, + required this.address, + required this.isSending, + required this.isFrozen, + required this.isChange, + required this.isSilentPayment, + required this.isFirst, + required this.isLast, + required this.isLoading, + this.onCheckBoxTap, + }); + + final String note; + final String amount; + final String fiatAmount; + final String address; + final bool isSending; + final bool isFrozen; + final bool isChange; + final bool isSilentPayment; + final bool isFirst; + final bool isLast; + final bool isLoading; + final Function()? onCheckBoxTap; + + @override + Widget build(BuildContext context) { + return Container( + height:70, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.vertical( + top: isFirst ? Radius.circular(12) : Radius.circular(0), + bottom: isLast ? Radius.circular(12) : Radius.circular(0), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + spacing: 12, + children: [ + _getLeading(context), + // NewSimpleCheckbox(value: isSending, onChanged: (value){onCheckBoxTap?.call();}), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + spacing: 4, + children: [ + Row( + spacing:4, + children: [ + Text(amount, style: TextStyle(fontSize:14,fontWeight: FontWeight.w400,color: Theme.of(context).colorScheme.onSurface),), + if(address.toLowerCase().contains("mweb")) // hack carried over from old ui, we really should just have a boolean in the object + SvgPicture.asset( + "assets/new-ui/address-type-picker-icons/mweb.svg", + width: 18, + height: 18, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn), + ), + if(isSilentPayment) + SvgPicture.asset( + "assets/new-ui/address-type-picker-icons/silent.svg", + width: 18, + height: 18, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn), + ), + ], + ), + AutoSizeText( + '${address.substring(0, 5)}...${address.substring(address.length - 5)}', + style: TextStyle(fontSize:12,fontWeight: FontWeight.w400,color: Theme.of(context).colorScheme.onSurfaceVariant), + maxLines: 1, + ), + ], + + ) + ], + ), + Row( + spacing: 8, + children: [ + Text( + fiatAmount, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + SvgPicture.asset( + "assets/new-ui/arrow_right.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn), + ), + ], + ) + ], + ), + ) + ); + } + + Widget _getLeading(BuildContext context) { + if(isLoading) { + return CircularProgressIndicator(color: Theme.of(context).colorScheme.primary,); + } + + if(isFrozen) { + return SvgPicture.asset("assets/new-ui/frozen.svg"); + } + + return NewSimpleCheckbox(value: isSending, onChanged: (value){onCheckBoxTap?.call();}); + + } +} diff --git a/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart b/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart new file mode 100644 index 000000000..40c62d82f --- /dev/null +++ b/lib/new-ui/widgets/coins_page/action_row/coin_action_button.dart @@ -0,0 +1,70 @@ +import 'dart:math'; + +import 'package:cake_wallet/themes/core/theme_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/svg.dart'; + +class CoinActionButton extends StatelessWidget { + const CoinActionButton({ + super.key, + required this.icon, + required this.label, + required this.action, + }); + + final SvgPicture icon; + final String label; + final VoidCallback action; + + static const sizeFactor = 0.16; + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size.width * sizeFactor; + final double effectiveSize = min(size, 80); + + return Column( + children: [ + Container( + width: effectiveSize, + height: effectiveSize, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [ + context.customColors.cardGradientColorPrimary, + context.customColors.cardGradientColorSecondary + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + border: Border.all( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + width: 1, + ), + ), + child: IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () { + HapticFeedback.mediumImpact(); + action(); + }, + icon: icon, + color: Theme.of(context).colorScheme.primary, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + label, + ), + ), + ], + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart b/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart new file mode 100644 index 000000000..2541e9efb --- /dev/null +++ b/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart @@ -0,0 +1,204 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/core/open_crypto_pay/open_cryptopay_service.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/qr_scanner.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/modal_navigator.dart'; +import 'package:cake_wallet/new-ui/pages/send_page.dart'; +import 'package:cake_wallet/new-ui/pages/swap_page.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/utils/feature_flag.dart'; +import 'package:cake_wallet/utils/payment_request.dart'; +import 'package:cake_wallet/view_model/send/send_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/lnurl.dart'; +import 'package:cw_core/unspent_coin_type.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; + +import '../../../pages/receive_page.dart'; +import '../../../pages/scan_page.dart'; +import 'coin_action_button.dart'; + +class CoinActionRow extends StatelessWidget { + const CoinActionRow({super.key, this.lightningMode = false, this.showSwap = true}); + + final bool lightningMode; + final bool showSwap; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + spacing: MediaQuery.of(context).size.width * 0.05, + children: [ + CoinActionButton( + icon: SvgPicture.asset( + "assets/new-ui/send.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + label: S.of(context).send, + action: () { + if (FeatureFlag.hasNewUiExtraPages) { + final sendPage = getIt.get( + param1: SendPageParams( + unspentCoinType: + lightningMode ? UnspentCoinType.lightning : UnspentCoinType.nonMweb, + ), + ); + + CupertinoScaffold.showCupertinoModalBottomSheet( + context: context, + barrierColor: Colors.black.withAlpha(60), + builder: (context) { + return Material( + child: ModalNavigator( + rootPage: sendPage, + parentContext: context, + ), + ); + }, + ); + } else { + Map? args; + if (lightningMode) args = {'coinTypeToSpendFrom' : UnspentCoinType.lightning}; + Navigator.of(context).pushNamed(Routes.send, arguments: args); + } + }, + ), + CoinActionButton( + icon: SvgPicture.asset( + "assets/new-ui/receive.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + label: S.of(context).receive, + action: () async { + if (FeatureFlag.hasNewUiExtraPages) { + final page = getIt.get(param1: lightningMode); + CupertinoScaffold.showCupertinoModalBottomSheet( + context: context, + barrierColor: Colors.black.withAlpha(60), + builder: (context) { + return Material(child: ModalNavigator(parentContext:context,rootPage: page)); + }, + ); + } else { + // ToDo: (Konsti) refactor as part of the derivation PR (I hate myself for it) + if (lightningMode) { + await getIt().setAddressType( + bitcoin!.getOptionToType(bitcoin!.getBitcoinLightningReceivePageOption())); + } else { + await getIt().setAddressType( + bitcoin!.getOptionToType(bitcoin!.getBitcoinSegwitPageOption())); + } + Navigator.of(context).pushNamed(Routes.addressPage); + } + }, + ), + if (showSwap) + CoinActionButton( + icon: SvgPicture.asset( + "assets/new-ui/exchange.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + label: S.of(context).swap, + action: () { + final page = getIt.get(param2: lightningMode ? CryptoCurrency.btcln : null); + if (FeatureFlag.hasNewUiExtraPages) { + CupertinoScaffold.showCupertinoModalBottomSheet( + context: context, + barrierColor: Colors.black.withAlpha(85), + builder: (context) => Material( + child: ModalNavigator( + rootPage: page, + parentContext: context, + )), + ); + } else { + Navigator.of(context).pushNamed(Routes.exchange); + } + }, + ), + CoinActionButton( + icon: SvgPicture.asset( + "assets/new-ui/scan.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + label: S.of(context).scan, + action: () => _onPressedScan(context), + ), + ], + ), + ); + } + + Future _onPressedScan(BuildContext context) async { + if (false && FeatureFlag.hasNewUiExtraPages) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => FractionallySizedBox( + heightFactor: 0.9, + child: ScanPage(), + ), + ); + } else { + final code = await presentQRScanner(context); + + if (code == null || code.isEmpty) return; + + late final PaymentRequest req; + var unspentCoinType = UnspentCoinType.any; + if (SendViewModelBase.isNonZeroAmountLightningInvoice(code)) { + unspentCoinType = UnspentCoinType.lightning; + final amount = CryptoCurrency.btcln.formatAmount(BigInt.from(getBolt11Amount(code) ?? 0)); + req = PaymentRequest(code, amount, "", "", ""); + } else if (SendViewModelBase.isLnurlInvoice(code)) { + unspentCoinType = UnspentCoinType.lightning; + final amount = CryptoCurrency.btcln.formatAmount(BigInt.from(await LNURL.getPayRequestAmount(code) ?? 0)); + req = PaymentRequest(code, amount, "", "", ""); + } else if (OpenCryptoPayService.isOpenCryptoPayQR(code)) { + req = PaymentRequest(code, "", "", "", ""); + } else { + final uri = Uri.tryParse(code); + if (uri == null) return; + req = PaymentRequest.fromUri(uri); + } + + final sendPage = getIt.get( + param1: SendPageParams( + initialPaymentRequest: req, + unspentCoinType: unspentCoinType, + ), + ); + + CupertinoScaffold.showCupertinoModalBottomSheet( + context: context, + barrierColor: Colors.black.withAlpha(60), + builder: (context) => Material( + child: ModalNavigator( + rootPage: sendPage, + parentContext: context, + ), + ), + ); + } + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/anonpay_history_tile.dart b/lib/new-ui/widgets/coins_page/assets_history/anonpay_history_tile.dart new file mode 100644 index 000000000..4937db946 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/anonpay_history_tile.dart @@ -0,0 +1,37 @@ +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_tile_base.dart'; +import 'package:flutter/material.dart'; + +class AnonpayHistoryTile extends StatelessWidget { + const AnonpayHistoryTile( + {super.key, + required this.provider, + required this.createdAt, + required this.amount, + required this.currency, + required this.roundedTop, + required this.roundedBottom, + required this.bottomSeparator}); + + final String provider; + final String createdAt; + final String amount; + final String currency; + final bool roundedTop; + final bool roundedBottom; + final bool bottomSeparator; + + @override + Widget build(BuildContext context) { + return HistoryTileBase( + title: provider, + date: createdAt, + amount: amount + " " + currency, + leadingIcon: ClipRRect( + borderRadius: BorderRadius.circular(50), + child: Image.asset('assets/images/trocador.png', width: 36, height: 36)), + amountFiat: "", + roundedTop: roundedTop, + roundedBottom: roundedBottom, + bottomSeparator: bottomSeparator); + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/asset_details_modal.dart b/lib/new-ui/widgets/coins_page/assets_history/asset_details_modal.dart new file mode 100644 index 000000000..032dfad03 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/asset_details_modal.dart @@ -0,0 +1,342 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/modal_navigator.dart'; +import 'package:cake_wallet/new-ui/pages/receive_page.dart'; +import 'package:cake_wallet/new-ui/pages/send_page.dart'; +import 'package:cake_wallet/new-ui/pages/swap_page.dart'; +import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/utils/payment_request.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/unspent_coin_type.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; + +enum AssetDetailsModalModes { normal, ltcTransparent, ltcPrivate } + +class AssetDetailsModal extends StatelessWidget { + const AssetDetailsModal( + {super.key, + required this.title, + required this.chainTitle, + required this.subtitle, + required this.amount, + required this.currencyTitle, + required this.fiatAmount, + required this.iconPath, + required this.chainIconPath, + required this.mode, + required this.wallet, + required this.showSwap, + this.asset}); + + final String title; + final CryptoCurrency? asset; + final String chainTitle; + final String subtitle; + final String amount; + final String currencyTitle; + final String fiatAmount; + final String iconPath; + final String chainIconPath; + final WalletBase wallet; + final bool showSwap; + final AssetDetailsModalModes mode; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(16))), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ModalTopBar( + title: "", + trailingIcon: Icon(Icons.close), + onTrailingPressed: Navigator.of(context).pop, + ), + SafeArea( + child: Column( + spacing: 24, + children: [ + Column( + spacing: 15, + children: [ + SizedBox( + width: 75, + height: 75, + child: Stack( + children: [ + if (iconPath.isNotEmpty) + Image.asset(iconPath, width: 75, height: 75) + else + Container( + width: 75, + height: 75, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular(99999)), + child: Center( + child: Text( + title.substring(0, 2), + style: TextStyle( + fontSize: 28, color: Theme.of(context).colorScheme.onPrimary), + )), + ), + if (chainIconPath.isNotEmpty) + Align( + alignment: Alignment.bottomRight, + child: Container( + decoration: ShapeDecoration( + shape: RoundedSuperellipseBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide(color: Colors.black)), + color: Colors.white), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: SvgPicture.asset( + chainIconPath, + width: 18, + height: 18, + colorFilter: + ColorFilter.mode(Colors.black, BlendMode.srcIn), + ), + ))) + ], + ), + ), + Column( + spacing: 4, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 4, + children: [ + Text( + title, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface), + ), + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(999999999)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4), + child: Text( + chainTitle, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + ), + ) + ], + ), + if (subtitle.isNotEmpty) + Text( + subtitle, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + ], + ) + ], + ), + Container( + height: 1, + width: MediaQuery.of(context).size.width * 0.8, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + Column( + spacing: 4, + children: [ + Row( + spacing: 4, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + amount, + style: TextStyle(fontSize: 28), + ), + Text(currencyTitle, + style: TextStyle( + fontSize: 28, + color: Theme.of(context).colorScheme.onSurfaceVariant)) + ], + ), + Text( + fiatAmount, + style: TextStyle( + fontSize: 20, color: Theme.of(context).colorScheme.onSurfaceVariant), + ) + ], + ), + SizedBox(), + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 20, + children: [ + if (mode == AssetDetailsModalModes.ltcTransparent) + AssetDetailsModalBottomButton( + iconPath: "assets/new-ui/mask.svg", + title: S.of(context).mask, + onPressed: () { + Navigator.of(context).pop(); + depositToL2(context); + }, + ), + if (mode == AssetDetailsModalModes.ltcPrivate) + AssetDetailsModalBottomButton( + iconPath: "assets/new-ui/unmask.svg", + title: S.of(context).unmask, + onPressed: () { + Navigator.of(context).pop(); + withdrawFromL2(context); + }, + ), + AssetDetailsModalBottomButton( + iconPath: "assets/new-ui/send.svg", + title: S.of(context).send, + onPressed: () => openPage( + context, + param1: SendPageParams( + initialCurrency: asset, + unspentCoinType: unspentCoinType, + ), + ), + ), + AssetDetailsModalBottomButton( + iconPath: "assets/new-ui/receive.svg", + title: S.of(context).receive, + onPressed: () async { + if (mode == AssetDetailsModalModes.ltcPrivate) { + await bitcoin!.setAddressType( + wallet, + bitcoin! + .getOptionToType(bitcoin!.getLitecoinMwebReceivePageOption())); + } + openPage(context, param2: asset); + }, + ), + if (showSwap && mode != AssetDetailsModalModes.ltcPrivate) + AssetDetailsModalBottomButton( + iconPath: "assets/new-ui/exchange.svg", + title: S.of(context).swap, + onPressed: () => openPage(context, param2: asset), + ), + ], + ), + SizedBox() + ], + ), + ) + ], + ), + ); + } + + UnspentCoinType get unspentCoinType { + switch (mode) { + case AssetDetailsModalModes.ltcTransparent: + return UnspentCoinType.nonMweb; + case AssetDetailsModalModes.ltcPrivate: + return UnspentCoinType.mweb; + case AssetDetailsModalModes.normal: + return UnspentCoinType.any; + } + } + + void openPage(BuildContext context, {dynamic param1, dynamic param2}) { + final page = getIt.get(param1: param1, param2: param2); + showCupertinoModalBottomSheet( + context: context, + barrierColor: Colors.black.withAlpha(60), + builder: (context) { + return Material(child: ModalNavigator(parentContext: context, rootPage: page as Widget)); + }, + ); + } + + Future depositToL2(BuildContext context) async { + PaymentRequest? paymentRequest = null; + + final depositAddress = bitcoin!.getUnusedMwebAddress(wallet); + if ((depositAddress?.isNotEmpty ?? false)) { + paymentRequest = PaymentRequest.fromUri(Uri.parse("litecoin:$depositAddress")); + } + + final page = getIt.get( + param1: SendPageParams( + initialPaymentRequest: paymentRequest, + unspentCoinType: UnspentCoinType.nonMweb, + mode: SendPageModes.mwebDeposit, + )); + showCupertinoModalBottomSheet( + context: context, + barrierColor: Colors.black.withAlpha(128), + builder: (context) { + return FractionallySizedBox( + heightFactor: 0.65, + child: ModalNavigator(parentContext: context, rootPage: Material(child: page))); + }); + } + + Future withdrawFromL2(BuildContext context) async { + PaymentRequest? paymentRequest = null; + UnspentCoinType unspentCoinType = UnspentCoinType.any; + final withdrawAddress = bitcoin!.getUnusedSegwitAddress(wallet); + + if ((withdrawAddress?.isNotEmpty ?? false)) { + paymentRequest = PaymentRequest.fromUri(Uri.parse("litecoin:$withdrawAddress")); + } + unspentCoinType = UnspentCoinType.mweb; + + final page = getIt.get( + param1: SendPageParams( + initialPaymentRequest: paymentRequest, + unspentCoinType: unspentCoinType, + mode: SendPageModes.mwebWithdrawal, + )); + showCupertinoModalBottomSheet( + context: context, + barrierColor: Colors.black.withAlpha(128), + builder: (context) { + return FractionallySizedBox( + heightFactor: 0.65, + child: ModalNavigator(parentContext: context, rootPage: Material(child: page))); + }); + } +} + +class AssetDetailsModalBottomButton extends StatelessWidget { + const AssetDetailsModalBottomButton( + {super.key, required this.iconPath, required this.title, required this.onPressed}); + + final String iconPath; + final String title; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return Column( + spacing: 8, + children: [ + ModernButton.svg( + onPressed: onPressed, + svgPath: iconPath, + size: 60, + iconSize: 32, + ), + Text(title) + ], + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/asset_tile.dart b/lib/new-ui/widgets/coins_page/assets_history/asset_tile.dart new file mode 100644 index 000000000..a23dfa257 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/asset_tile.dart @@ -0,0 +1,197 @@ +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/asset_details_modal.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/utils/string_parsing.dart'; +import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class AssetTile extends StatelessWidget { + const AssetTile( + {super.key, + required this.balance, + required this.chainIconPath, + this.showSecondary = false, + this.title, + this.trailingText, + this.modalMode = AssetDetailsModalModes.normal, + required this.wallet, required this.showSwap}); + + final BalanceRecord balance; + final bool showSecondary; + final bool showSwap; + final String chainIconPath; + final String? title; + final String? trailingText; + final AssetDetailsModalModes modalMode; + final WalletBase wallet; + + @override + Widget build(BuildContext context) { + final iconPath = _getIconPath(); + + return GestureDetector( + onTap: (){ + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) { + return AssetDetailsModal( + showSwap: showSwap, + asset: balance.asset, + title: title ?? balance.asset.fullName ?? balance.asset.name, + chainTitle: _getChainTitle(), + subtitle: trailingText ?? "", + amount: showSecondary ? balance.secondAvailableBalance : balance.availableBalance, + currencyTitle: balance.asset.title, + fiatAmount: showSecondary + ? balance.fiatSecondAvailableBalance + : balance.fiatAvailableBalance, + iconPath: balance.asset.iconPath ?? "", + chainIconPath: chainIconPath, + mode: modalMode, + wallet: wallet, + ); + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 6.0), + child: Container( + width: double.infinity, + height: 80, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Theme.of(context).colorScheme.surfaceContainerHigh, + Theme.of(context).colorScheme.surfaceContainer, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + borderRadius: BorderRadius.all(Radius.circular(20)), + border: Border.all( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + width: 1, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 45, + height: 45, + child: Stack( + children: [ + if((iconPath).isNotEmpty) + Image.asset(iconPath) + else + Container( + width: 45, + height: 45, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular(99999)), + child: Center( + child: Text( + balance.asset.name.substring(0, 2), + style: TextStyle( + fontSize: 20, color: Theme.of(context).colorScheme.onPrimary), + )), + ), + if (chainIconPath.isNotEmpty) + Align( + alignment: Alignment.bottomRight, + child: Container( + decoration: ShapeDecoration( + shape: RoundedSuperellipseBorder( + borderRadius: BorderRadius.circular(5),side: BorderSide(color: Colors.black)), + color: Colors.white), + child: Padding( + padding: const EdgeInsets.all(2.0), + child: SvgPicture.asset( + chainIconPath, + width: 12, + height: 12, + colorFilter: + ColorFilter.mode(Colors.black, BlendMode.srcIn), + ), + ))) + ], + )), + SizedBox(width: 8.0), + Expanded( + child: Column( + spacing: 4.0, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + spacing: 4, + children: [ + Text( + title ?? balance.asset.fullName ?? balance.asset.name, + style: TextStyle(fontWeight: FontWeight.bold), + ), + if(trailingText != null) + Text( + trailingText!, + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + ], + ), + Padding( + padding: const EdgeInsets.only(right: 4.0), + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + "${showSecondary ?balance.secondAvailableBalance: balance.availableBalance} ${balance.formattedAssetTitle.safeSubString(0, 6)}", + maxLines:1, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + Text( + showSecondary ? balance.fiatSecondAvailableBalance : balance.fiatAvailableBalance, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ], + ), + ), + ), + ), + ); + } + + String _getChainTitle() { + try { + return CryptoCurrency.fromString(wallet.currency.tag ??wallet.currency.title).title; + } catch(e) { + return wallet.currency.title; + } + } + + String _getIconPath() { + if(balance.asset == CryptoCurrency.baseEth) + return "assets/images/crypto/ethereum.webp"; + + return balance.asset.iconPath ?? ""; + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/assets_history_section.dart b/lib/new-ui/widgets/coins_page/assets_history/assets_history_section.dart new file mode 100644 index 000000000..df4e8253f --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/assets_history_section.dart @@ -0,0 +1,88 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/nft_listing_page.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:mobx/mobx.dart'; +import 'assets_section.dart'; +import 'history_section.dart'; + +class AssetsHistorySectionTab { + final String title; + final Widget content; + + AssetsHistorySectionTab(this.title, this.content); +} + +class AssetsHistorySection extends StatefulWidget { + AssetsHistorySection({super.key, required this.dashboardViewModel, required this.nftViewModel}); + + final DashboardViewModel dashboardViewModel; + final NFTViewModel nftViewModel; + + @override + State createState() => _AssetsHistorySectionState(); +} + +class _AssetsHistorySectionState extends State { + List tabs = []; + int _selectedTab = 0; + + void reloadTabs() { + final oldTabLength = tabs.length; + tabs = [ + if (widget.dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled || (widget.dashboardViewModel.hasMweb && widget.dashboardViewModel.mwebEnabled)) + AssetsHistorySectionTab( + S.current.assets, + AssetsSection( + dashboardViewModel: widget.dashboardViewModel, + )), + AssetsHistorySectionTab( + S.current.history, + HistorySection( + dashboardViewModel: widget.dashboardViewModel, + )), + if (isNFTACtivatedChain(widget.dashboardViewModel.wallet.type, + chainId: widget.dashboardViewModel.wallet.chainId)) + AssetsHistorySectionTab(S.current.nfts, NFTListingPage(nftViewModel: widget.nftViewModel)) + ]; + if (oldTabLength != tabs.length) { + setState(() { + _selectedTab = 0; + }); + } + } + + @override + void initState() { + super.initState(); + reloadTabs(); + + reaction((_)=>widget.dashboardViewModel.balanceViewModel.formattedBalances, (value) { + reloadTabs(); + }); + reaction((_)=>widget.dashboardViewModel.mwebEnabled, (_)=>reloadTabs()); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + if(tabs.length>1) + AssetsTopBar( + dashboardViewModel: widget.dashboardViewModel, + tabs: tabs.map((item) => item.title).toList(), + onTabChange: (index) { + setState(() { + _selectedTab = index; + }); + }, + selectedTab: _selectedTab, + ), + tabs[_selectedTab].content + ], + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/assets_section.dart b/lib/new-ui/widgets/coins_page/assets_history/assets_section.dart new file mode 100644 index 000000000..e2d15eea8 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/assets_section.dart @@ -0,0 +1,69 @@ +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/asset_details_modal.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + + +import 'asset_tile.dart'; + +class AssetsSection extends StatelessWidget { + const AssetsSection({super.key, required this.dashboardViewModel}); + + final DashboardViewModel dashboardViewModel; + + @override + Widget build(BuildContext context) { + + return Padding( + padding: const EdgeInsets.only(bottom: 64.0), + child: Observer( + builder: (context) { + final hasMweb = dashboardViewModel.hasMweb && dashboardViewModel.mwebEnabled; + return ListView.builder( + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: NeverScrollableScrollPhysics(), + itemCount: dashboardViewModel.balanceViewModel.formattedBalances.length + (hasMweb ? 1 : 0), + itemBuilder: (context, index) { + return Observer(builder: (context) { + if (hasMweb) { + return AssetTile( + showSwap: dashboardViewModel.isEnabledSwapAction, + balance: dashboardViewModel.balanceViewModel.formattedBalances.first, + showSecondary: index > 0 ? true : false, + wallet: dashboardViewModel.wallet, + chainIconPath: index > 0 ? dashboardViewModel.wallet.currency.chainIconPath! : "", + trailingText: index > 0 ? "MWEB" : null, + modalMode: index > 0 + ? AssetDetailsModalModes.ltcPrivate + : AssetDetailsModalModes.ltcTransparent, + title: index > 0 ? "Litecoin Private" : null, + ); + } + + final balance = dashboardViewModel.balanceViewModel.formattedBalances.elementAt(index); + return AssetTile( + showSwap: dashboardViewModel.isEnabledSwapAction, + balance: balance, + wallet: dashboardViewModel.wallet, + chainIconPath: _getChainIconPath(), + ); + }); + + }, + ); + } + ), + ); + } + + // TODO refactor chainIconPath to get rid of this ugly thing. it's needed because arbitrum's wallet currency isn't arb + String _getChainIconPath() { + try { + return CryptoCurrency.fromString(dashboardViewModel.wallet.currency.tag ??dashboardViewModel.wallet.currency.title).chainIconPath!; + } catch(e) { + return dashboardViewModel.wallet.currency.chainIconPath ?? ""; + } + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart b/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart new file mode 100644 index 000000000..b213ccf26 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart @@ -0,0 +1,76 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/line_tab_switcher.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class AssetsTopBar extends StatelessWidget { + const AssetsTopBar({ + super.key, + required this.onTabChange, + required this.selectedTab, + required this.tabs, required this.dashboardViewModel, + }); + + final void Function(int) onTabChange; + final int selectedTab; + final List tabs; + final DashboardViewModel dashboardViewModel; + + @override + Widget build(BuildContext context) { + final hasTokenSettingsButton = tabs[selectedTab] == S.of(context).assets && dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled; + + return Padding( + padding: const EdgeInsets.only(top: 12.0, bottom: 12.0, left: 12.0, right: 18.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LineTabSwitcher( + tabs: tabs, + onTabChange: onTabChange, + selectedTab: selectedTab, + ), + Opacity( + opacity: hasTokenSettingsButton ? 1 : 0, + child: ElevatedButton( + onPressed: () { + if (hasTokenSettingsButton) { + Navigator.of(context).pushNamed( + Routes.homeSettings, + arguments: dashboardViewModel.balanceViewModel, + ); + } + }, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(999999), + ), + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainer, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + spacing: 4.0, + children: [ + SvgPicture.asset("assets/new-ui/options_slider.svg", colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary,BlendMode.srcIn)), + Text( + S.of(context).tokens, + style: TextStyle(color: Theme.of(context).colorScheme.primary), + ) + ], + ), + ), + ), + ), + + ], + ), + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_order_tile.dart b/lib/new-ui/widgets/coins_page/assets_history/history_order_tile.dart new file mode 100644 index 000000000..994d73e5f --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/history_order_tile.dart @@ -0,0 +1,43 @@ +import 'package:cake_wallet/exchange/trade_state.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_tile_base.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:flutter/material.dart'; + +class HistoryOrderTile extends StatelessWidget { + const HistoryOrderTile( + {super.key, + required this.date, + required this.amount, + required this.amountFiat, + required this.roundedTop, + required this.roundedBottom, + required this.bottomSeparator, + }); + + final String date; + final String amount; + final String amountFiat; + final bool roundedTop; + final bool roundedBottom; + final bool bottomSeparator; + + + + + @override + Widget build(BuildContext context) { + + return HistoryTileBase( + title: "Order", + date: date, + amount: amount, + amountFiat: amountFiat, + leadingIcon:Image.asset("assets/images/cakepay.png"), + roundedTop: roundedTop, + roundedBottom: roundedBottom, + bottomSeparator: bottomSeparator, + ); + + } + +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_section.dart b/lib/new-ui/widgets/coins_page/assets_history/history_section.dart new file mode 100644 index 000000000..fd1617fec --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/history_section.dart @@ -0,0 +1,168 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/anonpay_history_tile.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_order_tile.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_tile.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_trade_tile.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/payjoin_history_tile.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/utils/date_formatter.dart'; +import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/date_section_item.dart'; +import 'package:cake_wallet/view_model/dashboard/order_list_item.dart'; +import 'package:cake_wallet/view_model/dashboard/payjoin_transaction_list_item.dart'; +import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart'; +import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:intl/intl.dart'; + +class HistorySection extends StatelessWidget { + const HistorySection({super.key, required this.dashboardViewModel}); + + final DashboardViewModel dashboardViewModel; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Observer( + builder: (_) => (dashboardViewModel.items.isEmpty && + dashboardViewModel.status is! SyncingSyncStatus) + ? Padding( + padding: EdgeInsets.only(top: 24), + child: Text(S.of(context).transactions_will_appear_here, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant))) + : ListView.builder( + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: dashboardViewModel.items.length, + itemBuilder: (context, index) => Observer(builder: (_) { + final prevItem = index == 0 ? null : dashboardViewModel.items[index - 1]; + final item = dashboardViewModel.items[index]; + final nextItem = index == dashboardViewModel.items.length - 1 + ? null + : dashboardViewModel.items[index + 1]; + + final roundedBottom = (nextItem == null || nextItem is DateSectionItem); + final roundedTop = (prevItem == null || prevItem is DateSectionItem); + + if (item is TransactionListItem) { + final transaction = item.transaction; + final transactionType = dashboardViewModel.getTransactionType(transaction); + + if (item.hasTokens && item.assetOfTransaction == null) { + return Container(); + } + + CryptoCurrency? asset; + if (transaction.additionalInfo["isLightning"] == true) + asset = CryptoCurrency.btcln; + + return GestureDetector( + onTap: () => Navigator.of(context) + .pushNamed(Routes.transactionDetails, arguments: transaction), + child: HistoryTile( + title: item.formattedTitle + transactionType, + date: DateFormat('HH:mm').format(transaction.date), + amount: item.formattedCryptoAmount, + amountFiat: item.formattedFiatAmount, + roundedBottom: roundedBottom, + roundedTop: roundedTop, + bottomSeparator: !roundedBottom, + direction: item.transaction.direction, + pending: item.transaction.isPending, + asset: asset, + ), + ); + } else if (item is TradeListItem) { + final trade = item.trade; + + final tradeFrom = trade.fromRaw >= 0 ? trade.from : trade.userCurrencyFrom; + + final tradeTo = trade.toRaw >= 0 ? trade.to : trade.userCurrencyTo; + + return GestureDetector( + onTap: () => Navigator.of(context) + .pushNamed(Routes.tradeDetails, arguments: trade), + child: HistoryTradeTile( + from: tradeFrom!, + to: tradeTo!, + date: DateFormat('HH:mm').format(item.trade.createdAt!), + amount: trade.amountFormatted(), + receiveAmount: trade.receiveAmountFormatted(), + roundedBottom: roundedBottom, + roundedTop: roundedTop, + bottomSeparator: !roundedBottom, + swapState: trade.state, + ), + ); + } else if (item is DateSectionItem) { + return Padding( + padding: EdgeInsets.only(left: 8.0, bottom: 8.0, top: 18.0), + child: Text(DateFormatter.convertDateTimeToReadableString(item.date), + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant))); + }else if(item is OrderListItem){ + return GestureDetector( + onTap: () => Navigator.of(context) + .pushNamed(Routes.orderDetails, arguments: item.order), + child: HistoryOrderTile( + date: DateFormat('HH:mm').format(item.order.createdAt), + amount: item.orderFormattedAmount, + amountFiat: "USD 0.00", + roundedBottom: roundedBottom, + roundedTop: roundedTop, + bottomSeparator: !roundedBottom, + ), + ); + } else if (item is PayjoinTransactionListItem) { + final session = item.session; + + return GestureDetector( + onTap: () => Navigator.of(context).pushNamed( + Routes.payjoinDetails, + arguments: [item.sessionId, item.transaction], + ), + child: PayjoinHistoryTile( + createdAt: DateFormat('HH:mm').format(session.inProgressSince!), + amount: dashboardViewModel.appStore.amountParsingProxy + .getDisplayCryptoString(session.amount.toInt(), CryptoCurrency.btc), + currency: item.transaction?.from ?? "BTC", + state: item.status, + isSending: session.isSenderSession, + roundedTop: roundedTop, + roundedBottom: roundedBottom, + bottomSeparator: !roundedBottom), + ); + } else if (item is AnonpayTransactionListItem) { + final transactionInfo = item.transaction; + + return GestureDetector( + onTap: () => Navigator.of(context) + .pushNamed(Routes.anonPayDetailsPage, arguments: transactionInfo), + child: AnonpayHistoryTile( + provider: transactionInfo.provider, + createdAt: DateFormat('HH:mm').format(transactionInfo.createdAt), + amount: transactionInfo.fiatAmount?.toString() ?? + (transactionInfo.amountTo?.toString() ?? ''), + currency: transactionInfo.fiatAmount != null + ? transactionInfo.fiatEquiv ?? '' + : CryptoCurrency.fromFullName(transactionInfo.coinTo).name.toUpperCase(), + roundedTop: roundedTop, + roundedBottom: roundedBottom, + bottomSeparator: !roundedBottom)); + } else + return Text(item.runtimeType.toString()); + }), + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_tile.dart b/lib/new-ui/widgets/coins_page/assets_history/history_tile.dart new file mode 100644 index 000000000..d27c8810c --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/history_tile.dart @@ -0,0 +1,118 @@ +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_tile_base.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class HistoryTile extends StatelessWidget { + const HistoryTile({ + super.key, + required this.title, + required this.date, + required this.amount, + required this.amountFiat, + required this.roundedTop, + required this.roundedBottom, + required this.direction, + required this.pending, + required this.bottomSeparator, + this.asset, + }); + + final String title; + final String date; + final String amount; + final String amountFiat; + final bool roundedTop; + final bool roundedBottom; + final bool bottomSeparator; + final TransactionDirection direction; + final bool pending; + final CryptoCurrency? asset; + + String _getDirectionIcon() { + if (pending) { + return direction == TransactionDirection.incoming + ? 'assets/new-ui/history-receiving.svg' + : 'assets/new-ui/history-sending.svg'; + } else { + return direction == TransactionDirection.incoming + ? 'assets/new-ui/history-received.svg' + : 'assets/new-ui/history-sent.svg'; + } + } + + String _getDirectionIconToken() { + if (pending) { + return direction == TransactionDirection.incoming + ? 'assets/new-ui/history-receiving.svg' + : 'assets/new-ui/history-sending.svg'; + } else { + return direction == TransactionDirection.incoming + ? 'assets/new-ui/token-received.svg' + : 'assets/new-ui/token-sent.svg'; + } + } + + Color? _getPrimaryTextColor() { + } + + Widget _getLeadingIcon(BuildContext context) { + if (asset == CryptoCurrency.btcln) { + return Stack( + children: [ + SvgPicture.asset( + "assets/images/lightning-icon.svg", + width: 34, + height: 34, + ), + Positioned( + top: 20, + left: 20, + child: Container( + width: 16, + height: 16, + decoration: BoxDecoration(color: Theme.of(context).colorScheme.onInverseSurface, shape: BoxShape.circle), + child: SvgPicture.asset( + _getDirectionIconToken(), + colorFilter: ColorFilter.mode( + direction == TransactionDirection.outgoing + ? Theme + .of(context) + .colorScheme + .inverseSurface.withAlpha(175) + : Colors.green, + BlendMode.srcIn), + width: 14, + height: 14, + ), + ), + ) + ], + ); + } + + return SvgPicture.asset(_getDirectionIcon(), + colorFilter: ColorFilter.mode( + direction == TransactionDirection.outgoing + ? Theme.of(context).colorScheme.inverseSurface.withAlpha(175) + : Colors.green, + BlendMode.srcIn)); + } + + @override + Widget build(BuildContext context) { + return HistoryTileBase( + title: title, + date: date, + amount: amount, + amountFiat: amountFiat, + leadingIcon: _getLeadingIcon(context), + primaryTextColor: _getPrimaryTextColor(), + roundedTop: roundedTop, + roundedBottom: roundedBottom, + bottomSeparator: bottomSeparator, + asset: asset, + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_tile_base.dart b/lib/new-ui/widgets/coins_page/assets_history/history_tile_base.dart new file mode 100644 index 000000000..e2a42f0bd --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/history_tile_base.dart @@ -0,0 +1,143 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:flutter/material.dart'; + +class HistoryTileBase extends StatelessWidget { + const HistoryTileBase({ + super.key, + required this.title, + required this.date, + required this.amount, + required this.leadingIcon, + required this.amountFiat, + required this.roundedTop, + required this.roundedBottom, + required this.bottomSeparator, + this.primaryTextColor, + this.asset, + }); + + final String title; + final String date; + final String amount; + final String amountFiat; + final Widget leadingIcon; + final bool roundedTop; + final bool roundedBottom; + final bool bottomSeparator; + final Color? primaryTextColor; + final CryptoCurrency? asset; + + // String _getDirectionIcon() { + // if (pending) { + // return direction == TransactionDirection.incoming + // ? 'assets/new-ui/history-receiving.svg' + // : 'assets/new-ui/history-sending.svg'; + // } else { + // return direction == TransactionDirection.incoming + // ? 'assets/new-ui/history-received.svg' + // : 'assets/new-ui/history-sent.svg'; + // } + // } + // + // Widget _getLeadingIcon(BuildContext context) { + // if (asset == CryptoCurrency.btcln) { + // return Stack( + // children: [ + // Image.asset( + // asset!.iconPath!, + // width: 34, + // height: 34, + // ), + // Positioned( + // top: 20, + // left: 20, + // child: SvgPicture.asset( + // 'assets/new-ui/chain_badges/lightning.svg', + // width: 16, + // height: 16, + // ), + // ) + // ], + // ); + // } + // + // return SvgPicture.asset(_getDirectionIcon()); + // } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + decoration: ShapeDecoration( + shape: RoundedSuperellipseBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(roundedTop ? 22 : 0), + topRight: Radius.circular(roundedTop ? 22 : 0), + bottomLeft: Radius.circular(roundedBottom ? 22 : 0), + bottomRight: Radius.circular(roundedBottom ? 22 : 0), + )), + color: Theme.of(context).colorScheme.onInverseSurface, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 12, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(right: 10), + child: SizedBox( + height: 36, + width: 36, + child: leadingIcon, + ), + ), + Expanded( + child: Padding( + padding: EdgeInsets.only(right: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, + style: TextStyle(color: Theme.of(context).colorScheme.onSurface)), + Text(date, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant)), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text(amount, + style: TextStyle( + color: primaryTextColor ?? + Theme.of(context).colorScheme.onSurface)), + Text(amountFiat, style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant)), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ), + if(bottomSeparator) Container( + color: Theme.of(context).colorScheme.onInverseSurface, + child: Padding( + padding: EdgeInsets.only(left: 56, right: 16), + child: Container(height: 1, color: Theme.of(context).colorScheme.outlineVariant.withAlpha(175)), + ), + ) + ], + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_trade_tile.dart b/lib/new-ui/widgets/coins_page/assets_history/history_trade_tile.dart new file mode 100644 index 000000000..81f1cd610 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/history_trade_tile.dart @@ -0,0 +1,94 @@ +import 'package:cake_wallet/exchange/trade_state.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_tile_base.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:flutter/material.dart'; + +class HistoryTradeTile extends StatelessWidget { + const HistoryTradeTile( + {super.key, + required this.date, + required this.amount, + required this.receiveAmount, + required this.roundedTop, + required this.roundedBottom, + required this.bottomSeparator, + required this.from, + required this.to, + required this.swapState}); + + final CryptoCurrency from; + final CryptoCurrency to; + final String date; + final String amount; + final String receiveAmount; + final bool roundedTop; + final bool roundedBottom; + final bool bottomSeparator; + final TradeState swapState; + + Widget _getLeadingStack(BuildContext context) { + double currencyIconSize = 22.0; + + return SizedBox( + height: 50, + width: 50, + child: Stack( + children: [ + Image.asset(_getIconPath(from), + width: currencyIconSize, height: currencyIconSize), + Positioned( + top: currencyIconSize / 2, + left: currencyIconSize / 2, + child: Container( + decoration: BoxDecoration( + border: Border.all( + width: 2, + color: Theme.of(context).colorScheme.surfaceContainer), + shape: BoxShape.circle), + child: Image.asset(_getIconPath(to), + width: currencyIconSize, height: currencyIconSize))), + ], + ), + ); + } + + + @override + Widget build(BuildContext context) { + + return HistoryTileBase( + title: + "${from.toString()}${from == CryptoCurrency.btcln ? "-LN" : ""} → ${to.toString()}${to == CryptoCurrency.btcln ? "-LN" : ""}", + date: date, + amount: amount, + amountFiat: receiveAmount, + leadingIcon: _getLeadingStack(context), + roundedTop: roundedTop, + roundedBottom: roundedBottom, + bottomSeparator: bottomSeparator, + ); + } + + String _getIconPath(CryptoCurrency currency) { + if (currency.iconPath != null) { + return currency.iconPath!; + } + + if (currency.name.isNotEmpty) { + final currencyFromName = CryptoCurrency.fromString(currency.name); + if (currencyFromName.iconPath != null) { + return currencyFromName.iconPath!; + } + } + + if (currency.title.isNotEmpty) { + final currencyFromTitle = CryptoCurrency.fromString(currency.title); + if (currencyFromTitle.iconPath != null) { + return currencyFromTitle.iconPath!; + } + } + + //TODO approporiate fallback + return ""; + } +} diff --git a/lib/new-ui/widgets/coins_page/assets_history/payjoin_history_tile.dart b/lib/new-ui/widgets/coins_page/assets_history/payjoin_history_tile.dart new file mode 100644 index 000000000..465f9630a --- /dev/null +++ b/lib/new-ui/widgets/coins_page/assets_history/payjoin_history_tile.dart @@ -0,0 +1,45 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_tile_base.dart'; +import 'package:flutter/material.dart'; + +class PayjoinHistoryTile extends StatelessWidget { + const PayjoinHistoryTile( + {super.key, + required this.createdAt, + required this.amount, + required this.currency, + required this.state, + required this.isSending, + required this.roundedTop, + required this.roundedBottom, + required this.bottomSeparator}); + + final String createdAt; + final String amount; + final String currency; + final String state; + final bool isSending; + final bool roundedTop; + final bool roundedBottom; + final bool bottomSeparator; + + @override + Widget build(BuildContext context) { + return HistoryTileBase( + title: "${isSending ? S.of(context).outgoing : S.of(context).incoming} Payjoin", + date: createdAt, + amount: amount + " " + currency, + leadingIcon: ClipRRect( + borderRadius: BorderRadius.circular(50), + child: Image.asset( + 'assets/images/payjoin.png', + width: 36, + height: 36, + ), + ), + amountFiat: state, + roundedTop: roundedTop, + roundedBottom: roundedBottom, + bottomSeparator: bottomSeparator); + } +} diff --git a/lib/new-ui/widgets/coins_page/cards/balance_card.dart b/lib/new-ui/widgets/coins_page/cards/balance_card.dart new file mode 100644 index 000000000..aee159117 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/cards/balance_card.dart @@ -0,0 +1,307 @@ +import 'package:cw_core/card_design.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class BalanceCardAction { + final String label; + final IconData icon; + final VoidCallback onTap; + + const BalanceCardAction({required this.label, required this.icon, required this.onTap}); +} + +class BalanceCard extends StatelessWidget { + const BalanceCard({ + super.key, + required this.width, + required this.design, + this.gradient, + this.borderRadius = 20, + this.selected = false, + this.accountName = "", + this.accountBalance = "", + this.balance = "", + this.fiatBalance = "", + this.assetName = "", + this.designSwitchDuration = const Duration(), + this.actions = const [], + this.capitalizeAssetName = true, + this.onCustomizeTapped, + this.accountIndex + }); + + final double width; + final double borderRadius; + final Gradient? gradient; + final String accountBalance; + final String accountName; + final String balance; + final String fiatBalance; + final int? accountIndex; + final bool capitalizeAssetName; + final String assetName; + final bool selected; + final CardDesign design; + final List actions; + final Duration designSwitchDuration; + final VoidCallback? onCustomizeTapped; + + @override + Widget build(BuildContext context) { + final Duration textFadeDuration = Duration(milliseconds: 80); + final double iconWidth = width * 0.15; + + final bool showText = accountBalance.isNotEmpty || + accountName.isNotEmpty || + balance.isNotEmpty || + fiatBalance.isNotEmpty || + assetName.isNotEmpty; + + final height = width * 0.62; + + return AnimatedContainer( + duration: designSwitchDuration, + width: width, + height: height, + decoration: ShapeDecoration( + gradient: gradient ?? design.gradient, + shape: RoundedSuperellipseBorder( + side: const BorderSide(color: Color(0x44FFFFFF), width: 1), + borderRadius: BorderRadiusGeometry.circular(borderRadius), + ), + ), + child: Stack( + children: [ + AnimatedSwitcher( + duration: designSwitchDuration, + switchInCurve: Curves.easeInOut, + switchOutCurve: Curves.easeInOut, + child: design.backgroundType == CardDesignBackgroundTypes.svgFull + ? ClipRSuperellipse( + borderRadius: BorderRadius.circular(borderRadius), + key: ValueKey(design.imagePath), + child: SvgPicture.asset( + design.imagePath, + width: width, + height: height, + fit: BoxFit.fill, + ), + ) + : const SizedBox.shrink( + key: ValueKey('svgFullOff'), + ), + ), + Padding( + padding: EdgeInsets.all(width * 0.05), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + children: [ + if (showText) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + if(accountName.isNotEmpty || accountBalance.isNotEmpty) + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + spacing: 4, + children: [ + if (accountIndex != null) + Opacity( + opacity: 0.5, + child: AnimatedDefaultTextStyle( + duration: designSwitchDuration, + style: Theme.of(context).textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w500, + color: design.colors.textColor), + child: Text("$accountIndex."), + ), + ), + AnimatedDefaultTextStyle( + duration: designSwitchDuration, + style: Theme.of(context).textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w500, color: design.colors.textColor), + child: Text(accountName), + ), + ], + ), + AnimatedOpacity( + opacity: selected ? 0 : 1, + duration: textFadeDuration, + child: Text( + accountBalance, + style: TextStyle(color: design.colors.textColor, fontSize: 14), + ), + ), + ], + ), + AnimatedOpacity( + opacity: selected ? 1 : 0, + duration: textFadeDuration, + child: AnimatedSwitcher( + duration: designSwitchDuration, + layoutBuilder: (currentChild, previousChildren) { + return Stack( + alignment: Alignment.centerLeft, + children: [ + ...previousChildren, + if (currentChild != null) currentChild, + ], + ); + }, + child: Row( + key: ValueKey("$balance ${assetName.toUpperCase()}"), + spacing: 8.0, + children: [ + AnimatedDefaultTextStyle( + duration: designSwitchDuration, + style: DefaultTextStyle.of(context) + .style + .copyWith(color: design.colors.textColor, fontSize: 28, fontWeight: FontWeight.w500, letterSpacing: -0.4), + child: Text(balance), + ), + AnimatedDefaultTextStyle( + duration: designSwitchDuration, + style: DefaultTextStyle.of(context) + .style + .copyWith(color: design.colors.textColorSecondary, fontSize: 28, fontWeight: FontWeight.w400, letterSpacing: -0.4), + child: Text(capitalizeAssetName ? assetName.toUpperCase() : assetName.toLowerCase()), + ), + ], + ), + ), + ), + AnimatedDefaultTextStyle( + duration: designSwitchDuration, + style: DefaultTextStyle.of(context).style.copyWith( + fontSize: 15, + fontWeight: FontWeight.w400, + color: design.colors.textColorSecondary), + child: AnimatedSwitcher( + duration: designSwitchDuration, + layoutBuilder: (currentChild, previousChildren) { + return Stack( + alignment: Alignment.centerLeft, + children: [ + ...previousChildren, + if (currentChild != null) currentChild, + ], + ); + }, + child: Text( + key: ValueKey(fiatBalance), + fiatBalance, + ), + ), + ), + ], + ) + else + Container(), + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + AnimatedSwitcher( + duration: designSwitchDuration, + switchInCurve: Curves.easeInOut, + switchOutCurve: Curves.easeInOut, + layoutBuilder: (currentChild, previousChildren) { + return Stack( + alignment: Alignment.centerLeft, + children: [ + ...previousChildren, + if (currentChild != null) currentChild, + ], + ); + }, + child: Row( + key: ValueKey(actions.toString()), + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.start, + children: actions.map(getBalanceCardActionButton).toList(), + ), + ), + AnimatedSwitcher( + duration: designSwitchDuration, + switchInCurve: Curves.easeInOut, + switchOutCurve: Curves.easeInOut, + child: design.backgroundType == CardDesignBackgroundTypes.svgIcon + ? SvgPicture.asset( + design.imagePath, + key: const ValueKey('svgIcon'), + height: iconWidth, + width: iconWidth, + colorFilter: ColorFilter.mode( + design.colors.backgroundImageColor.withAlpha(80), + BlendMode.srcIn, + ), + ) + : const SizedBox.shrink( + key: ValueKey('svgIconOff'), + ), + ), + ], + ), + ], + ), + ), + Positioned( + right: 8, + top: 8, + child: AnimatedOpacity( + duration: designSwitchDuration, + opacity: onCustomizeTapped == null ? 0 : 1, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onCustomizeTapped, + child: SizedBox( + height: 40, + width: 40, + child: Center( + child: SvgPicture.asset( + "assets/new-ui/3dots_vertical.svg", + colorFilter: + ColorFilter.mode(design.colors.textColorSecondary, BlendMode.srcIn), + ), + ), + ), + ), + ), + ) + ], + ), + ); + } + + Widget getBalanceCardActionButton(BalanceCardAction action) => GestureDetector( + onTap: action.onTap, + child: Container( + decoration: BoxDecoration( + color: design.colors.backgroundImageColor.withAlpha(75), + borderRadius: BorderRadius.circular(10000000), + ), + margin: const EdgeInsets.only(right: 10), + padding: const EdgeInsets.only(left: 10, right: 5, top: 5, bottom: 5), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(right: 6), + child: Text( + action.label, + style: TextStyle(color: design.colors.textColor, fontSize: 16), + ), + ), + Icon(action.icon, color: design.colors.textColorSecondary), + ], + ), + ), + ); +} diff --git a/lib/new-ui/widgets/coins_page/cards/cards_view.dart b/lib/new-ui/widgets/coins_page/cards/cards_view.dart new file mode 100644 index 000000000..64310a8e6 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/cards/cards_view.dart @@ -0,0 +1,386 @@ +import 'dart:math'; + +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/balance_display_mode.dart'; +import 'package:cake_wallet/entities/bitcoin_amount_display_mode.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/modal_navigator.dart'; +import 'package:cake_wallet/new-ui/pages/send_page.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/utils/feature_flag.dart'; +import 'package:cake_wallet/utils/payment_request.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart'; +import 'package:cw_core/card_design.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/unspent_coin_type.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; + +import 'balance_card.dart'; + +class CardsView extends StatefulWidget { + const CardsView( + {super.key, + required this.dashboardViewModel, + required this.accountListViewModel, + required this.lightningMode, + required this.onCompactModeBackgroundCardsTapped, required this.onCustomizeTapped}); + + final DashboardViewModel dashboardViewModel; + final MoneroAccountListViewModel? accountListViewModel; + final VoidCallback onCompactModeBackgroundCardsTapped; + final VoidCallback onCustomizeTapped; + final bool lightningMode; + + @override + _CardsViewState createState() => _CardsViewState(); +} + +class _CardsViewState extends State { + late int _selectedIndex; + bool isFirstBuild = true; + + @override + void initState() { + super.initState(); + _selectedIndex = widget.dashboardViewModel.cardOrder.length - 1; + reaction( + (_) => widget.dashboardViewModel.cardOrder.values.toList(), + (_) => setState(() { + _selectedIndex = widget.dashboardViewModel.cardOrder.length - 1; + })); + } + + static const Duration animDuration = Duration(milliseconds: 200); + static const int compactModeTreshold = 4; + static const int maxCards = 5; + late final double cardWidth = MediaQuery.of(context).size.width * 0.878; + late final double effectiveCardWidth = min(cardWidth, 768); + + Widget _buildCard(int visualIndex, int realIndex, int numCards, double parentWidth, + Map order, bool compactMode, double overlapAmount) { + final baseTop = overlapAmount * (numCards - 1); + final scaleFactor = compactMode ? 1 : 0.96; + + final howFarBehind = (_selectedIndex - visualIndex + numCards) % numCards; + final scale = pow(scaleFactor, howFarBehind).toDouble(); + + final top = baseTop - (howFarBehind * overlapAmount); + + final left = (parentWidth - effectiveCardWidth) / 2.0; + + return AnimatedPositioned( + key: ValueKey("$visualIndex $realIndex"), + duration: animDuration, + curve: Curves.easeOut, + top: top, + left: left, + child: AnimatedScale( + duration: animDuration, + curve: Curves.easeOut, + scale: scale, + child: GestureDetector( + onTap: () { + // printV(visualIndex); + if (compactMode && visualIndex != 0) { + widget.onCompactModeBackgroundCardsTapped(); + } else if(!compactMode) { + setState(() { + if (widget.accountListViewModel != null) + widget.accountListViewModel! + .select(widget.accountListViewModel!.accounts[realIndex]); + _selectedIndex = visualIndex; + }); + } + }, + onLongPress: () { + if (_selectedIndex == visualIndex) { + widget.dashboardViewModel.balanceViewModel.switchBalanceValue(); + }; + HapticFeedback.heavyImpact(); + }, + child: Observer(builder: (_) { + if(realIndex >= (widget.accountListViewModel?.accounts.length ?? 1)) { + return Container(); + } + final account = widget.accountListViewModel?.accounts[realIndex]; + + // The second balance should always be the lightning balance + // printV(widget.dashboardViewModel.balanceViewModel.formattedBalances.first.availableBalance); + final walletBalanceRecord = widget.dashboardViewModel.balanceViewModel.formattedBalances + .elementAtOrNull(widget.lightningMode ? 1 : 0); + + late final String walletBalance; + late final String walletFiatBalance; + if (widget.dashboardViewModel.mwebEnabled && widget.dashboardViewModel.hasMweb) { + if(widget.dashboardViewModel.balanceViewModel.displayMode == BalanceDisplayMode.hiddenBalance) { + walletBalance = '●●●●●●'; + walletFiatBalance = '●●●●●●'; + } else { + walletBalance = walletBalanceRecord?.combinedAvailableBalance ?? "0"; + walletFiatBalance = walletBalanceRecord?.combinedFiatAvailableBalance ?? "0.00"; + } + } else { + walletBalance = walletBalanceRecord?.availableBalance ?? "0"; + walletFiatBalance = walletBalanceRecord?.fiatAvailableBalance ?? "${widget.dashboardViewModel.appStore.settingsStore.fiatCurrency.title} 0.00"; + } + + // the card designs is empty if widget gets built before it loads. + // should get populated before user sees anything + final CardDesign cardDesign; + if (widget.dashboardViewModel.cardDesigns.isEmpty || + realIndex >= widget.dashboardViewModel.cardDesigns.length) + cardDesign = CardDesign.genericDefault; + else if(widget.lightningMode) + cardDesign = widget.dashboardViewModel.cardDesigns[realIndex + 1]; + else + cardDesign = widget.dashboardViewModel.cardDesigns[realIndex]; + + final String accountName; + final String accountBalance; + if (account == null) { + accountName = ""; + accountBalance = ""; + } else { + accountName = account.label; + accountBalance = account.balance ?? "0.00"; + } + + final List actions = widget.lightningMode + ? [ + BalanceCardAction( + label: S.current.bitcoin_lightning_deposit, + icon: Icons.arrow_downward, + onTap: depositToL2, + ), + BalanceCardAction( + label: S.current.bitcoin_lightning_withdraw, + icon: Icons.arrow_upward, + onTap: withdrawFromL2, + ) + ] + : widget.dashboardViewModel.isEnabledTradeAction + ? [ + BalanceCardAction( + label: S.current.buy, + icon: Icons.arrow_forward, + onTap: () => Navigator.of(context).pushNamed(Routes.buySellPage), + ) + ] + : []; + + return BalanceCard( + width: effectiveCardWidth, + accountName: accountName, + accountBalance: accountBalance, + designSwitchDuration: Duration(milliseconds: 150), + assetName: walletBalanceRecord?.formattedAssetTitle ?? assetTitleFallback, + capitalizeAssetName: _shouldCapitalizeAssetName(), + balance: walletBalance, + fiatBalance: walletFiatBalance, + selected: _selectedIndex == visualIndex, + onCustomizeTapped: _selectedIndex == visualIndex ? widget.onCustomizeTapped : null, + design: cardDesign, + actions: actions, + ); + }), + ), + ), + ); + } + + String get assetTitleFallback => widget.dashboardViewModel.appStore.amountParsingProxy.getCryptoSymbol( + widget.lightningMode + ? CryptoCurrency.btcln + : widget.dashboardViewModel.wallet.currency); + + bool _shouldCapitalizeAssetName() { + if(widget.dashboardViewModel.wallet.type != WalletType.bitcoin) { + return true; + } + + switch (widget.dashboardViewModel.settingsStore.displayAmountsInSatoshi) { + case BitcoinAmountDisplayMode.satoshi: + return false; + case BitcoinAmountDisplayMode.satoshiForLightning: + return !widget.lightningMode; + case BitcoinAmountDisplayMode.bitcoin: + return true; + default: + return true; + } + } + + double _getBoxHeight(int numCards, double overlapAmount) { + return + /* height of initial card */ + (2 / 3.2) * (effectiveCardWidth) + + /* height of bg card * amount of bg cards */ + overlapAmount * ((numCards) - 1); + } + + @override + Widget build(BuildContext context) { + return Observer(builder: (context) { + final parentWidth = MediaQuery.of(context).size.width; + final children = []; + + int numCards = widget.dashboardViewModel.wallet.type == WalletType.bitcoin + ? 1 + : widget.dashboardViewModel.cardDesigns.length; + if(numCards == 0) numCards = 1; + + if (_selectedIndex >= (numCards)) { + _selectedIndex = 0; + } + + Map order = widget.dashboardViewModel.cardOrder.length != numCards + ? Map.fromEntries( + List.generate(numCards, (i) => MapEntry(i, i)), + ) + : widget.dashboardViewModel.cardOrder; + + for (int i = min(numCards - 1, maxCards); i >= 0; i--) { + int visualIndex = (_selectedIndex - i + numCards) % numCards; + if (order[visualIndex] == null) { + order = Map.fromEntries( + List.generate(numCards, (i) => MapEntry(i, i)), + ); + } + } + + + final bool compactMode = numCards >= compactModeTreshold; + final double overlapAmount = compactMode ? 5.0 : 46.0; + for (int i = min(numCards - 1, maxCards); i >= 0; i--) { + int visualIndex = (_selectedIndex - i + numCards) % numCards; + + int realIndex = order[visualIndex]!; + + if (visualIndex == _selectedIndex && + widget.accountListViewModel != null && + widget.accountListViewModel?.selected.label != + widget.accountListViewModel?.accounts[realIndex].label) { + widget.accountListViewModel! + .select(widget.accountListViewModel!.accounts[realIndex]); + } + + children.add(_buildCard( + visualIndex, realIndex, numCards, parentWidth, order, compactMode, overlapAmount)); + } + + return AnimatedContainer( + duration: Duration(milliseconds: 200), + curve: Curves.easeOut, + width: double.infinity, + height: _getBoxHeight(numCards, overlapAmount), + child: AnimatedSwitcher( + duration: Duration(milliseconds: 200), + transitionBuilder: (child, animation) => FadeTransition(opacity: animation, child: child), + child: SizedBox( + key: ValueKey(_getBoxHeight(numCards, overlapAmount)), + width: double.infinity, + height: _getBoxHeight(numCards, overlapAmount), + child: Stack(alignment: Alignment.center, children: children), + ), + ), + ); + }); + } + + Future depositToL2() async { + PaymentRequest? paymentRequest = null; + + if (widget.dashboardViewModel.type == WalletType.litecoin) { + final depositAddress = bitcoin!.getUnusedMwebAddress(widget.dashboardViewModel.wallet); + if ((depositAddress?.isNotEmpty ?? false)) { + paymentRequest = PaymentRequest.fromUri(Uri.parse("litecoin:$depositAddress")); + } + } else if (widget.dashboardViewModel.type == WalletType.bitcoin) { + final depositAddress = + await bitcoin!.getUnusedSpakDepositAddress(widget.dashboardViewModel.wallet); + if ((depositAddress?.isNotEmpty ?? false)) { + paymentRequest = PaymentRequest.fromUri(Uri.parse("bitcoin:$depositAddress")); + } + } + + if (FeatureFlag.hasNewUiExtraPages && widget.dashboardViewModel.type == WalletType.bitcoin) { + final page = getIt.get( + param1: SendPageParams( + initialPaymentRequest: paymentRequest, + unspentCoinType: UnspentCoinType.nonMweb, + mode: SendPageModes.lightningDeposit, + )); + showCupertinoModalBottomSheet(context: context, barrierColor: Colors.black.withAlpha(128), builder: (context){ + return Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: SizedBox( + height:MediaQuery.of(context).size.height*0.6, + child:ModalNavigator(parentContext:context,rootPage: Material(child: page)) + ), + ); + }); + } else { + Navigator.pushNamed( + context, + Routes.send, + arguments: { + 'paymentRequest': paymentRequest, + 'coinTypeToSpendFrom': UnspentCoinType.nonMweb, + }, + ); + } + } + + Future withdrawFromL2() async { + PaymentRequest? paymentRequest = null; + UnspentCoinType unspentCoinType = UnspentCoinType.any; + final withdrawAddress = bitcoin!.getUnusedSegwitAddress(widget.dashboardViewModel.wallet); + + if (widget.dashboardViewModel.type == WalletType.litecoin) { + if ((withdrawAddress?.isNotEmpty ?? false)) { + paymentRequest = PaymentRequest.fromUri(Uri.parse("litecoin:$withdrawAddress")); + } + unspentCoinType = UnspentCoinType.mweb; + } else if (widget.dashboardViewModel.type == WalletType.bitcoin) { + if ((withdrawAddress?.isNotEmpty ?? false)) { + paymentRequest = PaymentRequest.fromUri(Uri.parse("bitcoin:$withdrawAddress")); + } + unspentCoinType = UnspentCoinType.lightning; + } + + if (FeatureFlag.hasNewUiExtraPages && widget.dashboardViewModel.type == WalletType.bitcoin) { + final page = getIt.get( + param1: SendPageParams( + initialPaymentRequest: paymentRequest, + unspentCoinType: unspentCoinType, + mode: SendPageModes.lightningWithdrawal, + )); + showCupertinoModalBottomSheet(context: context, barrierColor: Colors.black.withAlpha(128), builder: (context){ + return Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: SizedBox( +height:MediaQuery.of(context).size.height*0.6, + child:ModalNavigator(parentContext:context,rootPage: Material(child: page)) + ), + ); + }); + } else { + Navigator.pushNamed( + context, + Routes.send, + arguments: { + 'paymentRequest': paymentRequest, + 'coinTypeToSpendFrom': unspentCoinType, + }, + ); + } + } +} diff --git a/lib/new-ui/widgets/coins_page/mweb_ad.dart b/lib/new-ui/widgets/coins_page/mweb_ad.dart new file mode 100644 index 000000000..3791bd7f0 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/mweb_ad.dart @@ -0,0 +1,75 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class MwebAd extends StatelessWidget { + const MwebAd({super.key, required this.dashboardViewModel}); + + final DashboardViewModel dashboardViewModel; + + @override + Widget build(BuildContext context) { + return Observer(builder: (_) { + if (!dashboardViewModel.shouldShowMwebAd) return SizedBox.shrink(); + + return Padding( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 12, top: 24), + child: Column( + spacing: 12, + children: [ + GestureDetector( + onTap: () => Navigator.of(context).pushNamed(Routes.mwebSettings), + child: Container( + height: 64, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(18), + color: Theme.of(context).colorScheme.surfaceContainer), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SvgPicture.asset( + "assets/new-ui/settings_row_icons/mweb.svg", + width: 24, + height: 24, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Text( + S.of(context).mweb_ad, + softWrap: true, + style: TextStyle(fontSize: 12), + ), + ), + ), + Icon( + size: 16, + Icons.arrow_forward_ios, + color: Theme.of(context).colorScheme.primary, + ) + ], + ), + ), + ), + ), + GestureDetector( + onTap: () => dashboardViewModel.dismissMwebAd(false), + child: Text( + S.of(context).do_not_show_anymore, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary), + ), + ) + ], + ), + ); + }); + } +} diff --git a/lib/new-ui/widgets/coins_page/top_bar_widget/chain_icon.dart b/lib/new-ui/widgets/coins_page/top_bar_widget/chain_icon.dart new file mode 100644 index 000000000..921a41e2f --- /dev/null +++ b/lib/new-ui/widgets/coins_page/top_bar_widget/chain_icon.dart @@ -0,0 +1,59 @@ +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:flutter_svg/svg.dart'; + +class ChainIcon extends StatelessWidget { + const ChainIcon( + {super.key, + required this.iconPath, + required this.dashboardViewModel, + required this.isSyncHeavy}); + + final String iconPath; + final bool isSyncHeavy; + final DashboardViewModel dashboardViewModel; + + @override + Widget build(BuildContext context) { + return Observer( + builder: (_) { + final progress = dashboardViewModel.status.progress(); + final done = !isSyncHeavy || progress >= 1; + + return Stack( + children: [ + AnimatedOpacity( + duration: Duration(milliseconds: 100), + opacity: done ? 0 : 1, + child: CircularProgressIndicator( + value: progress, + color: Color(0xFFFFB84E), + strokeWidth: 2, + ), + ), + AnimatedScale( + duration: Duration(milliseconds: 150), + scale: done ? 1 : 0.8, + child: AnimatedSwitcher( + duration: Duration(milliseconds: 150), + child: SvgPicture.asset( + key: ValueKey(progress >= 1), + iconPath, + width: 36, + height: 36, + colorFilter: ColorFilter.mode( + done + ? Theme.of(context).colorScheme.primary.withOpacity(0.2) + : Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart b/lib/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart new file mode 100644 index 000000000..a89149cf8 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class LightningSwitcher extends StatelessWidget { + const LightningSwitcher( + {super.key, required this.lightningMode, required this.onLightningSwitchPress}); + + final bool lightningMode; + final VoidCallback onLightningSwitchPress; + + @override + Widget build(BuildContext context) { + return SizedBox( + child: InkWell( + onTap: () { + HapticFeedback.mediumImpact(); + onLightningSwitchPress(); + }, + child: Container( + decoration: ShapeDecoration( + shape: RoundedSuperellipseBorder(borderRadius: BorderRadiusGeometry.circular(900.0)), + color: Theme.of(context).colorScheme.surfaceContainer), + width: 70, + height: 36, + padding: EdgeInsets.symmetric(vertical: 2), + child: Stack( + children: [ + AnimatedContainer( + alignment: Alignment.centerRight, + margin: EdgeInsets.only(left: lightningMode ? 36 : 2), + duration: Duration(milliseconds: 250), + curve: Curves.easeOutCubic, + width: 32, + height: 32, + // height: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(9999990.0)), + color: Theme.of(context).colorScheme.primary), + ), + Container( + child: Row( + spacing: 2.0, + children: [ + SizedBox(), + AnimatedSwitcher( + duration: Duration(milliseconds: 150), + transitionBuilder: (child, animation) => + FadeTransition(opacity: animation, child: child), + child: SvgPicture.asset( + key: ValueKey(lightningMode), + 'assets/new-ui/switcher-bitcoin.svg', + width: 32, + height: 32, + colorFilter: ColorFilter.mode( + lightningMode + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.surfaceContainer, + BlendMode.srcIn, + ), + ), + ), + AnimatedSwitcher( + duration: Duration(milliseconds: 150), + transitionBuilder: (child, animation) => + FadeTransition(opacity: animation, child: child), + child: SvgPicture.asset( + key: ValueKey(lightningMode), + 'assets/new-ui/switcher-lightning.svg', + width: 32, + height: 32, + colorFilter: ColorFilter.mode( + lightningMode + ? Theme.of(context).colorScheme.surfaceContainer + : Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/new-ui/widgets/coins_page/top_bar_widget/pulsing_dot.dart b/lib/new-ui/widgets/coins_page/top_bar_widget/pulsing_dot.dart new file mode 100644 index 000000000..1545dc88e --- /dev/null +++ b/lib/new-ui/widgets/coins_page/top_bar_widget/pulsing_dot.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; + +class PulsingDot extends StatefulWidget { + const PulsingDot({super.key,}); + + + final double size = 5; + final Color color = const Color(0xFFFFC414); + final Duration fadeOutDuration = const Duration(milliseconds: 2000); + final Duration restDuration = const Duration(milliseconds: 2000); + final double restOpacity = 0.3; + + @override + State createState() => _PulsingDotState(); +} + +class _PulsingDotState extends State + with SingleTickerProviderStateMixin { + late final AnimationController controller; + + @override + void initState() { + super.initState(); + controller = AnimationController( + vsync: this, + duration: widget.fadeOutDuration, + reverseDuration: Duration.zero, + value: 1.0, + ); + _loop(); + } + + Future _loop() async { + while (mounted) { + await controller.animateTo( + widget.restOpacity, + curve: Curves.easeOutQuad, + duration: widget.fadeOutDuration, + ); + await Future.delayed(widget.restDuration); + if(!mounted) return; + controller.value = 1.0; + } + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return FadeTransition( + opacity: controller, + child: Container( + width: widget.size, + height: widget.size, + decoration: BoxDecoration( + color: widget.color, + shape: BoxShape.circle, + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart b/lib/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart new file mode 100644 index 000000000..1bc3cfe9d --- /dev/null +++ b/lib/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart @@ -0,0 +1,189 @@ +import 'package:cake_wallet/core/sync_status_title.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/pulsing_dot.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class SyncBar extends StatelessWidget { + SyncBar({super.key, required this.dashboardViewModel, required this.isSyncHeavy}); + + final DashboardViewModel dashboardViewModel; + final bool isSyncHeavy; + + static const failStatuses = [ + FailedSyncStatus, + LostConnectionSyncStatus, + TimedOutSyncStatus, + UnsupportedSyncStatus, + ]; + + static const progressStatuses = [ + SyncingSyncStatus, + NotConnectedSyncStatus, + SyncronizingSyncStatus, + AttemptingSyncStatus, + StartingScanSyncStatus, + AttemptingScanSyncStatus, + SyncedTipSyncStatus, + ProcessingSyncStatus, + ConnectingSyncStatus, + ConnectedSyncStatus, + ]; + + @override + Widget build(BuildContext context) { + return Observer( + builder: (_) { + final status = dashboardViewModel.status; + final Widget? icon = _getIcon(context, status.runtimeType); + + return Expanded( + child: Stack( + alignment: Alignment.centerLeft, + children: [ + if (!_showFullBar()) + Row( + mainAxisSize: MainAxisSize.min, + spacing: 6, + children: [ + if (dashboardViewModel.isTorEnabled) + CakeImageWidget( + imageUrl: "assets/new-ui/tor.svg", + width: 20, + height: 20, + ), + if (_showDot()) PulsingDot(), + ], + ), + if (_showFullBar()) + GestureDetector( + onTap: () { + Navigator.of(context, rootNavigator: true).pushNamed(Routes.connectionSync); + }, + child: AnimatedSwitcher( + duration: Duration(milliseconds: 100), + child: Container( + key: ValueKey(status.runtimeType), + height: 36, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(9999), + border: _getBorder(context, status.runtimeType), + color: _getBackgroundColor(context, status.runtimeType), + ), + child: Row( + spacing: 10, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + if (icon != null) icon, + // if (dashboardViewModel.silentPaymentsScanningActive && + // progressStatuses.contains(status.runtimeType)) ...[ + // Text( + // "${(status.progress() * 100).toInt()}%", + // style: TextStyle(fontSize: 12, color: Color(0xFFEFBA5E)), + // ), + // Text( + // "·", + // style: TextStyle(fontSize: 12), + // ) + // ], + Text( + syncStatusTitle( + status, dashboardViewModel.settingsStore.syncStatusDisplayMode), + style: _getTextStyle(context, status.runtimeType), + ), + ], + ), + ), + ), + ), + ], + ), + ); + }, + ); + } + + Color? _getBackgroundColor(BuildContext context, Type status) { + if (failStatuses.contains(status)) { + return Theme.of(context).colorScheme.errorContainer.withAlpha(64); + } + + return null; + } + + Border? _getBorder(BuildContext context, Type status) { + if (progressStatuses.contains(status)) { + return Border.all(color: Theme.of(context).colorScheme.surfaceContainerHigh, width: 1); + } + + return null; + } + + TextStyle? _getTextStyle(BuildContext context, Type status) { + if (failStatuses.contains(status)) { + return TextStyle( + fontSize: 12, fontWeight: FontWeight.w400, color: Theme.of(context).colorScheme.error); + } else { + return TextStyle( + fontSize: 12, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant); + } + } + + Widget? _getIcon(BuildContext context, Type status) { + if (status == LostConnectionSyncStatus || status == FailedSyncStatus) { + return SvgPicture.asset( + "assets/new-ui/offline.svg", + colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.error, BlendMode.srcIn), + ); + } + + if (failStatuses.contains(status)) { + return SvgPicture.asset( + "assets/new-ui/warning.svg", + colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.error, BlendMode.srcIn), + ); + } + + final List children = []; + + if (dashboardViewModel.isTorEnabled) { + children.add(SvgPicture.asset("assets/new-ui/tor_sync.svg", + colorFilter: ColorFilter.mode(Color(0xFF8A38F5), BlendMode.srcIn))); + } + if (dashboardViewModel.hasMweb) { + children.add(SvgPicture.asset( + "assets/new-ui/mweb_sync.svg", + colorFilter: + ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn), + )); + } + if (dashboardViewModel.hasSilentPayments) { + children.add(SvgPicture.asset( + "assets/new-ui/silent_sync.svg", + colorFilter: ColorFilter.mode(Color(0xFFEFBA5E), BlendMode.srcIn), + )); + } + + return Row( + spacing: 8, + children: children, + ); + } + + bool _showFullBar() { + if (dashboardViewModel.status.runtimeType == SyncedSyncStatus) return false; + return isSyncHeavy || failStatuses.contains(dashboardViewModel.status.runtimeType); + } + + bool _showDot() { + return !isSyncHeavy && progressStatuses.contains(dashboardViewModel.status.runtimeType); + } +} diff --git a/lib/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart b/lib/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart new file mode 100644 index 000000000..3bc48c837 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart @@ -0,0 +1,59 @@ +import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/chain_icon.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart'; +import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart'; +import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class TopBar extends StatelessWidget { + const TopBar({ + super.key, + required this.lightningMode, + required this.onLightningSwitchPress, + required this.dashboardViewModel, + required this.onSettingsButtonPress, + }); + + final bool lightningMode; + final VoidCallback onLightningSwitchPress; + final VoidCallback onSettingsButtonPress; + final DashboardViewModel dashboardViewModel; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10), + child: Observer( + builder: (_) => Row( + spacing: 12, + children: [ + (dashboardViewModel.hasLightning) + ? LightningSwitcher( + lightningMode: lightningMode, + onLightningSwitchPress: onLightningSwitchPress, + ) + : ChainIcon( + iconPath: dashboardViewModel.wallet.currency.flatIconPath ?? "", + dashboardViewModel: dashboardViewModel, + isSyncHeavy: dashboardViewModel.isSyncHeavy), + SyncBar( + dashboardViewModel: dashboardViewModel, + isSyncHeavy: dashboardViewModel.isSyncHeavy, + ), + ModernButton.svg( + iconColor: Theme.of(context).colorScheme.primary, + size: 36, + onPressed: () { + HapticFeedback.mediumImpact(); + onSettingsButtonPress(); + }, + svgPath: "assets/new-ui/top-settings.svg", + ), + ], + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart b/lib/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart new file mode 100644 index 000000000..f72b7f26d --- /dev/null +++ b/lib/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart @@ -0,0 +1,175 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; + +class UnconfirmedBalanceWidget extends StatelessWidget { + const UnconfirmedBalanceWidget({ + super.key, + required this.dashboardViewModel, + }); + + final DashboardViewModel dashboardViewModel; + + @override + Widget build(BuildContext context) { + final currency = dashboardViewModel.wallet.currency; + return Observer(builder: (_) { + final show = dashboardViewModel.balanceViewModel + .hasAdditionalBalance(dashboardViewModel.wallet.currency); + + return AnimatedSize( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + alignment: Alignment.topCenter, + child: show + ? Column( + children: [ + const SizedBox(height: 12, width: double.infinity), + Observer( + builder: (context) { + final balance = dashboardViewModel.balanceViewModel.additionalBalance(currency); + + return Container( + width: MediaQuery.of(context).size.width * 0.87, + height: 48, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(16.0), + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + showMaterialModalBottomSheet( + backgroundColor: Colors.transparent, + context: context, + builder: (context) { + return UnconfirmedBalanceModal( + balance: "${balance} ${currency.title}", + currencyIconPath: currency.iconPath ?? "", + ); + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + spacing: 12, + children: [ + SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Theme.of(context).colorScheme.primary, + value: dashboardViewModel.confirmationProgress, + ), + ), + Row( + spacing: 4, + children: [ + Text( + "$balance ${currency.title}", + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + ), + ), + Text(S.of(context).confirming), + ], + ) + ], + ), + Icon( + Icons.chevron_right, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ) + ], + ), + ), + ), + ), + ); + } + ), + ], + ) + : const SizedBox( + width: double.infinity, + )); + }); + } +} + +class UnconfirmedBalanceModal extends StatelessWidget { + const UnconfirmedBalanceModal({super.key, required this.balance, required this.currencyIconPath}); + + final String balance; + final String currencyIconPath; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(16))), + child: SafeArea( + top: false, + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 20, + children: [ + ModalTopBar( + title: S.of(context).balance_confirmation, + leadingIcon: Icon(Icons.close), + onLeadingPressed: Navigator.of(context).pop), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + spacing: 20, + children: [ + Stack( + alignment: Alignment.bottomRight, + children: [ + Image.asset(currencyIconPath, height: 100, width: 100), + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + border: + Border.all(color: Theme.of(context).colorScheme.surface, width: 3), + borderRadius: BorderRadius.circular(12), + color: Colors.white), + child: Icon( + Icons.lock_outlined, + size: 24, + color: Colors.black, + ), + ), + ], + ), + Text( + "${balance} ${S.of(context).balance_confirmation_desc_1}\n\n${S.of(context).balance_confirmation_desc_2}\n\n${S.of(context).balance_confirmation_desc_3}", + style: TextStyle(fontSize: 16), + textAlign: TextAlign.center, + ), + NewPrimaryButton( + onPressed: Navigator.of(context).pop, + text: S.of(context).close, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary), + SizedBox(), + ], + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/coins_page/wallet_info.dart b/lib/new-ui/widgets/coins_page/wallet_info.dart new file mode 100644 index 000000000..0ebdf2440 --- /dev/null +++ b/lib/new-ui/widgets/coins_page/wallet_info.dart @@ -0,0 +1,101 @@ +import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class WalletInfoBar extends StatelessWidget { + const WalletInfoBar( + {super.key, + required this.lightningMode, + required this.name, + required this.hardwareWalletType, + required this.onCustomizeButtonTap, required this.hasCustomize}); + + final bool lightningMode; + final String name; + final HardwareWalletType? hardwareWalletType; + final bool hasCustomize; + final VoidCallback onCustomizeButtonTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + if (hasCustomize) { + onCustomizeButtonTap(); + HapticFeedback.mediumImpact(); + } + }, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + + children: [ + AnimatedSwitcher( + duration: Duration(milliseconds: 150), + transitionBuilder: (child, animation) { + return SizeTransition( + axis: Axis.horizontal, + sizeFactor: animation, + child: FadeTransition(opacity: animation, child: child), + ); + }, + child: hardwareWalletIcon == null + ? const SizedBox.shrink(key: ValueKey("empty")) + : Padding( + padding: const EdgeInsets.only(right: 8), + child: SvgPicture.asset( + hardwareWalletIcon!, + key: ValueKey("hardware_wallet_icon"), + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, + BlendMode.srcIn, + ), + ), + ), + ), + Text( + name, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurface), + ), + if (hasCustomize) ...[ + SizedBox(width: 8), + ModernButton.svg( + size: 24, + onPressed: () { + if (hasCustomize) { + onCustomizeButtonTap(); + HapticFeedback.mediumImpact(); + } + }, + svgPath: "assets/new-ui/icon-accounts.svg", + ) + ] + ], + ), + ); + } + + String? get hardwareWalletIcon { + switch (hardwareWalletType) { + case null: + return null; + case HardwareWalletType.bitbox: + return "assets/images/hardware_wallet/device_bitbox.svg"; + case HardwareWalletType.ledger: + return "assets/images/hardware_wallet/device_ledger_nano_x.svg"; + case HardwareWalletType.trezor: + return "assets/images/hardware_wallet/device_trezor_safe_5.svg"; + case HardwareWalletType.cupcake: + return "assets/images/cupcake.svg"; + case HardwareWalletType.coldcard: + case HardwareWalletType.seedsigner: + case HardwareWalletType.keystone: + return "assets/images/hardware_wallet/device_qr.svg"; + } + } +} diff --git a/lib/new-ui/widgets/confirm_swiper.dart b/lib/new-ui/widgets/confirm_swiper.dart new file mode 100644 index 000000000..99b8a5cd2 --- /dev/null +++ b/lib/new-ui/widgets/confirm_swiper.dart @@ -0,0 +1,189 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +class ConfirmSwiper extends StatefulWidget { + final VoidCallback onConfirmed; + final String swiperText; + + const ConfirmSwiper({super.key, required this.onConfirmed, required this.swiperText}); + + @override + State createState() => _ConfirmSwiperState(); +} + +class _ConfirmSwiperState extends State { + double pillHorizontalPadding = 4; + double pillVerticalPadding = 2; + double pillSize = 48; + late double drag; + + @override + void initState() { + super.initState(); + setState(() { + drag = pillHorizontalPadding; + }); + } + + static const double _triggerThreshold = 20; + + @override + Widget build(BuildContext context) { + final radius = (pillSize + pillHorizontalPadding * 2) / 2; + + return LayoutBuilder( + builder: (context, constraints) { + final areaWidth = constraints.maxWidth.isFinite + ? constraints.maxWidth + : MediaQuery.of(context).size.width * 0.77; + + final maxDrag = areaWidth - pillSize - pillHorizontalPadding; + final triggerAt = maxDrag - _triggerThreshold; + + return GestureDetector( + onHorizontalDragUpdate: (d) { + setState(() { + drag = max(pillHorizontalPadding, + min(maxDrag, drag + d.delta.dx)); + }); + }, + onHorizontalDragEnd: (_) { + if (drag >= triggerAt) { + widget.onConfirmed(); + } + setState(() => drag = pillHorizontalPadding); + }, + child: SizedBox( + height: pillSize + pillHorizontalPadding * 2, + width: areaWidth, + child: Stack( + alignment: Alignment.centerLeft, + children: [ + ClipPath( + clipper: TrackClipper( + cut: drag - pillHorizontalPadding, + radius: radius, + ), + child: Container( + height: pillSize + pillHorizontalPadding * 2, + width: areaWidth, + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .surfaceContainerLowest, + borderRadius: BorderRadius.circular(99999), + ), + child: Center( + child: FlowingText( + text: widget.swiperText, + opacity: 1 - (drag / maxDrag), + ), + ), + ), + ), + Positioned( + left: drag, + child: Container( + height: pillSize, + width: pillSize, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + shape: BoxShape.circle, + ), + child: Icon( + Icons.arrow_forward, + color: Theme.of(context).colorScheme.onPrimary, + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} + +class TrackClipper extends CustomClipper { + final double cut; + final double radius; + + TrackClipper({required this.cut, required this.radius}); + + @override + Path getClip(Size size) { + final left = cut.clamp(0.0, size.width); + final rect = Rect.fromLTRB(left, 0, size.width, size.height); + return Path() + ..addRRect(RRect.fromRectAndRadius(rect, Radius.circular(radius))); + } + + @override + bool shouldReclip(covariant TrackClipper oldClipper) { + return oldClipper.cut != cut; + } +} + +class FlowingText extends StatefulWidget { + const FlowingText({super.key, required this.text, required this.opacity}); + + final String text; + final double opacity; + + @override + State createState() => _FlowingTextState(); +} + +class _FlowingTextState extends State with SingleTickerProviderStateMixin { + late AnimationController controller; + + @override + void initState() { + super.initState(); + controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 2400), + lowerBound: -1.0, + upperBound: 2.0) + ..repeat(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Opacity( + opacity: widget.opacity, + child: AnimatedBuilder( + animation: controller, + builder: (_, __) { + return ShaderMask( + shaderCallback: (r) { + return LinearGradient( + begin: Alignment(-1 + (controller.value) * 2, 0), + end: Alignment(controller.value * 2, 0), + colors: [Colors.transparent, Theme.of(context).colorScheme.surfaceContainerHighest, Colors.transparent], + ).createShader(r); + }, + blendMode: BlendMode.srcATop, + child: Text( + widget.text, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.w400, + fontSize: 16, + letterSpacing: 0.5, + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/new-ui/widgets/dropdown_row.dart b/lib/new-ui/widgets/dropdown_row.dart new file mode 100644 index 000000000..c555f6ab1 --- /dev/null +++ b/lib/new-ui/widgets/dropdown_row.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class DropdownRow extends StatelessWidget { + const DropdownRow({super.key, required this.expanded, required this.text, required this.onTap}); + + final bool expanded; + final VoidCallback onTap; + final String text; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.vertical( + top: Radius.circular(18), + bottom: expanded ? Radius.zero : Radius.circular(18), + ), + ), + height: 48, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + text, + style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.primary), + ), + AnimatedRotation( + duration: Duration(milliseconds: 300), + turns: expanded ? 0.0 : 0.5, + curve: Curves.easeOut, + child: SvgPicture.asset( + "assets/new-ui/dropdown_arrow.svg", + colorFilter: + ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn), + )) + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/keyboard_hide_overlay.dart b/lib/new-ui/widgets/keyboard_hide_overlay.dart new file mode 100644 index 000000000..83f31f8d2 --- /dev/null +++ b/lib/new-ui/widgets/keyboard_hide_overlay.dart @@ -0,0 +1,88 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class KeyboardHideOverlay extends StatelessWidget { + const KeyboardHideOverlay({super.key, required this.child, this.unfocusOnTap = false}); + + final Widget child; + final bool unfocusOnTap; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Positioned.fill( + child: GestureDetector( + onTap: () { + if (unfocusOnTap) { + FocusManager.instance.primaryFocus?.unfocus(); + } + }, + child: child)), + if (Platform.isIOS) KeyboardHideButton() + ], + ); + } +} + +class KeyboardHideButton extends StatefulWidget { + const KeyboardHideButton({super.key}); + + @override + State createState() => _KeyboardHideButtonState(); +} + +class _KeyboardHideButtonState extends State { + double _lastInset = 0; + bool _visible = false; + + @override + Widget build(BuildContext context) { + final inset = MediaQuery.viewInsetsOf(context).bottom; + + if (inset != _lastInset) { + _lastInset = inset; + _visible = false; + + if (inset > 0) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted && MediaQuery.viewInsetsOf(context).bottom == inset) { + setState(() => _visible = true); + } + }); + } + } + + return Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: EdgeInsets.only(bottom: inset), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 18), + child: AnimatedOpacity( + opacity: _visible ? 1 : 0, + duration: const Duration(milliseconds: 100), + child: GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular(999), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + "assets/new-ui/hide_keyboard.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onPrimary, BlendMode.srcIn), + ), + )), + ), + ), + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/line_tab_switcher.dart b/lib/new-ui/widgets/line_tab_switcher.dart new file mode 100644 index 000000000..30076ac21 --- /dev/null +++ b/lib/new-ui/widgets/line_tab_switcher.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +class LineTabSwitcher extends StatefulWidget { + const LineTabSwitcher({ + super.key, + required this.tabs, + required this.onTabChange, + required this.selectedTab, + }); + + final List tabs; + final void Function(int index) onTabChange; + final int selectedTab; + + @override + State createState() => _LineTabSwitcherState(); +} + +class _LineTabSwitcherState extends State { + List textWidgetKeys = []; + List textWidgetSizes = []; + double totalWidth = 0; + final double itemPadding = 16; + final double barPadding = 8; + bool textWidgetsMeasured = false; + + @override + void didUpdateWidget(covariant LineTabSwitcher oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.tabs.length != widget.tabs.length) { + textWidgetKeys = List.generate(widget.tabs.length, (_) => GlobalKey()); + textWidgetSizes = []; + totalWidth = 0; + textWidgetsMeasured = false; + + WidgetsBinding.instance.addPostFrameCallback((_) => measure()); + } + } + + double _calcBarLeft() { + double left = 0; + + if (textWidgetKeys.isEmpty || textWidgetSizes.isEmpty) { + return 0; + } + + for (int i = 0; i < widget.selectedTab; i++) { + left += textWidgetSizes[i].width + itemPadding; + } + + left += barPadding; + + return left; + } + + @override + void initState() { + super.initState(); + textWidgetKeys = List.generate(widget.tabs.length, (index) => GlobalKey()); + WidgetsBinding.instance.addPostFrameCallback((_) => measure()); + } + + void measure() { + if(textWidgetsMeasured) return; + setState(() { + textWidgetSizes = textWidgetKeys + .map((k) => k.currentContext!.size) + .whereType() + .toList(); + + for(final size in textWidgetSizes) { + totalWidth += size.width+itemPadding; + } + totalWidth += barPadding; + textWidgetsMeasured = true; + }); + } + + @override + Widget build(BuildContext context) { + if (!textWidgetsMeasured) { + WidgetsBinding.instance.addPostFrameCallback((_) => measure()); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + height: 28, + child: Row( + children: widget.tabs.map((item){ + + final index = widget.tabs.indexOf(item); + + return GestureDetector( + onTap: () { + widget.onTabChange(index); + }, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedDefaultTextStyle( + duration: Duration(milliseconds: 150), + style: DefaultTextStyle.of(context).style.copyWith( + inherit: true, + fontSize: 16, + fontWeight: FontWeight.w500, + color: widget.selectedTab == index + ? Theme.of(context).colorScheme.onSurface + : Theme.of(context).colorScheme.onSurfaceVariant, + ), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: itemPadding/2), + child: Text( + item, + key: textWidgetKeys[index], + ), + ), + ), + ], + ), + ); + }).toList() + ) + ), + Container( + width: totalWidth, + height: 2, + child: Stack( + children: [ + AnimatedPositioned( + curve: Curves.easeOut, + left: _calcBarLeft(), + bottom: 0, + duration: Duration(milliseconds: 150), + child: AnimatedSize( + duration: Duration(milliseconds: 150), + child: Container( + height: 2, + width: textWidgetSizes.isEmpty + ? 0 + : textWidgetSizes[widget.selectedTab].width, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/new-ui/widgets/long_press_menu.dart b/lib/new-ui/widgets/long_press_menu.dart new file mode 100644 index 000000000..43a0e9cca --- /dev/null +++ b/lib/new-ui/widgets/long_press_menu.dart @@ -0,0 +1,100 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class LongPressMenuItem { + final String label; + final String iconPath; + final VoidCallback onSelected; + final Color? color; + + LongPressMenuItem( + {required this.label, required this.iconPath, required this.onSelected, this.color}); +} + +class LongPressMenu extends StatefulWidget { + const LongPressMenu({super.key, required this.items}); + + final List items; + + @override + State createState() => _LongPressMenuState(); +} + +class _LongPressMenuState extends State { + bool _isVisible = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + _isVisible = true; + }); + }); + } + + @override + Widget build(BuildContext context) { + return AnimatedSize( + duration: Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + alignment: Alignment.topCenter, + child: SizedBox( + height: _isVisible ? null : 0, + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Theme.of(context).colorScheme.primary.withAlpha(60), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: widget.items.map((item) { + final color = item.color ?? Theme.of(context).colorScheme.onSurface; + return Material( + color: Colors.transparent, + child: InkWell( + onTap: item.onSelected, + child: Padding( + padding: EdgeInsets.only( + left: 16, + right: 16, + top: 12, + bottom: 12, + ), + child: Container( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + spacing: 8, + children: [ + SvgPicture.asset( + item.iconPath, + height: 20, + width: 20, + colorFilter: ColorFilter.mode(color, BlendMode.srcIn), + ), + Text(item.label, + style: TextStyle( + color: color, fontSize: 14, fontWeight: FontWeight.w500)), + ], + ), + ), + ), + ), + ); + }).toList(), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/modal_grab_handle.dart b/lib/new-ui/widgets/modal_grab_handle.dart new file mode 100644 index 000000000..61873cf18 --- /dev/null +++ b/lib/new-ui/widgets/modal_grab_handle.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class ModalGrabHandle extends StatelessWidget { + const ModalGrabHandle({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(9999999)), + height: 5, + width: 48), + ); + } +} diff --git a/lib/new-ui/widgets/modal_header.dart b/lib/new-ui/widgets/modal_header.dart new file mode 100644 index 000000000..7f1c90805 --- /dev/null +++ b/lib/new-ui/widgets/modal_header.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class ModalHeader extends StatelessWidget { + const ModalHeader( + {super.key, required this.iconPath, required this.message, required this.title}); + + final String iconPath; + final String title; + final String message; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + spacing: 10, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SvgPicture.asset(iconPath, width: 36, height: 36), + Text( + title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 32), + child: Text( + message, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant), + textAlign: TextAlign.center, + )) + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/new-ui/widgets/modern_button.dart b/lib/new-ui/widgets/modern_button.dart new file mode 100644 index 000000000..988d887fd --- /dev/null +++ b/lib/new-ui/widgets/modern_button.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class ModernButton extends StatelessWidget { + final double size; + final String? svgPath; + final Widget? icon; + final VoidCallback onPressed; + final Color? iconColor; + final Color? backgroundColor; + final double? iconSize; + final String? label; + static const iconSvgSizeRatio = 2 / 3; + + const ModernButton( + {super.key, + required this.size, + required this.icon, + required this.onPressed, + this.iconSize, + this.iconColor, + this.backgroundColor, + this.label}) + : svgPath = null; + + const ModernButton.svg( + {super.key, + required this.size, + required this.svgPath, + required this.onPressed, + this.iconSize, + this.iconColor, + this.backgroundColor, + this.label}) + : icon = null; + + @override + Widget build(BuildContext context) { + final resolvedIconColor = iconColor ?? Theme.of(context).colorScheme.primary; + final resolvedIconSize = iconSize ?? size * (svgPath != null ? 1 : iconSvgSizeRatio); + final resolvedBackgroundColor = + backgroundColor ?? Theme.of(context).colorScheme.surfaceContainer; + final Widget resolvedIcon = svgPath != null + ? SvgPicture.asset( + svgPath!, + width: resolvedIconSize, + height: resolvedIconSize, + fit: BoxFit.contain, + alignment: Alignment.center, + allowDrawingOutsideViewBox: true, + colorFilter: ColorFilter.mode(resolvedIconColor, BlendMode.srcIn), + ) + : IconTheme( + data: IconThemeData(color: resolvedIconColor, size: resolvedIconSize), + child: icon!, + ); + + return Column( + spacing: 8, + children: [ + Container( + decoration: BoxDecoration( + color: resolvedBackgroundColor, + borderRadius: BorderRadius.circular(size), + ), + width: size, + height: size, + child: IconButton( + padding: EdgeInsets.zero, + onPressed: onPressed, + icon: resolvedIcon, + ), + ), + if (label != null && label!.isNotEmpty) + Text( + label!, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface), + ) + ], + ); + } +} \ No newline at end of file diff --git a/lib/new-ui/widgets/new_primary_button.dart b/lib/new-ui/widgets/new_primary_button.dart new file mode 100644 index 000000000..f580136c0 --- /dev/null +++ b/lib/new-ui/widgets/new_primary_button.dart @@ -0,0 +1,64 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class NewPrimaryButton extends StatelessWidget { + const NewPrimaryButton({ + required this.onPressed, + this.image, + required this.text, + required this.color, + required this.textColor, + this.isLoading = false, + this.borderColor = Colors.transparent, + this.disabled = false, + super.key, + }); + + final VoidCallback onPressed; + final bool isLoading; + final bool disabled; + final SvgPicture? image; + final Color color; + final Color textColor; + final Color borderColor; + final String text; + + @override + Widget build(BuildContext context) { + + return SizedBox( + height: 52.0, + child: TextButton( + onPressed: disabled ? null : onPressed, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + disabled ? color.withAlpha(128) : color, + ), + shape: WidgetStateProperty.all( + RoundedSuperellipseBorder( + borderRadius: BorderRadius.circular(18.0), + ), + ), + ), + child: Center( + child: isLoading ? CupertinoActivityIndicator() : Row( + spacing: 10, + mainAxisSize: MainAxisSize.min, + children: [ + if(image != null) image!, + Text( + text, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontSize: 14, + fontWeight: FontWeight.w600, + color: textColor, + ), + ) + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/new-ui/widgets/picker.dart b/lib/new-ui/widgets/picker.dart new file mode 100644 index 000000000..233770967 --- /dev/null +++ b/lib/new-ui/widgets/picker.dart @@ -0,0 +1,406 @@ +import 'dart:io'; + +import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/new_simple_checkbox.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:flutter_svg/svg.dart'; + +class PickerItem { + final String title; + final String? subtitle; + final String? hint; + final Item value; + final bool isSliderItem; + + PickerItem( + {required this.title, + this.subtitle, + this.hint, + required this.value, + this.isSliderItem = false}); +} + +class NewPicker extends StatefulWidget { + const NewPicker({ + super.key, + this.title = "Please select", + this.sliderPageTitle = "Custom value", + required this.items, + this.description, + required this.onItemSelected, + this.sliderInitialValue, + this.sliderMaxValue, + this.sliderValueDescription, + this.onSliderChanged, + required this.selectedIndex, + this.closeOnSelection = false, + }); + + final String title; + final String sliderPageTitle; + final String? description; + final String? sliderValueDescription; + final List> items; + final int selectedIndex; + final Function(Item) onItemSelected; + final double? sliderInitialValue; + final double? sliderMaxValue; + final Function(double)? onSliderChanged; + final bool closeOnSelection; + + @override + State> createState() => _NewPickerState(); +} + +class _NewPickerState extends State> { + late double? sliderCurrentValue; + + @override + void initState() { + super.initState(); + sliderCurrentValue = widget.sliderInitialValue; + } + + @override + Widget build(BuildContext context) { + return Observer( + builder: (_)=>Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(24))), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ModalTopBar( + title: widget.title, + leadingIcon: Icon(Icons.arrow_back_ios_new), + onLeadingPressed: Navigator.of(context).maybePop, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.description != null && widget.description!.isNotEmpty) + Text(widget.description!,textAlign: TextAlign.center,style: TextStyle(fontSize:14,fontWeight: FontWeight.w400,color: Theme.of(context).colorScheme.onSurfaceVariant),), + SizedBox(height:16), + ...widget.items.map((item) { + final isSelected = widget.items.indexOf(item) == widget.selectedIndex; + return Column( children: [ + if (item.isSliderItem) + PickerSliderButton( + key: ValueKey(isSelected), + item: item, + isSelected: isSelected, + onSelected: (item) { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => Material( + child: PickerSliderPage( + title: widget.sliderPageTitle, + sliderInitialValue: sliderCurrentValue!, + valueDescription: widget.sliderValueDescription ?? "", + sliderMaxValue: widget.sliderMaxValue!, + onSubmitted: (value) { + setState(() { + sliderCurrentValue = value; + }); + widget.onSliderChanged?.call(value); + widget.onItemSelected(item); + }), + ))); + }, + isFirst: widget.items.indexOf(item) == 0, + customSubtitle: "${sliderCurrentValue?.toInt()} ${widget.sliderValueDescription}", + isLast: widget.items.indexOf(item) == widget.items.length - 1, + ) + else + PickerRow( + item: item, + isSelected: isSelected, + onSelected: _itemSelected, + isFirst: widget.items.indexOf(item) == 0, + isLast: widget.items.indexOf(item) == widget.items.length - 1, + ), + if (widget.items.indexOf(item) != widget.items.length - 1) + Container( + color: Theme.of(context).colorScheme.surfaceContainer, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Container( + height: 1, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + ), + ) + ]); + }), + SizedBox(height: 16), + NewPrimaryButton( + onPressed: Navigator.of(context).maybePop, + text: "Continue", + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary), + ], + ), + ), + ) + ], + ), + ), + ); + } + + void _itemSelected(Item value) { + widget.onItemSelected(value); + if (widget.closeOnSelection) { + Navigator.of(context).pop(); + } + } +} + +class PickerSliderButton extends StatelessWidget { + const PickerSliderButton( + {super.key, + required this.item, + required this.isSelected, + required this.onSelected, + required this.isFirst, + required this.isLast, this.customSubtitle}); + + final PickerItem item; + final bool isSelected; + final bool isFirst; + final bool isLast; + final String? customSubtitle; + final Function(Item) onSelected; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + onSelected(item.value); + }, + child: Container( + height: 64, + width: MediaQuery.of(context).size.height*0.9, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.vertical( + top: isFirst ? Radius.circular(16) : Radius.circular(0), + bottom: isLast ? Radius.circular(16) : Radius.circular(0))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + spacing: 8, + children: [ + Text( + item.title, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.w500), + ), + if (item.hint != null) + Text(item.hint!, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w400)), + if(isSelected) SvgPicture.asset( + "assets/new-ui/arrow_right.svg", + colorFilter: + ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),) + ], + ), + if (customSubtitle != null || item.subtitle != null) Text(customSubtitle ?? item.subtitle!,style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w400)) + ], + ), + isSelected? NewSimpleCheckbox(value: true, onChanged: (val){}): + SvgPicture.asset( + "assets/new-ui/arrow_right.svg", + colorFilter: + ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn), + ) + ], + ), + ), + ), + ); + } +} + +class PickerRow extends StatelessWidget { + const PickerRow( + {super.key, + required this.item, + required this.isSelected, + required this.onSelected, + required this.isFirst, + required this.isLast}); + + final PickerItem item; + final bool isSelected; + final bool isFirst; + final bool isLast; + final Function(Item) onSelected; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + onSelected(item.value); + }, + child: Container( + height: 64, + width: MediaQuery.of(context).size.height*0.9, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.vertical( + top: isFirst ? Radius.circular(16) : Radius.circular(0), + bottom: isLast ? Radius.circular(16) : Radius.circular(0))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + spacing: 4, + children: [ + Text(item.title, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.w500)), + if (item.hint != null) + Text(item.hint!, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w400)) + ], + ), + item.subtitle != null ? Text(item.subtitle!,style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w400)) : SizedBox.shrink() + ], + ), + AnimatedSwitcher( + duration: Duration(milliseconds: 100), + + child: isSelected + ? NewSimpleCheckbox(key: ValueKey(1), value: true, onChanged: (val){onSelected(item.value);}) + : NewSimpleCheckbox(key: ValueKey(0), value: false, onChanged: (val){onSelected(item.value);}) + ) + ], + ), + ), + ), + ); + } +} + +class PickerSliderPage extends StatefulWidget { + const PickerSliderPage( + {super.key, + required this.sliderInitialValue, + required this.sliderMaxValue, + required this.onSubmitted, + required this.title, + this.valueDescription = ""}); + + final double sliderInitialValue; + final double sliderMaxValue; + final Function(double) onSubmitted; + final String title; + final String valueDescription; + + @override + State> createState() => _PickerSliderPageState(); +} + +class _PickerSliderPageState extends State> { + late double sliderValue; + + @override + void initState() { + super.initState(); + sliderValue = widget.sliderInitialValue; + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(16))), + child: SafeArea( + child: Column( + spacing: 36, + mainAxisSize: MainAxisSize.max, + children: [ + ModalTopBar( + title: widget.title, + leadingIcon: Icon(Icons.arrow_back_ios_new), + onLeadingPressed: Navigator.of(context).pop, + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Row(mainAxisAlignment:MainAxisAlignment.center,spacing:4,children: [ + Text(sliderValue.toStringAsFixed(0), style: TextStyle(fontSize:18,fontWeight: FontWeight.w600,color: Theme.of(context).colorScheme.primary),), + Text(widget.valueDescription, style: TextStyle(fontSize:18,fontWeight: FontWeight.w600,color: Theme.of(context).colorScheme.onSurface)), + ],), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Slider( + activeColor: Theme.of(context).colorScheme.primary, + inactiveColor: Theme.of(context).colorScheme.surfaceContainer, + thumbColor: Theme.of(context).colorScheme.onSurface, + value: sliderValue, + onChanged: (value) { + setState(() { + sliderValue = value.roundToDouble(); + }); + }, + min: 1, + max: widget.sliderMaxValue, + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: NewPrimaryButton(onPressed: (){ + widget.onSubmitted(sliderValue); + Navigator.of(context).pop(); + }, text: "Continue", color: Theme.of(context).colorScheme.primary, textColor: Theme.of(context).colorScheme.onPrimary), + ) + ], + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/receive_page/payjoin_copy_modal.dart b/lib/new-ui/widgets/receive_page/payjoin_copy_modal.dart new file mode 100644 index 000000000..0f048c880 --- /dev/null +++ b/lib/new-ui/widgets/receive_page/payjoin_copy_modal.dart @@ -0,0 +1,65 @@ +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart'; +import 'package:cw_core/payment_uris.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class PayjoinCopyModal extends StatelessWidget { + const PayjoinCopyModal({super.key, required this.uri}); + + final PaymentURI uri; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + color: Theme.of(context).colorScheme.surface), + child: SafeArea( + top: false, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ModalTopBar( + title: S.of(context).select_address_to_copy, + leadingIcon: Icon(Icons.close), + onLeadingPressed: Navigator.of(context).pop), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: NewListSections(sections: { + "": [ + ListItemRegularRow( + keyValue: "btc", + label: "Standard", + iconPath: "assets/new-ui/pjmodal_btc.svg", + onTap: () { + Clipboard.setData( + ClipboardData( + text: uri.amount.isNotEmpty + ? BitcoinURI(amount: uri.amount, address: uri.address).toString() + : uri.address), + ); + Navigator.of(context).pop(); + }), + ListItemRegularRow( + keyValue: "pj", + label: "Payjoin", + iconPath: "assets/new-ui/pjmodal_pj.svg", + onTap: () { + Clipboard.setData( + ClipboardData(text: uri.toString()), + ); + Navigator.of(context).pop(); + }) + ] + }), + ), + SizedBox(height:128) + ], + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/receive_page/receive_address_type.dart b/lib/new-ui/widgets/receive_page/receive_address_type.dart new file mode 100644 index 000000000..0fe737dfa --- /dev/null +++ b/lib/new-ui/widgets/receive_page/receive_address_type.dart @@ -0,0 +1,112 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_address_type_selector.dart'; +import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart'; +import 'package:cw_core/receive_page_option.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; + +class ReceiveAddressTypeDisplay extends StatelessWidget { + const ReceiveAddressTypeDisplay( + {super.key, + required this.receiveOptionViewModel, + required this.largeQrMode, + required this.lightningMode}); + + final ReceiveOptionViewModel receiveOptionViewModel; + final bool largeQrMode; + final bool lightningMode; + + @override + Widget build(BuildContext context) { + return Observer( + builder: (_) { + String text = receiveOptionViewModel.selectedReceiveOption.value; + if (largeQrMode && receiveOptionViewModel.selectedReceiveOption.addAddressWord) { + text += " Address"; + } + + if (text == "mainnet") { + if (largeQrMode) { + text = "${receiveOptionViewModel.walletTypeString} ${S.of(context).address}"; + } else { + text = "${receiveOptionViewModel.walletTypeString} (Mainnet)"; + } + } + String? iconPath = receiveOptionViewModel.selectedReceiveOption.iconPath; + if (iconPath != null && + receiveOptionViewModel.walletTypeString == "Litecoin" && + text.contains("Standard")) { + iconPath = "assets/new-ui/address-type-picker-icons/litecoin.svg"; + } + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => _showPicker(context), + child: Row( + key: ValueKey("$text$largeQrMode"), + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + spacing: 12.0, + children: [ + if (iconPath != null) + SvgPicture.asset( + width: 24, + height: 24, + iconPath!, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + Text( + text, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.primary, + ), + ), + if (!largeQrMode) + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(999999), + ), + child: IconButton( + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + onPressed: () => _showPicker(context), + icon: (Icon( + color: Theme.of(context).colorScheme.primary, + size: 20, + Icons.keyboard_arrow_down, + )), + ), + ), + ], + ), + ); + }, + ); + } + + void _showPicker(BuildContext context) async { + final res = await showCupertinoModalBottomSheet( + context: context, + barrierColor: Colors.black.withAlpha(80), + builder: (context) { + return Material( + child: ReceiveAddressTypeSelector( + lightningMode: lightningMode, + receiveOptionViewModel: receiveOptionViewModel, + )); + }); + + if (res != null && res is ReceivePageOption) { + receiveOptionViewModel.selectReceiveOption(res); + } + } +} diff --git a/lib/new-ui/widgets/receive_page/receive_address_type_selector.dart b/lib/new-ui/widgets/receive_page/receive_address_type_selector.dart new file mode 100644 index 000000000..50d5f63b6 --- /dev/null +++ b/lib/new-ui/widgets/receive_page/receive_address_type_selector.dart @@ -0,0 +1,320 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/src/widgets/rounded_checkbox.dart'; +import 'package:cake_wallet/src/widgets/section_divider.dart'; +import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart'; +import 'package:cw_core/receive_page_option.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; + +class ReceiveAddressTypeSelector extends StatefulWidget { + const ReceiveAddressTypeSelector( + {super.key, required this.receiveOptionViewModel, required this.lightningMode}); + + final ReceiveOptionViewModel receiveOptionViewModel; + final bool lightningMode; + + static const otherOptionsExpandDuration = Duration(milliseconds: 300); + static const otherOptionsTreshold = 3; + + @override + State createState() => _ReceiveAddressTypeSelectorState(); +} + +class _ReceiveAddressTypeSelectorState extends State { + late bool _otherOptionsExpanded; + + @override + void initState() { + super.initState(); + _otherOptionsExpanded = !widget.receiveOptionViewModel.selectedReceiveOption.isCommon; + } + + @override + Widget build(BuildContext context) { + final commonOptions = widget.receiveOptionViewModel.options + .where((element) => + element.isCommon || + widget.receiveOptionViewModel.options.length <= + ReceiveAddressTypeSelector.otherOptionsTreshold) + .toList(); + commonOptions.sort( + (a, b) { + if (widget.lightningMode && a.value.contains("Lightning")) return -1; + if (widget.lightningMode && b.value.contains("Lightning")) return 1; + if (!widget.lightningMode && a.value.contains("Standard")) return -1; + if (!widget.lightningMode && b.value.contains("Standard")) return 1; + return a.value.compareTo(b.value); + }, + ); + final otherOptions = widget.receiveOptionViewModel.options + .where((element) => + !element.isCommon && + widget.receiveOptionViewModel.options.length > + ReceiveAddressTypeSelector.otherOptionsTreshold) + .toList(); + + return SafeArea( + child: Container( + width: MediaQuery.of(context).size.width, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(20), bottom: Radius.zero), + ), + child: ListView( + controller: ModalScrollController.of(context), + padding: EdgeInsets.zero, + shrinkWrap: true, + children: [ + ModalTopBar( + title: S.of(context).address_type, + leadingIcon: Icon(Icons.close), + onLeadingPressed: Navigator.of(context).pop, + onTrailingPressed: () {}), + SizedBox(height: 24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(20), + ), + child: ListView.separated( + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: NeverScrollableScrollPhysics(), + itemCount: commonOptions.length, + itemBuilder: (context, index) { + final opt = commonOptions[index]; + + return ReceiveAddressTypeRow( + option: opt, + roundedTop: index == 0, + roundedBottom: index == commonOptions.length - 1, + selected: widget.receiveOptionViewModel.selectedReceiveOption == opt, + onItemTap: () { + Navigator.of(context).pop(opt); + }, + receiveOptionViewModel: widget.receiveOptionViewModel, + ); + }, + separatorBuilder: (context, index) { + if ((widget.receiveOptionViewModel.selectedReceiveOption == + commonOptions[index]) || + (index != commonOptions.length - 1 && + widget.receiveOptionViewModel.selectedReceiveOption == + commonOptions[index + 1])) return Container(); + + return Padding( + padding: const EdgeInsets.only(left: 44.0, right: 36.0), + child: HorizontalSectionDivider(), + ); + }, + ), + ), + ), + if (otherOptions.isNotEmpty) ...[ + SizedBox(height: 24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(20), + ), + child: Column( + children: [ + Material( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + bottom: _otherOptionsExpanded ? Radius.zero : Radius.circular(20)), + child: InkWell( + highlightColor: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + bottom: !_otherOptionsExpanded ? Radius.zero : Radius.circular(20)), + onTap: () { + setState(() { + _otherOptionsExpanded = !_otherOptionsExpanded; + }); + }, + child: Container( + height: 64.0, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + S.of(context).more_options, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.primary), + ), + AnimatedRotation( + duration: + ReceiveAddressTypeSelector.otherOptionsExpandDuration, + turns: _otherOptionsExpanded ? 0.0 : 0.5, + curve: Curves.easeOut, + child: SvgPicture.asset("assets/new-ui/dropdown_arrow.svg")) + ], + ), + ), + ), + ), + ), + if (_otherOptionsExpanded) + Padding( + padding: const EdgeInsets.only(left: 44.0, right: 36.0), + child: HorizontalSectionDivider(), + ), + AnimatedSize( + duration: ReceiveAddressTypeSelector.otherOptionsExpandDuration, + curve: Curves.easeOut, + alignment: Alignment.topCenter, + child: Container( + height: _otherOptionsExpanded ? null : 0, + child: ListView.separated( + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: NeverScrollableScrollPhysics(), + itemCount: otherOptions.length, + itemBuilder: (context, index) { + final opt = otherOptions[index]; + + return ReceiveAddressTypeRow( + option: opt, + roundedTop: false, + roundedBottom: index == otherOptions.length - 1, + selected: + widget.receiveOptionViewModel.selectedReceiveOption == opt, + onItemTap: () { + Navigator.of(context).pop(opt); + }, + receiveOptionViewModel: widget.receiveOptionViewModel, + ); + }, + separatorBuilder: (context, index) { + if ((widget.receiveOptionViewModel.selectedReceiveOption == + otherOptions[index]) || + (index != otherOptions.length - 1 && + widget.receiveOptionViewModel.selectedReceiveOption == + otherOptions[index + 1])) return Container(); + + return Padding( + padding: const EdgeInsets.only(left: 44.0, right: 36.0), + child: HorizontalSectionDivider(), + ); + }, + ), + ), + ), + ], + ), + ), + ), + ], + SizedBox(height: 24), + ], + )), + ); + } +} + +class ReceiveAddressTypeRow extends StatelessWidget { + const ReceiveAddressTypeRow( + {super.key, + required this.option, + required this.roundedTop, + required this.roundedBottom, + required this.selected, + required this.onItemTap, + required this.receiveOptionViewModel}); + + final ReceivePageOption option; + final bool roundedTop; + final bool roundedBottom; + final bool selected; + final VoidCallback onItemTap; + final ReceiveOptionViewModel receiveOptionViewModel; + + static const iconSize = 24.0; + static const rowHeight = 64.0; + + @override + Widget build(BuildContext context) { + String? iconPath = option.iconPath; + if (iconPath != null && + receiveOptionViewModel.walletTypeString == "Litecoin" && + option.value.contains("Standard")) { + iconPath = "assets/new-ui/address-type-picker-icons/litecoin.svg"; + } + return Material( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.vertical( + top: roundedTop ? Radius.circular(20) : Radius.zero, + bottom: roundedBottom ? Radius.circular(20) : Radius.zero, + ), + child: InkWell( + onTap: onItemTap, + child: Container( + height: rowHeight, + decoration: BoxDecoration( + color: selected + ? Theme.of(context).colorScheme.surfaceContainerHigh + : Colors.transparent, + borderRadius: BorderRadius.vertical( + top: roundedTop ? Radius.circular(20) : Radius.zero, + bottom: roundedBottom ? Radius.circular(20) : Radius.zero, + )), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + children: [ + Row( + children: [ + if (iconPath != null) + SvgPicture.asset(iconPath!, width: iconSize, height: iconSize,colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant,BlendMode.srcIn),) + else + Container(width: iconSize, height: iconSize), + Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + option.value, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface), + ), + if (option.description != null) + Text( + option.description!, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant), + ) + ], + ), + ) + ], + ), + RoundedCheckbox(value: selected) + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/receive_page/receive_address_widget.dart b/lib/new-ui/widgets/receive_page/receive_address_widget.dart new file mode 100644 index 000000000..1584d411c --- /dev/null +++ b/lib/new-ui/widgets/receive_page/receive_address_widget.dart @@ -0,0 +1,29 @@ +import 'package:cake_wallet/utils/address_formatter.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class ReceiveAddressWidget extends StatelessWidget { + const ReceiveAddressWidget({super.key, required this.addressListViewModel}); + + final WalletAddressListViewModel addressListViewModel; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 50.0), + child: Observer( + builder: (_) => AddressFormatter.buildSegmentedAddress( + address: addressListViewModel.uri.address, + walletType: addressListViewModel.type, + textAlign: TextAlign.center, + evenTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 15, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface, + fontFamily: "IBM Plex Mono"), + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/receive_page/receive_amount_display.dart b/lib/new-ui/widgets/receive_page/receive_amount_display.dart new file mode 100644 index 000000000..283a3e976 --- /dev/null +++ b/lib/new-ui/widgets/receive_page/receive_amount_display.dart @@ -0,0 +1,83 @@ +import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + + +class ReceiveAmountDisplay extends StatelessWidget { + const ReceiveAmountDisplay( + {super.key, required this.walletAddressListViewModel, required this.largeQrMode}); + + final WalletAddressListViewModel walletAddressListViewModel; + final bool largeQrMode; + + + @override + Widget build(BuildContext context) { + return Observer( + builder: (_) => + AnimatedOpacity( + duration: Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + opacity: largeQrMode || walletAddressListViewModel.displayAmount.isEmpty ? 0 : 1, + child: AnimatedAlign( + duration: Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + heightFactor: largeQrMode || walletAddressListViewModel.displayAmount.isEmpty ? 0 : 1, + alignment: Alignment.topCenter, + child: Container( + width: double.infinity, + child: Row( + spacing: 4, + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Theme + .of(context) + .colorScheme + .surfaceContainer + ), + child: + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + spacing: 8.0, + children: [ + Text(walletAddressListViewModel.displayAmount, style: TextStyle(color: Theme + .of(context) + .colorScheme + .primary, fontSize: 16, fontWeight: FontWeight.w500),), + Text(walletAddressListViewModel.cryptoCurrencySymbol, + style: TextStyle(color: Theme + .of(context) + .colorScheme + .primary, fontSize: 16, fontWeight: FontWeight.w500)) + ], + ), + ), + + ), + if(!walletAddressListViewModel.isFiatDisabled) + Padding( + padding: const EdgeInsets.all(8.0), + child: Text(_getFiatAmount(), + style: TextStyle(color: Theme + .of(context) + .colorScheme + .onSurfaceVariant, fontSize: 16, fontWeight: FontWeight.w500),), + ) + ], + ), + ), + ), + ), + ); + } + + String _getFiatAmount() { + return "${walletAddressListViewModel.fiatAmount} ${walletAddressListViewModel.selectedCurrency is FiatCurrency && walletAddressListViewModel.selectedCurrencyFiatAmount.isNotEmpty ? walletAddressListViewModel.selectedCurrency.name : walletAddressListViewModel.fiatCurrency.name}"; + } +} diff --git a/lib/new-ui/widgets/receive_page/receive_amount_modal.dart b/lib/new-ui/widgets/receive_page/receive_amount_modal.dart new file mode 100644 index 000000000..3adb70522 --- /dev/null +++ b/lib/new-ui/widgets/receive_page/receive_amount_modal.dart @@ -0,0 +1,249 @@ +import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:flutter_svg/svg.dart'; + +class ReceiveAmountModal extends StatefulWidget { + const ReceiveAmountModal( + {super.key, required this.walletAddressListViewModel, required this.onSubmitted}); + + final WalletAddressListViewModel walletAddressListViewModel; + final Function(String) onSubmitted; + + @override + State createState() => _ReceiveAmountModalState(); +} + +class _ReceiveAmountModalState extends State { + final TextEditingController _amountController = TextEditingController(); + + @override + void initState() { + super.initState(); + if(widget.walletAddressListViewModel.selectedCurrency is FiatCurrency) { + _amountController.text = widget.walletAddressListViewModel.fiatAmount; + } else { + _amountController.text = widget.walletAddressListViewModel.displayAmount; + } + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: SafeArea( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical(top: Radius.circular(18)), + color: Theme.of(context).colorScheme.surface, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ModalTopBar( + title: S.of(context).set_amount, + onLeadingPressed: Navigator.of(context).pop, + onTrailingPressed: () {}, + leadingIcon: Icon(Icons.close), + ), + Padding( + padding: const EdgeInsets.all(18.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 12, + children: [ + if (widget.walletAddressListViewModel.hasTokensList) ...[ + Text("Token"), + GestureDetector( + onTap: () { + _presentTokenCurrencyPicker(context); + }, + child: Observer( + builder: (_) => Container( + height: 60, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + spacing: 8, + children: [ + Image.asset( + widget.walletAddressListViewModel.tokenCurrency + ?.iconPath ?? + widget.walletAddressListViewModel.currencies.first + .iconPath ?? + "", + width: 32, + height: 32), + Text((widget.walletAddressListViewModel.tokenCurrency ?? + widget.walletAddressListViewModel.currencies.first + as CryptoCurrency) + .title + .toUpperCase()) + ], + ), + RotatedBox( + quarterTurns: 2, + child: SvgPicture.asset("assets/new-ui/dropdown_arrow.svg")) + ], + ), + ), + ), + )), + ], + SizedBox(), + Text(S.of(context).amount), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + flex: 75, + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + height: 60, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + bottomLeft: Radius.circular(16), + ), + border: Border.all( + color: Theme.of(context).colorScheme.surfaceContainer, + width: 2, + ), + ), + child: TextField( + textAlign: TextAlign.left, + textAlignVertical: TextAlignVertical.center, + controller: _amountController, + keyboardType: + TextInputType.numberWithOptions(signed: false, decimal: !widget.walletAddressListViewModel.useSatoshi), + decoration: InputDecoration( + hint: Text( + widget.walletAddressListViewModel.useSatoshi ? "0" : "0.00000000", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 16, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.5), + ), + ), + border: InputBorder.none, + filled: true, + fillColor: Colors.transparent, + ), + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + ), + ), + ), + Expanded( + flex: 25, + child: GestureDetector( + onTap: () { + _presentFiatCurrencyPicker(context); + }, + child: Container( + height: 60, + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(0), + bottomLeft: Radius.circular(0), + topRight: Radius.circular(18), + bottomRight: Radius.circular(18), + ), + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 4.0, + children: [ + Observer( + builder: (_) => Text( + widget.walletAddressListViewModel.selectedCurrencySymbol, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + Icon( + Icons.keyboard_arrow_down, + color: Theme.of(context).colorScheme.primary, + ), + ], + ), + ), + ), + ), + ], + ), + SizedBox(), + NewPrimaryButton( + text: S.of(context).continue_text, + onPressed: () { + if(double.tryParse(_amountController.text) != null) { + widget.walletAddressListViewModel.changeAmount(_amountController.text); + } + Navigator.of(context).pop(); + }, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + ) + ], + ), + ) + ], + ), + ), + ), + ); + } + + void _presentTokenCurrencyPicker(BuildContext context) async { + await showPopUp( + builder: (_) => CurrencyPicker( + selectedAtIndex: widget.walletAddressListViewModel.tokenCurrencyIndex, + items: widget.walletAddressListViewModel.tokenCurrencies, + hintText: S.of(context).search_currency, + onItemSelected: widget.walletAddressListViewModel.setTokenCurrency, + ), + context: context, + ); + } + + void _presentFiatCurrencyPicker(BuildContext context) async { + final currs = widget.walletAddressListViewModel.currencies.toList(); + // the first options should be the crypto and the default fiat currency + currs.sort((a, b) { + if(a == widget.walletAddressListViewModel.fiatCurrency || a is CryptoCurrency) return -1; + if(b == widget.walletAddressListViewModel.fiatCurrency || a is CryptoCurrency) return 1; + return 0; + }); + + await showPopUp( + builder: (_) => CurrencyPicker( + selectedAtIndex: currs.indexOf(widget.walletAddressListViewModel.selectedCurrency), + items: currs, + hintText: S.of(context).search_currency, + onItemSelected: widget.walletAddressListViewModel.selectCurrency, + ), + context: context, + ); + } +} diff --git a/lib/new-ui/widgets/receive_page/receive_bottom_buttons.dart b/lib/new-ui/widgets/receive_page/receive_bottom_buttons.dart new file mode 100644 index 000000000..9b0f80f57 --- /dev/null +++ b/lib/new-ui/widgets/receive_page/receive_bottom_buttons.dart @@ -0,0 +1,124 @@ +import 'dart:io'; + +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/material.dart'; + +class ReceiveBottomButtons extends StatefulWidget { + final bool largeQrMode; + final VoidCallback onCopyButtonPressed; + final VoidCallback onAmountButtonPressed; + final VoidCallback onLabelButtonPressed; + final VoidCallback onAccountsButtonPressed; + final bool showLabelButton; + final bool showAccountsButton; + + const ReceiveBottomButtons({ + super.key, + required this.largeQrMode, + required this.onCopyButtonPressed, + required this.onAccountsButtonPressed, + required this.onAmountButtonPressed, + required this.onLabelButtonPressed, + required this.showLabelButton, + required this.showAccountsButton, + }); + + @override + State createState() => _ReceiveBottomButtonsState(); +} + +class _ReceiveBottomButtonsState extends State { + bool copied = false; + + void handleCopy() async { + widget.onCopyButtonPressed(); + if (await shouldShowCopied()) { + setState(() => copied = true); + Future.delayed(const Duration(milliseconds: 1200), () { + if (mounted) setState(() => copied = false); + }); + } + } + + @override + Widget build(BuildContext context) { + final double targetOpacity = widget.largeQrMode ? 0 : 1; + + return ClipRect( + child: AnimatedAlign( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + heightFactor: widget.largeQrMode ? 0 : 1, + alignment: Alignment.bottomCenter, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: targetOpacity, + curve: Curves.easeOut, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32.0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + spacing: 16, + children: [ + AnimatedSwitcher( + duration: Duration(milliseconds: 200), + child: ModernButton.svg( + key: ValueKey(copied), + size: 60, + iconSize: 32, + svgPath: "assets/new-ui/copy.svg", + onPressed: handleCopy, + label: copied ? S.of(context).copied : S.of(context).copy, + iconColor: copied ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.surfaceContainer, + backgroundColor: copied ? Theme.of(context).colorScheme.surfaceContainerHighest : Theme.of(context).colorScheme.primary, + ), + ), + ModernButton.svg( + size: 60, + iconSize: 32, + svgPath: "assets/new-ui/set-amount.svg", + onPressed: widget.onAmountButtonPressed, + label: S.of(context).set_amount), + if (widget.showLabelButton) + ModernButton.svg( + size: 60, + iconSize: 32, + svgPath: "assets/new-ui/add-label.svg", + onPressed: widget.onLabelButtonPressed, + label: S.of(context).label), + if (widget.showAccountsButton) + ModernButton.svg( + size: 60, + iconSize: 32, + svgPath: "assets/new-ui/addr-book.svg", + onPressed: widget.onAccountsButtonPressed, + label: S.of(context).addresses), + ], + ), + ), + ), + ), + ); + } + + + + // android 13 (sdk 33) added a built-in "text was copied to clipboard" ui element + // older android and iphone still needs an indicator though + Future shouldShowCopied() async { + if (!Platform.isAndroid) return true; + + try { + final deviceInfo = DeviceInfoPlugin(); + final androidInfo = await deviceInfo.androidInfo; + final sdk = androidInfo.version.sdkInt; + + return sdk < 33; + } catch (_) { + return true; + } + } +} \ No newline at end of file diff --git a/lib/new-ui/widgets/receive_page/receive_info_box.dart b/lib/new-ui/widgets/receive_page/receive_info_box.dart new file mode 100644 index 000000000..a8f1ef8af --- /dev/null +++ b/lib/new-ui/widgets/receive_page/receive_info_box.dart @@ -0,0 +1,96 @@ +import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class ReceiveInfoBox extends StatelessWidget { + ReceiveInfoBox( + {super.key, required this.iconPath, required this.message, required this.onDismissed}); + + static ReceiveInfoBox? forWalletType(WalletType type, {required VoidCallback onDismissed, required AutoGenerateSubaddressStatus autoGenerateSubaddressStatus}) { + switch (type) { + case WalletType.nano: + return null; + case WalletType.ethereum: + case WalletType.base: + case WalletType.solana: + case WalletType.arbitrum: + case WalletType.tron: + case WalletType.polygon: + case WalletType.zano: + case WalletType.bsc: + if(autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.disabled) + return null; + return ReceiveInfoBox( + iconPath: "assets/new-ui/chain_badges/${walletTypeToString(type).toLowerCase()}.svg", + message: + "${S.current.infobox_multichain} ${walletTypeToString(type)}", + onDismissed: onDismissed, + ); + default: + return ReceiveInfoBox( + iconPath: "assets/new-ui/info.svg", + message: S.current.infobox_auto_address, + onDismissed: onDismissed, + ); + } + } + + late final String iconPath; + late final String message; + final VoidCallback onDismissed; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 25), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 10, + children: [ + SvgPicture.asset( + iconPath, + width: 16, + height: 16, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn), + ), + Flexible( + child: Column( + spacing: 10, + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + message, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 12, + fontWeight: FontWeight.w300), + ), + GestureDetector( + onTap: onDismissed, + child: Text( + S.of(context).dismiss, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontSize: 12, + fontWeight: FontWeight.w300), + )) + ]), + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/receive_page/receive_label_modal.dart b/lib/new-ui/widgets/receive_page/receive_label_modal.dart new file mode 100644 index 000000000..5f7132ad3 --- /dev/null +++ b/lib/new-ui/widgets/receive_page/receive_label_modal.dart @@ -0,0 +1,144 @@ +import 'dart:async'; +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_text_field.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; + + + +class ReceiveLabelModal extends StatefulWidget { + const ReceiveLabelModal({super.key, required this.walletAddressEditOrCreateViewModel}); + + final WalletAddressEditOrCreateViewModel walletAddressEditOrCreateViewModel; + + @override + State createState() => _ReceiveLabelModalState(); +} + +class _ReceiveLabelModalState extends State { + late final TextEditingController _controller; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _controller.text = widget.walletAddressEditOrCreateViewModel.label; + }); + reaction((_) => widget.walletAddressEditOrCreateViewModel.state, + (AddressEditOrCreateState state) { + printV(state); + if (state is AddressSavedSuccessfully) { + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.of(context).pop(widget.walletAddressEditOrCreateViewModel.label); + }); + } + }); + } + + @override + Widget build(BuildContext context) { + List defaultLabels = [S.of(context).donation, S.of(context).savings, S.of(context).business, S.of(context).mining, S.of(context).salary]; + + return Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(16))), + child: SafeArea( + top:false, + child: Column( + spacing: 24, + mainAxisSize: MainAxisSize.min, + children: [ + ModalTopBar( + title: S.of(context).label_address, + leadingIcon: Icon(Icons.close), + onLeadingPressed: Navigator.of(context).pop, + onTrailingPressed: () {}), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column(spacing: 24, children: [ + Text( + S.of(context).address_label_explainer, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + NewListSections(sections: { + "": [ListItemTextField(keyValue: "label", label: S.of(context).label)] + }, controllers: { + "label": _controller + }), + SizedBox( + height: 36, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: defaultLabels.length, + separatorBuilder: (context, index) { + return SizedBox(width: 8); + }, + itemBuilder: (context, index) { + return Container( + height: 36, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(999)), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(999), + child: InkWell( + borderRadius: BorderRadius.circular(999), + onTap: () { + _controller.text = defaultLabels[index]; + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Center( + child: Text( + defaultLabels[index], + style: TextStyle( + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.primary), + ), + ), + ), + ), + ), + ); + }), + ), + Observer( + builder: (_) { + final isSaving = + (widget.walletAddressEditOrCreateViewModel.state is AddressIsSaving); + + return NewPrimaryButton( + isLoading: isSaving, + onPressed: () { + if (isSaving) return; + widget.walletAddressEditOrCreateViewModel.label = _controller.text; + widget.walletAddressEditOrCreateViewModel.save(); + }, + text: S.of(context).continue_text, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary); + }, + ) + ]), + ), + SizedBox() + ], + ), + )), + ); + } +} diff --git a/lib/new-ui/widgets/receive_page/receive_label_widget.dart b/lib/new-ui/widgets/receive_page/receive_label_widget.dart new file mode 100644 index 000000000..f5f077f54 --- /dev/null +++ b/lib/new-ui/widgets/receive_page/receive_label_widget.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class ReceiveLabelWidget extends StatelessWidget { + const ReceiveLabelWidget({super.key, required this.name, required this.largeQrMode}); + + final String name; + final bool largeQrMode; + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + duration: Duration(milliseconds: 200), + height: largeQrMode || name.isEmpty ? 0 : 36, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(999)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Row( + spacing: 8, + mainAxisSize: MainAxisSize.min, + children: [ + ClipRect( + child: SvgPicture.asset( + width: 24, + height: 24, + "assets/new-ui/label.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn), + )), + Text(name, + style: TextStyle( + fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant)), + ], + ), + )); + } +} diff --git a/lib/new-ui/widgets/receive_page/receive_large_amount_preview.dart b/lib/new-ui/widgets/receive_page/receive_large_amount_preview.dart new file mode 100644 index 000000000..3942bdf61 --- /dev/null +++ b/lib/new-ui/widgets/receive_page/receive_large_amount_preview.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class ReceiveLargeAmountPreview extends StatelessWidget { + const ReceiveLargeAmountPreview( + {super.key, required this.amount, required this.currency, required this.largeQrMode}); + + final String amount; + final String currency; + final bool largeQrMode; + + @override + Widget build(BuildContext context) { + return AnimatedOpacity( + duration: Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + opacity: largeQrMode && amount.isNotEmpty ? 1 : 0, + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + transformAlignment: Alignment.bottomCenter, + height: largeQrMode && amount.isNotEmpty ? 52 : 0, + width: MediaQuery.of(context).size.width * 0.7, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(16)), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + spacing: 12, + children: [ + SvgPicture.asset("assets/new-ui/send.svg", + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn)), + Row( + spacing: 4, + children: [ + Text( + amount, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface), + ), + Text( + currency, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurfaceVariant), + ) + ], + ) + ], + )), + ); + } +} diff --git a/lib/new-ui/widgets/receive_page/receive_qr_code.dart b/lib/new-ui/widgets/receive_page/receive_qr_code.dart new file mode 100644 index 000000000..416717255 --- /dev/null +++ b/lib/new-ui/widgets/receive_page/receive_qr_code.dart @@ -0,0 +1,120 @@ +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/themes/core/theme_store.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import '../../../src/screens/receive/widgets/qr_image.dart'; + +class ReceiveQrCode extends StatelessWidget { + ReceiveQrCode({ + super.key, + required this.onTap, + required this.largeQrMode, + required this.addressListViewModel, + }); + + final VoidCallback onTap; + final bool largeQrMode; + final WalletAddressListViewModel addressListViewModel; + + static const double largeQrModeBottomPadding = 70; + final bool isLightMode = !(getIt.get().currentTheme.isDark); + + @override + Widget build(BuildContext context) { + final double targetY = largeQrMode ? 60 : 0; + final double resolvedSize = MediaQuery.of(context).size.width * (largeQrMode ? 0.85 : 0.5); + + return Stack( + alignment: Alignment.topCenter, + children: [ + AnimatedOpacity( + duration: Duration(milliseconds: 300), + opacity: largeQrMode ? 1 : 0, + child: SvgPicture.asset( + isLightMode + ? "assets/new-ui/cakewallet-wordmark-light.svg" + : "assets/new-ui/cakewallet-wordmark.svg", + height: 45, + )), + GestureDetector( + onTap: onTap, + child: TweenAnimationBuilder( + tween: Tween(begin: 0, end: targetY), + duration: const Duration(milliseconds: 400), + curve: Curves.easeOut, + builder: (context, value, child) { + return Transform.translate( + offset: Offset(0, value), + child: Observer( + builder: (_)=>Column( + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 400), + curve: Curves.easeOutCubic, + width: resolvedSize, + height: resolvedSize, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Colors.white, + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + color: addressListViewModel.hasPayjoin && !largeQrMode + ? Theme.of(context).colorScheme.surfaceContainer + : Colors.transparent), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: LayoutBuilder( + builder: (context, constraints) => Observer( + builder: (_) => QrImage( + data: addressListViewModel.uri.toString(), + embeddedImagePath: + addressListViewModel.tokenCurrency != null + ? addressListViewModel.tokenCurrency == CryptoCurrency.btcln + ? addressListViewModel.qrImage + : addressListViewModel.tokenCurrency!.iconPath + : addressListViewModel.qrImage, + size: constraints.maxWidth)))), + ), + ), + if (addressListViewModel.hasPayjoin) + Opacity( + opacity: largeQrMode ? 0 : 1, + child: Container( + width:resolvedSize, + decoration: BoxDecoration( + borderRadius: + BorderRadius.vertical(bottom: Radius.circular(16)), + color: Theme.of(context).colorScheme.surfaceContainer), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 4, + children: [ + SvgPicture.asset("assets/new-ui/payjoin.svg"), + Text(S.of(context).payjoin_enabled) + ], + ), + ))), + AnimatedSize( + duration: Duration(milliseconds: 300), + // curve: Curves.easeOutCubic, + child: SizedBox(height: largeQrMode ? largeQrModeBottomPadding : 0)) + ], + ), + ), + ); + }, + ), + ), + ], + ); + } +} diff --git a/lib/new-ui/widgets/receive_page/receive_token_display.dart b/lib/new-ui/widgets/receive_page/receive_token_display.dart new file mode 100644 index 000000000..601dbb27b --- /dev/null +++ b/lib/new-ui/widgets/receive_page/receive_token_display.dart @@ -0,0 +1,57 @@ +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class ReceiveTokenDisplay extends StatelessWidget { + const ReceiveTokenDisplay({super.key, required this.addressListViewModel}); + + final WalletAddressListViewModel addressListViewModel; + + @override + Widget build(BuildContext context) { + return Observer( + builder: (_)=>Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 8, + children: [ + Text( + addressListViewModel.tokenCurrency!.title, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.primary), + ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(99999), + color: Theme.of(context).colorScheme.surfaceContainer), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Row( + spacing:4, + children: [ + SvgPicture.asset( + "assets/new-ui/chain_badges/${walletTypeToString(addressListViewModel.wallet.type).toLowerCase()}.svg", + width: 16, + height: 16, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn), + ), + Text(walletTypeToString(addressListViewModel.wallet.type), + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary)), + SizedBox() + ], + ), + ), + ) + ], + ), + ); + } +} diff --git a/lib/new-ui/widgets/receive_page/receive_top_bar.dart b/lib/new-ui/widgets/receive_page/receive_top_bar.dart new file mode 100644 index 000000000..f92f00e7e --- /dev/null +++ b/lib/new-ui/widgets/receive_page/receive_top_bar.dart @@ -0,0 +1,102 @@ +import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; +import 'package:flutter/material.dart'; + +void nothing(){} + +class ModalTopBar extends StatelessWidget { + ModalTopBar( + {super.key, + required this.title, + this.subtitle, + this.onLeadingPressed=nothing, + this.onTrailingPressed=nothing, + this.leadingIcon, + this.trailingIcon, + this.leadingWidget, + this.trailingWidget}) { + if(leadingIcon != null && leadingWidget != null) { + throw Exception("Cannot have both leadingIcon and leadingWidget"); + } + if(trailingIcon != null && trailingWidget != null) { + throw Exception("Cannot have both trailingIcon and trailingWidget"); + } + } + + final String title; + final String? subtitle; + final VoidCallback onLeadingPressed; + final VoidCallback onTrailingPressed; + final Widget? leadingIcon; + final Widget? trailingIcon; + final Widget? leadingWidget; + final Widget? trailingWidget; + + static const buttonSize = 36.0; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(18), + child: Stack( + alignment: Alignment.center, + children: [ + Align( + alignment: Alignment.center, + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + spacing: 4, + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: Text( + title, + key: ValueKey(title), + style: Theme.of(context).textTheme.headlineMedium, + ), + ), + if (subtitle != null && subtitle!.isNotEmpty) + Text( + subtitle!, + style: Theme.of(context) + .textTheme + .headlineMedium + ?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant)) + ], + ), + ), + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (leadingIcon != null || leadingWidget != null) + leadingIcon != null + ? ModernButton( + key: ValueKey(leadingIcon.hashCode), + size: buttonSize, + onPressed: onLeadingPressed, + icon: leadingIcon!, + iconColor: Theme.of(context).colorScheme.onSurfaceVariant) + : leadingWidget! + else + Container(width: buttonSize), + if (trailingIcon != null || trailingWidget != null) + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: trailingIcon != null + ? ModernButton( + key: ValueKey(trailingIcon.hashCode), + size: buttonSize, + onPressed: onTrailingPressed, + icon: trailingIcon!,) + : trailingWidget!, + ) + else + Container(width: buttonSize), + ], + ), + ], + ), + ); + } +} diff --git a/lib/new-ui/widgets/send_page/directional_switcher.dart b/lib/new-ui/widgets/send_page/directional_switcher.dart new file mode 100644 index 000000000..d29785d0d --- /dev/null +++ b/lib/new-ui/widgets/send_page/directional_switcher.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; + +class DirectionalAnimatedSwitcher extends StatefulWidget { + final Widget child; + final Duration duration; + + const DirectionalAnimatedSwitcher({ + super.key, + required this.child, + required this.duration, + }); + + @override + State createState() => _DirectionalAnimatedSwitcherState(); +} + +class _DirectionalAnimatedSwitcherState extends State { + bool _isForward = true; + + @override + void didUpdateWidget(DirectionalAnimatedSwitcher oldWidget) { + super.didUpdateWidget(oldWidget); + + final oldKey = (oldWidget.child.key as ValueKey).value; + final newKey = (widget.child.key as ValueKey).value; + + if (newKey != oldKey) { + _isForward = newKey > oldKey; + } + } + + @override + Widget build(BuildContext context) { + return AnimatedSwitcher( + duration: widget.duration, + transitionBuilder: (Widget child, Animation animation) { + final double offset = _isForward ? 1.2 : -1.2; + + final inTween = Tween(begin: Offset(offset, 0.0), end: Offset.zero) + .chain(CurveTween(curve: Curves.easeInOutCubic)); + + final outTween = Tween(begin: Offset(-offset, 0.0), end: Offset.zero) + .chain(CurveTween(curve: Curves.easeInOutCubic)); + + if (child.key == widget.child.key) { + return SlideTransition(position: animation.drive(inTween), child: child); + } else { + return SlideTransition(position: animation.drive(outTween), child: child); + } + }, + child: widget.child, + ); + } +} diff --git a/lib/new-ui/widgets/send_page/fiat_amount_bar.dart b/lib/new-ui/widgets/send_page/fiat_amount_bar.dart new file mode 100644 index 000000000..6fb3e4dc3 --- /dev/null +++ b/lib/new-ui/widgets/send_page/fiat_amount_bar.dart @@ -0,0 +1,101 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; +import 'package:cw_core/crypto_amount_format.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class FiatAmountBar extends StatelessWidget { + const FiatAmountBar( + {super.key, + required this.fiatInputMode, + required this.onSwitchButtonPressed, + this.onAllButtonPressed, + required this.cryptoAmount, + required this.fiatAmount, + required this.cryptoCurrency, + required this.fiatCurrency, + this.allAmount, + this.foregroundElementColor, + this.textColor, this.allAmountColor, this.allAmountTextColor}); + + final bool fiatInputMode; + final VoidCallback onSwitchButtonPressed; + final VoidCallback? onAllButtonPressed; + + final String cryptoAmount; + final String fiatAmount; + final String cryptoCurrency; + final String fiatCurrency; + final String? allAmount; + final Color? foregroundElementColor; + final Color? textColor; + final Color? allAmountColor; + final Color? allAmountTextColor; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + spacing: 8, + children: [ + ModernButton.svg( + backgroundColor: foregroundElementColor, + size: 28, + svgPath: "assets/new-ui/switch.svg", + iconSize: 18, + onPressed: onSwitchButtonPressed, + ), + Observer( + builder: (_) => GestureDetector( + onTap: onSwitchButtonPressed, + child: Text( + fiatInputMode + ? "${cryptoAmount.isEmpty ? "0" : cryptoAmount.withMaxDecimals(8)} ${cryptoCurrency}" + : "${fiatAmount.isEmpty ? "0" : fiatAmount} ${fiatCurrency}", + style: TextStyle(color: textColor ?? Theme.of(context).colorScheme.onSurface), + ), + )), + ], + ), + if (allAmount != null && allAmount!.isNotEmpty) + Row( + spacing: 8, + children: [ + Text( + "${S.of(context).max}.", + style: TextStyle(color: textColor ?? Theme.of(context).colorScheme.onSurface), + ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(999999)), + child: Material( + color: allAmountColor ?? foregroundElementColor ?? Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(99999), + child: InkWell( + borderRadius: BorderRadius.circular(99999), + onTap: onAllButtonPressed, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4), + child: Text( + formatAmount(allAmount!), + style: TextStyle(color: allAmountTextColor ?? Theme.of(context).colorScheme.primary), + ), + ), + )), + ) + ], + ) + ], + ); + } + + String formatAmount(String amount) { + try { + return double.parse(amount).toStringAsPrecision(8).replaceFirst(RegExp(r"\.?0+$"), ""); + } catch(e) { + return amount; + } + } +} diff --git a/lib/new-ui/widgets/send_page/floating_icon_button.dart b/lib/new-ui/widgets/send_page/floating_icon_button.dart new file mode 100644 index 000000000..84cd3dfc6 --- /dev/null +++ b/lib/new-ui/widgets/send_page/floating_icon_button.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class FloatingIconButton extends StatelessWidget { + const FloatingIconButton({super.key, required this.iconPath, required this.onPressed}); + + final String iconPath; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + borderRadius: const BorderRadius.all(Radius.circular(6)), + child: InkWell( + borderRadius: const BorderRadius.all(Radius.circular(6)), + onTap: onPressed, + child: Padding( + padding: const EdgeInsets.all(4.0), + child: SvgPicture.asset( + width: 22, height: 22, + iconPath, + colorFilter: ColorFilter.mode(Theme + .of(context) + .colorScheme + .primary, BlendMode.srcIn), + ), + ) + ), + ); + } +} \ No newline at end of file diff --git a/lib/new-ui/widgets/send_page/l2_action_wallet_selector.dart b/lib/new-ui/widgets/send_page/l2_action_wallet_selector.dart new file mode 100644 index 000000000..f21039d39 --- /dev/null +++ b/lib/new-ui/widgets/send_page/l2_action_wallet_selector.dart @@ -0,0 +1,414 @@ +import 'dart:async'; + +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/main.dart'; +import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/new-ui/widgets/send_page/l2_send_external_modal.dart'; +import 'package:cake_wallet/new-ui/widgets/send_page/send_address_input.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/new_simple_checkbox.dart'; +import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; +import 'package:cake_wallet/view_model/send/send_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart'; +import 'package:cw_core/currency_for_wallet_type.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; + +enum l2actions { deposit, withdraw } + +class L2ActionWalletSelector extends StatefulWidget { + const L2ActionWalletSelector({ + super.key, + required this.showOtherWallets, + required this.sendViewModel, + required this.action, + required this.onSendInitiated, + required this.contactListViewModel, + required this.walletSwitcherViewModel, + }); + + final bool showOtherWallets; + final SendViewModel sendViewModel; + final l2actions action; + final VoidCallback onSendInitiated; + final ContactListViewModel contactListViewModel; + final WalletSwitcherViewModel walletSwitcherViewModel; + + @override + State createState() => _L2ActionWalletSelectorState(); +} + +class _L2ActionWalletSelectorState extends State { + final TextEditingController addressController = TextEditingController(); + List items = []; + bool textEntered = false; + int _selectedWalletIndex = 0; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + if (widget.showOtherWallets) { + () async { + items.addAll((await WalletInfo.getAll()) + .where((item) => item.type == widget.sendViewModel.walletType)); + items.sort((a, b) { + if (a.name == widget.sendViewModel.wallet.name) + return -1; + else if (b.name == widget.sendViewModel.wallet.name) return 1; + return 0; + }); + setState(() {}); + }.call(); + } + addressController.addListener(() { + setState(() { + textEntered = addressController.text.isNotEmpty; + }); + }); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ModalTopBar( + title: widget.action == l2actions.deposit + ? "${S.of(context).send_from}..." + : "${S.of(context).receive_to}...", + leadingIcon: Icon(Icons.arrow_back_ios_new), + onLeadingPressed: Navigator.of(context).pop, + ), + Flexible( + child: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + spacing: 12, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (!widget.showOtherWallets) + Column(spacing: 8, crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(S.of(context).choose_wallet), + WalletRow( + isCurrent: true, + currencyIconPath: widget.sendViewModel.wallet.currency.iconPath ?? "", + walletName: widget.sendViewModel.wallet.name, + onTap: () { + Navigator.of(context).push(CupertinoPageRoute( + builder: (context) => Material( + child: L2ActionWalletSelector( + showOtherWallets: true, + sendViewModel: widget.sendViewModel, + action: widget.action, + onSendInitiated: widget.onSendInitiated, + contactListViewModel: widget.contactListViewModel, + walletSwitcherViewModel: widget.walletSwitcherViewModel, + )))); + }, + ) + ]), + if (widget.showOtherWallets) ...[ + Flexible( + child: ListView.builder( + shrinkWrap: true, + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: WalletRow( + currencyIconPath: + walletTypeToCryptoCurrency(item.type).iconPath ?? "", + walletName: item.name, + isCurrent: item.name == widget.sendViewModel.wallet.name, + isSelected: _selectedWalletIndex == index && !textEntered, + onTap: () async { + setState(() { + addressController.text = ""; + _selectedWalletIndex = index; + }); + // if (widget.action == l2actions.withdraw) { + // widget.sendViewModel.outputs.first.address = item.address; + // widget.onSendInitiated(); + // } else if (widget.action == l2actions.deposit) { + // setState(() { + // loadingWalletName = item.name; + // }); + // await _handleChangeWallet(item); + // widget.onSendInitiated(); + // setState(() { + // loadingWalletName = null; + // }); + }, + ), + ); + }), + ), + Column( + spacing: 12, + children: [ + // Container( + // height: 64, + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(16), + // color: Theme.of(context).colorScheme.surfaceContainer, + // ), + // child: Material( + // borderRadius: BorderRadius.circular(16), + // color: Colors.transparent, + // child: InkWell( + // borderRadius: BorderRadius.circular(16), + // onTap: () { + // Navigator.of(context).push(CupertinoPageRoute( + // builder: (context) => Material( + // child: L2ActionWalletSelector( + // showOtherWallets: true, + // sendViewModel: widget.sendViewModel, + // action: widget.action, + // onSendInitiated: widget.onSendInitiated, + // contactListViewModel: + // widget.contactListViewModel,walletSwitcherViewModel: widget.walletSwitcherViewModel,)))); + // }, + // child: Padding( + // padding: const EdgeInsets.symmetric(horizontal: 12.0), + // child: Row( + // spacing: 10, + // children: [ + // SvgPicture.asset( + // "assets/new-ui/select_wallet.svg", + // colorFilter: ColorFilter.mode( + // Theme.of(context).colorScheme.primary, + // BlendMode.srcIn), + // ), + // Text( + // S.of(context).select_other_wallet, + // style: TextStyle( + // color: Theme.of(context).colorScheme.primary,fontWeight: FontWeight.w500,fontSize:15), + // ) + // ], + // ), + // ), + // ), + // ), + // ), + if (widget.action == l2actions.withdraw) + Row( + children: [ + Flexible( + child: NewSendAddressInput( + addressController: addressController, + selectedCurrency: widget.sendViewModel.selectedCryptoCurrency, + onEditingComplete: () {}), + ), + ], + ), + ], + ), + ], + Column( + spacing: 8, + children: [ + if (widget.action == l2actions.deposit && widget.showOtherWallets) ...[ + Container( + height: 1, + color: Theme.of(context).colorScheme.onSurfaceVariant.withAlpha(64), + ), + SizedBox(), + Container( + height: 52, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: Theme.of(context).colorScheme.surfaceContainer, + ), + child: Material( + borderRadius: BorderRadius.circular(16), + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () async { + Navigator.of(context, rootNavigator: true).pop(); + showCupertinoModalBottomSheet( + context: navigatorKey.currentContext ?? context, + builder: (context) { + return Material( + child: L2SendExternalModal( + sendViewModel: widget.sendViewModel, + ), + ); + }, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 10, + children: [ + SvgPicture.asset( + "assets/new-ui/send_from_external.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, BlendMode.srcIn), + ), + Text( + S.of(context).send_from_external, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.w500, + fontSize: 15, + ), + ) + ], + ), + ), + ), + ), + ), + ], + NewPrimaryButton( + onPressed: () async { + if (widget.sendViewModel.wallet.type == WalletType.bitcoin || + widget.sendViewModel.wallet.type == WalletType.litecoin) { + if (widget.action == l2actions.withdraw) { + widget.sendViewModel.outputs.first.address = + bitcoin!.getUnusedSegwitAddress(widget.sendViewModel.wallet)!; + } + + if (widget.showOtherWallets) { + if (widget.action == l2actions.deposit) { + await _handleChangeWallet(items[_selectedWalletIndex]); + } else { + widget.sendViewModel.outputs.first.address = + addressController.text.isNotEmpty + ? addressController.text + : items[_selectedWalletIndex].address; + } + } + widget.onSendInitiated(); + } + }, + text: S.of(context).continue_text, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + isLoading: _isLoading, + ), + SizedBox() + ], + ), + ], + ), + ), + ), + ) + ], + ); + } + + Future _handleChangeWallet(WalletInfo wallet) async { + if (wallet == widget.sendViewModel.wallet) { + return; + } + setState(() { + _isLoading = true; + }); + // this waits for the ui to animate nicely + // if wallet switching is done alongside animations, they lag HARD + await Future.delayed(const Duration(milliseconds: 500)); + widget.walletSwitcherViewModel.selectWallet(wallet); + final success = await widget.walletSwitcherViewModel.switchToSelectedWallet(); + if (success) { + await Future.delayed(const Duration(seconds: 2)); + await widget.sendViewModel.updateWalletBalance(); + if (bitcoin != null) { + await bitcoin!.updateFeeRates(widget.sendViewModel.wallet); + } + } + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } +} + +class WalletRow extends StatelessWidget { + const WalletRow({ + super.key, + required this.currencyIconPath, + required this.walletName, + required this.onTap, + this.isLoading = false, + this.isCurrent = false, + this.isSelected, + }); + + final String currencyIconPath; + final String walletName; + final VoidCallback onTap; + final bool isCurrent; + final bool isLoading; + final bool? isSelected; + + @override + Widget build(BuildContext context) { + return Container( + height: isCurrent ? 64 : 48, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(16), + ), + child: Material( + borderRadius: BorderRadius.circular(16), + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: onTap, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + spacing: 12, + children: [ + Image.asset( + currencyIconPath, + height: 24, + width: 24, + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(walletName), + if (isCurrent) + Text( + S.of(context).current_wallet, + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant), + ) + ], + ) + ], + ), + isSelected != null + ? NewSimpleCheckbox(value: isSelected!, onChanged: (val) {}) + : Icon( + Icons.chevron_right, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ) + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/send_page/l2_send_external_modal.dart b/lib/new-ui/widgets/send_page/l2_send_external_modal.dart new file mode 100644 index 000000000..de0fb6d4d --- /dev/null +++ b/lib/new-ui/widgets/send_page/l2_send_external_modal.dart @@ -0,0 +1,211 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart'; +import 'package:cake_wallet/utils/address_formatter.dart'; +import 'package:cake_wallet/view_model/send/send_view_model.dart'; +import 'package:cw_core/payment_uris.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import "package:cw_core/wallet_type.dart"; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class L2SendExternalModal extends StatefulWidget { + const L2SendExternalModal({super.key, required this.sendViewModel}); + + final SendViewModel sendViewModel; + + @override + State createState() => _L2SendExternalModalState(); +} + +class _L2SendExternalModalState extends State { + PaymentURI? uri; + bool largeQrMode = false; + static const warningTextColor = Color(0xFFFFB84E); + static const warningBackgroundColor = Color(0xFF8E5800); + + @override + void initState() { + super.initState(); + () async { + if (widget.sendViewModel.wallet.type == WalletType.bitcoin) { + await bitcoin!.setAddressType(widget.sendViewModel.wallet, + bitcoin!.getOptionToType(bitcoin!.getBitcoinLightningReceivePageOption())); + } else if(widget.sendViewModel.wallet.type == WalletType.litecoin) { + await bitcoin!.setAddressType(widget.sendViewModel.wallet, + bitcoin!.getOptionToType(bitcoin!.getLitecoinMwebReceivePageOption())); + } + final newUri = await widget.sendViewModel.wallet.walletAddresses + .getPaymentRequestUri(widget.sendViewModel.outputs.first.cryptoAmount); + setState(() { + uri = newUri; + }); + }.call(); + } + + @override + Widget build(BuildContext context) { + final output = widget.sendViewModel.outputs.first; + final resolvedSize = MediaQuery.of(context).size.width * (largeQrMode ? 0.8 : 0.54); + + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ModalTopBar( + title: "", + leadingWidget: Row( + spacing: 12, + children: [ + if(widget.sendViewModel.walletType == WalletType.bitcoin) + SvgPicture.asset("assets/new-ui/lightning_deposit_help.svg", height: 36), + Text( + S.of(context).bitcoin_lightning_deposit, + style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20), + ) + ], + ), + trailingIcon: Icon(Icons.close), + onTrailingPressed: Navigator.of(context).pop, + ), + SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + spacing: 24, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 12, + children: [ + Text( + S.of(context).send_exactly, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.w500), + ), + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + spacing: 8, + children: [ + Text( + output.cryptoAmount, + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16, + color: Theme.of(context).colorScheme.primary), + ), + Text( + widget.sendViewModel.currency.title, + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 16, + color: Theme.of(context).colorScheme.primary), + ), + ], + ), + )), + Text("${S.of(context).to}:", + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.w500)) + ], + ), + uri == null + ? Container( + width: resolvedSize, + height: resolvedSize, + child: CupertinoActivityIndicator(), + ) + : GestureDetector( + onTap: () { + setState(() { + largeQrMode = !largeQrMode; + }); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 400), + curve: Curves.easeOutCubic, + width: resolvedSize, + height: resolvedSize, + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: QrImage( + embeddedImagePath: widget.sendViewModel.currency.iconPath, + data: uri.toString(), + ), + ), + ), + ), + AddressFormatter.buildSegmentedAddress( + address: output.address, + evenTextStyle: TextStyle(color: Theme.of(context).colorScheme.onSurface,fontFamily: "IBM Plex Mono"), + textAlign: TextAlign.center), + Container( + decoration: BoxDecoration( + color: warningBackgroundColor, + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: EdgeInsets.all(20), + child: Text( + S.of(context).lightning_external_disclaimer, + textAlign: TextAlign.center, + style: TextStyle( + color: warningTextColor, + fontSize: 14, + fontWeight: FontWeight.w500), + ), + )), + Row( + spacing: 8, + children: [ + Flexible( + child: NewPrimaryButton( + onPressed:(){ + Clipboard.setData( + ClipboardData(text: output.address), + ); + }, + text: S.of(context).copy, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary), + ), + Flexible( + child: NewPrimaryButton( + onPressed: Navigator.of(context).pop, + text: S.of(context).sent_the_funds, + color: Theme.of(context).colorScheme.surfaceContainer, + textColor: Theme.of(context).colorScheme.primary), + ), + ], + ), + SizedBox() + ], + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/new-ui/widgets/send_page/recipient_dot_row.dart b/lib/new-ui/widgets/send_page/recipient_dot_row.dart new file mode 100644 index 000000000..2f500eba2 --- /dev/null +++ b/lib/new-ui/widgets/send_page/recipient_dot_row.dart @@ -0,0 +1,217 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +class RecipientDotRow extends StatefulWidget { + const RecipientDotRow( + {super.key, required this.numDots, required this.selectedDot, required this.onSelected}); + + final int numDots; + final int selectedDot; + final Function(int) onSelected; + + @override + State createState() => _RecipientDotRowState(); +} + +class _RecipientDotRowState extends State { + static const double _outputDotSize = 36; + static const double _outputDotSpacing = 8; + static const double _deadzoneSize = 0.35; + late ScrollController _outputDotsController; + bool _wasScrollable = false; + + @override + void initState() { + super.initState(); + _outputDotsController = ScrollController(); + } + + @override + void didUpdateWidget(RecipientDotRow oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.numDots != oldWidget.numDots) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_outputDotsController.hasClients) { + _outputDotsController.animateTo(_outputDotsController.position.maxScrollExtent, + duration: Duration(milliseconds: 500), curve: Curves.easeOutCubic); + } + }); + } + } + + @override + Widget build(BuildContext context) { + + return LayoutBuilder( + builder: (context, constraints) { + final viewportWidth = constraints.maxWidth; + + final double itemExtent = _outputDotSize + _outputDotSpacing; + final totalContentWidth = (widget.numDots * itemExtent) - _outputDotSpacing; + final isScrollable = totalContentWidth > viewportWidth; + + final sidePadding = isScrollable ? viewportWidth * (_deadzoneSize/2) : 0.0; + + if (isScrollable && !_wasScrollable) { + _outputDotsController + .jumpTo((widget.numDots * (_deadzoneSize/2)) * itemExtent - (_outputDotSpacing * (_deadzoneSize+1))); + } + + _wasScrollable = isScrollable; + + return SizedBox( + height: _outputDotSize, + width: double.infinity, + child: Center( + child: AnimatedSize( + duration: const Duration(milliseconds: 150), + curve: Curves.easeOutCubic, + alignment: Alignment.centerLeft, + child: ListView.builder( + key: ValueKey(_outputDotsController), + controller: _outputDotsController, + scrollDirection: Axis.horizontal, + shrinkWrap: true, + padding: EdgeInsets.symmetric(horizontal: sidePadding), + physics: BouncingScrollPhysics(), + itemCount: widget.numDots, + itemBuilder: (context, index) { + return AnimatedBuilder( + animation: _outputDotsController, + builder: (context, child) { + double scale = 1.0; + + if (isScrollable) { + final RenderBox? box = context.findRenderObject() as RenderBox?; + final RenderBox? viewportBox = context + .findAncestorRenderObjectOfType(); + + if (box != null && viewportBox != null) { + final dotPosition = box.localToGlobal(Offset.zero, ancestor: viewportBox); + final dotCenter = dotPosition.dx + (_outputDotSize / 2); + final viewportCenter = viewportWidth / 2; + final distance = (dotCenter - viewportCenter).abs(); + final threshold = viewportWidth * _deadzoneSize; + final maxDistance = viewportWidth / 2; + + if (distance > threshold) { + final t = ((distance - threshold) / (maxDistance - threshold)) + .clamp(0.0, 1.0); + scale = 1.0 - (t * 0.6); + } + } else { + if (index == widget.numDots - 1) scale = 0.0; + } + } + + return Transform.scale( + scale: scale, + child: RecipientDot( + textOpacity: scale, + size: _outputDotSize, + spacing: _outputDotSpacing, + index: index, + onTap: () { + if (scale != 1.0) { + _outputDotsController.animateTo(index * _outputDotSize, + duration: Duration(milliseconds: 300), + curve: Curves.easeOutCubic); + } + widget.onSelected(index); + }, + selected: index == widget.selectedDot, + )); + }, + ); + }, + ), + ), + ), + ); + }, + ); + } +} + +class RecipientDot extends StatefulWidget { + const RecipientDot( + {super.key, + required this.size, + required this.spacing, + required this.index, + required this.onTap, + required this.selected, required this.textOpacity}); + + final double size; + final double spacing; + final double textOpacity; + final int index; + final VoidCallback onTap; + final bool selected; + + @override + State createState() => _RecipientDotState(); +} + +class _RecipientDotState extends State { + bool _created = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + _created = true; + }); + }); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: widget.size + widget.spacing, + height: widget.size, + child: Stack( + children: [ + AnimatedPositioned( + duration: const Duration(milliseconds: 150), + curve: Curves.easeOutCubic, + top: _created ? 0 : widget.size, + child: Material( + child: AnimatedSwitcher( + duration: Duration(milliseconds: 150), + child: GestureDetector( + key: ValueKey(widget.selected), + onTap: widget.onTap, + child: Container( + width: widget.size, + height: widget.size, + margin: EdgeInsets.symmetric(horizontal: widget.spacing / 2), + decoration: BoxDecoration( + color: widget.selected + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.surfaceContainer, + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: Opacity( + opacity: widget.textOpacity, + child: Text((widget.index+1).toString(), + style: TextStyle( + color: widget.selected + ? Theme.of(context).colorScheme.onPrimary + : Theme.of(context).colorScheme.onSurface, + fontSize:14,fontWeight: FontWeight.w500 + ),), + ), + ), + ), + ), + ), + ) + ], + ), + ); + } +} diff --git a/lib/new-ui/widgets/send_page/send_address_input.dart b/lib/new-ui/widgets/send_page/send_address_input.dart new file mode 100644 index 000000000..8988f5cc8 --- /dev/null +++ b/lib/new-ui/widgets/send_page/send_address_input.dart @@ -0,0 +1,239 @@ +import 'package:cake_wallet/entities/contact_base.dart'; +import 'package:cake_wallet/entities/qr_scanner.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/send_page/floating_icon_button.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/utils/permission_handler.dart'; +import 'package:cw_core/currency.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import "package:permission_handler_platform_interface/permission_handler_platform_interface.dart"; + +class NewSendAddressInput extends StatefulWidget { + const NewSendAddressInput({ + super.key, + required this.addressController, + this.onURIScanned, + this.onPushPasteButton, + required this.selectedCurrency, + this.onSelectedContact, + this.onPushAddressBookButton, + required this.onEditingComplete, + this.bottomPadding = false, + this.validator, + this.focusNode, this.displayName, + }); + + final TextEditingController addressController; + final Function(Uri)? onURIScanned; + final Function(BuildContext)? onPushPasteButton; + final Function(BuildContext)? onPushAddressBookButton; + final Function(ContactBase)? onSelectedContact; + final String? displayName; + final Currency selectedCurrency; + final VoidCallback onEditingComplete; + final bool bottomPadding; + final FormFieldValidator? validator; + final FocusNode? focusNode; + + @override + State createState() => _NewSendAddressInputState(); +} + +class _NewSendAddressInputState extends State { + FocusNode? node; + GlobalKey> formFieldKey = GlobalKey>(); + + @override + void initState() { + super.initState(); + node = widget.focusNode ?? FocusNode(); + node!.addListener(_onFocusChange); + widget.addressController.addListener(()=>formFieldKey.currentState?.didChange(widget.addressController.text)); + } + + void _onFocusChange() { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: widget.bottomPadding + ? EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ) + : EdgeInsets.zero, + child: FormField( + key: formFieldKey, + validator: widget.validator, + builder: (state)=>Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(18)), + child: Row( + children: [ + Expanded( + child: Stack( + children: [ + TextField( + focusNode: widget.focusNode, + onSubmitted: (val)=> FocusScope.of(context).unfocus(), + onChanged: state.didChange, + onEditingComplete: (){widget.onEditingComplete();}, + onTapOutside: (_) { + widget.onEditingComplete(); + }, + controller: widget.addressController, + decoration: InputDecoration( + hintText: S.of(context).search_or_enter, + errorMaxLines: 3, + ), + ), + Positioned.fill( + child: IgnorePointer( + child: AnimatedOpacity( + duration: Duration(milliseconds: 150), + opacity: (widget.focusNode == null || widget.focusNode!.hasFocus || widget.addressController.text.isEmpty) ? 0 : 1, + child: SendAddressOverlay( + address: widget.addressController.text, + displayName: widget.displayName, + ) + ), + ), + ), + ], + ), + ), + Row( + spacing: 12, + children: [ + SizedBox.shrink(), + FloatingIconButton( + iconPath: "assets/new-ui/paste.svg", + onPressed: () async { + _pasteAddress(context); + }), + FloatingIconButton( + iconPath: "assets/new-ui/scan.svg", + onPressed: () { + _presentQRScanner(context); + }), + FloatingIconButton( + iconPath: "assets/new-ui/contacts_outlined.svg", + onPressed: () { + _presetAddressBookPicker(context); + }), + SizedBox.shrink() + ], + ) + ], + ), + ), + if(state.hasError) + Padding(padding: EdgeInsets.only(top:6,left: 8),child: Text(state.errorText!, style: TextStyle(fontSize:12,color: Theme.of(context).colorScheme.error),),) + ], + ), + ), + ); + } + + Future _presentQRScanner(BuildContext context) async { + bool isCameraPermissionGranted = + await PermissionHandler.checkPermission(Permission.camera, context); + if (!isCameraPermissionGranted) return; + final code = await presentQRScanner(context); + if (code == null) return; + if (code.isEmpty) return; + + try { + final uri = Uri.parse(code); + widget.addressController.text = uri.path; + widget.onURIScanned?.call(uri); + } catch (_) { + widget.addressController.text = code; + } + } + + Future _pasteAddress(BuildContext context) async { + final clipboard = await Clipboard.getData('text/plain'); + final address = clipboard?.text ?? ''; + + if (address.isNotEmpty) { + // if it has query parameters then it's a valid uri + // added because Uri.parse(address) can parse a normal address string and would still be valid + if (address.contains("=")) { + try { + final uri = Uri.parse(address); + widget.addressController.text = uri.path; + widget.onURIScanned?.call(uri); + return; + } catch (_) { + widget.addressController.text = address; + } + } else { + widget.addressController.text = address; + } + } + + widget.onPushPasteButton?.call(context); + } + + Future _presetAddressBookPicker(BuildContext context) async { + final contact = await Navigator.of(context) + .pushNamed(Routes.pickerAddressBook, arguments: [widget.selectedCurrency, false]); + + if (contact is ContactBase) { + widget.addressController.text = contact.address; + widget.onPushAddressBookButton?.call(context); + widget.onSelectedContact?.call(contact); + } + } +} + +class SendAddressOverlay extends StatelessWidget { + const SendAddressOverlay({super.key, required this.address, this.displayName}); + + final String address; + final String? displayName; + + @override + Widget build(BuildContext context) { + final primaryTextStyle = TextStyle(fontSize: 16.5); + final secondaryTextStyle = + TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant); + + final showDisplayName = + displayName != null && displayName!.isNotEmpty && displayName != address; + + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (showDisplayName) + Text( + displayName!,maxLines: 1, + overflow: TextOverflow.ellipsis, + style: primaryTextStyle, + ), + Text( + address, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: showDisplayName ? secondaryTextStyle : primaryTextStyle, + ) + ], + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/send_page/send_amount_input.dart b/lib/new-ui/widgets/send_page/send_amount_input.dart new file mode 100644 index 000000000..dd06dca75 --- /dev/null +++ b/lib/new-ui/widgets/send_page/send_amount_input.dart @@ -0,0 +1,142 @@ +import 'package:cake_wallet/new-ui/widgets/send_page/floating_icon_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class NewSendAmountInput extends StatefulWidget { + const NewSendAmountInput( + {super.key, + required this.currency, + required this.hasPicker, + required this.onPickerClicked, + required this.currencyIconPath, + required this.amountController, + this.validator}); + + final String currency; + final String currencyIconPath; + final bool hasPicker; + final VoidCallback onPickerClicked; + final TextEditingController amountController; + final FormFieldValidator? validator; + + @override + State createState() => _NewSendAmountInputState(); +} + +class _NewSendAmountInputState extends State { + final formFieldKey = GlobalKey>(); + + @override + void initState() { + widget.amountController.addListener(()=>formFieldKey.currentState?.didChange(widget.amountController.text)); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return FormField( + key: formFieldKey, + validator: widget.validator, + builder: (state) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: BoxDecoration( + color: widget.hasPicker + ? Theme.of(context).colorScheme.surfaceContainerHigh + : Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(18)), + child: Row( + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.horizontal(left: Radius.circular(18))), + child: Row( + mainAxisSize: MainAxisSize.max, + spacing: 8, + children: [ + Expanded( + child: TextField( + keyboardType: + TextInputType.numberWithOptions(signed: false, decimal: true), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*$')) + ], + onChanged: state.didChange, + controller: widget.amountController, + decoration: InputDecoration(hintText: "0", errorMaxLines: 3), + ), + ), + FloatingIconButton( + iconPath: "assets/new-ui/paste.svg", + onPressed: () async { + final data = await Clipboard.getData(Clipboard.kTextPlain); + if (data != null && data.text != null) { + widget.amountController.text = data.text!; + } + }), + SizedBox.shrink() + ], + ), + ), + ), + IntrinsicWidth( + child: Observer( + builder: (_) { + return GestureDetector( + onTap: widget.onPickerClicked, + child: Container( + alignment: Alignment.center, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topRight: Radius.circular(18), + bottomRight: Radius.circular(18)), + color: widget.hasPicker + ? Theme.of(context).colorScheme.surfaceContainerHigh + : Theme.of(context).colorScheme.surfaceContainer, + ), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + spacing: 8, + children: [ + if (widget.hasPicker && widget.currencyIconPath.isNotEmpty) + Image.asset(widget.currencyIconPath, width: 24, height: 24), + Text(widget.currency), + if (widget.hasPicker) + SvgPicture.asset( + "assets/new-ui/chooser.svg", + width: 12, + height: 12, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn), + ) + ], + ))), + ); + }, + ), + ), + ], + )), + if (state.hasError) + Padding( + padding: EdgeInsets.only(top: 6, left: 8), + child: Text( + state.errorText!, + style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.error), + ), + ) + ], + ), + ); + } +} diff --git a/lib/new-ui/widgets/send_page/send_confirm_bottom_widget.dart b/lib/new-ui/widgets/send_page/send_confirm_bottom_widget.dart new file mode 100644 index 000000000..ab04cd192 --- /dev/null +++ b/lib/new-ui/widgets/send_page/send_confirm_bottom_widget.dart @@ -0,0 +1,228 @@ +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/confirm_swiper.dart'; +import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; +import 'package:cake_wallet/view_model/send/send_view_model.dart'; +import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:flutter_svg/svg.dart'; + +class SendConfirmBottomWidget extends StatelessWidget { + const SendConfirmBottomWidget({super.key, required this.sendViewModel}); + + final SendViewModel sendViewModel; + + @override + Widget build(BuildContext context) { + return Observer(builder: (_) { + return Center( + child: AnimatedSize( + alignment: Alignment.bottomCenter, + duration: Duration(milliseconds: 150), + curve: Curves.easeOutCubic, + child: AnimatedSwitcher( + duration: Duration(milliseconds: 150), + switchInCurve: Curves.easeOutCubic, + switchOutCurve: Curves.easeInCubic, + child: Container( + key: ValueKey(sendViewModel.state.runtimeType), + child: _buildBottomWidget( + context, + sendViewModel.state.runtimeType, + ), + ), + ), + ), + ); + }); + } + + Widget _buildBottomWidget(BuildContext context, Type state) { + switch (state) { + case ExecutedSuccessfullyState: + return ConfirmSwiper( + onConfirmed: () { + sendViewModel.commitTransaction(context); + }, + swiperText: "${S.of(context).swipe_to_send}"); + case IsExecutingState: + return LoadingBottomWidget( + text: "${S.of(context).generating_transaction}...", + ); + case FailureState: + return TransactionErrorActions(errorText: (sendViewModel.state as FailureState).error); + case IsDeviceSigningResponseState: + return LoadingBottomWidget( + text: "${S.of(context).signing_transaction}...", + ); + case IsAwaitingDeviceResponseState: + return HardwareWalletConfirmationMessage( + hardwareWalletType: sendViewModel.wallet.hardwareWalletType!); + case TransactionCommitting: + return LoadingBottomWidget( + text: "${S.of(context).sending}...", + ); + case TransactionCommitted: + return SizedBox.shrink(); + default: + return SizedBox.shrink(); + } + } +} + +class LoadingBottomWidget extends StatelessWidget { + const LoadingBottomWidget({super.key, required this.text}); + + final String text; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 12, + children: [ + CupertinoActivityIndicator( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + Text( + text, + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), + ) + ], + ); + } +} + +class TransactionErrorActions extends StatelessWidget { + const TransactionErrorActions({super.key, required this.errorText}); + + final String errorText; + + @override + Widget build(BuildContext context) { + return Column( + spacing: 12, + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.errorContainer.withAlpha(64), + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + spacing: 12, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 8, + children: [ + SvgPicture.asset( + "assets/new-ui/warning.svg", + height: 24, + width: 24, + colorFilter: + ColorFilter.mode(Theme.of(context).colorScheme.error, BlendMode.srcIn), + ), + Text( + S.of(context).transaction_error, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.error), + ) + ], + ), + Text( + errorText, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.error), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + Row( + spacing: 8, + children: [ + // Flexible( + // child: NewPrimaryButton( + // onPressed: () {}, + // text: S.of(context).more_details, + // color: Theme.of(context).colorScheme.surfaceContainer, + // textColor: Theme.of(context).colorScheme.primary), + // ), + Flexible( + child: NewPrimaryButton( + onPressed: Navigator.of(context).maybePop, + text: S.of(context).close, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary), + ), + ], + ) + ], + ); + } +} + +class HardwareWalletConfirmationMessage extends StatelessWidget { + const HardwareWalletConfirmationMessage({super.key, required this.hardwareWalletType}); + + final HardwareWalletType hardwareWalletType; + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.primary, + ), + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column(spacing: 8, children: [ + if (hardwareWalletIcon != null) + SvgPicture.asset( + hardwareWalletIcon!, + width: 36, + height: 36, + colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn), + ), + Text( + S.of(context).proceed_on_device, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface), + ) + ]), + ), + ); + } + + String? get hardwareWalletIcon { + switch (hardwareWalletType) { + case HardwareWalletType.bitbox: + return "assets/images/hardware_wallet/device_bitbox.svg"; + case HardwareWalletType.ledger: + return "assets/images/hardware_wallet/device_ledger_nano_x.svg"; + case HardwareWalletType.trezor: + return "assets/images/hardware_wallet/device_trezor_safe_5.svg"; + case HardwareWalletType.cupcake: + return "assets/images/cupcake.svg"; + case HardwareWalletType.coldcard: + case HardwareWalletType.seedsigner: + case HardwareWalletType.keystone: + return "assets/images/hardware_wallet/device_qr.svg"; + } + } +} diff --git a/lib/new-ui/widgets/send_page/send_confirm_sheet.dart b/lib/new-ui/widgets/send_page/send_confirm_sheet.dart new file mode 100644 index 000000000..7f801e6cd --- /dev/null +++ b/lib/new-ui/widgets/send_page/send_confirm_sheet.dart @@ -0,0 +1,492 @@ +import 'dart:io'; + +import 'package:cake_wallet/core/address_validator.dart'; +import 'package:cake_wallet/core/utilities.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/animated_dropdown.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/new-ui/widgets/send_page/send_confirm_bottom_widget.dart'; +import 'package:cake_wallet/utils/address_formatter.dart'; +import 'package:cake_wallet/view_model/send/send_view_model.dart'; +import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cw_core/crypto_amount_format.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:mobx/mobx.dart'; + +class SendConfirmSheet extends StatefulWidget { + const SendConfirmSheet({super.key, required this.sendViewModel, this.isPage = false, this.title, this.iconPath}); + + final SendViewModel sendViewModel; + final bool isPage; + final String? title; + final String? iconPath; + + @override + State createState() => _SendConfirmSheetState(); +} + +class _SendConfirmSheetState extends State { + void initState() { + super.initState(); + reaction((context) => widget.sendViewModel.state, (state) { + if (state is TransactionCommitted) { + Future.delayed(const Duration(seconds: 2), () { + if (mounted) Navigator.of(context).maybePop(); + }); + } + }); + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: !widget.isPage, + onPopInvokedWithResult: (didPop, result) { + if (widget.isPage) { + Navigator.of(context, rootNavigator: true).pop(); + } + }, + child: SafeArea( + bottom:false, + minimum: widget.isPage ? EdgeInsets.zero : EdgeInsets.only(top: 64), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + child: SafeArea( + child: Observer( + builder: (_) { + final commited = widget.sendViewModel.state is TransactionCommitted; + return Stack( + fit: StackFit.loose, + children: [ + Positioned.fill( + child: AnimatedSlide( + offset: commited ? Offset.zero : const Offset(1, 0), + duration: const Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + child: const TransactionCommitedScreen(), + )), + AnimatedSlide( + offset: commited ? const Offset(-1, 0) : Offset.zero, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + child: SendTransactionDetails( + sendViewModel: widget.sendViewModel, + isPage: widget.isPage, + title: widget.title, + iconPath: widget.iconPath), + ), + ], + ); + }, + ), + ), + ), + ), + ); + } +} + +class SendTransactionDetails extends StatelessWidget { + const SendTransactionDetails({super.key, required this.sendViewModel, required this.isPage, this.title, this.iconPath}); + + final SendViewModel sendViewModel; + final bool isPage; + final String? title; + final String? iconPath; + + + @override + Widget build(BuildContext context) { + final resolvedIconPath = iconPath ?? sendViewModel.currency.iconPath ?? ""; + + return LayoutBuilder( + builder: (context, constraints) { + return Column( + key: ValueKey(0), + mainAxisSize: isPage ? MainAxisSize.max : MainAxisSize.min, + children: [ + ModalTopBar( + title: "", + leadingWidget: Row( + spacing: 8, + children: [ + if (resolvedIconPath.toLowerCase().endsWith(".svg")) + SvgPicture.asset( + resolvedIconPath, + width: 28, + height: 28, + ) + else + Image.asset( + resolvedIconPath, + width: 28, + height: 28, + ), + Text( + title ?? S.of(context).send, + style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20), + ) + ], + ), + trailingIcon: Icon(Icons.close), + onTrailingPressed: Navigator.of(context).maybePop, + ), + isPage ? Expanded(child: _buildMainContent(context)) : Flexible(child: _buildMainContent(context)) + ]); + }, + ); + } + + double sumBy(List list, double Function(T) picker) => + list.map(picker).fold(0.0, (a, b) => a + b); + + BigInt sumByBigInt(List list, BigInt Function(T) picker) => + list.map(picker).fold(BigInt.zero, (a, b) => a + b); + + String sumStr(List list, double Function(T) picker) => + sumBy(list, picker).toString(); + + String sumWithUnit(List list, double Function(T) picker, String unit) => + "${sumStr(list, picker)} $unit"; + + + Widget _buildMainContent(BuildContext context) { + final transaction = sendViewModel.pendingTransaction; + + final currencySymbol = sendViewModel.amountParsingProxy.getCryptoSymbol(sendViewModel.selectedCryptoCurrency); + + final amount = (transaction == null) + ? sendViewModel.amountParsingProxy.getDisplayCryptoStringFromBigInt( + sumByBigInt(sendViewModel.outputs, (o) { + if (o.sendAll) + return sendViewModel.amountParsingProxy.tryParseCryptoString( + sendViewModel.balance, sendViewModel.selectedCryptoCurrency) ?? + BigInt.zero; + + return sendViewModel.selectedCryptoCurrency.tryParseAmount(o.cryptoAmount) ?? + BigInt.zero; + }), + sendViewModel.selectedCryptoCurrency) + : sendViewModel.amountParsingProxy.getDisplayCryptoAmount( + formatAmount(transaction.amountFormatted), sendViewModel.selectedCryptoCurrency); + + final fee = + "${(transaction == null) ? sendViewModel.amountParsingProxy.getDisplayCryptoStringFromBigInt(sumByBigInt( + sendViewModel.outputs.where((e) => !e.sendAll).toList(), + (o) { + if (sendViewModel.selectedCryptoCurrency == CryptoCurrency.btcln) { + return sendViewModel.amountParsingProxy.tryParseCryptoString( + o.estimatedFee.replaceAll(",", ""), + sendViewModel.selectedCryptoCurrency) ?? + BigInt.zero; + } + return sendViewModel.currency.tryParseAmount(o.estimatedFee.replaceAll(",", "")) ?? + BigInt.zero; + }, + ), sendViewModel.currency) : sendViewModel.amountParsingProxy.getDisplayCryptoAmount(transaction.feeFormattedValue, sendViewModel.currency)} ${sendViewModel.currencySymbol}"; + + final fiatAmount = (transaction == null) + ? sumWithUnit( + sendViewModel.outputs, + (o) => double.tryParse(o.fiatAmount.replaceAll(",", "")) ?? 0, + sendViewModel.fiatCurrency.title, + ) + : sendViewModel.pendingTransactionFiatAmountFormatted; + + final fiatFee = (transaction == null) + ? sumWithUnit( + sendViewModel.outputs, + (o) => double.tryParse(o.estimatedFeeFiatAmount.replaceAll(",", "")) ?? 0, + sendViewModel.fiatCurrency.title, + ) + : sendViewModel.pendingTransactionFeeFiatAmountFormatted; + + final showAddress = !sendViewModel.outputs.any( + (e) => RegExp(AddressValidator.bolt11InvoiceMatcher).hasMatch(e.address.toLowerCase())); + + final outputs = sendViewModel.outputs; + + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + spacing: 24, + children: [ + Column( + children: [ + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + spacing: 4, + children: [ + Text( + amount, + style: TextStyle( + fontSize: 36, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface), + ), + Text(currencySymbol, + style: TextStyle( + fontSize: 36, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant)) + ], + ), + Text( + fiatAmount, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + ], + ), + if (outputs.length >= 1 && (outputs.first.extractedAddress.isNotEmpty || outputs.first.address.isNotEmpty) && showAddress) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 12, + children: [ + Text( + S.of(context).send_to, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + if (outputs.length == 1) + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: AddressFormatter.buildSegmentedAddress( + address: outputs.first.isParsedAddress ? outputs.first.extractedAddress : outputs.first.address, + evenTextStyle: + TextStyle(color: Theme.of(context).colorScheme.onSurface)), + ), + ) + else + AnimatedDropdown( + content: Column( + children: outputs + .map( + (item) => Column(children: [ + MultiSendAddressPreview( + index: outputs.indexOf(item) + 1, + address: item.isParsedAddress + ? item.extractedAddress + : item.address, + amount: + "${item.roundedCryptoAmount(8).withLocalSeperator(sendViewModel.languageCode)} ${sendViewModel.currency.title}", + fiatAmount: + "${item.fiatAmount.withLocalSeperator(sendViewModel.languageCode)} ${sendViewModel.fiatCurrency.title}", + ), + if(item != outputs.last) + Container(width: double.infinity, height: 1, color: Theme.of(context).colorScheme.surfaceContainerHigh) + ],), + ) + .toList(), + ), + dropdownText: "${outputs.length} ${S.of(context).addresses}"), + ], + ), + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(S.of(context).fee, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface)), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + fee.withLocalSeperator(sendViewModel.languageCode), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + Text(fiatFee.withLocalSeperator(sendViewModel.languageCode), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant)) + ], + ) + ], + ), + ), + if (sendViewModel.isElectrumWallet) ...[ + Padding( + padding: EdgeInsets.symmetric(horizontal: 12), + child: Container( + height: 1, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + ), + Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(S.of(context).network, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface)), + Column( + children: [ + Text( + sendViewModel.selectedCryptoCurrency == CryptoCurrency.btcln + ? "Lightning" + : bitcoin!.getNetworkName(sendViewModel.wallet), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant)) + ], + ) + ], + ), + ) + ], + ], + ), + ), + SendConfirmBottomWidget(sendViewModel: sendViewModel), + if(Platform.isAndroid) // spacing between bottom widget and system navbar + SizedBox(), + ], + ), + ), + ); + } + + String formatAmount(String amount) { + try { + return amount.withMaxDecimals(8); + } catch(e) { + return amount; + } + } +} + +class TransactionCommitedScreen extends StatelessWidget { + const TransactionCommitedScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 24.0), + child: Column( + spacing: 12, + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + S.of(context).transaction_sent_new, + style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600), + ), + Image.asset(width: 256, height: 256, "assets/images/birthday_cake.png") + ], + ), + ); + } +} + + +class MultiSendAddressPreview extends StatefulWidget { + const MultiSendAddressPreview( + {super.key, + required this.index, + required this.address, + required this.amount, + required this.fiatAmount}); + + final int index; + final String address; + final String amount; + final String fiatAmount; + + @override + State createState() => _MultiSendAddressPreviewState(); +} + +class _MultiSendAddressPreviewState extends State { + bool _expanded = false; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + spacing: 4, + children: [ + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${widget.index}:", + style: TextStyle(fontFamily: "IBM Plex Mono"), + ), + if (!_expanded) + GestureDetector( + onTap: () { + setState(() { + _expanded = true; + }); + }, + child: Text(middleTruncate(widget.address, 8, 8), + style: TextStyle( + fontFamily: "IBM Plex Mono", + color: Theme.of(context).colorScheme.primary))) + else + AddressFormatter.buildSegmentedAddress( + address: widget.address, + evenTextStyle: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurface, + fontFamily: "IBM Plex Mono")), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text(widget.amount), + Text( + widget.fiatAmount, + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), + ) + ], + ), + ], + ), + ); + } +} diff --git a/lib/new-ui/widgets/send_page/send_memo_input.dart b/lib/new-ui/widgets/send_page/send_memo_input.dart new file mode 100644 index 000000000..dd5e21682 --- /dev/null +++ b/lib/new-ui/widgets/send_page/send_memo_input.dart @@ -0,0 +1,66 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/send_page/floating_icon_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class NewSendMemoInput extends StatelessWidget { + const NewSendMemoInput( + {super.key, + required this.memoController, + required this.maxMemoLength, + required this.memoLength}); + + final TextEditingController memoController; + final int maxMemoLength; + final int memoLength; + + @override + Widget build(BuildContext context) { + return Column( + spacing: 8, + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Theme.of(context).colorScheme.surfaceContainer, + ), + child: Row( + children: [ + Expanded( + child: TextField( + maxLength: maxMemoLength, + controller: memoController, + decoration: + InputDecoration(hintText: S.of(context).memo_optional, counterText: ""), + ), + ), + SizedBox(width: 12), + FloatingIconButton( + iconPath: "assets/new-ui/paste.svg", + onPressed: () async { + final data = await Clipboard.getData(Clipboard.kTextPlain); + if (data != null && data.text != null) { + memoController.text = data.text!; + } + }), + SizedBox(width: 12) + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(S.of(context).memo_disclaimer, + style: TextStyle( + fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant)), + Text("${memoController.text.length} / ${maxMemoLength}", + style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.primary)) + ], + ), + ) + ], + ); + } +} diff --git a/lib/new-ui/widgets/send_page/send_syncing_indicator.dart b/lib/new-ui/widgets/send_page/send_syncing_indicator.dart new file mode 100644 index 000000000..e6d7dc969 --- /dev/null +++ b/lib/new-ui/widgets/send_page/send_syncing_indicator.dart @@ -0,0 +1,52 @@ +import 'package:cw_core/sync_status.dart'; +import 'package:flutter/material.dart'; + +class SendSyncingIndicator extends StatelessWidget { + const SendSyncingIndicator({super.key, required this.status}); + + final SyncStatus status; + + static const outlineColor = Color(0xFFFFB84E); + static const backgroundColor = Color(0xFF8E5800); + + + @override + Widget build(BuildContext context) { + late final String eta; + if(status is SyncingSyncStatus) { + eta = (status as SyncingSyncStatus).getFormattedEta()??""; + } else { + eta = ""; + } + + return Container( + height:48, + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(99999) + ), + child: + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: 10, + children: [ + if(status.progress() > 0) + SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + value: status.progress(), + strokeWidth: 3, + strokeCap: StrokeCap.round, + color: outlineColor, + ), + ), + Text("Synchronizing... ${eta}", style: TextStyle(color: outlineColor)), + ], + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/swap_page/provider_options_page.dart b/lib/new-ui/widgets/swap_page/provider_options_page.dart new file mode 100644 index 000000000..d354f521c --- /dev/null +++ b/lib/new-ui/widgets/swap_page/provider_options_page.dart @@ -0,0 +1,221 @@ +import 'package:cake_wallet/core/utilities.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_Item_checkbox.dart'; +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_selector.dart'; +import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/new-ui/widgets/swap_page/trocador_providers_settings.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart'; +import 'package:cake_wallet/view_model/settings/trocador_providers_view_model.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class ProviderOptionsPage extends StatelessWidget { + const ProviderOptionsPage({super.key, required this.exchangeViewModel}); + + final ExchangeViewModel exchangeViewModel; + + @override + Widget build(BuildContext context) { + return Observer( + builder: (_) { + final decentralizedProviders = exchangeViewModel.providerList + .where((provider) => !provider.description.isCentralized) + .toList(); + final centralizedProviders = exchangeViewModel.providerList + .where((provider) => provider.description.isCentralized) + .toList(); + return Column( + children: [ + ModalTopBar( + title: S.of(context).swap_providers, + onLeadingPressed: Navigator.of(context).pop, + leadingIcon: Icon(Icons.arrow_back_ios_new), + ), + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + NewListSections(sections: { + "": [ + ListItemSelector( + options: [ + exchangeViewModel.forceDecentralizedExchanges + ? S.of(context).decentralized_only + : S.of(context).best_rate + ], + keyValue: "pref", + label: S.of(context).preference, + onTap: exchangeViewModel.toggleForceDecentralizedExchanges) + ] + }), + SizedBox(height: 24), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Text( + S.of(context).decentralized, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + Row( + spacing: 20, + children: [ + GestureDetector( + onTap: () { + for (final provider in decentralizedProviders) { + _switchProviderStatus(provider, true, context); + } + }, + child: Text( + S.of(context).select_all, + style: TextStyle(color: Theme.of(context).colorScheme.primary), + ), + ), + GestureDetector( + onTap: () { + for (final provider in decentralizedProviders) { + _switchProviderStatus(provider, false, context); + } + }, + child: Text(S.of(context).unselect_all, + style: TextStyle(color: Theme.of(context).colorScheme.primary)), + ) + ], + ) + ]), + SizedBox(height: 12), + NewListSections( + getCheckboxValue: (key) => + exchangeViewModel.selectedProviders + .firstWhereOrNull((e) => e.title == key) != + null, + updateCheckboxValue: (key, val) {}, + sections: { + S.of(context).decentralized: decentralizedProviders.map((item) { + return ListItemCheckbox( + iconPath: item.description.image, + keyValue: item.title, + label: item.title, + value: exchangeViewModel.selectedProviders.contains(item), + onChanged: (val) { + _switchProviderStatus(item, val, context); + }); + }).toList(), + }), + SizedBox(height: 24), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Text(S.of(context).centralized, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant)), + Row( + spacing: 20, + children: [ + GestureDetector( + onTap: () { + for (final provider in centralizedProviders) { + _switchProviderStatus(provider, true, context); + } + }, + child: Text( + S.of(context).select_all, + style: TextStyle(color: Theme.of(context).colorScheme.primary), + ), + ), + GestureDetector( + onTap: () { + for (final provider in centralizedProviders) { + _switchProviderStatus(provider, false, context); + } + }, + child: Text(S.of(context).unselect_all, + style: TextStyle(color: Theme.of(context).colorScheme.primary)), + ) + ], + ) + ]), + SizedBox(height: 12), + Stack( + children: [ + NewListSections( + getCheckboxValue: (key) => + exchangeViewModel.selectedProviders + .firstWhereOrNull((e) => e.title == key) != + null, + updateCheckboxValue: (key, val) {}, + sections: { + S.of(context).centralized: centralizedProviders.map((item) { + return ListItemCheckbox( + iconPath: item.description.image, + keyValue: item.title, + label: item.title, + showArrow: item.title == "Trocador", + value: exchangeViewModel.selectedProviders.contains(item), + subtitle: item.title == "Trocador" ? S.of(context).manage_providers : null, + onTap: item.title == "Trocador" ? (){ + _openTrocadorProvidersPage(context); + } : null, + onChanged: (val) { + _switchProviderStatus(item, val, context); + }); + }).toList(), + }), + if (exchangeViewModel.forceDecentralizedExchanges) + Positioned.fill( + child: Container( + decoration: BoxDecoration( + color: Colors.black12.withAlpha(128), + borderRadius: BorderRadius.circular(16)), + )) + ], + ), + SizedBox(height:36) + ], + ), + ), + ), + ) + ], + ); + }, + ); + } + + void _switchProviderStatus(ExchangeProvider provider, bool status, BuildContext context) { + if (!provider.isAvailable) { + showPopUp( + builder: (BuildContext popUpContext) => AlertWithOneAction( + alertTitle: 'Error', + alertContent: 'The exchange is blocked in your region.', + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()), + context: context); + return; + } + if (status) { + exchangeViewModel.addExchangeProvider(provider); + } else { + exchangeViewModel.removeExchangeProvider(provider); + } + + exchangeViewModel.saveSelectedProviders(); + } + + void _openTrocadorProvidersPage(BuildContext context) { + final vm = getIt.get(); + Navigator.of(context).push(CupertinoPageRoute( + builder: (context) => Material( + child: TrocadorProvidersSettings( + trocadorProvidersViewModel: vm, + )))); + } +} diff --git a/lib/new-ui/widgets/swap_page/provider_selector_page.dart b/lib/new-ui/widgets/swap_page/provider_selector_page.dart new file mode 100644 index 000000000..e521ea937 --- /dev/null +++ b/lib/new-ui/widgets/swap_page/provider_selector_page.dart @@ -0,0 +1,96 @@ +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_Item_checkbox.dart'; +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart'; +import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; + +class ProviderSelectorPage extends StatelessWidget { + const ProviderSelectorPage({super.key, required this.exchangeViewModel}); + + final ExchangeViewModel exchangeViewModel; + + @override + Widget build(BuildContext context) { + final decentralizedProviders = + exchangeViewModel.selectedProviders.where((item) => !item.description.isCentralized); + final centralizedProviders = + exchangeViewModel.selectedProviders.where((item) => item.description.isCentralized); + + return Column( + children: [ + ModalTopBar( + title: S.of(context).change_provider, + leadingIcon: Icon(Icons.arrow_back_ios_new), + onLeadingPressed: Navigator.of(context).maybePop, + ), + Expanded( + child: SingleChildScrollView( + controller: ModalScrollController.of(context), + child: Column( + spacing: 24, + children: [ + Text( + "${S.of(context).change_provider_desc_1}\n${S.of(context).change_provider_desc_2}", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant)), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: NewListSections( + showHeader: true, + getCheckboxValue: (_) => exchangeViewModel.forcedProvider == null, + updateCheckboxValue: (key, value) {}, + sections: { + if (exchangeViewModel.bestRateProvider != null) + S.of(context).best_rate: [ + ListItemCheckbox( + iconPath: exchangeViewModel.bestRateProvider!.description.image, + keyValue: "bestrate", + label: exchangeViewModel.bestRateProvider!.title, + subtitle: + exchangeViewModel.bestRateProvider!.description.isCentralized + ? S.of(context).centralized + : S.of(context).decentralized, + value: exchangeViewModel.forcedProvider == null, + onChanged: (val) { + exchangeViewModel.setForcedProvider(null); + Navigator.of(context).pop(); + }), + ], + S.of(context).decentralized: decentralizedProviders + .map((item) => ListItemRegularRow( + iconPath: item.description.image, + keyValue: item.title, + label: item.title, + onTap: () { + exchangeViewModel.setForcedProvider(item); + Navigator.of(context).pop(); + })) + .toList(), + S.of(context).centralized: centralizedProviders + .map((item) => ListItemRegularRow( + iconPath: item.description.image, + keyValue: item.title, + label: item.title, + onTap: () { + exchangeViewModel.setForcedProvider(item); + Navigator.of(context).pop(); + })) + .toList(), + }, + ), + ), + SizedBox(height: 18), + ], + ), + ), + ) + ], + ); + } +} diff --git a/lib/new-ui/widgets/swap_page/refund_address_modal.dart b/lib/new-ui/widgets/swap_page/refund_address_modal.dart new file mode 100644 index 000000000..3de5ccb8d --- /dev/null +++ b/lib/new-ui/widgets/swap_page/refund_address_modal.dart @@ -0,0 +1,101 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/new-ui/widgets/send_page/send_address_input.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class RefundAddressModal extends StatefulWidget { + const RefundAddressModal( + {super.key, this.isFromWalletSelection = false, required this.selectedCurrency}); + + final bool isFromWalletSelection; + final CryptoCurrency selectedCurrency; + + @override + State createState() => _RefundAddressModalState(); +} + +class _RefundAddressModalState extends State { + final addressController = TextEditingController(); + bool _textEntered = false; + + @override + void initState() { + super.initState(); + addressController.addListener(() { + setState(() { + _textEntered = addressController.text.isNotEmpty; + }); + }); + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(16))), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ModalTopBar( + title: S.of(context).set_refund_address, + trailingIcon: Icon(Icons.close), + onTrailingPressed: Navigator.of(context).pop, + ), + SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + spacing: 24, + children: [ + SvgPicture.asset( + "assets/new-ui/refund_address.svg", + colorFilter: + ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn), + ), + Text( + S.of(context).return_address_desc + + (widget.isFromWalletSelection + ? "\n\n${S.of(context).return_address_desc_external}" + : ""), + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 14, + fontWeight: FontWeight.w400), + ), + Column( + spacing: 12, + children: [ + NewSendAddressInput( + bottomPadding: true, + addressController: addressController, + selectedCurrency: widget.selectedCurrency, + onEditingComplete: () {}), + if (widget.isFromWalletSelection || _textEntered) + NewPrimaryButton( + onPressed: () { + Navigator.of(context).pop(addressController.text); + }, + text: !_textEntered ? S.of(context).skip_set_later : S.of(context).continue_text, + color: addressController.text.isEmpty + ? Theme.of(context).colorScheme.surfaceContainer + : Theme.of(context).colorScheme.primary, + textColor: addressController.text.isEmpty + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onPrimary) + ], + ), + SizedBox() + ], + ), + ), + ) + ], + ), + ); + } +} diff --git a/lib/new-ui/widgets/swap_page/swap_address_selection_modal.dart b/lib/new-ui/widgets/swap_page/swap_address_selection_modal.dart new file mode 100644 index 000000000..e5eb77423 --- /dev/null +++ b/lib/new-ui/widgets/swap_page/swap_address_selection_modal.dart @@ -0,0 +1,339 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/new-ui/widgets/send_page/send_address_input.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/new_simple_checkbox.dart'; +import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart'; +import 'package:cw_core/currency_for_wallet_type.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; + +class SwapAddressSelectionResult { + String? address; + String? walletName; + String? accountName; + + SwapAddressSelectionResult({this.address, this.walletName, this.accountName}); +} + +class SwapAddressSelectionModal extends StatefulWidget { + const SwapAddressSelectionModal( + {super.key, required this.isSelectingReceiver, required this.exchangeViewModel}); + + final bool isSelectingReceiver; + final ExchangeViewModel exchangeViewModel; + + @override + State createState() => _SwapAddressSelectionModalState(); +} + +class _SwapAddressSelectionModalState extends State { + List items = []; + Map> accounts = {}; + final addressController = TextEditingController(); + bool textEntered = false; + + bool _itemsLoaded = false; + + @override + void initState() { + super.initState(); + + addressController.addListener(() { + setState(() { + textEntered = addressController.text.isNotEmpty; + }); + }); + + () async { + items = widget.isSelectingReceiver + ? await widget.exchangeViewModel.receiveWallets + : await widget.exchangeViewModel.depositWallets; + for (final item in items) { + if (item.type.toString() == "WalletType.monero") { + accounts[item.id] = await widget.exchangeViewModel.addressesForWallet(item); + } + } + setState(() { + _itemsLoaded = true; + }); + }.call(); + } + + @override + Widget build(BuildContext context) { + final currency = widget.isSelectingReceiver ? widget.exchangeViewModel.receiveCurrency : widget.exchangeViewModel.depositCurrency; + + if (!_itemsLoaded) return SizedBox.shrink(); + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(16))), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ModalTopBar( + title: widget.isSelectingReceiver ? "${S.of(context).receive_to}..." : "${S.of(context).send_from}...", + leadingIcon: Icon(Icons.close), + onLeadingPressed: Navigator.of(context).pop, + ), + Flexible( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: items.isEmpty + ? Center( + child: Text( + "${S.of(context).no_wallets_for} ${widget.isSelectingReceiver ? currency.fullName : currency.fullName}.", + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), + )) + : ListView.builder( + padding: EdgeInsets.only(bottom: 12), + shrinkWrap: true, + controller: ModalScrollController.of(context), + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + + late final bool selected; + if (widget.isSelectingReceiver) { + selected = widget.exchangeViewModel.receiveAddress == item.address; + } else { + selected = widget.exchangeViewModel.wallet.name == item.name && + !widget.exchangeViewModel.isSendFromExternal; + } + + final String currencyIconPath = walletTypeToCryptoCurrency(item.type).iconPath ?? ""; + + final bool hasAccounts = + item.type.toString() == "WalletType.monero" && widget.isSelectingReceiver; + + List? accounts = + hasAccounts ? this.accounts[item.id] : null; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0), + child: SwapAddressSelectionModalRow( + wallet: item, + iconPath: currencyIconPath, + isSelected: selected, + accounts: accounts, + onAddressChosen: (address, accountName) { + Navigator.of(context).pop(SwapAddressSelectionResult( + address: address, + walletName: item.name, + accountName: accountName)); + }, + ), + ); + }, + ), + ), + ), + Container( + height: 1, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0,vertical: 18), + child: !widget.isSelectingReceiver + ? GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.of(context).pop(SwapAddressSelectionResult()); + }, + child: Container( + height: 64, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(16)), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 10, + children: [ + SvgPicture.asset("assets/new-ui/send_from_external.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, BlendMode.srcIn)), + Text(S.of(context).send_from_external, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary)) + ], + ), + ), + ) + : Row( + children: [ + Flexible( + child: NewSendAddressInput( + addressController: addressController, + selectedCurrency: widget.exchangeViewModel.receiveCurrency, + onEditingComplete: () {}, bottomPadding: true,), + ), + AnimatedScale( + alignment: Alignment.centerLeft, + scale: textEntered ? 1.0 : 0.0, + duration: const Duration(milliseconds: 200), + curve: Curves.easeOutCubic, + child: Row( + children: [ + SizedBox(width: textEntered ? 8 : 0), + GestureDetector( + onTap: () { + Navigator.of(context).pop(SwapAddressSelectionResult(address:addressController.text)); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeOutCubic, + width: textEntered ? 48 : 0, + height: 48, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + shape: BoxShape.circle, + ), + child: Icon( + Icons.arrow_forward_ios, + color: Theme.of(context).colorScheme.onPrimary, + ), + ), + ), + ], + ), + ) + ], + ), + ) + ], + ), + )); + } +} + +class SwapAddressSelectionModalRow extends StatefulWidget { + const SwapAddressSelectionModalRow( + {super.key, + required this.wallet, + required this.iconPath, + required this.isSelected, + required this.onAddressChosen, + this.accounts}); + + final WalletInfo wallet; + final String iconPath; + final bool isSelected; + final Function(String, String?) onAddressChosen; + final List? accounts; + + @override + State createState() => _SwapAddressSelectionModalRowState(); +} + +class _SwapAddressSelectionModalRowState extends State { + bool _isExpanded = false; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Column( + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (widget.accounts != null) { + setState(() { + _isExpanded = !_isExpanded; + }); + } else { + widget.onAddressChosen(widget.wallet.address, null); + } + }, + child: Container( + height: 64, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + spacing: 12, + children: [ + Image.asset(widget.iconPath, width: 24, height: 24), + Text(widget.wallet.name) + ], + ), + widget.accounts != null + ? AnimatedRotation( + duration: Duration(milliseconds: 200), + curve: Curves.easeOutCubic, + turns: _isExpanded ? 0 : 0.5, + child: SvgPicture.asset( + "assets/new-ui/dropdown_arrow.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn), + )) + : NewSimpleCheckbox(value: widget.isSelected, onChanged: (value) {}), + ], + ), + ), + ), + if (widget.accounts != null) + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + switchInCurve: Curves.easeOutCubic, + switchOutCurve: Curves.easeInCubic, + transitionBuilder: (Widget child, Animation animation) { + return SizeTransition( + sizeFactor: animation, + axisAlignment: -1.0, + child: child, + ); + }, + child: Container( + key: ValueKey(_isExpanded), + height: _isExpanded ? null : 0, + width: double.infinity, + child: Column( + children: widget.accounts!.map((item) { + return Column( + children: [ + Container( + height: 1, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + widget.onAddressChosen(item.address, item.label); + }, + child: SizedBox( + height: 48, + child: Row( + spacing: 12, + children: [ + SvgPicture.asset( + "assets/new-ui/account.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, + BlendMode.srcIn), + ), + Text(item.label) + ], + ), + ), + ), + ], + ); + }).toList(), + ), + )) + ], + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/swap_page/swap_confirm_sheet.dart b/lib/new-ui/widgets/swap_page/swap_confirm_sheet.dart new file mode 100644 index 000000000..d2a7a85e6 --- /dev/null +++ b/lib/new-ui/widgets/swap_page/swap_confirm_sheet.dart @@ -0,0 +1,247 @@ +import 'package:cake_wallet/core/utilities.dart'; +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart'; +import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/new-ui/widgets/send_page/send_confirm_bottom_widget.dart'; +import 'package:cake_wallet/new-ui/widgets/send_page/send_confirm_sheet.dart'; +import 'package:cake_wallet/new-ui/widgets/swap_page/swap_modal_header.dart'; +import 'package:cake_wallet/new-ui/widgets/swap_page/swap_send_external_modal.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart'; +import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart'; +import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart'; +import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; + +class SwapConfirmSheet extends StatefulWidget { + const SwapConfirmSheet( + {super.key, + required this.exchangeViewModel, + required this.exchangeTradeViewModel, + required this.receiveAmount}); + + final ExchangeViewModel exchangeViewModel; + final ExchangeTradeViewModel exchangeTradeViewModel; + final String receiveAmount; + + @override + State createState() => _SwapConfirmSheetState(); +} + +class _SwapConfirmSheetState extends State { + + void beginSend() async { + final sendVM = widget.exchangeTradeViewModel.sendViewModel; + + if (sendVM.wallet.isHardwareWallet) { + if (!sendVM.hardwareWalletViewModel!.isConnected) { + await Navigator.of(context).pushNamed(Routes.connectDevices, + arguments: ConnectDevicePageParams( + walletType: sendVM.walletType, + hardwareWalletType: sendVM.wallet.walletInfo.hardwareWalletType!, + onConnectDevice: (context, _) { + sendVM.hardwareWalletViewModel!.initWallet(sendVM.wallet); + Navigator.of(context).pop(); + }, + )); + } else { + sendVM.hardwareWalletViewModel!.initWallet(sendVM.wallet); + } + } + + widget.exchangeTradeViewModel.confirmSending(); + } + + @override + void initState() { + super.initState(); + + if (!widget.exchangeViewModel.isSendFromExternal) { + beginSend(); + } + + reaction((context) => widget.exchangeTradeViewModel.sendViewModel.state, (state) { + if (state is TransactionCommitted) { + Future.delayed(const Duration(seconds: 2), () { + if (mounted) Navigator.of(context).pop(); + }); + } + }); + } + + @override + Widget build(BuildContext context) { + return PopScope( + onPopInvokedWithResult: (didPop, result) { + Navigator.of(context, rootNavigator: true).pop(); + }, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(30)), + ), + child: SafeArea( + child: Observer( + builder: (_) { + final commited = widget.exchangeTradeViewModel.sendViewModel.state is TransactionCommitted; + return Stack( + fit: StackFit.loose, + children: [ + Positioned.fill( + child: AnimatedSlide( + offset: commited ? Offset.zero : const Offset(1, 0), + duration: const Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + child: const TransactionCommitedScreen(), + )), + AnimatedSlide( + offset: commited ? const Offset(-1, 0) : Offset.zero, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + child: SwapTransactionDetails( + exchangeViewModel: widget.exchangeViewModel, + exchangeTradeViewModel: widget.exchangeTradeViewModel, + receiveAmount: widget.receiveAmount, + ), + ), + ], + ); + }, + ), + ), + )); + } + + +} + + +class SwapTransactionDetails extends StatelessWidget { + const SwapTransactionDetails({super.key, required this.exchangeViewModel, required this.exchangeTradeViewModel, required this.receiveAmount}); + + final ExchangeViewModel exchangeViewModel; + final ExchangeTradeViewModel exchangeTradeViewModel; + final String receiveAmount; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ModalTopBar( + title: "", + leadingWidget: SwapModalHeader( + fromIconPath: exchangeViewModel.depositCurrency.iconPath ?? "", + toIconPath: exchangeViewModel.receiveCurrency.iconPath ?? ""), + trailingIcon: Icon(Icons.close), + onTrailingPressed: Navigator.of(context).maybePop, + ), + SafeArea( + top:false, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + spacing: 24, + children: [ + Observer( + builder: (_) => NewListSections(showHeader: true, sections: { + S.of(context).send: [ + ListItemRegularRow( + showArrow: false, + keyValue: "send value", + label: exchangeViewModel.depositCurrency.fullName ?? "", + iconPath: exchangeViewModel.depositCurrency.iconPath ?? "", + trailingText: exchangeTradeViewModel.trade.amountFormatted() + + " " + + (exchangeViewModel.depositCurrency.title)), + if(exchangeTradeViewModel.sendViewModel.pendingTransaction != null) + ListItemRegularRow( + showArrow: false, + keyValue: "fee", + label: S.of(context).fee, + trailingText: + "${exchangeTradeViewModel.sendViewModel.pendingTransaction?.feeFormatted} (${exchangeTradeViewModel.pendingTransactionFeeFiatAmountFormatted})"), + ListItemRegularRow( + keyValue: "sender", + label: S.of(context).from, + trailingText: exchangeViewModel.isSendFromExternal + ? S.of(context).external_wallet + : exchangeViewModel.wallet.name, + showArrow: false) + ], + S.of(context).receive: [ + ListItemRegularRow( + showArrow: false, + keyValue: "receive value", + label: exchangeViewModel.receiveCurrency.fullName ?? "", + iconPath: exchangeViewModel.receiveCurrency.iconPath ?? "", + trailingText: (receiveAmount) + + " " + + (exchangeViewModel.receiveCurrency.title)), + ListItemRegularRow( + keyValue: "receiver", + label: S.of(context).to, + showArrow: false, + trailingText: exchangeViewModel.receiveAddressDisplayName ?? middleTruncate(exchangeTradeViewModel.trade.payoutAddress ?? "", 8, 8)) + ], + "${S.of(context).swap_id} (${S.of(context).tap_to_copy})": [ + ListItemRegularRow( + showArrow: false, + keyValue: "provider", + onTap: () => Clipboard.setData( + ClipboardData(text: exchangeTradeViewModel.trade.id)), + label: exchangeTradeViewModel.trade.provider.title, + iconPath: exchangeTradeViewModel.trade.provider.image, + trailingIconPath: "assets/new-ui/copy.svg", + trailingText: exchangeTradeViewModel.trade.id, + truncateTrailingText: !exchangeTradeViewModel.trade.provider.isCentralized), + if(exchangeTradeViewModel.trade.provider == ExchangeProviderDescription.trocador) + ListItemRegularRow( + showArrow: false, + keyValue: "trocador provider name", + label: "Trocador ${S.of(context).provider}", + trailingText: exchangeTradeViewModel.trade.providerName??"" + ) + ] + }), + ), + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: exchangeViewModel.isSendFromExternal + ? NewPrimaryButton( + onPressed: ()=>_showExternalSendModal(context), + text: S.of(context).continue_text, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary) + : SendConfirmBottomWidget( + sendViewModel: exchangeTradeViewModel.sendViewModel), + ), + ], + ), + ), + ) + ], + ); + } + + void _showExternalSendModal(BuildContext context) { + showMaterialModalBottomSheet( + context: context, + builder: (context) { + return SwapSendExternalModal( + amount: exchangeTradeViewModel.trade.amount, + exchangeTradeViewModel: exchangeTradeViewModel, + from: exchangeTradeViewModel.trade.from ?? exchangeViewModel.depositCurrency, + to: exchangeTradeViewModel.trade.to ?? exchangeViewModel.receiveCurrency, + address: exchangeTradeViewModel.trade.inputAddress ?? ""); + }); + } + +} diff --git a/lib/new-ui/widgets/swap_page/swap_limit_popup.dart b/lib/new-ui/widgets/swap_page/swap_limit_popup.dart new file mode 100644 index 000000000..a6520ddf6 --- /dev/null +++ b/lib/new-ui/widgets/swap_page/swap_limit_popup.dart @@ -0,0 +1,62 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart'; +import 'package:cw_core/crypto_amount_format.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class SwapLimitPopup extends StatelessWidget { + const SwapLimitPopup({super.key, required this.exchangeViewModel}); + + final ExchangeViewModel exchangeViewModel; + + static const outlineColor = Color(0xFFFFB84E); + static const backgroundColor = Color(0xFF8E5800); + + @override + Widget build(BuildContext context) { + return AnimatedSize( + duration: Duration(milliseconds: 200), + curve: Curves.easeInOutCubic, + child: Container( + width: double.infinity, + child: Observer(builder: (_) { + final double? amount = double.tryParse(exchangeViewModel.depositAmountCanonical); + final max = exchangeViewModel.limits.max ?? double.infinity; + final min = exchangeViewModel.limits.min ?? 0; + final tooLarge = amount != null && amount > max; + final tooSmall = amount != null && min != 0 && amount < min; + final show = amount != null && (tooLarge || tooSmall); + + final askText = + tooLarge ? S.of(context).enter_less_than : S.of(context).enter_greater_than; + final neededAmount = (tooLarge ? max : min).toString().withMaxDecimals(8); + final currency = exchangeViewModel.depositCurrency.title; + + return Padding( + padding: const EdgeInsets.only(top: 12.0), + child: AnimatedOpacity( + duration: Duration(milliseconds: 200), + curve: Curves.easeInOut, + opacity: show ? 1 : 0, + child: Container( + height: show ? null : 0, + decoration: BoxDecoration( + color: backgroundColor, borderRadius: BorderRadius.circular(99999)), + width: double.infinity, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: Text( + "$askText $neededAmount $currency", + textAlign: TextAlign.center, + style: + TextStyle(color: outlineColor, fontWeight: FontWeight.w500, fontSize: 12), + ), + ), + ), + ), + ); + }), + ), + ); + } +} diff --git a/lib/new-ui/widgets/swap_page/swap_modal_header.dart b/lib/new-ui/widgets/swap_page/swap_modal_header.dart new file mode 100644 index 000000000..c88387924 --- /dev/null +++ b/lib/new-ui/widgets/swap_page/swap_modal_header.dart @@ -0,0 +1,32 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:flutter/material.dart'; + +class SwapModalHeader extends StatelessWidget { + const SwapModalHeader({super.key, required this.fromIconPath, required this.toIconPath}); + + final String fromIconPath; + final String toIconPath; + + @override + Widget build(BuildContext context) { + return Row( + spacing: 8, + children: [ + SizedBox( + height: 36, + width: 36, + child: Stack( + children: [ + Image.asset(fromIconPath, width: 24, height: 24), + Positioned(top: 12, left: 12, child: Image.asset(toIconPath, width: 24, height: 24)), + ], + ), + ), + Text( + S.of(context).swap, + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500), + ) + ], + ); + } +} diff --git a/lib/new-ui/widgets/swap_page/swap_options_page.dart b/lib/new-ui/widgets/swap_page/swap_options_page.dart new file mode 100644 index 000000000..71e74f72c --- /dev/null +++ b/lib/new-ui/widgets/swap_page/swap_options_page.dart @@ -0,0 +1,121 @@ +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart'; +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_selector.dart'; +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_toggle.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/new-ui/widgets/swap_page/provider_options_page.dart'; +import 'package:cake_wallet/new-ui/widgets/swap_page/refund_address_modal.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart'; +import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class SwapOptionsPage extends StatelessWidget { + const SwapOptionsPage({super.key, required this.exchangeViewModel}); + + final ExchangeViewModel exchangeViewModel; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + ModalTopBar( + title: S.of(context).configure, + leadingIcon: Icon(Icons.arrow_back_ios_new), + onLeadingPressed: Navigator.of(context).pop, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + spacing: 24, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context).current_swap, + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + color: Theme.of(context).colorScheme.onSurface), + ), + Text( + "${exchangeViewModel.depositCurrency.title} → ${exchangeViewModel.receiveCurrency.title}", + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 12, + color: Theme.of(context).colorScheme.primary), + ) + ], + ), + Observer( + builder: (_) => NewListSections( + showHeader: true, + getCheckboxValue: (key) => exchangeViewModel.isFixedRateMode, + updateCheckboxValue: (key, val) {}, + sections: { + "": [ + ListItemToggle( + keyValue: "fixed rate", + label: S.of(context).fixed_rate, + value: exchangeViewModel.isFixedRateMode, + onChanged: (val) { + if (val) + exchangeViewModel.enableFixedRateMode(); + else + exchangeViewModel.isFixedRateMode = false; + }), + ListItemRegularRow( + keyValue: "refund", + label: S.of(context).set_refund_address, + onTap: () { + showModalBottomSheet( + isScrollControlled: true, + context: context, + builder: (context) { + return RefundAddressModal( + selectedCurrency: exchangeViewModel.depositCurrency); + }).then((val) { + if (val != null && val is String) { + exchangeViewModel.depositAddress = val; + } + }); + }) + ], + S.of(context).general: [ + ListItemRegularRow( + keyValue: "providers", + label: S.of(context).swap_providers, + onTap: () { + Navigator.of(context).push(CupertinoPageRoute( + builder: (context) => Material( + child: ProviderOptionsPage( + exchangeViewModel: exchangeViewModel)))); + }), + ListItemRegularRow( + keyValue: "coin control", + label: "Coin Control", + onTap: () { + Navigator.of(context).pushNamed(Routes.unspentCoinsList); + }), + ListItemSelector( + keyValue: "curr", + label: S.of(context).change_fiat_currency, + options: [exchangeViewModel.fiat.name], + onTap: () { + exchangeViewModel.showFiatCurrencyPicker(context); + }) + ] + }), + ) + ], + ), + ) + ], + ); + } +} diff --git a/lib/new-ui/widgets/swap_page/swap_provider_initial_preference_modal.dart b/lib/new-ui/widgets/swap_page/swap_provider_initial_preference_modal.dart new file mode 100644 index 000000000..971cbaf01 --- /dev/null +++ b/lib/new-ui/widgets/swap_page/swap_provider_initial_preference_modal.dart @@ -0,0 +1,61 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class SwapProviderInitialPreferenceModal extends StatelessWidget { + const SwapProviderInitialPreferenceModal({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(16)), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(), + ModalTopBar(title: S.of(context).swap_providers), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + spacing: 12, + children: [ + SvgPicture.asset("assets/new-ui/exchange_providers.svg"), + Text( + "${S.of(context).swap_provider_initial_desc} Cake Wallet.", + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 16), + ), + Column( + spacing: 12, + children: [ + NewPrimaryButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + text: S.of(context).decentralized_only, + color: Theme.of(context).colorScheme.surfaceContainer, + textColor: Theme.of(context).colorScheme.primary), + NewPrimaryButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + text: S.of(context).best_rate_mixed, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary) + ], + ), + SizedBox(), + ], + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/swap_page/swap_send_external_modal.dart b/lib/new-ui/widgets/swap_page/swap_send_external_modal.dart new file mode 100644 index 000000000..9ee6fadcb --- /dev/null +++ b/lib/new-ui/widgets/swap_page/swap_send_external_modal.dart @@ -0,0 +1,193 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/new-ui/widgets/swap_page/swap_modal_header.dart'; +import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart'; +import 'package:cake_wallet/utils/address_formatter.dart'; +import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/payment_uris.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class SwapSendExternalModal extends StatefulWidget { + const SwapSendExternalModal( + {super.key, + required this.amount, + required this.from, + required this.to, + required this.address, required this.exchangeTradeViewModel}); + + final String amount; + final ExchangeTradeViewModel exchangeTradeViewModel; + final CryptoCurrency from; + final CryptoCurrency to; + final String address; + + @override + State createState() => _SwapSendExternalModalState(); +} + +class _SwapSendExternalModalState extends State { + PaymentURI? uri; + bool largeQrMode = false; + static const warningTextColor = Color(0xFFFFB84E); + static const warningBackgroundColor = Color(0xFF8E5800); + + @override + void initState() { + super.initState(); + final newUri = widget.exchangeTradeViewModel.paymentUri; + setState(() { + uri = newUri; + }); + } + + @override + Widget build(BuildContext context) { + if (uri == null) return SizedBox.shrink(); + final resolvedSize = MediaQuery.of(context).size.width * (largeQrMode ? 0.8 : 0.54); + + return PopScope( + onPopInvokedWithResult: (didPop, result) { + Navigator.of(context, rootNavigator: true).pop(); + }, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ModalTopBar( + title: "", + leadingWidget: SwapModalHeader( + fromIconPath: widget.from.iconPath ?? "", toIconPath: widget.to.iconPath ?? ""), + trailingIcon: Icon(Icons.close), + onTrailingPressed: Navigator.of(context).pop, + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Column( + spacing: 24, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 12, + children: [ + Text( + S.of(context).send_exactly, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.w500), + ), + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + spacing: 8, + children: [ + Text( + widget.amount, + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16, + color: Theme.of(context).colorScheme.primary), + ), + Text( + widget.from.title, + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 16, + color: Theme.of(context).colorScheme.primary), + ), + ], + ), + )), + Text("${S.of(context).to}:", + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.w500)) + ], + ), + GestureDetector( + onTap: () { + setState(() { + largeQrMode = !largeQrMode; + }); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 400), + curve: Curves.easeOutCubic, + width: resolvedSize, + height: resolvedSize, + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: QrImage( + embeddedImagePath: widget.from.iconPath, + data: uri.toString(), + ), + ), + ), + ), + AddressFormatter.buildSegmentedAddress( + address: widget.address, + evenTextStyle: TextStyle(color: Theme.of(context).colorScheme.onSurface), + textAlign: TextAlign.center), + Container( + decoration: BoxDecoration( + color: warningBackgroundColor, + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: EdgeInsets.all(20), + child: Text( + S.of(context).send_external_desc, + textAlign: TextAlign.center, + style: TextStyle( + color: warningTextColor, fontSize: 14, fontWeight: FontWeight.w500), + ), + )), + if(widget.exchangeTradeViewModel.trade.extraId != null) + Text( + "${S.of(context).destination_tag} ${widget.exchangeTradeViewModel.trade.extraId}", + style: TextStyle( + fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + Row( + spacing:8, + children: [ + Flexible( + child: NewPrimaryButton( + onPressed: ()=> Clipboard.setData(ClipboardData(text:widget.address)), + text: S.of(context).copy, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary), + ), + Flexible( + child: NewPrimaryButton( + onPressed: Navigator.of(context).pop, + text: S.of(context).sent_the_funds, + color: Theme.of(context).colorScheme.surfaceContainer, + textColor: Theme.of(context).colorScheme.primary), + ), + ], + ), + SizedBox() + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/swap_page/trocador_providers_settings.dart b/lib/new-ui/widgets/swap_page/trocador_providers_settings.dart new file mode 100644 index 000000000..37f9a9693 --- /dev/null +++ b/lib/new-ui/widgets/swap_page/trocador_providers_settings.dart @@ -0,0 +1,114 @@ +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_toggle.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/modal_header.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart'; +import 'package:cake_wallet/view_model/settings/trocador_providers_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class TrocadorProvidersSettings extends StatelessWidget { + const TrocadorProvidersSettings({super.key, required this.trocadorProvidersViewModel}); + + final TrocadorProvidersViewModel trocadorProvidersViewModel; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + spacing: 24, + children: [ + ModalTopBar( + title: "Trocador ${S.of(context).providers}", + leadingIcon: Icon(Icons.arrow_back_ios_new), + onLeadingPressed: Navigator.of(context).pop, + ), + Expanded( + child: SafeArea( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + spacing: 12, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ModalHeader( + title: "${S.of(context).about} Trocador", + iconPath: "assets/new-ui/trade_providers/trocador.svg", + message: + "Trocador ${S.of(context).trocador_desc}"), + SizedBox(), + Text(S.of(context).providers), + Text( + S.of(context).trocador_desc_kyc, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 12), + ), + SizedBox(), + Observer( + builder: (_) { + if (trocadorProvidersViewModel.isLoading) { + return Center(child: CircularProgressIndicator()); + } + final providerStates = trocadorProvidersViewModel.providerStates; + final providerRatings = trocadorProvidersViewModel.providerRatings; + if (providerStates.isEmpty) { + return Center(child: Text(S.of(context).no_providers_available)); + } + return NewListSections( + getCheckboxValue: (key) => providerStates[key] ?? false, + updateCheckboxValue: (key, val) {}, + sections: { + "": [ + for (var providerName in providerStates.keys) + ListItemToggle( + keyValue: providerName, + label: providerName, + value: providerStates[providerName] ?? false, + onChanged: (val) { + trocadorProvidersViewModel + .toggleProviderState(providerName); + }, + leadingEndWidget: providerRatings[providerName] == null + ? null + : Container( + width: 18, + height: 18, + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + width: 1), + borderRadius: BorderRadius.circular(99999)), + child: Center( + child: Text( + providerRatings[providerName] ?? "", + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + height: 1), + ), + ), + )) + ] + }); + }, + ) + ], + ), + ), + ), + ), + ) + ], + ), + ); + } +} diff --git a/lib/reactions/wallet_connect.dart b/lib/reactions/wallet_connect.dart index 2c1bece63..96fe0a63c 100644 --- a/lib/reactions/wallet_connect.dart +++ b/lib/reactions/wallet_connect.dart @@ -25,7 +25,7 @@ bool canSupportBlinkProtection(int? chainId) { return chainId == 1 || chainId == 8453; } -bool isNFTACtivatedChain(WalletType walletType, int? chainId) { +bool isNFTACtivatedChain(WalletType walletType, {int? chainId}) { if (chainId != null) { switch (chainId) { case 1: diff --git a/lib/router.dart b/lib/router.dart index 7ef811ce2..60ca5d05c 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -2,6 +2,12 @@ import 'dart:io'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/core/new_wallet_arguments.dart'; +import 'package:cake_wallet/new-ui/new_dashboard.dart'; +import 'package:cake_wallet/new-ui/pages/about_page.dart'; +import 'package:cake_wallet/new-ui/pages/coin_control_page.dart'; +import 'package:cake_wallet/new-ui/pages/addresses_page.dart'; +import 'package:cake_wallet/new-ui/pages/lightning_username_page.dart'; +import 'package:cake_wallet/new-ui/pages/send_page.dart'; import 'package:cake_wallet/order/order.dart'; import 'package:cake_wallet/core/new_wallet_type_arguments.dart'; import 'package:cake_wallet/core/totp_request_details.dart'; @@ -40,6 +46,7 @@ import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart'; import 'package:cake_wallet/src/screens/dev/moneroc_cache_debug.dart'; import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart'; import 'package:cake_wallet/src/screens/dev/network_requests.dart'; +import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:cake_wallet/src/screens/dev/qr_tools_page.dart'; import 'package:cake_wallet/src/screens/dev/secure_preferences_page.dart'; import 'package:cake_wallet/src/screens/dev/shared_preferences_page.dart'; @@ -143,11 +150,11 @@ import 'package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart'; import 'package:cake_wallet/view_model/wallet_new_vm.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cake_wallet/wallet_types.g.dart'; -import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/nano_account.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/unspent_coin_type.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/cupertino.dart'; @@ -173,6 +180,8 @@ Route handleRouteWithPlatformAwareness( Route createRoute(RouteSettings settings) { currentRouteSettings = settings; + printV(settings.name); + switch (settings.name) { case Routes.welcome: return MaterialPageRoute( @@ -248,7 +257,7 @@ Route createRoute(RouteSettings settings) { case Routes.chooseHardwareWalletAccount: final arguments = settings.arguments as List; final type = arguments[0] as WalletType; - final hardwareWallet = arguments [1] as HardwareWalletType; + final hardwareWallet = arguments[1] as HardwareWalletType; final walletVM = getIt.get( param1: type, param2: getIt(param1: hardwareWallet)); @@ -323,7 +332,7 @@ Route createRoute(RouteSettings settings) { case Routes.restoreWalletFromHardwareWallet: final arguments = settings.arguments as Map?; - final showUnavailable = (arguments?['showUnavailable'] as bool?) ?? true; + final showUnavailable = (arguments?['showUnavailable'] as bool?) ?? false; final onSelect = arguments?['onSelect'] as void Function(BuildContext, HardwareWalletType)?; final availableHardwareWalletTypes = arguments?['availableHardwareWalletTypes'] as List?; @@ -391,6 +400,14 @@ Route createRoute(RouteSettings settings) { ), ); + case Routes.lightningUsernamePage: + return handleRouteWithPlatformAwareness( + (context) => getIt.get(param1: settings.arguments as bool?)); + + case Routes.receiveAddresses: + return handleRouteWithPlatformAwareness( + (context) => getIt.get(param1: settings.arguments as bool)); + case Routes.seed: return handleRouteWithPlatformAwareness( (context) => getIt.get(param1: settings.arguments as bool), @@ -412,7 +429,9 @@ Route createRoute(RouteSettings settings) { case Routes.dashboard: return CupertinoPageRoute( - settings: settings, builder: (_) => getIt.get()); + settings: settings, + builder: (_) => + FeatureFlag.hasNewUi ? getIt.get() : getIt.get()); case Routes.send: final args = settings.arguments as Map?; @@ -420,11 +439,12 @@ Route createRoute(RouteSettings settings) { final coinTypeToSpendFrom = args?['coinTypeToSpendFrom'] as UnspentCoinType?; return handleRouteWithPlatformAwareness( - (context) => getIt.get( - param1: initialPaymentRequest, - param2: coinTypeToSpendFrom, + (context) => Material( + child: getIt.get( + param1: SendPageParams(initialPaymentRequest: initialPaymentRequest,unspentCoinType: coinTypeToSpendFrom ?? UnspentCoinType.any), + ), ), - settings: settings + settings: settings, ); case Routes.sendTemplate: @@ -590,9 +610,10 @@ Route createRoute(RouteSettings settings) { case Routes.newNode: final args = settings.arguments as Map?; + final page = getIt.get( + param1: args?['editingNode'] as Node?, param2: args?['isSelected'] as bool?); return CupertinoPageRoute( - builder: (_) => getIt.get( - param1: args?['editingNode'] as Node?, param2: args?['isSelected'] as bool?)); + builder: (_) => page); case Routes.login: return CupertinoPageRoute( @@ -628,9 +649,9 @@ Route createRoute(RouteSettings settings) { ); case Routes.pickerAddressBook: - final selectedCurrency = settings.arguments as CryptoCurrency?; + final args = settings.arguments as List; return MaterialPageRoute( - builder: (_) => getIt.get(param1: selectedCurrency)); + builder: (_) => getIt.get(param1: args[0], param2: args[1])); case Routes.pickerWalletAddress: return MaterialPageRoute(builder: (_) => getIt.get()); @@ -661,7 +682,7 @@ Route createRoute(RouteSettings settings) { builder: (_) => getIt.get(param1: settings.arguments as Order)); case Routes.buySellPage: - final args = settings.arguments as bool; + final args = settings.arguments as bool?; return handleRouteWithPlatformAwareness( (context) => getIt.get(param1: args), ); @@ -690,7 +711,8 @@ Route createRoute(RouteSettings settings) { return CupertinoPageRoute(builder: (_) => getIt.get()); case Routes.rescan: - return MaterialPageRoute(builder: (_) => getIt.get()); + final page = getIt.get(); + return MaterialPageRoute(builder: (_) => page); case Routes.faq: return MaterialPageRoute(builder: (_) => getIt.get()); @@ -733,7 +755,9 @@ Route createRoute(RouteSettings settings) { case Routes.unspentCoinsList: final coinTypeToSpendFrom = settings.arguments as UnspentCoinType?; return handleRouteWithPlatformAwareness( - (context) => getIt.get(param1: coinTypeToSpendFrom), + (context) => FeatureFlag.hasNewUi + ? getIt.get(param1: coinTypeToSpendFrom) + : getIt.get(param1: coinTypeToSpendFrom), ); case Routes.unspentCoinsDetails: @@ -753,6 +777,10 @@ Route createRoute(RouteSettings settings) { (context) => getIt.get(), ); + case Routes.aboutPage: + final page = getIt.get(); + return handleRouteWithPlatformAwareness((context) => page); + case Routes.cakePayBuyCardPage: final args = settings.arguments as List; return handleRouteWithPlatformAwareness( @@ -770,7 +798,6 @@ Route createRoute(RouteSettings settings) { (context) => getIt.get(param1: args), ); - case Routes.cakePayAccountPage: return handleRouteWithPlatformAwareness( (context) => getIt.get(), @@ -792,6 +819,8 @@ Route createRoute(RouteSettings settings) { final toggleTestnet = args['toggleTestnet'] as Function(bool? val); final restoredWallet = args['restoredWallet'] as RestoredWallet?; + final viewModelParam = {'type' : type, 'isPow' : false}; + return handleRouteWithPlatformAwareness( (context) => AdvancedPrivacySettingsPage( isFromRestore: isFromRestore, @@ -800,7 +829,7 @@ Route createRoute(RouteSettings settings) { toggleUseTestnet: toggleTestnet, advancedPrivacySettingsViewModel: getIt.get(param1: type), - nodeViewModel: getIt.get(param1: type, param2: false), + nodeViewModel: getIt.get(param1: viewModelParam), seedSettingsViewModel: getIt.get(), ), ); @@ -909,10 +938,9 @@ Route createRoute(RouteSettings settings) { ); case Routes.signPage: + final vm = getIt.get(); return MaterialPageRoute( - builder: (_) => SignPage( - getIt.get(), - ), + builder: (_) => SignPage(vm), ); case Routes.connectDevices: @@ -960,12 +988,12 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute( builder: (_) => getIt.get(), ); - + case Routes.devSocketHealthLogs: return CupertinoPageRoute( builder: (_) => getIt.get(), ); - + case Routes.devQRTools: return MaterialPageRoute( builder: (_) => getIt.get(), @@ -975,12 +1003,12 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute( builder: (_) => getIt.get(), ); - + case Routes.devExchangeProviderLogs: return MaterialPageRoute( builder: (_) => getIt.get(), ); - + case Routes.devMoneroCallProfiler: return MaterialPageRoute( builder: (_) => getIt.get(), @@ -995,7 +1023,7 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute( builder: (_) => getIt.get(), ); - + case Routes.startTor: return MaterialPageRoute( builder: (_) => getIt.get(), diff --git a/lib/routes.dart b/lib/routes.dart index 1032bee0c..f61c35531 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -116,6 +116,7 @@ class Routes { static const importNFTPage = '/import_nft_page'; static const backgroundSync = '/background_sync'; static const startTor = '/start_tor'; + static const receiveAddresses = "/receive_addresses"; static const devMoneroBackgroundSync = '/dev/monero_background_sync'; static const devMoneroCallProfiler = '/dev/monero_call_profiler'; @@ -140,4 +141,7 @@ class Routes { static const exchangeTradeExternalSendPage = '/exchange_trade_external_send_page'; static const dEuroSavings = '/integration/dEuro/savings'; + + static const lightningUsernamePage = "/lightning_username_page"; + static const aboutPage = "/about_page"; } diff --git a/lib/src/screens/auth/auth_page.dart b/lib/src/screens/auth/auth_page.dart index b5800a874..620176736 100644 --- a/lib/src/screens/auth/auth_page.dart +++ b/lib/src/screens/auth/auth_page.dart @@ -1,4 +1,5 @@ import 'package:another_flushbar/flushbar.dart'; +import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:mobx/mobx.dart'; import 'package:flutter/material.dart'; @@ -32,7 +33,6 @@ class AuthPage extends StatefulWidget { class AuthPagePinCodeStateImpl extends AuthPageState { final _key = GlobalKey(); final _pinCodeKey = GlobalKey(); - final _backArrowImageDarkTheme = Image.asset('assets/images/close_button.png'); ReactionDisposer? _reaction; Flushbar? _authBar; Flushbar? _progressBar; @@ -127,26 +127,23 @@ class AuthPagePinCodeStateImpl extends AuthPageState { @override Widget build(BuildContext context) { - return Scaffold( - key: _key, - appBar: CupertinoNavigationBar( - leading: widget.closable - ? Container( - padding: EdgeInsets.only(top: 10), - child: SizedBox( - height: 37, - width: 37, - child: InkWell( - onTap: () => Navigator.of(context).pop(), - child: _backArrowImageDarkTheme, - ), - )) - : Container(), - backgroundColor: Theme.of(context).colorScheme.surface, - border: null), - resizeToAvoidBottomInset: false, - body: PinCode((pin, _) => widget.authViewModel.auth(password: pin), (_) => null, - widget.authViewModel.pinLength, false, _pinCodeKey)); + return Container( + color: Theme.of(context).colorScheme.surface, + child: Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Scaffold( + key: _key, + appBar: CupertinoNavigationBar( + leading: widget.closable + ? ModernButton(size: 36, icon: Icon(Icons.close), onPressed: Navigator.of(context).pop, iconColor: Theme.of(context).colorScheme.onSurfaceVariant) + : Container(), + backgroundColor: Theme.of(context).colorScheme.surface, + border: null), + resizeToAvoidBottomInset: false, + body: PinCode((pin, _) => widget.authViewModel.auth(password: pin), (_) => null, + widget.authViewModel.pinLength, false, _pinCodeKey)), + ), + ); } void dismissFlushBar(Flushbar? bar) { diff --git a/lib/src/screens/backup/backup_page.dart b/lib/src/screens/backup/backup_page.dart index b682f7479..3f108f302 100644 --- a/lib/src/screens/backup/backup_page.dart +++ b/lib/src/screens/backup/backup_page.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:ui'; import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; @@ -12,6 +13,7 @@ import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/backup_view_model.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -116,7 +118,9 @@ class BackupPage extends BasePage { leftButtonText: S.of(context).seed_alert_back, actionRightButton: () async { Navigator.of(dialogContext).pop(); - final backup = await backupViewModelBase.exportBackup(); + final backupFuture = backupViewModelBase.exportBackup(); + showPersistentActionOverlay(context, backupFuture, text: S.of(context).creating_backup); + final backup = await backupFuture; if (backup == null) { return; @@ -178,3 +182,53 @@ class BackupPage extends BasePage { } } } + +void showPersistentActionOverlay(BuildContext context, Future action, {String text = ""}) async { + final navigator = Navigator.of(context, rootNavigator: true); + showPopUp( + useRootNavigator: true, + context: context, + builder: (_) => PersistentActionOverlay(text: text), + barrierDismissible: false); + try { + await action; + } finally { + // this way even if action throws, it'll still pop and not softlock the app + navigator.pop(); + } +} + +class PersistentActionOverlay extends StatelessWidget { + const PersistentActionOverlay({super.key, required this.text}); + + final String text; + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: EdgeInsets.all(20), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Theme.of(context).colorScheme.surfaceContainer.withAlpha(80)), + child: Padding( + padding: EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: 36, height: 36, child: CupertinoActivityIndicator()), + if (text.isNotEmpty) + Text(text, style: TextStyle(fontSize: 24, fontWeight: FontWeight.w500)) + ], + ), + ), + ))), + ), + ); + } +} diff --git a/lib/src/screens/backup/edit_backup_password_page.dart b/lib/src/screens/backup/edit_backup_password_page.dart index 50d720b60..92e08fb44 100644 --- a/lib/src/screens/backup/edit_backup_password_page.dart +++ b/lib/src/screens/backup/edit_backup_password_page.dart @@ -1,75 +1,89 @@ -import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/view_model/edit_backup_password_view_model.dart'; -import 'package:mobx/mobx.dart'; -class EditBackupPasswordPage extends BasePage { - EditBackupPasswordPage(this.editBackupPasswordViewModel) - : textEditingController = TextEditingController() { - textEditingController.text = editBackupPasswordViewModel.backupPassword; +class EditBackupPasswordPage extends StatefulWidget { + EditBackupPasswordPage(this.editBackupPasswordViewModel); + + final EditBackupPasswordViewModel editBackupPasswordViewModel; + + @override + State createState() => _EditBackupPasswordPageState(); +} + +class _EditBackupPasswordPageState extends State { + final textEditingController = TextEditingController(); + + @override + void initState() { + super.initState(); + textEditingController.text = widget.editBackupPasswordViewModel.backupPassword; textEditingController - .addListener(() => editBackupPasswordViewModel.backupPassword = textEditingController.text); - - reaction((_) => editBackupPasswordViewModel.backupPassword, (_) { - if (textEditingController.text != editBackupPasswordViewModel.backupPassword) { - textEditingController.text = editBackupPasswordViewModel.backupPassword; + .addListener(() { + if(textEditingController.text != widget.editBackupPasswordViewModel.backupPassword) { + widget.editBackupPasswordViewModel.backupPassword = textEditingController.text; } }); } - final EditBackupPasswordViewModel editBackupPasswordViewModel; - final TextEditingController textEditingController; - @override - String get title => S.current.edit_backup_password; - - @override - Widget body(BuildContext context) { - return Padding( - padding: EdgeInsets.only(left: 24, right: 24), - child: Stack( - fit: StackFit.expand, - children: [ - Center( - child: Observer( - builder: (_) => BaseTextFormField( - autofocus: true, - enableSuggestions: false, - autocorrect: false, - keyboardType: TextInputType.visiblePassword, - controller: textEditingController, - textStyle: Theme.of(context).textTheme.titleLarge?.copyWith( - color: Theme.of(context).colorScheme.onSurface, + Widget build(BuildContext context) { + return Column(children: [ + ModalTopBar( + title: S.of(context).edit_backup_password, + leadingIcon: Icon(Icons.arrow_back_ios_new), + onLeadingPressed: Navigator.of(context).pop, + ), + Expanded( + child: SafeArea( + child: Padding( + padding: EdgeInsets.only( + left: 24, right: 24, bottom: MediaQuery.of(context).viewInsets.bottom), + child: Stack( + fit: StackFit.expand, + children: [ + Center( + child: Observer( + builder: (_) => TextFormField( + enableSuggestions: false, + autocorrect: false, + keyboardType: TextInputType.visiblePassword, + controller: textEditingController, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), ), - ), + ), + ), + Positioned( + child: Observer( + builder: (_) => PrimaryButton( + onPressed: () => onSave(context), + text: S.of(context).save, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + isDisabled: !widget.editBackupPasswordViewModel.canSave, + ), + ), + bottom: 24, + left: 0, + right: 0, + ) + ], ), ), - Positioned( - child: Observer( - builder: (_) => PrimaryButton( - onPressed: () => onSave(context), - text: S.of(context).save, - color: Theme.of(context).colorScheme.primary, - textColor: Theme.of(context).colorScheme.onPrimary, - isDisabled: !editBackupPasswordViewModel.canSave, - ), - ), - bottom: 24, - left: 0, - right: 0, - ) - ], + ), ), - ); + ]); } void onSave(BuildContext context) { + FocusScope.of(context).unfocus(); showPopUp( context: context, builder: (dialogContext) { @@ -79,7 +93,8 @@ class EditBackupPasswordPage extends BasePage { rightButtonText: S.of(context).ok, leftButtonText: S.of(context).cancel, actionRightButton: () async { - await editBackupPasswordViewModel.save(); + widget.editBackupPasswordViewModel.backupPassword = textEditingController.text; + await widget.editBackupPasswordViewModel.save(); Navigator.of(dialogContext).pop(); if (context.mounted) { Navigator.of(context).pop(); diff --git a/lib/src/screens/base_page.dart b/lib/src/screens/base_page.dart index 5adf565e0..e5f3781e1 100644 --- a/lib/src/screens/base_page.dart +++ b/lib/src/screens/base_page.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/themes/core/material_base_theme.dart'; import 'package:cake_wallet/themes/core/theme_store.dart'; @@ -82,20 +83,17 @@ abstract class BasePage extends StatelessWidget { } return MergeSemantics( - child: SizedBox( - height: 37, - width: 37, + child: Padding( + padding: const EdgeInsets.only(top: 4.0), child: ButtonTheme( minWidth: double.minPositive, child: Semantics( label: S.of(context).seed_alert_back, - child: TextButton( - style: TextButton.styleFrom( - foregroundColor: Theme.of(context).colorScheme.onSurface, - overlayColor: WidgetStateColor.resolveWith((states) => Colors.transparent), - ), + child: ModernButton( + size: 37, + icon: Icon(CupertinoIcons.back), + iconColor: Theme.of(context).colorScheme.onSurfaceVariant, onPressed: () => onClose(context), - child: backButton(context), ), ), ), @@ -180,11 +178,11 @@ abstract class BasePage extends StatelessWidget { child: Observer( builder: (context) { final backgroundImage = getIt.get().backgroundImage; - return Container( width: double.infinity, height: double.infinity, decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, image: backgroundImage.isNotEmpty ? DecorationImage( image: FileImage(File(backgroundImage)), @@ -193,21 +191,24 @@ abstract class BasePage extends StatelessWidget { : null, // color: Colors.grey[200], ), - child: Scaffold( - key: _scaffoldKey, - backgroundColor: pageBackgroundColor(context), - resizeToAvoidBottomInset: resizeToAvoidBottomInset, - extendBodyBehindAppBar: extendBodyBehindAppBar, - endDrawer: endDrawer, - appBar: appBar(context), - body: SafeArea( - left: false, - right: false, - top: false, - bottom: Platform.isAndroid, - child: body(context), + child: Padding( + padding: const EdgeInsets.only(top:6.0), + child: Scaffold( + key: _scaffoldKey, + backgroundColor: pageBackgroundColor(context), + resizeToAvoidBottomInset: resizeToAvoidBottomInset, + extendBodyBehindAppBar: extendBodyBehindAppBar, + endDrawer: endDrawer, + appBar: appBar(context), + body: SafeArea( + left: false, + right: false, + top: false, + bottom: Platform.isAndroid, + child: body(context), + ), + floatingActionButton: floatingActionButton(context), ), - floatingActionButton: floatingActionButton(context), ), ); }, diff --git a/lib/src/screens/buy/buy_sell_page.dart b/lib/src/screens/buy/buy_sell_page.dart index 53c8cdec9..1bdcadba1 100644 --- a/lib/src/screens/buy/buy_sell_page.dart +++ b/lib/src/screens/buy/buy_sell_page.dart @@ -1,7 +1,6 @@ import 'package:cake_wallet/buy/sell_buy_states.dart'; import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/di.dart'; -import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; @@ -12,7 +11,7 @@ import 'package:cake_wallet/src/screens/exchange/widgets/mobile_exchange_cards_s import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/provider_optoin_tile.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/trail_button.dart'; import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; @@ -56,7 +55,7 @@ class BuySellPage extends BasePage { bool get extendBodyBehindAppBar => true; @override - AppBarStyle get appBarStyle => AppBarStyle.transparent; + AppBarStyle get appBarStyle => AppBarStyle.completelyTransparent; @override Function(BuildContext)? get pushToNextWidget => (context) { @@ -256,9 +255,7 @@ class BuySellPage extends BasePage { } void _setReactions(BuildContext context, BuySellViewModel buySellViewModel) { - if (_isReactionsSet) { - return; - } + if (_isReactionsSet) return; final fiatAmountController = fiatCurrencyKey.currentState!.amountController; final cryptoAmountController = cryptoCurrencyKey.currentState!.amountController; @@ -269,28 +266,27 @@ class BuySellPage extends BasePage { reaction( (_) => buySellViewModel.wallet.name, - (String _) => _onWalletNameChange( + (_) => _onWalletNameChange( buySellViewModel, buySellViewModel.cryptoCurrency, cryptoCurrencyKey)); reaction( (_) => buySellViewModel.cryptoCurrency, - (CryptoCurrency currency) => - _onCurrencyChange(currency, buySellViewModel, cryptoCurrencyKey)); + (currency) => _onCurrencyChange(currency, buySellViewModel, cryptoCurrencyKey)); reaction((_) => buySellViewModel.fiatCurrency, - (FiatCurrency currency) => _onCurrencyChange(currency, buySellViewModel, fiatCurrencyKey)); + (currency) => _onCurrencyChange(currency, buySellViewModel, fiatCurrencyKey)); - reaction((_) => buySellViewModel.fiatAmount, (String amount) { + reaction((_) => buySellViewModel.fiatAmount, (amount) { if (fiatCurrencyKey.currentState!.amountController.text != amount) { fiatCurrencyKey.currentState!.amountController.text = amount; } }); - reaction((_) => buySellViewModel.isCryptoCurrencyAddressEnabled, (bool isEnabled) { + reaction((_) => buySellViewModel.isCryptoCurrencyAddressEnabled, (isEnabled) { cryptoCurrencyKey.currentState!.isAddressEditable(isEditable: isEnabled); }); - reaction((_) => buySellViewModel.cryptoAmount, (String amount) { + reaction((_) => buySellViewModel.cryptoAmount, (amount) { if (cryptoCurrencyKey.currentState!.amountController.text != amount) { cryptoCurrencyKey.currentState!.amountController.text = amount; } @@ -453,6 +449,7 @@ class BuySellPage extends BasePage { fillColor: buySellViewModel.isBuyAction ? Theme.of(context).colorScheme.surfaceContainerLow : Theme.of(context).colorScheme.surfaceContainer, + useSatoshis: buySellViewModel.useSatoshi, ), ); diff --git a/lib/src/screens/connect_device/monero_hardware_wallet_options_page.dart b/lib/src/screens/connect_device/monero_hardware_wallet_options_page.dart index a129d6f65..c01a4a269 100644 --- a/lib/src/screens/connect_device/monero_hardware_wallet_options_page.dart +++ b/lib/src/screens/connect_device/monero_hardware_wallet_options_page.dart @@ -1,12 +1,12 @@ import 'package:cake_wallet/core/wallet_name_validator.dart'; -import 'package:cake_wallet/entities/generate_name.dart'; +import 'package:cw_core/generate_name.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/src/widgets/blockchain_height_widget.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart'; diff --git a/lib/src/screens/connect_device/select_device_manufacturer_page.dart b/lib/src/screens/connect_device/select_device_manufacturer_page.dart index 1ab2eca24..b9df3292d 100644 --- a/lib/src/screens/connect_device/select_device_manufacturer_page.dart +++ b/lib/src/screens/connect_device/select_device_manufacturer_page.dart @@ -50,7 +50,7 @@ class SelectDeviceManufacturerPage extends BasePage { colorFilter: _colorFilter, ), hardwareWalletType: HardwareWalletType.trezor, - tag: S.current.new_tag, + // tag: S.current.new_tag, ), if (Platform.isAndroid) ...[ _DeviceManufacturer( @@ -60,7 +60,7 @@ class SelectDeviceManufacturerPage extends BasePage { colorFilter: _colorFilter, ), hardwareWalletType: HardwareWalletType.bitbox, - tag: S.current.new_tag, + // tag: S.current.new_tag, ), ], _DeviceManufacturer( @@ -70,7 +70,7 @@ class SelectDeviceManufacturerPage extends BasePage { colorFilter: _colorFilter, ), hardwareWalletType: HardwareWalletType.cupcake, - tag: S.current.new_tag, + // tag: S.current.new_tag, ), _DeviceManufacturer( image: SvgPicture.asset( @@ -79,7 +79,7 @@ class SelectDeviceManufacturerPage extends BasePage { colorFilter: _colorFilter, ), hardwareWalletType: HardwareWalletType.coldcard, - tag: S.current.new_tag, + // tag: S.current.new_tag, ), // _DeviceManufacturer( // image: SvgPicture.asset( diff --git a/lib/src/screens/connect_device/select_hardware_wallet_account_page.dart b/lib/src/screens/connect_device/select_hardware_wallet_account_page.dart index 1f47cdb92..678b5e3b8 100644 --- a/lib/src/screens/connect_device/select_hardware_wallet_account_page.dart +++ b/lib/src/screens/connect_device/select_hardware_wallet_account_page.dart @@ -1,17 +1,16 @@ import 'package:cake_wallet/core/wallet_name_validator.dart'; -import 'package:cake_wallet/entities/generate_name.dart'; +import 'package:cw_core/generate_name.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; @@ -149,10 +148,9 @@ class _SelectHardwareWalletAccountFormState extends State true; @override String get title => S.current.address_book; @override Widget? trailing(BuildContext context) { + if(!showAddButton) return SizedBox.shrink(); return MergeSemantics( child: Container( - width: 32.0, - height: 32.0, + width: 36.0, + height: 36.0, decoration: BoxDecoration( shape: BoxShape.circle, - color: Theme.of(context).colorScheme.surfaceContainerHighest, + color: Theme.of(context).colorScheme.surfaceContainer, ), child: Semantics( label: S.of(context).add_contact, @@ -47,7 +54,7 @@ class ContactListPage extends BasePage { children: [ Icon( Icons.add, - color: Theme.of(context).colorScheme.onSurface, + color: Theme.of(context).colorScheme.primary, size: 22.0, ), ButtonTheme( @@ -111,57 +118,59 @@ class _ContactPageBodyState extends State with SingleTickerProv @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only(left: 24, right: 24, bottom: 8), - child: Align( - alignment: Alignment.centerLeft, - child: TabBar( + return GradientBackground( + scaffold: Padding( + padding: const EdgeInsets.only(), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(left: 24, right: 24, bottom: 8), + child: Align( + alignment: Alignment.centerLeft, + child: TabBar( + controller: _tabController, + splashFactory: NoSplash.splashFactory, + indicatorSize: TabBarIndicatorSize.label, + isScrollable: true, + labelStyle: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontSize: 20, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.primary, + ), + unselectedLabelStyle: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontSize: 20, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + indicatorColor: Theme.of(context).colorScheme.primary, + indicatorPadding: EdgeInsets.zero, + labelPadding: EdgeInsets.only(right: 24), + tabAlignment: TabAlignment.start, + dividerColor: Colors.transparent, + padding: EdgeInsets.zero, + tabs: [ + Tab(text: S.of(context).wallets), + Tab(text: S.of(context).contact_list_contacts), + ], + ), + ), + ), + Expanded( + child: TabBarView( controller: _tabController, - splashFactory: NoSplash.splashFactory, - indicatorSize: TabBarIndicatorSize.label, - isScrollable: true, - labelStyle: Theme.of(context).textTheme.bodyLarge!.copyWith( - fontSize: 20, - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.primary, - ), - unselectedLabelStyle: Theme.of(context).textTheme.bodyLarge!.copyWith( - fontSize: 20, - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - indicatorColor: Theme.of(context).colorScheme.primary, - indicatorPadding: EdgeInsets.zero, - labelPadding: EdgeInsets.only(right: 24), - tabAlignment: TabAlignment.start, - dividerColor: Colors.transparent, - padding: EdgeInsets.zero, - tabs: [ - Tab(text: S.of(context).wallets), - Tab(text: S.of(context).contact_list_contacts), + children: [ + Observer( + builder: (final BuildContext context) => _buildWalletContacts(context), + ), + ContactListBody( + contactListViewModel: widget.contactListViewModel, + tabController: _tabController, + ), ], ), ), - ), - Expanded( - child: TabBarView( - controller: _tabController, - children: [ - Observer( - builder: (final BuildContext context) => _buildWalletContacts(context), - ), - ContactListBody( - contactListViewModel: widget.contactListViewModel, - tabController: _tabController, - ), - ], - ), - ), - ], + ], + ), ), ); } @@ -176,6 +185,7 @@ class _ContactPageBodyState extends State with SingleTickerProv } return ListView.builder( + padding: EdgeInsets.only(bottom: 128), itemCount: groupedContacts.length * 2, itemBuilder: (context, index) { if (index.isOdd) { @@ -208,8 +218,8 @@ class _ContactPageBodyState extends State with SingleTickerProv expandedAlignment: Alignment.topLeft, backgroundColor: Theme.of(context).colorScheme.surfaceContainer, collapsedBackgroundColor: Theme.of(context).colorScheme.surfaceContainer, - collapsedShape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + collapsedShape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)), children: groupContacts.map((contact) => generateRaw(context, contact)).toList(), ), ); @@ -244,7 +254,7 @@ class _ContactPageBodyState extends State with SingleTickerProv behavior: HitTestBehavior.opaque, child: Container( decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(10)), + borderRadius: BorderRadius.all(Radius.circular(18)), color: Theme.of(context).colorScheme.surfaceContainer, ), margin: const EdgeInsets.only(top: 4, bottom: 4, left: 16, right: 16), @@ -317,7 +327,7 @@ class _ContactListBodyState extends State { ? widget.contactListViewModel.contacts : widget.contactListViewModel.contactsToShow; return Scaffold( - backgroundColor: Theme.of(context).colorScheme.surface, + backgroundColor: Colors.transparent, body: Container( child: FilteredList( list: contacts, @@ -371,7 +381,7 @@ class _ContactListBodyState extends State { Container( key: Key('${contact.name}'), decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(8)), + borderRadius: BorderRadius.all(Radius.circular(18)), color: Theme.of(context).colorScheme.surfaceContainer, ), margin: const EdgeInsets.only(top: 4, bottom: 4, left: 16, right: 16), @@ -431,40 +441,43 @@ class _ContactListBodyState extends State { 'assets/images/filter_icon.png', color: Theme.of(context).colorScheme.onSurface, ); - return MergeSemantics( - child: SizedBox( - height: 58, - width: 58, - child: ButtonTheme( - minWidth: double.minPositive, - child: Semantics( - container: true, - child: GestureDetector( - onTap: () async { - await showPopUp( - context: context, - builder: (context) => FilterListWidget( - initalType: contactListViewModel.orderType, - initalAscending: contactListViewModel.ascending, - onClose: (bool ascending, FilterListOrderType type) async { - contactListViewModel.setAscending(ascending); - await contactListViewModel.setOrderType(type); - }, + return Padding( + padding: const EdgeInsets.only(bottom: FeatureFlag.hasNewUi ? 48 : 0), + child: MergeSemantics( + child: SizedBox( + height: 58, + width: 58, + child: ButtonTheme( + minWidth: double.minPositive, + child: Semantics( + container: true, + child: GestureDetector( + onTap: () async { + await showPopUp( + context: context, + builder: (context) => FilterListWidget( + initalType: contactListViewModel.orderType, + initalAscending: contactListViewModel.ascending, + onClose: (bool ascending, FilterListOrderType type) async { + contactListViewModel.setAscending(ascending); + await contactListViewModel.setOrderType(type); + }, + ), + ); + }, + child: Semantics( + label: 'Transaction Filter', + button: true, + enabled: true, + child: Container( + height: 36, + width: 36, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.surfaceContainer, + ), + child: filterIcon, ), - ); - }, - child: Semantics( - label: 'Transaction Filter', - button: true, - enabled: true, - child: Container( - height: 36, - width: 36, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).colorScheme.surfaceContainer, - ), - child: filterIcon, ), ), ), @@ -501,7 +514,7 @@ class DialogService { alertContentTextWidget: AddressFormatter.buildSegmentedAddress( address: contact.address, textAlign: TextAlign.center, - walletType: cryptoCurrencyToWalletType(contact.type), + walletType: cryptoCurrencyOrTokenToWalletType(contact.type), evenTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( fontSize: 16, decoration: TextDecoration.none, diff --git a/lib/src/screens/contact/contact_page.dart b/lib/src/screens/contact/contact_page.dart index 1cc030fa0..d5ca5fc0e 100644 --- a/lib/src/screens/contact/contact_page.dart +++ b/lib/src/screens/contact/contact_page.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/core/address_validator.dart'; +import 'package:cake_wallet/new-ui/widgets/send_page/send_address_input.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cw_core/currency.dart'; import 'package:flutter/material.dart'; @@ -12,8 +13,7 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/address_text_field.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; @@ -22,18 +22,21 @@ class ContactPage extends BasePage { : _formKey = GlobalKey(), _nameController = TextEditingController(), _addressController = TextEditingController(), - _currencyTypeController = TextEditingController() { + _currencyTypeController = TextEditingController(), + addressFocusNode = FocusNode(){ _nameController.text = contactViewModel.name; _addressController.text = contactViewModel.address; _nameController.addListener(() => contactViewModel.name = _nameController.text); _addressController.addListener(() => contactViewModel.address = _addressController.text); - autorun((_) => _currencyTypeController.text = contactViewModel.currency?.toString() ?? ''); + autorun((_) => _currencyTypeController.text = "${contactViewModel.currency?.toString() ?? ""} ${contactViewModel.currency?.tag != null ? "(${contactViewModel.currency?.tag})" : ""}"); } @override String get title => S.current.contact; + FocusNode addressFocusNode; + final ContactViewModel contactViewModel; final GlobalKey _formKey; final TextEditingController _nameController; @@ -89,15 +92,17 @@ class ContactPage extends BasePage { if (contactViewModel.currency != null) Padding( padding: EdgeInsets.only(top: 20), - child: AddressTextField( - controller: _addressController, - options: [ - AddressTextFieldOption.paste, - AddressTextFieldOption.qrCode, - ], - buttonColor: Theme.of(context).colorScheme.surfaceContainerHighest, - iconColor: Theme.of(context).colorScheme.onSurfaceVariant, - validator: AddressValidator(type: contactViewModel.currency!), + child: Observer( + builder: (_) { + return NewSendAddressInput( + focusNode: addressFocusNode, + displayName: contactViewModel.displayName, + addressController: _addressController, + onEditingComplete: ()=> contactViewModel.extractParsedAddress(context), + validator: AddressValidator(type: contactViewModel.currency!), + selectedCurrency: contactViewModel.currency!, + ); + }, ), ) ], @@ -123,6 +128,10 @@ class ContactPage extends BasePage { child: Observer( builder: (_) => PrimaryButton( onPressed: () async { + FocusScope.of(context).unfocus(); + + await contactViewModel.extractParsedAddress(context); + if (_formKey.currentState != null && !_formKey.currentState!.validate()) { return; } @@ -187,5 +196,11 @@ class ContactPage extends BasePage { _onContactSavedSuccessfully(context); } }); + + reaction((_)=>contactViewModel.address, (val){ + if(val != _addressController.text) { + _addressController.text = val; + } + }); } } diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index cb9d1c674..a69c868af 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/cake_features_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/page_indicator.dart'; +import 'package:cake_wallet/src/screens/dashboard/widgets/new_main_navbar_widget.dart'; import 'package:cake_wallet/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_listener_widget.dart'; import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart'; import 'package:cake_wallet/src/widgets/evm_switcher.dart'; @@ -25,7 +26,6 @@ import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/menu_widget.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/balance/balance_page.dart'; -import 'package:cake_wallet/src/screens/dashboard/pages/navigation_dock.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; @@ -304,9 +304,11 @@ class _DashboardPageView extends BasePage { ), ), ), - NavigationDock( + NewMainNavBar( dashboardViewModel: dashboardViewModel, - ) + selectedIndex: 0, + onItemTap: (index) {} + ), ], ), ], diff --git a/lib/src/screens/dashboard/edit_token_page.dart b/lib/src/screens/dashboard/edit_token_page.dart index 26ec4c725..dca3b2fac 100644 --- a/lib/src/screens/dashboard/edit_token_page.dart +++ b/lib/src/screens/dashboard/edit_token_page.dart @@ -8,7 +8,7 @@ import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/src/widgets/checkbox_widget.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/warning_box_widget.dart'; import 'package:cake_wallet/themes/core/theme_extension.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; diff --git a/lib/src/screens/dashboard/home_settings_page.dart b/lib/src/screens/dashboard/home_settings_page.dart index a51f665bf..d1067ceff 100644 --- a/lib/src/screens/dashboard/home_settings_page.dart +++ b/lib/src/screens/dashboard/home_settings_page.dart @@ -36,16 +36,16 @@ class HomeSettingsPage extends BasePage { onItemSelected: _homeSettingsViewModel.setSortBalanceBy, ), ), - Divider(color: Theme.of(context).colorScheme.outlineVariant), - Observer( - builder: (_) => SettingsSwitcherCell( - title: S.of(context).pin_at_top(_homeSettingsViewModel.nativeToken.title), - value: _homeSettingsViewModel.pinNativeToken, - onValueChange: (_, bool value) { - _homeSettingsViewModel.setPinNativeToken(value); - }, - ), - ), + // Divider(color: Theme.of(context).colorScheme.outlineVariant), + // Observer( + // builder: (_) => SettingsSwitcherCell( + // title: S.of(context).pin_at_top(_homeSettingsViewModel.nativeToken.title), + // value: _homeSettingsViewModel.pinNativeToken, + // onValueChange: (_, bool value) { + // _homeSettingsViewModel.setPinNativeToken(value); + // }, + // ), + // ), Divider(color: Theme.of(context).colorScheme.outlineVariant), const SizedBox(height: 20), Row( diff --git a/lib/src/screens/dashboard/pages/address_page.dart b/lib/src/screens/dashboard/pages/address_page.dart index 561210b44..a055ffcf4 100644 --- a/lib/src/screens/dashboard/pages/address_page.dart +++ b/lib/src/screens/dashboard/pages/address_page.dart @@ -177,7 +177,7 @@ class AddressPage extends BasePage { decoration: BoxDecoration( border: Border.all(color: Theme.of(context).colorScheme.surfaceContainer), color: Theme.of(context).colorScheme.surfaceContainer, - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(18), ), child: Column( children: [ @@ -315,8 +315,7 @@ class AddressPage extends BasePage { } break; default: - if (addressListViewModel.type == WalletType.bitcoin || - addressListViewModel.type == WalletType.litecoin) { + if ([WalletType.bitcoin, WalletType.litecoin].contains(addressListViewModel.type)) { addressListViewModel.setAddressType(bitcoin!.getBitcoinAddressType(option)); } if (addressListViewModel.type == WalletType.zcash) { diff --git a/lib/src/screens/dashboard/pages/balance/balance_page.dart b/lib/src/screens/dashboard/pages/balance/balance_page.dart index 8fd769cc5..260b9b075 100644 --- a/lib/src/screens/dashboard/pages/balance/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance/balance_page.dart @@ -22,7 +22,7 @@ class BalancePage extends StatelessWidget { Widget build(BuildContext context) { return Observer( builder: (context) { - final isNFTActivated = isNFTACtivatedChain(dashboardViewModel.type, dashboardViewModel.wallet.chainId); + final isNFTActivated = isNFTACtivatedChain(dashboardViewModel.type, chainId: dashboardViewModel.wallet.chainId); return DefaultTabController( key: ValueKey(isNFTActivated), length: isNFTActivated ? 2 : 1, diff --git a/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart b/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart index b9e170599..ce05dad05 100644 --- a/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart +++ b/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart @@ -12,7 +12,9 @@ import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/unspent_coin_type.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -218,7 +220,7 @@ class BalanceRowWidget extends StatelessWidget { if (currency.isPotentialScam) Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - margin: EdgeInsets.only(top: 4), + margin: const EdgeInsets.only(top: 4), decoration: BoxDecoration( color: Theme.of(context).colorScheme.errorContainer, borderRadius: BorderRadius.circular(8), @@ -246,7 +248,7 @@ class BalanceRowWidget extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 26), + const SizedBox(height: 26), Row( children: [ Text( @@ -259,7 +261,7 @@ class BalanceRowWidget extends StatelessWidget { ), ], ), - SizedBox(height: 8), + const SizedBox(height: 8), AutoSizeText( frozenBalance, style: Theme.of(context).textTheme.bodyLarge!.copyWith( @@ -270,14 +272,12 @@ class BalanceRowWidget extends StatelessWidget { maxLines: 1, textAlign: TextAlign.center, ), - SizedBox(height: 4), + const SizedBox(height: 4), if (!isTestnet) Text( frozenFiatBalance, textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodySmall!.copyWith( - height: 1, - ), + style: Theme.of(context).textTheme.bodySmall!.copyWith(height: 1), ), ], ), @@ -285,7 +285,7 @@ class BalanceRowWidget extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 24), + const SizedBox(height: 24), Text( '${additionalBalanceLabel}', textAlign: TextAlign.center, @@ -294,7 +294,7 @@ class BalanceRowWidget extends StatelessWidget { height: 1, ), ), - SizedBox(height: 8), + const SizedBox(height: 8), AutoSizeText( additionalBalance, style: Theme.of(context).textTheme.bodyLarge!.copyWith( @@ -305,14 +305,12 @@ class BalanceRowWidget extends StatelessWidget { maxLines: 1, textAlign: TextAlign.center, ), - SizedBox(height: 4), + const SizedBox(height: 4), if (!isTestnet) Text( '${additionalFiatBalance}', textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodySmall!.copyWith( - height: 1, - ), + style: Theme.of(context).textTheme.bodySmall!.copyWith(height: 1), ), ], ), @@ -335,15 +333,6 @@ class BalanceRowWidget extends StatelessWidget { begin: Alignment.topCenter, end: Alignment.bottomCenter, ), - // boxShadow: [ - // BoxShadow( - // color: Theme.of(context) - // .extension()! - // .cardBorderColor - // .withAlpha(50), - // spreadRadius: dashboardViewModel.getShadowSpread(), - // blurRadius: dashboardViewModel.getShadowBlur()) - // ], ), child: TextButton( onPressed: _showToast, @@ -362,27 +351,48 @@ class BalanceRowWidget extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - Container( - child: Column( - children: [ - Container( - child: ImageIcon( - AssetImage('assets/images/mweb_logo.png'), - size: 40, - ), - ), - const SizedBox(height: 10), - Text( - 'MWEB', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontSize: 16, - fontWeight: FontWeight.w700, - color: Theme.of(context).colorScheme.onSurface, - height: 1, - ), - ), - ], - ), + Column( + children: [ + ImageIcon( + AssetImage('assets/images/mweb_logo.png'), + size: 40, + ), + const SizedBox(height: 10), + Text( + 'MWEB', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontSize: 16, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.onSurface, + height: 1, + ), + ), + ], + ), + ], + ), + if (currency == CryptoCurrency.btc) + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Column( + children: [ + SvgPicture.asset( + 'assets/images/lightning-icon.svg', + width: 40, + height: 40, + ), + const SizedBox(height: 10), + Text( + 'Lightning', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontSize: 16, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.onSurface, + height: 1, + ), + ), + ], ), ], ), @@ -394,15 +404,11 @@ class BalanceRowWidget extends StatelessWidget { children: [ GestureDetector( behavior: HitTestBehavior.opaque, - onTap: () => launchUrl( - Uri.parse( - "https://docs.cakewallet.com/cryptos/litecoin#mweb"), - mode: LaunchMode.externalApplication, - ), + onTap: onPressedHelp, child: Row( children: [ Text( - '${secondAvailableBalanceLabel}', + secondAvailableBalanceLabel, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: @@ -436,7 +442,7 @@ class BalanceRowWidget extends StatelessWidget { SizedBox(height: 6), if (!isTestnet) Text( - '${secondAvailableFiatBalance}', + secondAvailableFiatBalance, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontSize: 16, @@ -464,7 +470,7 @@ class BalanceRowWidget extends StatelessWidget { children: [ SizedBox(height: 24), Text( - '${secondAdditionalBalanceLabel}', + secondAdditionalBalanceLabel, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, @@ -500,31 +506,15 @@ class BalanceRowWidget extends StatelessWidget { ), IntrinsicHeight( child: Container( - padding: EdgeInsets.symmetric(horizontal: 12), + padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Semantics( - label: S.of(context).litecoin_mweb_pegin, + label: depositToL2Label, child: OutlinedButton( - onPressed: () { - final mwebAddress = - bitcoin!.getUnusedMwebAddress(dashboardViewModel.wallet); - PaymentRequest? paymentRequest = null; - if ((mwebAddress?.isNotEmpty ?? false)) { - paymentRequest = PaymentRequest.fromUri( - Uri.parse("litecoin:${mwebAddress}")); - } - Navigator.pushNamed( - context, - Routes.send, - arguments: { - 'paymentRequest': paymentRequest, - 'coinTypeToSpendFrom': UnspentCoinType.nonMweb, - }, - ); - }, + onPressed: () => depositToL2(context), style: OutlinedButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.primary, side: BorderSide( @@ -537,7 +527,7 @@ class BalanceRowWidget extends StatelessWidget { ), ), child: Container( - padding: EdgeInsets.symmetric(vertical: 12), + padding: const EdgeInsets.symmetric(vertical: 12), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -549,7 +539,7 @@ class BalanceRowWidget extends StatelessWidget { ), const SizedBox(width: 8), Text( - S.of(context).litecoin_mweb_pegin, + depositToL2Label, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onPrimary, fontWeight: FontWeight.w700, @@ -564,25 +554,9 @@ class BalanceRowWidget extends StatelessWidget { SizedBox(width: 16), Expanded( child: Semantics( - label: S.of(context).litecoin_mweb_pegout, + label: withdrawFromL2Label, child: OutlinedButton( - onPressed: () { - final litecoinAddress = - bitcoin!.getUnusedSegwitAddress(dashboardViewModel.wallet); - PaymentRequest? paymentRequest = null; - if ((litecoinAddress?.isNotEmpty ?? false)) { - paymentRequest = PaymentRequest.fromUri( - Uri.parse("litecoin:${litecoinAddress}")); - } - Navigator.pushNamed( - context, - Routes.send, - arguments: { - 'paymentRequest': paymentRequest, - 'coinTypeToSpendFrom': UnspentCoinType.mweb, - }, - ); - }, + onPressed: () => withdrawFromL2(context), style: OutlinedButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.surface, side: BorderSide( @@ -607,7 +581,7 @@ class BalanceRowWidget extends StatelessWidget { ), const SizedBox(width: 8), Text( - S.of(context).litecoin_mweb_pegout, + withdrawFromL2Label, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context) .colorScheme @@ -625,7 +599,7 @@ class BalanceRowWidget extends StatelessWidget { ), ), ), - SizedBox(height: 16), + const SizedBox(height: 16), ], ), ), @@ -635,20 +609,74 @@ class BalanceRowWidget extends StatelessWidget { ); } - // double getShadowSpread(){ - // double spread = 3; - // else if (!dashboardViewModel.settingsStore.currentTheme.isDark) spread = 3; - // else if (dashboardViewModel.settingsStore.currentTheme.isDark) spread = 1; - // return spread; - // } - // - // - // double getShadowBlur(){ - // double blur = 7; - // else if (dashboardViewModel.settingsStore.currentTheme.isDark) blur = 7; - // else if (dashboardViewModel.settingsStore.currentTheme.isDark) blur = 3; - // return blur; - // } + String get depositToL2Label => dashboardViewModel.type == WalletType.litecoin + ? S.current.litecoin_mweb_pegin + : S.current.bitcoin_lightning_deposit; + + String get withdrawFromL2Label => dashboardViewModel.type == WalletType.litecoin + ? S.current.litecoin_mweb_pegout + : S.current.bitcoin_lightning_withdraw; + + Future depositToL2(BuildContext context) async { + PaymentRequest? paymentRequest = null; + + if (dashboardViewModel.type == WalletType.litecoin) { + final depositAddress = bitcoin!.getUnusedMwebAddress(dashboardViewModel.wallet); + if ((depositAddress?.isNotEmpty ?? false)) { + paymentRequest = PaymentRequest.fromUri(Uri.parse("litecoin:$depositAddress")); + } + } else if (dashboardViewModel.type == WalletType.bitcoin) { + final depositAddress = await bitcoin!.getUnusedSpakDepositAddress(dashboardViewModel.wallet); + if ((depositAddress?.isNotEmpty ?? false)) { + paymentRequest = PaymentRequest.fromUri(Uri.parse("bitcoin:$depositAddress")); + } + } + + Navigator.pushNamed( + context, + Routes.send, + arguments: { + 'paymentRequest': paymentRequest, + 'coinTypeToSpendFrom': UnspentCoinType.nonMweb, + }, + ); + } + + Future withdrawFromL2(BuildContext context) async { + PaymentRequest? paymentRequest = null; + UnspentCoinType unspentCoinType = UnspentCoinType.any; + final withdrawAddress = bitcoin!.getUnusedSegwitAddress(dashboardViewModel.wallet); + + if (dashboardViewModel.type == WalletType.litecoin) { + if ((withdrawAddress?.isNotEmpty ?? false)) { + paymentRequest = PaymentRequest.fromUri(Uri.parse("litecoin:$withdrawAddress")); + } + unspentCoinType = UnspentCoinType.mweb; + } else if (dashboardViewModel.type == WalletType.bitcoin) { + if ((withdrawAddress?.isNotEmpty ?? false)) { + paymentRequest = PaymentRequest.fromUri(Uri.parse("bitcoin:$withdrawAddress")); + } + unspentCoinType = UnspentCoinType.lightning; + } + + Navigator.pushNamed( + context, + Routes.send, + arguments: { + 'paymentRequest': paymentRequest, + 'coinTypeToSpendFrom': unspentCoinType, + }, + ); + } + + void onPressedHelp() { + var helpUri = Uri.parse("https://docs.cakewallet.com/cryptos/bitcoin#lightning"); + if (dashboardViewModel.type == WalletType.litecoin) { + helpUri = Uri.parse("https://docs.cakewallet.com/cryptos/litecoin#mweb"); + } + + launchUrl(helpUri, mode: LaunchMode.externalApplication); + } void _showBalanceDescription(BuildContext context, String content) { showPopUp(context: context, builder: (_) => InformationPage(information: content)); diff --git a/lib/src/screens/dashboard/pages/cake_features_page.dart b/lib/src/screens/dashboard/pages/cake_features_page.dart index 2aad19743..b74ee87df 100644 --- a/lib/src/screens/dashboard/pages/cake_features_page.dart +++ b/lib/src/screens/dashboard/pages/cake_features_page.dart @@ -1,9 +1,12 @@ import 'dart:io'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/apps_widget.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/src/widgets/dashboard_card_widget.dart'; +import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/dashboard/cake_features_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; @@ -12,6 +15,7 @@ import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:cake_wallet/src/widgets/gradient_background.dart'; class CakeFeaturesPage extends StatelessWidget { CakeFeaturesPage({required this.dashboardViewModel, required this.cakeFeaturesViewModel}); @@ -21,112 +25,195 @@ class CakeFeaturesPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.only(left: 24, top: 16), - child: Text( - S.of(context).apps, - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ), - Expanded( - child: ListView( - children: [ - SizedBox(height: 2), - DashBoardRoundedCardWidget( - shadowBlur: dashboardViewModel.getShadowBlur(), - shadowSpread: dashboardViewModel.getShadowSpread(), - onTap: () { - if (Platform.isMacOS) { - _launchUrl("buy.cakepay.com"); - } else { - _navigatorToGiftCardsPage(context); - } - }, - title: 'Cake Pay', - subTitle: S.of(context).cake_pay_subtitle, - image: Image.asset( - 'assets/images/cakepay.png', - height: 74, - width: 70, - fit: BoxFit.cover, - ), - ), - Observer(builder: (_) { - if (dashboardViewModel.wallet.chainId == 1) { - return DashBoardRoundedCardWidget( - shadowBlur: dashboardViewModel.getShadowBlur(), - shadowSpread: dashboardViewModel.getShadowSpread(), - onTap: () => Navigator.of(context).pushNamed(Routes.dEuroSavings), - title: S.of(context).deuro_savings, - subTitle: S.of(context).deuro_savings_subtitle, - image: Image.asset( - 'assets/images/deuro_icon.png', - height: 80, - width: 80, - fit: BoxFit.cover, + return GradientBackground( + scaffold: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(context), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: !FeatureFlag.hasNewUi ? _buildOldUi(context) : _buildNewUi(context), + ), ), ); - } - - return const SizedBox(); - }), - DashBoardRoundedCardWidget( - shadowBlur: dashboardViewModel.getShadowBlur(), - shadowSpread: dashboardViewModel.getShadowSpread(), - onTap: () => _launchUrl("cake.nano-gpt.com"), - title: "NanoGPT", - subTitle: S.of(context).nanogpt_subtitle, - image: Image.asset( - 'assets/images/nanogpt.png', - height: 80, - width: 80, - fit: BoxFit.cover, - ), + }, ), - SizedBox(height: 125), - ], - ), + ), + ], ), - ], + ), + ); + } + + Widget _buildHeader(BuildContext context) { + if (!FeatureFlag.hasNewUi) { + return Padding( + padding: const EdgeInsets.only(left: 24, top: 16, bottom: 16), + child: Text( + S.of(context).apps, + style: Theme.of(context).textTheme.headlineLarge?.copyWith( + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ); + } + + return Align( + alignment: Alignment.center, + child: Padding( + padding: const EdgeInsets.only(top: 24, bottom: 24), + child: Text( + S.of(context).apps, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontSize: 18.0, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurface, + ), + ), ), ); } + Widget _buildOldUi(BuildContext context) { + return Column( + children: [ + const SizedBox(height: 2), + DashBoardRoundedCardWidget( + shadowBlur: dashboardViewModel.getShadowBlur(), + shadowSpread: dashboardViewModel.getShadowSpread(), + onTap: () => _onCakePayTap(context), + title: 'Cake Pay', + subTitle: S.of(context).cake_pay_subtitle, + image: Image.asset('assets/images/cakepay.png', height: 74, width: 70, fit: BoxFit.cover), + ), + Observer(builder: (_) { + if (dashboardViewModel.type == WalletType.ethereum) { + return DashBoardRoundedCardWidget( + shadowBlur: dashboardViewModel.getShadowBlur(), + shadowSpread: dashboardViewModel.getShadowSpread(), + onTap: () => Navigator.of(context).pushNamed(Routes.dEuroSavings), + title: S.of(context).deuro_savings, + subTitle: S.of(context).deuro_savings_subtitle, + image: Image.asset('assets/images/deuro_icon.png', height: 80, width: 80, fit: BoxFit.cover), + ); + } + return const SizedBox(); + }), + DashBoardRoundedCardWidget( + shadowBlur: dashboardViewModel.getShadowBlur(), + shadowSpread: dashboardViewModel.getShadowSpread(), + onTap: () => _launchUrl("cake.nano-gpt.com"), + title: "NanoGPT", + subTitle: S.of(context).nanogpt_subtitle, + image: Image.asset('assets/images/nanogpt.png', height: 80, width: 80, fit: BoxFit.cover), + ), + const Spacer(), + const SizedBox(height: 125), + ], + ); + } + + Widget _buildNewUi(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only(left: 24, bottom: 8), + child: CakeImageWidget(imageUrl: "assets/new-ui/by-cakelabs.svg", height: 20, color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + ), + AppsWidget( + isWide: true, + isCake: true, + onTap: () => _onCakePayTap(context), + title: 'Cake Pay', + subTitle: S.of(context).cake_pay_subtitle, + image: 'assets/images/cakepay.png', + ), + AppsWidget( + isWide: true, + isLink: true, + isCake: true, + onTap: () => _launchUrl("cupcakewallet.com"), + title: "Cupcake", + subTitle: "Turn your old phone into your new hardware wallet with our new app", + image: 'assets/images/cupcake.png', + ), + const SizedBox(height: 12), + Padding( + padding: const EdgeInsets.only(left: 24, top: 16, bottom: 8), + child: Text( + "Featured Apps", + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + AppsWidget( + isWide: true, + isLink: true, + onTap: () => _launchUrl("cake.nano-gpt.com"), + title: "NanoGPT", + subTitle: S.of(context).nanogpt_subtitle, + image: 'assets/images/nanogpt.png', + ), + Observer(builder: (_) { + if (dashboardViewModel.type == WalletType.ethereum) { + return AppsWidget( + isWide: true, + onTap: () => Navigator.of(context).pushNamed(Routes.dEuroSavings), + title: S.of(context).deuro_savings, + subTitle: S.of(context).deuro_savings_subtitle, + image: 'assets/images/deuro_icon.png', + ); + } + return const SizedBox(); + }), + const Spacer(), + const SizedBox(height: 125), + ], + ); + } + + void _onCakePayTap(BuildContext context) { + if (Platform.isMacOS) { + _launchUrl("buy.cakepay.com"); + } else { + _navigatorToGiftCardsPage(context); + } + } + void _launchUrl(String url) { try { - launchUrl( - Uri.https(url), - mode: LaunchMode.externalApplication, - ); + launchUrl(Uri.https(url), mode: LaunchMode.externalApplication); } catch (e) { printV(e); } } void _navigatorToGiftCardsPage(BuildContext context) { - final walletType = dashboardViewModel.type; - - switch (walletType) { - case WalletType.haven: - showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.of(context).error, - alertContent: S.of(context).gift_cards_unavailable, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); - break; - default: - Navigator.pushNamed(context, Routes.cakePayCardsPage); + if (dashboardViewModel.type == WalletType.haven) { + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: S.of(context).gift_cards_unavailable, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + } else { + Navigator.pushNamed(context, Routes.cakePayCardsPage); } } -} +} \ No newline at end of file diff --git a/lib/src/screens/dashboard/pages/nft_details_page.dart b/lib/src/screens/dashboard/pages/nft_details_page.dart index 86ed136ed..f7076c486 100644 --- a/lib/src/screens/dashboard/pages/nft_details_page.dart +++ b/lib/src/screens/dashboard/pages/nft_details_page.dart @@ -36,23 +36,24 @@ class NFTDetailsPage extends BasePage { @override Widget trailing(BuildContext context) { - final menuButton = Image.asset( - 'assets/images/menu.png', - color: Theme.of(context).colorScheme.onSurface, - ); - - return Container( - alignment: Alignment.centerRight, - width: 40, - child: TextButton( - // FIX-ME: Style - //highlightColor: Colors.transparent, - //splashColor: Colors.transparent, - //padding: EdgeInsets.all(0), - onPressed: () => onOpenEndDrawer(), - child: Semantics(label: S.of(context).wallet_menu, child: menuButton), - ), - ); + return SizedBox.shrink(); + // final menuButton = Image.asset( + // 'assets/images/menu.png', + // color: Theme.of(context).colorScheme.onSurface, + // ); + // + // return Container( + // alignment: Alignment.centerRight, + // width: 40, + // child: TextButton( + // // FIX-ME: Style + // //highlightColor: Colors.transparent, + // //splashColor: Colors.transparent, + // //padding: EdgeInsets.all(0), + // onPressed: () => onOpenEndDrawer(), + // child: Semantics(label: S.of(context).wallet_menu, child: menuButton), + // ), + // ); } @override diff --git a/lib/src/screens/dashboard/pages/nft_import_page.dart b/lib/src/screens/dashboard/pages/nft_import_page.dart index 612889539..7efa7bc26 100644 --- a/lib/src/screens/dashboard/pages/nft_import_page.dart +++ b/lib/src/screens/dashboard/pages/nft_import_page.dart @@ -139,7 +139,7 @@ class _ImportNFTPage extends BasePage { textColor: Theme.of(context).colorScheme.onPrimary, onPressed: () async { await nftViewModel.importNFT(tokenAddressController.text, tokenIDController.text); - Navigator.pop(context); + if (context.mounted && Navigator.of(context).canPop()) Navigator.pop(context); }, ); }), diff --git a/lib/src/screens/dashboard/pages/nft_listing_page.dart b/lib/src/screens/dashboard/pages/nft_listing_page.dart index ced4a656c..c59300860 100644 --- a/lib/src/screens/dashboard/pages/nft_listing_page.dart +++ b/lib/src/screens/dashboard/pages/nft_listing_page.dart @@ -50,20 +50,16 @@ class _NFTListingPageState extends State { ), ), if (widget.nftViewModel.isLoading) - Expanded( - child: Center( - child: CircularProgressIndicator( - backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, - valueColor: AlwaysStoppedAnimation( - Theme.of(context).colorScheme.primary, - ), + Center( + child: CircularProgressIndicator( + backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, + valueColor: AlwaysStoppedAnimation( + Theme.of(context).colorScheme.primary, ), ), ) else - Expanded( - child: NFTListWidget(nftViewModel: widget.nftViewModel), - ), + NFTListWidget(nftViewModel: widget.nftViewModel) ], ); }, @@ -95,26 +91,38 @@ class NFTListWidget extends StatelessWidget { if (isSolana) { if (nftViewModel.solanaNftAssetModels.isEmpty) return emptyMessage; - return ListView.separated( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 16), - separatorBuilder: (context, index) => const SizedBox(height: 8), - itemCount: nftViewModel.solanaNftAssetModels.length, - itemBuilder: (context, index) { - final nftAsset = nftViewModel.solanaNftAssetModels[index]; - return SolanaNFTTileWidget(nftAsset: nftAsset); - }, + return Padding( + padding: EdgeInsets.only( + bottom: 64), + child: ListView.separated( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 16), + separatorBuilder: (context, index) => const SizedBox(height: 8), + itemCount: nftViewModel.solanaNftAssetModels.length, + itemBuilder: (context, index) { + final nftAsset = nftViewModel.solanaNftAssetModels[index]; + return SolanaNFTTileWidget(nftAsset: nftAsset); + }, + ), ); } else { if (nftViewModel.nftAssetByWalletModels.isEmpty) return emptyMessage; - return ListView.separated( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 16), - separatorBuilder: (context, index) => const SizedBox(height: 8), - itemCount: nftViewModel.nftAssetByWalletModels.length, - itemBuilder: (context, index) { - final nftAsset = nftViewModel.nftAssetByWalletModels[index]; - return NFTTileWidget(nftAsset: nftAsset); - }, + return Padding( + padding: EdgeInsets.only( + bottom: 64), + child: ListView.separated( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 16), + separatorBuilder: (context, index) => const SizedBox(height: 8), + itemCount: nftViewModel.nftAssetByWalletModels.length, + itemBuilder: (context, index) { + final nftAsset = nftViewModel.nftAssetByWalletModels[index]; + return NFTTileWidget(nftAsset: nftAsset); + }, + ), ); } }, diff --git a/lib/src/screens/dashboard/pages/transactions_page.dart b/lib/src/screens/dashboard/pages/transactions_page.dart index 8b8d15339..5a0b7c446 100644 --- a/lib/src/screens/dashboard/pages/transactions_page.dart +++ b/lib/src/screens/dashboard/pages/transactions_page.dart @@ -155,8 +155,8 @@ class TransactionsPage extends StatelessWidget { ), currency: "BTC", state: item.status, - amount: bitcoin! - .formatterBitcoinAmountToString(amount: session.amount.toInt()), + amount: dashboardViewModel.appStore.amountParsingProxy + .getDisplayCryptoString(session.amount.toInt(), CryptoCurrency.btc), createdAt: DateFormat('HH:mm').format(session.inProgressSince!), isSending: session.isSenderSession, ); @@ -173,18 +173,22 @@ class TransactionsPage extends StatelessWidget { return tradeFrom != null && tradeTo != null ? Observer( builder: (_) => TradeRow( - key: item.key, - onTap: () => Navigator.of(context) - .pushNamed(Routes.tradeDetails, arguments: trade), - swapState: trade.state, - provider: trade.provider, - from: tradeFrom, - to: tradeTo, - createdAtFormattedDate: trade.createdAt != null - ? DateFormat('HH:mm').format(trade.createdAt!) - : null, - formattedAmount: item.tradeFormattedAmount, - formattedReceiveAmount: item.tradeFormattedReceiveAmount), + key: item.key, + onTap: () => Navigator.of(context) + .pushNamed(Routes.tradeDetails, arguments: trade), + swapState: trade.state, + provider: trade.provider, + title: "$tradeFrom → $tradeTo", + fromSymbol: dashboardViewModel.appStore.amountParsingProxy + .getCryptoSymbol(tradeFrom), + toSymbol: dashboardViewModel.appStore.amountParsingProxy + .getCryptoSymbol(tradeTo), + createdAtFormattedDate: trade.createdAt != null + ? DateFormat("HH:mm").format(trade.createdAt!) + : null, + formattedAmount: item.tradeFormattedAmount, + formattedReceiveAmount: item.tradeFormattedReceiveAmount, + ), ) : Container(); } diff --git a/lib/src/screens/dashboard/sign_page.dart b/lib/src/screens/dashboard/sign_page.dart index 83ce7b9dd..b04d3e91d 100644 --- a/lib/src/screens/dashboard/sign_page.dart +++ b/lib/src/screens/dashboard/sign_page.dart @@ -1,133 +1,133 @@ import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/sign_form.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/verify_form.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:keyboard_actions/keyboard_actions.dart'; import 'package:mobx/mobx.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart'; -class SignPage extends BasePage { - SignPage(this.signViewModel) - : signFormKey = GlobalKey(), - verifyFormKey = GlobalKey(), - _pages = [], - _controller = PageController(initialPage: 0) { +class SignPage extends StatefulWidget { + SignPage(this.signViewModel); + + + final SignViewModel signViewModel; + + + @override + State createState() => _SignPageState(); +} + +class _SignPageState extends State { + List _pages = []; + final signFormKey = GlobalKey(); + final verifyFormKey = GlobalKey(); + final PageController _controller = PageController(initialPage: 0) ; + + bool _isEffectsInstalled = false; + + void initState() { + super.initState(); _pages.add(SignForm( key: signFormKey, - type: signViewModel.wallet.type, - chainId: signViewModel.wallet.chainId, - includeAddress: signViewModel.signIncludesAddress, + type: widget.signViewModel.wallet.type, + chainId: widget.signViewModel.wallet.chainId, + includeAddress: widget.signViewModel.signIncludesAddress, )); _pages.add(VerifyForm( key: verifyFormKey, - type: signViewModel.wallet.type, - chainId: signViewModel.wallet.chainId, + type: widget.signViewModel.wallet.type, + chainId: widget.signViewModel.wallet.chainId, )); } @override - Widget middle(BuildContext context) => Observer( - builder: (_) { - return Text( - S.current.sign_verify_title, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w600, - fontSize: 18, - ), - ); - }, - ); - - final SignViewModel signViewModel; - final PageController _controller; - final List _pages; - final GlobalKey signFormKey; - final GlobalKey verifyFormKey; - bool _isEffectsInstalled = false; - - @override - Widget body(BuildContext context) { + Widget build(BuildContext context) { _setEffects(context); - return KeyboardActions( - config: KeyboardActionsConfig( - keyboardActionsPlatform: KeyboardActionsPlatform.IOS, - keyboardBarColor: Theme.of(context).colorScheme.surfaceVariant, - nextFocus: false, - actions: [ - KeyboardActionsItem( - focusNode: FocusNode(), - toolbarButtons: [(_) => KeyboardDoneButton()], - ) - ], - ), - child: Container( - height: 0, - color: Theme.of(context).colorScheme.surface, - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: PageView.builder( - onPageChanged: (page) { - signViewModel.isSigning = page == 0; - }, - controller: _controller, - itemCount: _pages.length, - itemBuilder: (_, index) => SingleChildScrollView(child: _pages[index]), - ), - ), - if (_pages.length > 1) - Padding( - padding: EdgeInsets.only(top: 10), - child: SmoothPageIndicator( - controller: _controller, - count: _pages.length, - effect: ColorTransitionEffect( - spacing: 6.0, - radius: 6.0, - dotWidth: 6.0, - dotHeight: 6.0, - dotColor: Theme.of(context).colorScheme.outline.withOpacity(0.5), - activeDotColor: Theme.of(context).colorScheme.outline, + return Container( + color: Theme.of(context).colorScheme.surface, + child: SafeArea( + top:false, + child: Column( + children: [ + ModalTopBar(title: S.current.sign_verify_title, + leadingIcon: Icon(Icons.arrow_back_ios_new), + onLeadingPressed: Navigator + .of(context) + .pop,), + Expanded( + child: KeyboardHideOverlay( + child: Container( + height: 0, + color: Theme.of(context).colorScheme.surface, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: PageView.builder( + onPageChanged: (page) { + widget.signViewModel.isSigning = page == 0; + }, + controller: _controller, + itemCount: _pages.length, + itemBuilder: (_, index) => SingleChildScrollView(child: _pages[index]), + ), + ), + if (_pages.length > 1) + Padding( + padding: EdgeInsets.only(top: 10), + child: SmoothPageIndicator( + controller: _controller, + count: _pages.length, + effect: ColorTransitionEffect( + spacing: 6.0, + radius: 6.0, + dotWidth: 6.0, + dotHeight: 6.0, + dotColor: Theme.of(context).colorScheme.outline.withOpacity(0.5), + activeDotColor: Theme.of(context).colorScheme.outline, + ), + ), + ), + Padding( + padding: EdgeInsets.only(top: 20, bottom: 24, left: 24, right: 24), + child: Column( + children: [ + Observer( + builder: (context) { + return LoadingPrimaryButton( + onPressed: () async { + await _confirmForm(context); + }, + text: widget.signViewModel.isSigning + ? S.current.sign_message + : S.current.verify_message, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + isLoading: widget.signViewModel.state is IsExecutingState, + isDisabled: widget.signViewModel.state is IsExecutingState, + ); + }, + ), + ], + ), + ) + ], ), ), ), - Padding( - padding: EdgeInsets.only(top: 20, bottom: 24, left: 24, right: 24), - child: Column( - children: [ - Observer( - builder: (context) { - return LoadingPrimaryButton( - onPressed: () async { - await _confirmForm(context); - }, - text: signViewModel.isSigning - ? S.current.sign_message - : S.current.verify_message, - color: Theme.of(context).colorScheme.primary, - textColor: Theme.of(context).colorScheme.onPrimary, - isLoading: signViewModel.state is IsExecutingState, - isDisabled: signViewModel.state is IsExecutingState, - ); - }, - ), - ], - ), - ) - ], - ), + ), + ), + ], ), ), ); @@ -139,7 +139,7 @@ class SignPage extends BasePage { } _isEffectsInstalled = true; - reaction((_) => signViewModel.state, (ExecutionState state) { + reaction((_) => widget.signViewModel.state, (ExecutionState state) { if (state is FailureState) { WidgetsBinding.instance.addPostFrameCallback((_) { showPopUp( @@ -150,14 +150,14 @@ class SignPage extends BasePage { alertContent: state.error, buttonText: S.of(context).ok, buttonAction: () { - if (Navigator.canPop(context)) Navigator.of(context).pop(); + if (context.mounted && Navigator.canPop(context)) Navigator.of(context).pop(); }, ); }); }); } if (state is ExecutedSuccessfullyState) { - if (signViewModel.isSigning) { + if (widget.signViewModel.isSigning) { signFormKey.currentState!.signatureController.text = state.payload as String; } else { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -182,18 +182,18 @@ class SignPage extends BasePage { Future _confirmForm(BuildContext context) async { FocusManager.instance.primaryFocus?.unfocus(); - if (signViewModel.isSigning) { + if (widget.signViewModel.isSigning) { String message = signFormKey.currentState!.messageController.text; String? address; - if (signViewModel.signIncludesAddress) { + if (widget.signViewModel.signIncludesAddress) { address = signFormKey.currentState!.addressController.text; } - await signViewModel.sign(message, address: address); + await widget.signViewModel.sign(message, address: address); } else { String message = verifyFormKey.currentState!.messageController.text; String signature = verifyFormKey.currentState!.signatureController.text; String address = verifyFormKey.currentState!.addressController.text; - await signViewModel.verify(message, signature, address: address); + await widget.signViewModel.verify(message, signature, address: address); } } } diff --git a/lib/src/screens/dashboard/widgets/action_button.dart b/lib/src/screens/dashboard/widgets/action_button.dart index 4569393a4..dc2e35b2b 100644 --- a/lib/src/screens/dashboard/widgets/action_button.dart +++ b/lib/src/screens/dashboard/widgets/action_button.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; class ActionButton extends StatelessWidget { ActionButton({ diff --git a/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart b/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart new file mode 100644 index 000000000..330bb8dd5 --- /dev/null +++ b/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart @@ -0,0 +1,314 @@ +import 'dart:io'; +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:cake_wallet/entities/new_main_actions.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; + +class NewMainNavBar extends StatefulWidget { + const NewMainNavBar({ + super.key, + required this.dashboardViewModel, + required this.selectedIndex, + required this.onItemTap, + }); + + final DashboardViewModel dashboardViewModel; + final int selectedIndex; + final Function(int index) onItemTap; + + static const barHeight = 68.0; + static final barBottomPadding = Platform.isIOS ? 22.0 : 12.0; + + @override + State createState() => _NEWNewMainNavBarState(); +} + +class _NEWNewMainNavBarState extends State { + + + static const iconBoxWidth = 48.0; + static const iconWidth = 28.0; + static const iconHeight = 28.0; + static const iconHorizontalPadding = 18.0; + + static const pillIconWidth = 24.0; + static const pillIconHeight = 24.0; + static const pillIconSpacing = 18.0; + static const pillHorizontalPadding = 20.0; + + static const barBorderRadius = 50.0; + static const pillBorderRadius = 50.0; + + static const barHorizontalPadding = 6.0; + + static const barResizeDuration = Duration(milliseconds: 300); + static const inactiveIconMoveDuration = Duration(milliseconds: 300); + static const inactiveIconFadeDuration = Duration(milliseconds: 300); + static const inactiveIconAppearDuration = Duration(milliseconds: 300); + static const pillMoveDuration = Duration(milliseconds: 250); + static const pillResizeDuration = Duration(milliseconds: 250); + static const iconColorChangeDuration = Duration(milliseconds: 200); + + static const pillTextStyle = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ); + + bool _firstFrame = true; + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + setState(() => _firstFrame = false); + }); + } + + void _onItemTap(int index) { + // if (index == widget.selectedIndex) return; + // + // setState(() { + // widget.selectedIndex = index; + // }); + + widget.onItemTap(index); + + NewMainActions.all[index].onTap.call(); + } + + // Estimate pill width based on text length + double _estimatePillWidthForAction( + BuildContext context, NewMainActions action, + {Color? color}) { + final text = action.name(context); + final textPainter = TextPainter( + text: TextSpan( + text: text, + style: pillTextStyle.copyWith(color: color), + ), + maxLines: 1, + textDirection: TextDirection.ltr, + )..layout(); + return pillIconWidth + + pillIconSpacing + + textPainter.width + + pillHorizontalPadding; + } + + double calcLeft(int index, double pillWidth) { + final baseOffset = iconBoxWidth * index; + + double additionalSpacing; + if (index > widget.selectedIndex) + additionalSpacing = pillWidth - iconBoxWidth; + else + additionalSpacing = 0; + + return baseOffset + additionalSpacing; + } + + double calcBarWidth(double pillWidth, int visibleActionsLength) { + return (iconWidth + iconHorizontalPadding) * visibleActionsLength + (pillWidth - iconWidth) + barHorizontalPadding + pillIconSpacing / double.infinity - 2; + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final backgroundColor = theme.colorScheme.surfaceContainer.withAlpha(127); + final pillColor = theme.colorScheme.onSurface.withAlpha(25); + final activeColor = theme.colorScheme.onSurface; + final inactiveColor = theme.colorScheme.primary; + + final visibleActions = NewMainActions.all + .where( + (action) => action.canShow?.call(widget.dashboardViewModel) ?? true) + .toList(); + + final pillWidth = _estimatePillWidthForAction( + context, visibleActions[widget.selectedIndex], + color: activeColor); + + final barWidth = calcBarWidth(pillWidth, visibleActions.length); + + final currentAction = visibleActions[widget.selectedIndex]; + + return Align( + alignment: Alignment.bottomCenter, + child: SafeArea( + bottom: !(Platform.isIOS), + top: false, + child: Padding( + // tux PLEASE consult me (malik) before removing this padding. + padding: EdgeInsets.only(bottom: NewMainNavBar.barBottomPadding), + child: AnimatedContainer( + duration: barResizeDuration, + curve: Curves.easeOutCubic, + width: barWidth, + child: ClipRSuperellipse( + borderRadius: BorderRadius.circular(barBorderRadius), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3), + child: Container( + height: NewMainNavBar.barHeight, + decoration: ShapeDecoration( + color: backgroundColor, + shape: RoundedSuperellipseBorder(borderRadius: BorderRadius.circular(barBorderRadius), + side: const BorderSide(color: Color(0x14FFFFFF), width: 1), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: barHorizontalPadding), + child: Stack( + alignment: Alignment.center, + children: [ + AnimatedPill( + left: calcLeft(widget.selectedIndex, pillWidth), + pillColor: pillColor, + currentAction: currentAction, + pillIconHeight: pillIconHeight, + pillIconWidth: pillIconWidth, + pillIconSpacing: pillIconSpacing, + pillBorderRadius: pillBorderRadius, + contentColor: activeColor, + estimateWidthForAction: pillWidth, + pillTextStyle: pillTextStyle, + pillMoveDuration: pillMoveDuration, + pillResizeDuration: pillResizeDuration, + ), + for (int i = 0; i < visibleActions.length; i++) + AnimatedPositioned( + duration: pillResizeDuration, + width: iconBoxWidth, + left: calcLeft(i, pillWidth)+((i == widget.selectedIndex) ? iconHorizontalPadding/100 : 0), + curve: Curves.easeOutCubic, + child: InkWell( + splashFactory: NoSplash.splashFactory, + splashColor: Colors.transparent, + borderRadius: BorderRadius.circular(pillBorderRadius), + onTap: () => _onItemTap(i), + child: AnimatedContainer( + duration: _firstFrame + ? Duration.zero + : inactiveIconMoveDuration, + curve: Curves.easeOutCubic, + width: + i == widget.selectedIndex ? pillWidth : iconBoxWidth, + alignment: Alignment.center, + child: AnimatedAlign( + duration: inactiveIconFadeDuration, + curve: Curves.easeOutCubic, + alignment: Alignment.center, + child: AnimatedScale( + duration: inactiveIconAppearDuration, + curve: Curves.easeOutCubic, + scale: (i == widget.selectedIndex) ? 0.857 : 1.0, + child: TweenAnimationBuilder( + tween: ColorTween( + begin: (i == widget.selectedIndex) ? inactiveColor : activeColor, + end: (i==widget.selectedIndex) ? activeColor : inactiveColor, + ), + duration: iconColorChangeDuration, + builder: (context, value, child) { + return Container( + height: NewMainNavBar.barHeight, + child: SvgPicture.asset( + visibleActions[i].image, + width: iconWidth, + height: iconHeight, + //fit: BoxFit.scaleDown, + colorFilter: ColorFilter.mode( + value ?? inactiveColor, + BlendMode.srcIn, + ), + ), + ); + } + ), + ), + ), + ), + ), + ), + ], + ), + )), + ), + ), + ), + ), + ), + ); + } +} + +class AnimatedPill extends StatelessWidget { + const AnimatedPill({ + super.key, + required this.left, + required this.pillColor, + required this.contentColor, + required this.pillIconHeight, + required this.pillIconWidth, + required this.pillBorderRadius, + required this.currentAction, + required this.estimateWidthForAction, + required this.pillIconSpacing, + required this.pillTextStyle, + required this.pillMoveDuration, + required this.pillResizeDuration, + }); + + final double left; + final Color pillColor; + final Color contentColor; + final double pillIconHeight; + final double pillIconWidth; + final double pillBorderRadius; + final NewMainActions currentAction; + final double estimateWidthForAction; + final double pillIconSpacing; + final TextStyle pillTextStyle; + final Duration pillMoveDuration; + final Duration pillResizeDuration; + + @override + Widget build(BuildContext context) { + return AnimatedPositioned( + duration: pillMoveDuration, + curve: Curves.easeOutCubic, + left: left, + top: 6, + bottom: 6, + child: AnimatedContainer( + duration: pillResizeDuration, + curve: Curves.easeOutCubic, + width: estimateWidthForAction, + decoration: ShapeDecoration( + color: pillColor, + shape: RoundedSuperellipseBorder(borderRadius: BorderRadius.circular(pillBorderRadius)), + ), + clipBehavior: Clip.hardEdge, + alignment: Alignment.center, + child: FittedBox( + fit: BoxFit.scaleDown, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding(padding: EdgeInsets.only(left: pillIconWidth + 2), + child: Text( + currentAction.name(context), + style: pillTextStyle.copyWith(color: contentColor), + overflow: TextOverflow.fade, + softWrap: false, + ), + ), + ], + ), + ), + )); + } +} diff --git a/lib/src/screens/dashboard/widgets/sign_form.dart b/lib/src/screens/dashboard/widgets/sign_form.dart index 8556f2085..942995600 100644 --- a/lib/src/screens/dashboard/widgets/sign_form.dart +++ b/lib/src/screens/dashboard/widgets/sign_form.dart @@ -25,15 +25,13 @@ class SignForm extends StatefulWidget { class SignFormState extends State with AutomaticKeepAliveClientMixin { SignFormState() - : formKey = GlobalKey(), - messageController = TextEditingController(), + : messageController = TextEditingController(), addressController = TextEditingController(), signatureController = TextEditingController(); final TextEditingController messageController; final TextEditingController addressController; final TextEditingController signatureController; - final GlobalKey formKey; @override void initState() { @@ -55,35 +53,33 @@ class SignFormState extends State with AutomaticKeepAliveClientMixin { padding: EdgeInsets.only(left: 24, right: 24), child: Column( children: [ - Form( - key: formKey, - child: Column( - children: [ - AddressTextField( - controller: messageController, - placeholder: S.current.message, - options: [AddressTextFieldOption.paste], - buttonColor: Theme.of(context).colorScheme.surfaceContainerHighest, - fillColor: Theme.of(context).colorScheme.surface, - ), - if (widget.includeAddress) ...[ - const SizedBox(height: 20), - AddressTextField( - controller: addressController, - options: [ - AddressTextFieldOption.paste, - AddressTextFieldOption.walletAddresses - ], - buttonColor: Theme.of(context).colorScheme.surfaceContainerHighest, - onSelectedContact: (contact) { - addressController.text = contact.address; - }, - selectedCurrency: walletTypeToCryptoCurrency(widget.type, chainId: widget.chainId), - fillColor: Theme.of(context).colorScheme.surface, - ), + Column( + children: [ + AddressTextField( + controller: messageController, + placeholder: S.current.message, + options: [AddressTextFieldOption.paste], + buttonColor: Theme.of(context).colorScheme.surfaceContainerHighest, + fillColor: Theme.of(context).colorScheme.surface, + ), + if (widget.includeAddress) ...[ + const SizedBox(height: 20), + AddressTextField( + controller: addressController, + options: [ + AddressTextFieldOption.paste, + AddressTextFieldOption.walletAddresses ], - ], - )), + buttonColor: Theme.of(context).colorScheme.surfaceContainerHighest, + onSelectedContact: (contact) { + addressController.text = contact.address; + }, + selectedCurrency: walletTypeToCryptoCurrency(widget.type, chainId: widget.chainId), + fillColor: Theme.of(context).colorScheme.surface, + ), + ], + ], + ), const SizedBox(height: 20), GestureDetector( onTap: () async { diff --git a/lib/src/screens/dashboard/widgets/trade_row.dart b/lib/src/screens/dashboard/widgets/trade_row.dart index 7d163076e..72e1f0f7b 100644 --- a/lib/src/screens/dashboard/widgets/trade_row.dart +++ b/lib/src/screens/dashboard/widgets/trade_row.dart @@ -1,15 +1,15 @@ -import 'package:cake_wallet/exchange/trade_state.dart'; -import 'package:cake_wallet/palette.dart'; -import 'package:cake_wallet/utils/image_utill.dart'; -import 'package:flutter/material.dart'; -import 'package:cake_wallet/exchange/exchange_provider_description.dart'; -import 'package:cw_core/crypto_currency.dart'; +import "package:cake_wallet/exchange/trade_state.dart"; +import "package:cake_wallet/palette.dart"; +import "package:cake_wallet/utils/image_utill.dart"; +import "package:flutter/material.dart"; +import "package:cake_wallet/exchange/exchange_provider_description.dart"; class TradeRow extends StatelessWidget { TradeRow({ required this.provider, - required this.from, - required this.to, + required this.title, + required this.fromSymbol, + required this.toSymbol, required this.createdAtFormattedDate, this.onTap, this.formattedAmount, @@ -20,8 +20,9 @@ class TradeRow extends StatelessWidget { final VoidCallback? onTap; final ExchangeProviderDescription provider; - final CryptoCurrency from; - final CryptoCurrency to; + final String fromSymbol; + final String toSymbol; + final String title; final String? createdAtFormattedDate; final String? formattedAmount; final String? formattedReceiveAmount; @@ -29,9 +30,6 @@ class TradeRow extends StatelessWidget { @override Widget build(BuildContext context) { - final amountCrypto = from.toString(); - final receiveAmountCrypto = to.toString(); - return InkWell( onTap: onTap, child: Container( @@ -73,7 +71,7 @@ class TradeRow extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - '${from.toString()} → ${to.toString()}', + title, style: Theme.of(context).textTheme.bodyLarge?.copyWith( fontSize: 16, fontWeight: FontWeight.w600, @@ -81,7 +79,7 @@ class TradeRow extends StatelessWidget { ), formattedAmount != null ? Text( - formattedAmount! + ' ' + amountCrypto, + "$formattedAmount $fromSymbol", style: Theme.of(context).textTheme.bodyLarge?.copyWith( fontSize: 16, fontWeight: FontWeight.w500, @@ -106,7 +104,7 @@ class TradeRow extends StatelessWidget { : Container(), formattedReceiveAmount != null ? Text( - formattedReceiveAmount! + ' ' + receiveAmountCrypto, + "$formattedReceiveAmount $toSymbol", style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w500, color: Theme.of(context).colorScheme.onSurfaceVariant, diff --git a/lib/src/screens/dashboard/widgets/verify_form.dart b/lib/src/screens/dashboard/widgets/verify_form.dart index 66d8978a4..13a6f10f1 100644 --- a/lib/src/screens/dashboard/widgets/verify_form.dart +++ b/lib/src/screens/dashboard/widgets/verify_form.dart @@ -20,15 +20,13 @@ class VerifyForm extends StatefulWidget { class VerifyFormState extends State with AutomaticKeepAliveClientMixin { VerifyFormState() - : formKey = GlobalKey(), - messageController = TextEditingController(), + : messageController = TextEditingController(), addressController = TextEditingController(), signatureController = TextEditingController(); final TextEditingController messageController; final TextEditingController addressController; final TextEditingController signatureController; - final GlobalKey formKey; @override void initState() { @@ -48,38 +46,35 @@ class VerifyFormState extends State with AutomaticKeepAliveClientMix super.build(context); return Container( padding: EdgeInsets.only(left: 24, right: 24), - child: Form( - key: formKey, - child: Column( - children: [ - AddressTextField( - controller: messageController, - placeholder: S.current.message, - options: [AddressTextFieldOption.paste], - buttonColor: Theme.of(context).colorScheme.surfaceContainerHighest, - fillColor: Theme.of(context).colorScheme.surface, - ), - const SizedBox(height: 20), - AddressTextField( - controller: addressController, - options: [AddressTextFieldOption.paste, AddressTextFieldOption.walletAddresses], - buttonColor: Theme.of(context).colorScheme.surfaceContainerHighest, - fillColor: Theme.of(context).colorScheme.surface, - onSelectedContact: (contact) { - addressController.text = contact.address; - }, - selectedCurrency: walletTypeToCryptoCurrency(widget.type, chainId: widget.chainId), - ), - const SizedBox(height: 20), - AddressTextField( - controller: signatureController, - placeholder: S.current.signature, - options: [AddressTextFieldOption.paste], - buttonColor: Theme.of(context).colorScheme.surfaceContainerHighest, - fillColor: Theme.of(context).colorScheme.surface, - ), - ], - ), + child: Column( + children: [ + AddressTextField( + controller: messageController, + placeholder: S.current.message, + options: [AddressTextFieldOption.paste], + buttonColor: Theme.of(context).colorScheme.surfaceContainerHighest, + fillColor: Theme.of(context).colorScheme.surface, + ), + const SizedBox(height: 20), + AddressTextField( + controller: addressController, + options: [AddressTextFieldOption.paste, AddressTextFieldOption.walletAddresses], + buttonColor: Theme.of(context).colorScheme.surfaceContainerHighest, + fillColor: Theme.of(context).colorScheme.surface, + onSelectedContact: (contact) { + addressController.text = contact.address; + }, + selectedCurrency: walletTypeToCryptoCurrency(widget.type, chainId: widget.chainId), + ), + const SizedBox(height: 20), + AddressTextField( + controller: signatureController, + placeholder: S.current.signature, + options: [AddressTextFieldOption.paste], + buttonColor: Theme.of(context).colorScheme.surfaceContainerHighest, + fillColor: Theme.of(context).colorScheme.surface, + ), + ], ), ); } diff --git a/lib/src/screens/dev/exchange_provider_logs_page.dart b/lib/src/screens/dev/exchange_provider_logs_page.dart index 1b3d84dca..26aa5e8fc 100644 --- a/lib/src/screens/dev/exchange_provider_logs_page.dart +++ b/lib/src/screens/dev/exchange_provider_logs_page.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:intl/intl.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; class DevExchangeProviderLogsPage extends BasePage { final ExchangeProviderLogsViewModel viewModel; @@ -266,6 +267,7 @@ class _LogsList extends StatelessWidget { Widget build(BuildContext context) { return Observer( builder: (_) => ListView.builder( + controller: ModalScrollController.of(context), itemCount: viewModel.filteredLogs.length, itemBuilder: (context, index) { final log = viewModel.filteredLogs[index]; diff --git a/lib/src/screens/disclaimer/disclaimer_page.dart b/lib/src/screens/disclaimer/disclaimer_page.dart index ef7d42369..09b6db0ca 100644 --- a/lib/src/screens/disclaimer/disclaimer_page.dart +++ b/lib/src/screens/disclaimer/disclaimer_page.dart @@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; class DisclaimerPage extends BasePage { DisclaimerPage({this.isReadOnly = false}); @@ -59,6 +60,8 @@ class DisclaimerBodyState extends State { child: Stack( children: [ SingleChildScrollView( + controller: ModalScrollController.of(context), + physics: ClampingScrollPhysics(), padding: EdgeInsets.only(left: 24.0, right: 24.0), child: Column( children: [ diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index 32a1b4eeb..dee12e1b5 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -32,7 +32,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/exchange_card.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart'; import 'package:cake_wallet/core/address_validator.dart'; @@ -615,7 +615,7 @@ class ExchangePage extends BasePage { try { exchangeViewModel.receiveCurrency = CryptoCurrency.fromString(initialPaymentRequest!.scheme); - exchangeViewModel.receiveAmount = initialPaymentRequest!.amount; + exchangeViewModel.setCanonicalReceiveAmount(initialPaymentRequest!.amount); exchangeViewModel.receiveAddress = initialPaymentRequest!.address; } catch (e) { printV('error: ${e.toString()}'); @@ -718,6 +718,7 @@ class ExchangePage extends BasePage { ? AmountValidator( isAutovalidate: true, currency: exchangeViewModel.depositCurrency, + amountParsingProxy: exchangeViewModel.amountParsingProxy, minValue: exchangeViewModel.limits.min.toString(), maxValue: exchangeViewModel.limits.max.toString(), ).call(value) @@ -734,6 +735,7 @@ class ExchangePage extends BasePage { exchangeViewModel.depositAddress = await fetchParsedAddress(context, domain, exchangeViewModel.depositCurrency); }, + useSatoshis: exchangeViewModel.useSatoshiDeposit, ), ); @@ -765,6 +767,7 @@ class ExchangePage extends BasePage { ? AmountValidator( isAutovalidate: true, currency: exchangeViewModel.receiveCurrency, + amountParsingProxy: exchangeViewModel.amountParsingProxy, minValue: exchangeViewModel.limits.min.toString(), maxValue: exchangeViewModel.limits.max.toString(), ).call(value) @@ -781,6 +784,7 @@ class ExchangePage extends BasePage { exchangeViewModel.receiveAddress = await fetchParsedAddress(context, domain, exchangeViewModel.receiveCurrency); }, + useSatoshis: exchangeViewModel.useSatoshisReceive, ), ); diff --git a/lib/src/screens/exchange/exchange_template_page.dart b/lib/src/screens/exchange/exchange_template_page.dart index 0af011435..893238455 100644 --- a/lib/src/screens/exchange/exchange_template_page.dart +++ b/lib/src/screens/exchange/exchange_template_page.dart @@ -9,7 +9,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/exchange_card.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart'; import 'package:cake_wallet/core/amount_validator.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker.dart'; @@ -129,8 +129,10 @@ class ExchangeTemplatePage extends BasePage { Theme.of(context).colorScheme.surfaceContainerHighest, borderColor: Theme.of(context).colorScheme.outlineVariant, fillColor: Theme.of(context).colorScheme.surfaceContainer, - currencyValueValidator: - AmountValidator(currency: exchangeViewModel.depositCurrency), + currencyValueValidator: AmountValidator( + currency: exchangeViewModel.depositCurrency, + amountParsingProxy: exchangeViewModel.amountParsingProxy, + ), ), ), ), @@ -160,8 +162,10 @@ class ExchangeTemplatePage extends BasePage { Theme.of(context).colorScheme.surfaceContainerHighest, borderColor: Theme.of(context).colorScheme.outlineVariant, fillColor: Theme.of(context).colorScheme.surfaceContainerLow, - currencyValueValidator: - AmountValidator(currency: exchangeViewModel.receiveCurrency), + currencyValueValidator: AmountValidator( + currency: exchangeViewModel.receiveCurrency, + amountParsingProxy: exchangeViewModel.amountParsingProxy, + ), ), ), ) diff --git a/lib/src/screens/exchange/widgets/currency_picker.dart b/lib/src/screens/exchange/widgets/currency_picker.dart index 998fc9a0a..fe6a6b3aa 100644 --- a/lib/src/screens/exchange/widgets/currency_picker.dart +++ b/lib/src/screens/exchange/widgets/currency_picker.dart @@ -11,7 +11,6 @@ class CurrencyPicker extends StatefulWidget { required this.onItemSelected, this.title, this.hintText, - this.isMoneroWallet = false, this.isConvertFrom = false, super.key}); @@ -19,7 +18,6 @@ class CurrencyPicker extends StatefulWidget { final List items; final String? title; final Function(Currency) onItemSelected; - final bool isMoneroWallet; final bool isConvertFrom; final String? hintText; diff --git a/lib/src/screens/exchange/widgets/exchange_card.dart b/lib/src/screens/exchange/widgets/exchange_card.dart index 15426b668..baca78e04 100644 --- a/lib/src/screens/exchange/widgets/exchange_card.dart +++ b/lib/src/screens/exchange/widgets/exchange_card.dart @@ -47,6 +47,7 @@ class ExchangeCard extends StatefulWidget { this.onPushPasteButton, this.onPushAddressBookButton, this.onDispose, + this.useSatoshis = false, required this.cardInstanceName, }) : super(key: key); @@ -82,6 +83,7 @@ class ExchangeCard extends StatefulWidget { final Function()? onDispose; final String cardInstanceName; final Color fillColor; + final bool useSatoshis; @override ExchangeCardState createState() => ExchangeCardState(); @@ -224,8 +226,8 @@ class ExchangeCardState extends State> { currencyAmountTextFieldWidgetKey: ValueKey('${_cardInstanceName}_currency_amount_textfield_widget_key'), imageArrow: widget.imageArrow, - selectedCurrency: _selectedCurrency.toString(), - selectedCurrencyDecimals: _selectedCurrency.decimals, + selectedCurrency: widget.useSatoshis ? "SATS" : "$_selectedCurrency", + selectedCurrencyDecimals: widget.useSatoshis ? 0 : _selectedCurrency.decimals, amountFocusNode: widget.amountFocusNode, amountController: amountController, onTapPicker: () => _presentPicker(context), @@ -383,7 +385,7 @@ class ExchangeCardState extends State> { onTap: () async { final contact = await Navigator.of(context).pushNamed( Routes.pickerAddressBook, - arguments: widget.initialCurrency, + arguments: [widget.initialCurrency, true], ); if (contact is ContactBase) { @@ -452,7 +454,6 @@ class ExchangeCardState extends State> { selectedAtIndex: widget.currencies.indexOf(_selectedCurrency), items: widget.currencies, hintText: S.of(context).search_currency, - isMoneroWallet: _isMoneroWallet, isConvertFrom: widget.hasRefundAddress, onItemSelected: (Currency item) => widget.onCurrencySelected(item as T), ), diff --git a/lib/src/screens/exchange/widgets/mobile_exchange_cards_section.dart b/lib/src/screens/exchange/widgets/mobile_exchange_cards_section.dart index e7186e0fa..09889fb27 100644 --- a/lib/src/screens/exchange/widgets/mobile_exchange_cards_section.dart +++ b/lib/src/screens/exchange/widgets/mobile_exchange_cards_section.dart @@ -86,6 +86,7 @@ class _BuySellOptionButtonsState extends State { Expanded( flex: 5, child: SelectButton( + center:true, height: 44, text: S.of(context).buy, isSelected: isBuySelected, @@ -107,6 +108,7 @@ class _BuySellOptionButtonsState extends State { Expanded( flex: 5, child: SelectButton( + center:true, height: 44, text: S.of(context).sell, isSelected: !isBuySelected, diff --git a/lib/src/screens/exchange_trade/exchange_trade_external_send_page.dart b/lib/src/screens/exchange_trade/exchange_trade_external_send_page.dart index 03e9496fe..12dd33b85 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_external_send_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_external_send_page.dart @@ -11,7 +11,7 @@ import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; class ExchangeTradeExternalSendPage extends BasePage { ExchangeTradeExternalSendPage({required this.exchangeTradeViewModel}); diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index 29d6c6641..c89a2eb9a 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/routes.dart'; @@ -10,6 +11,7 @@ import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_shee import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; import 'package:cake_wallet/utils/request_review_handler.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cake_wallet/view_model/send/output.dart'; import 'package:mobx/mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter/material.dart'; @@ -22,7 +24,7 @@ import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/exchange_trade/widgets/timer_widget.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; void showInformation(ExchangeTradeViewModel exchangeTradeViewModel, BuildContext context) { @@ -176,8 +178,8 @@ class ExchangeTradeState extends State { textColor: widget.exchangeTradeViewModel.isSendable ? Theme.of(context).colorScheme.onSecondaryContainer : Theme.of(context).colorScheme.onPrimary, - isDisabled: widget.exchangeTradeViewModel.isSwapsXyzSendingEVMTokenSwap, ), + SizedBox(height: 16), Observer( builder: (_) { @@ -293,49 +295,54 @@ class ExchangeTradeState extends State { if (state is ExecutedSuccessfullyState) { WidgetsBinding.instance.addPostFrameCallback((_) async { final trade = widget.exchangeTradeViewModel.trade; + final isSwapsXyz = trade.provider == ExchangeProviderDescription.swapsXyz; final isEVMWallet = widget.exchangeTradeViewModel.sendViewModel.isEVMWallet; - final amountValue = isSwapsXyz && isEVMWallet + final amountValue = isSwapsXyz && isEVMWallet && !widget.exchangeTradeViewModel.isSwapsXYZCanSendFromExternal ? trade.amount : widget.exchangeTradeViewModel.sendViewModel.pendingTransaction!.amountFormatted; + final fiatAmountValue = isSwapsXyz && isEVMWallet && !widget.exchangeTradeViewModel.isSwapsXYZCanSendFromExternal + ? widget.exchangeTradeViewModel.sendViewModel.calculateTransactionFiatAmount(amountValue) + : widget.exchangeTradeViewModel.sendViewModel.pendingTransactionFiatAmountFormatted; + if (context.mounted) { final result = await showModalBottomSheet( context: context, isDismissible: false, isScrollControlled: true, - builder: (BuildContext bottomSheetContext) { + builder: (bottomSheetContext) { + final sendVM = widget.exchangeTradeViewModel.sendViewModel; + return ConfirmSendingBottomSheet( key: ValueKey('exchange_trade_page_confirm_sending_bottom_sheet_key'), footerType: FooterType.slideActionButton, - isSlideActionEnabled: - widget.exchangeTradeViewModel.sendViewModel.isReadyForSend, - walletType: widget.exchangeTradeViewModel.sendViewModel.walletType, + isSlideActionEnabled: sendVM.isReadyForSend, + walletType: sendVM.walletType, titleText: S.of(bottomSheetContext).confirm_transaction, - titleIconPath: - widget.exchangeTradeViewModel.sendViewModel.selectedCryptoCurrency.iconPath, + titleIconPath: sendVM.selectedCryptoCurrency.iconPath, currency: widget.exchangeTradeViewModel.sendViewModel.selectedCryptoCurrency, amount: S.of(bottomSheetContext).send_amount, - amountValue: amountValue, - fiatAmountValue: widget - .exchangeTradeViewModel.sendViewModel.pendingTransactionFiatAmountFormatted, - fee: - isEVMCompatibleChain(widget.exchangeTradeViewModel.sendViewModel.walletType) - ? S.of(bottomSheetContext).send_estimated_fee - : S.of(bottomSheetContext).send_fee, - feeValue: widget - .exchangeTradeViewModel.sendViewModel.pendingTransaction!.feeFormatted, - feeFiatAmount: widget.exchangeTradeViewModel.sendViewModel - .pendingTransactionFeeFiatAmountFormatted, - outputs: widget.exchangeTradeViewModel.sendViewModel.outputs, + amountValue: sendVM.amountParsingProxy + .getDisplayCryptoAmount(amountValue, sendVM.selectedCryptoCurrency), + fiatAmountValue: fiatAmountValue, + fee: isEVMCompatibleChain(sendVM.walletType) + ? S.of(bottomSheetContext).send_estimated_fee + : S.of(bottomSheetContext).send_fee, + feeValue: "${sendVM.amountParsingProxy.getDisplayCryptoAmount( + sendVM.pendingTransaction!.feeFormattedValue, + sendVM.selectedCryptoCurrency)} ${sendVM.amountParsingProxy + .getCryptoSymbol(sendVM.wallet.currency)}", + feeFiatAmount: sendVM.pendingTransactionFeeFiatAmountFormatted, + outputs: sendVM.outputs, onSlideActionComplete: () async { - if (bottomSheetContext.mounted && Navigator.canPop(bottomSheetContext)) { - Navigator.of(bottomSheetContext).pop(true); - } - widget.exchangeTradeViewModel.sendViewModel.commitTransaction(context); + if (bottomSheetContext.mounted && Navigator.canPop(bottomSheetContext)) Navigator.of(bottomSheetContext).pop(true); + + sendVM.commitTransaction(context); widget.exchangeTradeViewModel.registerSwapsXyzTransaction(); }, + amountParsingProxy: sendVM.amountParsingProxy, ); }, ); diff --git a/lib/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart b/lib/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart index 85be26c92..347498ca7 100644 --- a/lib/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart +++ b/lib/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart @@ -217,7 +217,7 @@ class FeeSelectionWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( - '${output.estimatedFee} ${feesViewModel.currency}', + '${output.estimatedFee} ${feesViewModel.amountParsingProxy.getCryptoSymbol(feesViewModel.currency)}', style: Theme.of(context).textTheme.bodySmall?.copyWith( fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.onSurface, diff --git a/lib/src/screens/nano/nano_change_rep_page.dart b/lib/src/screens/nano/nano_change_rep_page.dart index bd06cd8c5..22a96514a 100644 --- a/lib/src/screens/nano/nano_change_rep_page.dart +++ b/lib/src/screens/nano/nano_change_rep_page.dart @@ -14,7 +14,7 @@ import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; class NanoChangeRepPage extends BasePage { NanoChangeRepPage({required SettingsStore settingsStore, required WalletBase wallet}) diff --git a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart index bfdb65ae9..383e0d161 100644 --- a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart +++ b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart @@ -12,7 +12,7 @@ import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell. import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart'; import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; @@ -84,7 +84,7 @@ class _AdvancedPrivacySettingsBody extends StatefulWidget { class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBody> { final TextEditingController passphraseController = TextEditingController(); final TextEditingController confirmPassphraseController = TextEditingController(); - final _formKey = GlobalKey(); + final _formKey = GlobalKey(); final _passphraseFormKey = GlobalKey(); bool? testnetValue; @@ -274,7 +274,7 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo Padding( padding: EdgeInsets.only(left: 24, right: 24, top: 24), child: NodeForm( - formKey: _formKey, + key: _formKey, nodeViewModel: widget.nodeViewModel, ), ) diff --git a/lib/src/screens/new_wallet/new_wallet_page.dart b/lib/src/screens/new_wallet/new_wallet_page.dart index 7fed51a7d..fe6e6dfea 100644 --- a/lib/src/screens/new_wallet/new_wallet_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_page.dart @@ -1,6 +1,6 @@ import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/core/wallet_name_validator.dart'; -import 'package:cake_wallet/entities/generate_name.dart'; +import 'package:cw_core/generate_name.dart'; import 'package:cake_wallet/entities/seed_type.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/main.dart'; @@ -13,7 +13,7 @@ import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/src/widgets/gradient_background.dart'; import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/seed_language_picker.dart'; import 'package:cake_wallet/src/widgets/seed_language_selector.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; @@ -203,7 +203,7 @@ class _WalletNameFormState extends State { icon: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6.0), + borderRadius: BorderRadius.circular(12.0), color: Theme.of(context).colorScheme.surface, ), width: 34, @@ -267,7 +267,7 @@ class _WalletNameFormState extends State { child: SelectButton( key: ValueKey('new_wallet_page_monero_seed_type_button_key'), text: widget._seedSettingsViewModel.moneroSeedType.title, - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(18), onTap: () async { await showPopUp( context: context, @@ -291,7 +291,7 @@ class _WalletNameFormState extends State { builder: (BuildContext build) => Padding( padding: EdgeInsets.only(top: 10), child: SeedLanguageSelector( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(18), key: _languageSelectorKey, buttonKey: ValueKey('new_wallet_page_seed_language_selector_button_key'), initialSelected: defaultSeedLanguage, diff --git a/lib/src/screens/new_wallet/new_wallet_type_page.dart b/lib/src/screens/new_wallet/new_wallet_type_page.dart index 6829967bf..b8c9610b5 100644 --- a/lib/src/screens/new_wallet/new_wallet_type_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_type_page.dart @@ -9,7 +9,7 @@ import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart'; import 'package:cake_wallet/src/screens/setup_2fa/widgets/popup_cancellable_alert.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/search_bar_widget.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; @@ -37,19 +37,19 @@ class NewWalletTypePage extends BasePage { @override Function(BuildContext)? get pushToNextWidget => (context) { - FocusScopeNode currentFocus = FocusScope.of(context); - if (!currentFocus.hasPrimaryFocus) { - currentFocus.focusedChild?.unfocus(); - } - }; + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.focusedChild?.unfocus(); + } + }; @override Widget body(BuildContext context) => WalletTypeForm( - walletImage: currentTheme.isDark ? walletTypeImage : walletTypeLightImage, - isCreate: newWalletTypeArguments.isCreate, - onTypeSelected: newWalletTypeArguments.onTypeSelected, - hardwareWalletType: newWalletTypeArguments.hardwareWalletType, - ); + walletImage: currentTheme.isDark ? walletTypeImage : walletTypeLightImage, + isCreate: newWalletTypeArguments.isCreate, + onTypeSelected: newWalletTypeArguments.onTypeSelected, + hardwareWalletType: newWalletTypeArguments.hardwareWalletType, + ); } class WalletTypeForm extends StatefulWidget { @@ -86,10 +86,10 @@ class WalletTypeFormState extends State { void initState() { types = filteredTypes = availableWalletTypes .where((element) => - !widget.isHardwareWallet || - DeviceConnectionType.supportedConnectionTypes( - element, widget.hardwareWalletType!, Platform.isIOS) - .isNotEmpty) + !widget.isHardwareWallet || + DeviceConnectionType.supportedConnectionTypes( + element, widget.hardwareWalletType!, Platform.isIOS) + .isNotEmpty) .toList(); super.initState(); @@ -116,8 +116,8 @@ class WalletTypeFormState extends State { S.of(context).choose_wallet_currency, textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), ), Padding( @@ -132,12 +132,12 @@ class WalletTypeFormState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ ...filteredTypes.map( - (type) => Padding( + (type) => Padding( padding: EdgeInsets.only(top: 12), child: SelectButton( key: ValueKey('new_wallet_type_${type.name}_button_key'), image: Image.asset( - getCryptoCurrencyForWalletListItem(type).iconPath ?? '', + getCryptoCurrencyIconForWalletListItem(type), height: 24, width: 24, ), @@ -148,7 +148,7 @@ class WalletTypeFormState extends State { onTap: () => setState(() => selected = type), deviceConnectionTypes: widget.isHardwareWallet ? DeviceConnectionType.supportedConnectionTypes( - type, widget.hardwareWalletType!, Platform.isIOS) + type, widget.hardwareWalletType!, Platform.isIOS) : [], ), ), @@ -201,4 +201,4 @@ class WalletTypeFormState extends State { Navigator.of(context).pushNamed(Routes.walletGroupDescription, arguments: selected!); } } -} +} \ No newline at end of file diff --git a/lib/src/screens/new_wallet/wallet_group_display_page.dart b/lib/src/screens/new_wallet/wallet_group_display_page.dart index da9a325f3..b51fd34bc 100644 --- a/lib/src/screens/new_wallet/wallet_group_display_page.dart +++ b/lib/src/screens/new_wallet/wallet_group_display_page.dart @@ -95,7 +95,7 @@ class WalletGroupsDisplayBody extends StatelessWidget { isSelected: walletGroupsDisplayViewModel.selectedSingleWallet == wallet, leadingWidget: Image.asset( - getCryptoCurrencyForWalletListItem(wallet.type).iconPath!, + getCryptoCurrencyIconForWalletListItem(wallet.type), width: 32, height: 32, ), @@ -146,14 +146,16 @@ class WalletGroupsDisplayBody extends StatelessWidget { Future onTypeSelected(BuildContext context) async { final mnemonic = await walletGroupsDisplayViewModel.getSelectedWalletMnemonic(); - Navigator.of(context).pushNamed( - Routes.newWallet, - arguments: NewWalletArguments( - type: walletGroupsDisplayViewModel.type, - mnemonic: mnemonic, - isChildWallet: true, - ), - ); + if (context.mounted) { + Navigator.of(context).pushNamed( + Routes.newWallet, + arguments: NewWalletArguments( + type: walletGroupsDisplayViewModel.type, + mnemonic: mnemonic, + isChildWallet: true, + ), + ); + } } } diff --git a/lib/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart b/lib/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart index f77a4e60e..000fafeab 100644 --- a/lib/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart +++ b/lib/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart @@ -67,8 +67,8 @@ class GroupedWalletExpansionTile extends StatelessWidget { return Padding( padding: EdgeInsets.symmetric(vertical: 6), child: ExpansionTile( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - collapsedShape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + shape: RoundedSuperellipseBorder(borderRadius: BorderRadius.circular(18)), + collapsedShape: RoundedSuperellipseBorder(borderRadius: BorderRadius.circular(18)), collapsedBackgroundColor: backgroundColor, backgroundColor: backgroundColor, onExpansionChanged: onExpansionChanged, @@ -80,7 +80,13 @@ class GroupedWalletExpansionTile extends StatelessWidget { EdgeInsets.symmetric(vertical: 1, horizontal: !isCurrentlySelectedWallet ? 16 : 0), iconColor: effectiveArrowColor, collapsedIconColor: effectiveArrowColor, - leading: leadingWidget, + leading: (childWallets.isEmpty && onTitleTapped != null && leadingWidget != null) + ? GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onTitleTapped, + child: leadingWidget, + ) + : leadingWidget, trailing: trailingWidget ?? (childWallets.isEmpty ? SizedBox.shrink() : null), title: GestureDetector( onTap: onTitleTapped, @@ -123,7 +129,7 @@ class GroupedWalletExpansionTile extends StatelessWidget { : SizedBox(width: 7), SizedBox(width: 24), Image.asset( - getCryptoCurrencyForWalletListItem(item.type).iconPath!, + getCryptoCurrencyIconForWalletListItem(item.type), width: 32, height: 32, ), diff --git a/lib/src/screens/new_wallet/widgets/select_button.dart b/lib/src/screens/new_wallet/widgets/select_button.dart index b3bfb5616..f5a173544 100644 --- a/lib/src/screens/new_wallet/widgets/select_button.dart +++ b/lib/src/screens/new_wallet/widgets/select_button.dart @@ -17,7 +17,7 @@ class SelectButton extends StatelessWidget { this.deviceConnectionTypes, this.borderRadius, this.padding, - super.key, + super.key, this.center = false, }); final Widget? image; @@ -34,6 +34,7 @@ class SelectButton extends StatelessWidget { final Color? borderColor; final BorderRadius? borderRadius; final EdgeInsets? padding; + final bool center; @override Widget build(BuildContext context) { @@ -64,10 +65,9 @@ class SelectButton extends StatelessWidget { child: Container( width: double.infinity, height: height, - padding: padding ?? EdgeInsets.only(left: 30, right: 30), alignment: Alignment.center, decoration: BoxDecoration( - borderRadius: borderRadius ?? BorderRadius.all(Radius.circular(15)), + borderRadius: borderRadius ?? BorderRadius.all(Radius.circular(18)), color: backgroundColor, border: borderColor != null ? Border.all(color: borderColor!, width: 2) : null, ), @@ -78,12 +78,13 @@ class SelectButton extends StatelessWidget { children: [ Expanded( child: Row( - mainAxisAlignment: MainAxisAlignment.start, + mainAxisAlignment: center ? MainAxisAlignment.center : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ + if(!center) SizedBox(width:16), image ?? Offstage(), Padding( - padding: image != null ? EdgeInsets.only(left: 15) : EdgeInsets.only(left: 0), + padding: image != null ? EdgeInsets.only(left: 14) : EdgeInsets.only(left: 0), child: Text( text, style: Theme.of(context).textTheme.bodyMedium!.copyWith( @@ -96,7 +97,8 @@ class SelectButton extends StatelessWidget { ], ), ), - ...trailingIcons + ...trailingIcons, + if(!center) SizedBox(width:16), ], ), ), diff --git a/lib/src/screens/nodes/node_create_or_edit_page.dart b/lib/src/screens/nodes/node_create_or_edit_page.dart index f5bd48704..8b0397c45 100644 --- a/lib/src/screens/nodes/node_create_or_edit_page.dart +++ b/lib/src/screens/nodes/node_create_or_edit_page.dart @@ -1,53 +1,51 @@ import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; import 'package:cake_wallet/src/screens/nodes/widgets/node_form.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:mobx/mobx.dart'; -class NodeCreateOrEditPage extends BasePage { +class NodeCreateOrEditPage extends StatefulWidget { NodeCreateOrEditPage({ required this.nodeCreateOrEditViewModel, this.editingNode, this.isSelected, this.type, - }) : _formKey = GlobalKey(); + }); - final GlobalKey _formKey; final NodeCreateOrEditViewModel nodeCreateOrEditViewModel; final Node? editingNode; final bool? isSelected; final WalletType? type; @override - String get title => editingNode != null ? S.current.edit_node : S.current.node_new; + State createState() => _NodeCreateOrEditPageState(); +} + +class _NodeCreateOrEditPageState extends State { + final _nodeFormKey = GlobalKey(); + @override - Widget trailing(BuildContext context) => IconButton( - onPressed: () => nodeCreateOrEditViewModel.scanQRCodeForNewNode(context), - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - hoverColor: Colors.transparent, - icon: Image.asset('assets/images/qr_code_icon.png'), - ); - - @override - Widget body(BuildContext context) { + void initState() { + super.initState(); reaction( - (_) => nodeCreateOrEditViewModel.connectionState, - (ExecutionState state) { + (_) => widget.nodeCreateOrEditViewModel.connectionState, + (ExecutionState state) { if (state is ExecutedSuccessfullyState) { WidgetsBinding.instance.addPostFrameCallback( - (_) => showPopUp( + (_) => showPopUp( context: context, builder: (context) => AlertWithOneAction( alertTitle: S.of(context).new_node_testing, @@ -63,7 +61,7 @@ class NodeCreateOrEditPage extends BasePage { if (state is FailureState) { WidgetsBinding.instance.addPostFrameCallback( - (_) => showPopUp( + (_) => showPopUp( context: context, builder: (context) => AlertWithOneAction( alertTitle: S.of(context).error, @@ -77,78 +75,104 @@ class NodeCreateOrEditPage extends BasePage { }, ); + } + + @override + Widget build(BuildContext context) { + return Container( - padding: const EdgeInsets.only(left: 24, right: 24), - child: ScrollableWithBottomSection( - contentPadding: const EdgeInsets.only(bottom: 24.0, top: 8), - content: NodeForm( - formKey: _formKey, - nodeViewModel: nodeCreateOrEditViewModel, - editingNode: editingNode, - type: type, - ), - bottomSectionPadding: const EdgeInsets.only(bottom: 24), - bottomSection: Observer( - builder: (_) => Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( + color: Theme.of(context).colorScheme.surface, + child: SafeArea( + child: Column( + children: [ + ModalTopBar( + title: widget.editingNode != null ? S.current.edit_node : S.current.node_new, + leadingIcon: Icon(Icons.arrow_back_ios_new), + onLeadingPressed: Navigator.of(context).pop, + trailingIcon: SvgPicture.asset( + "assets/new-ui/scan.svg",width:24,height:24, + colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn), + ), + onTrailingPressed: () => widget.nodeCreateOrEditViewModel.scanQRCodeForNewNode(context), + ), + Expanded( + child: KeyboardHideOverlay( child: Container( - padding: const EdgeInsets.only(right: 8.0), - child: LoadingPrimaryButton( - onPressed: () async { - final confirmed = await showPopUp( - context: context, - builder: (context) => AlertWithTwoActions( - alertTitle: S.of(context).remove_node, - alertContent: S.of(context).remove_node_message, - rightButtonText: S.of(context).remove, - leftButtonText: S.of(context).cancel, - actionRightButton: () => Navigator.pop(context, true), - actionLeftButton: () => Navigator.pop(context, false), + padding: const EdgeInsets.only(left: 18, right: 18), + child: ScrollableWithBottomSection( + contentPadding: const EdgeInsets.only(bottom: 24.0, top: 8), + content: NodeForm( + key: _nodeFormKey, + nodeViewModel: widget.nodeCreateOrEditViewModel, + ), + bottomSectionPadding: const EdgeInsets.only(bottom: 24), + bottomSection: Observer( + builder: (_) => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Container( + padding: const EdgeInsets.only(right: 8.0), + child: LoadingPrimaryButton( + onPressed: () async { + final confirmed = await showPopUp( + context: context, + builder: (context) => AlertWithTwoActions( + alertTitle: S.of(context).remove_node, + alertContent: S.of(context).remove_node_message, + rightButtonText: S.of(context).remove, + leftButtonText: S.of(context).cancel, + actionRightButton: () => Navigator.pop(context, true), + actionLeftButton: () => Navigator.pop(context, false), + ), + ) ?? + false; + + if (confirmed) { + await widget.editingNode!.delete(); + Navigator.of(context).pop(); + } + }, + text: S.of(context).delete, + isDisabled: widget.editingNode == null || + !widget.nodeCreateOrEditViewModel.isReady || + (widget.isSelected ?? false), + color: Theme.of(context).colorScheme.errorContainer, + textColor: Theme.of(context).colorScheme.onErrorContainer, + ), ), - ) ?? - false; - - if (confirmed) { - await editingNode!.delete(); - Navigator.of(context).pop(); - } - }, - text: S.of(context).delete, - isDisabled: editingNode == null || - !nodeCreateOrEditViewModel.isReady || - (isSelected ?? false), - color: Theme.of(context).colorScheme.errorContainer, - textColor: Theme.of(context).colorScheme.onErrorContainer, + ), + Flexible( + child: Container( + padding: const EdgeInsets.only(left: 8.0), + child: PrimaryButton( + onPressed: () async { + if (_nodeFormKey.currentState != null && !_nodeFormKey.currentState!.validate()) { + return; + } + + await widget.nodeCreateOrEditViewModel.save( + editingNode: widget.editingNode, saveAsCurrent: widget.isSelected ?? false); + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + text: S.of(context).save, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + isDisabled: (!widget.nodeCreateOrEditViewModel.isReady) || + (widget.nodeCreateOrEditViewModel.connectionState is IsExecutingState), + ), + ), + ), + ], + ), + ), ), ), ), - Flexible( - child: Container( - padding: const EdgeInsets.only(left: 8.0), - child: PrimaryButton( - onPressed: () async { - if (_formKey.currentState != null && !_formKey.currentState!.validate()) { - return; - } - - await nodeCreateOrEditViewModel.save( - editingNode: editingNode, saveAsCurrent: isSelected ?? false); - if (context.mounted) { - Navigator.of(context).pop(); - } - }, - text: S.of(context).save, - color: Theme.of(context).colorScheme.primary, - textColor: Theme.of(context).colorScheme.onPrimary, - isDisabled: (!nodeCreateOrEditViewModel.isReady) || - (nodeCreateOrEditViewModel.connectionState is IsExecutingState), - ), - ), - ), - ], - ), + ), + ], ), ), ); diff --git a/lib/src/screens/nodes/pow_node_create_or_edit_page.dart b/lib/src/screens/nodes/pow_node_create_or_edit_page.dart index 2fc58a960..96f77858e 100644 --- a/lib/src/screens/nodes/pow_node_create_or_edit_page.dart +++ b/lib/src/screens/nodes/pow_node_create_or_edit_page.dart @@ -11,13 +11,12 @@ import 'package:mobx/mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; class PowNodeCreateOrEditPage extends BasePage { PowNodeCreateOrEditPage( {required this.nodeCreateOrEditViewModel, this.editingNode, this.isSelected}) - : _formKey = GlobalKey(), - _addressController = TextEditingController(), + : _addressController = TextEditingController(), _portController = TextEditingController(), _loginController = TextEditingController(), _passwordController = TextEditingController() { @@ -55,7 +54,6 @@ class PowNodeCreateOrEditPage extends BasePage { .addListener(() => nodeCreateOrEditViewModel.password = _passwordController.text); } - final GlobalKey _formKey; final TextEditingController _addressController; final TextEditingController _portController; final TextEditingController _loginController; @@ -80,6 +78,7 @@ class PowNodeCreateOrEditPage extends BasePage { final NodeCreateOrEditViewModel nodeCreateOrEditViewModel; final Node? editingNode; final bool? isSelected; + final _formKey = GlobalKey(); @override Widget body(BuildContext context) { @@ -118,9 +117,8 @@ class PowNodeCreateOrEditPage extends BasePage { child: ScrollableWithBottomSection( contentPadding: EdgeInsets.only(bottom: 24.0, top: 8), content: NodeForm( - formKey: _formKey, + key: _formKey, nodeViewModel: nodeCreateOrEditViewModel, - editingNode: editingNode, ), bottomSectionPadding: EdgeInsets.only(bottom: 24), bottomSection: Observer( diff --git a/lib/src/screens/nodes/widgets/node_form.dart b/lib/src/screens/nodes/widgets/node_form.dart index 3d3dd175e..96ef7d6df 100644 --- a/lib/src/screens/nodes/widgets/node_form.dart +++ b/lib/src/screens/nodes/widgets/node_form.dart @@ -1,270 +1,210 @@ import 'package:cake_wallet/core/node_address_validator.dart'; import 'package:cake_wallet/core/node_port_validator.dart'; import 'package:cake_wallet/core/socks_proxy_node_address_validator.dart'; +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_Item_checkbox.dart'; +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_text_field.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; -import 'package:cake_wallet/src/widgets/standard_checkbox.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart'; import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; -import 'package:cw_core/node.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:fluttertoast/fluttertoast.dart'; import 'package:mobx/mobx.dart'; class NodeForm extends StatefulWidget { NodeForm({ + super.key, required this.nodeViewModel, - required this.formKey, - this.editingNode, - this.type, }); final NodeCreateOrEditViewModel nodeViewModel; - final GlobalKey formKey; - final Node? editingNode; - final WalletType? type; @override - State createState() => - _NodeFormState(nodeViewModel: nodeViewModel, editingNode: this.editingNode); + State createState() => NodeFormState(vm: nodeViewModel); } -class _NodeFormState extends State { - _NodeFormState({ - required this.nodeViewModel, - Node? editingNode, - }) : _addressController = TextEditingController(text: editingNode?.uri.host.toString()), - _pathController = TextEditingController(text: editingNode?.path.toString()), - _portController = TextEditingController( - text: (editingNode != null && editingNode.uri.hasPort) - ? editingNode.uri.port.toString() - : ''), - _loginController = TextEditingController(text: editingNode?.login), - _passwordController = TextEditingController(text: editingNode?.password), - _socksAddressController = TextEditingController(text: editingNode?.socksProxyAddress) { - if (editingNode != null) { - nodeViewModel - ..setAddress(editingNode.uri.host.toString()) - ..setPath(editingNode.path.toString()) - ..setPort(editingNode.uri.hasPort ? editingNode.uri.port.toString() : '') - ..setPassword(editingNode.password ?? '') - ..setLogin(editingNode.login ?? '') - ..setSSL(editingNode.isSSL) - ..setTrusted(editingNode.trusted) - ..setIsEnabledForAutoSwitching(editingNode.isEnabledForAutoSwitching) - ..setSocksProxy(editingNode.useSocksProxy) - ..setSocksProxyAddress(editingNode.socksProxyAddress ?? ''); - } +class NodeFormState extends State { + NodeFormState({required this.vm}); - if (nodeViewModel.hasAuthCredentials) { - reaction((_) => nodeViewModel.login, (String login) { - if (login != _loginController.text) _loginController.text = login; - }); - - reaction((_) => nodeViewModel.password, (String password) { - if (password != _passwordController.text) _passwordController.text = password; - }); - } - reaction((_) => nodeViewModel.address, (String address) { - if (address != _addressController.text) { - _addressController.text = address; - } - }); - - reaction((_) => nodeViewModel.port, (String port) { - if (port != _portController.text) _portController.text = port; - }); - - reaction((_) => nodeViewModel.path, (String path) { - if (path != _pathController.text) _pathController.text = path; - }); - - _addressController.addListener(() => nodeViewModel.address = _addressController.text); - _pathController.addListener(() => nodeViewModel.path = _pathController.text); - _portController.addListener(() => nodeViewModel.port = _portController.text); - _loginController.addListener(() => nodeViewModel.login = _loginController.text); - _passwordController.addListener(() => nodeViewModel.password = _passwordController.text); - _socksAddressController - .addListener(() => nodeViewModel.socksProxyAddress = _socksAddressController.text); - } - - final NodeCreateOrEditViewModel nodeViewModel; - - final TextEditingController _addressController; - final TextEditingController _pathController; - final TextEditingController _portController; - final TextEditingController _loginController; - final TextEditingController _passwordController; - final TextEditingController _socksAddressController; + final Map _controllers = {}; @override - Widget build(BuildContext context) => Form( - key: widget.formKey, - child: Column(children: [ - Row(children: [ - Expanded( - child: BaseTextFormField( - controller: _addressController, - hintText: S.of(context).node_address, - validator: widget.type == WalletType.decred + void initState() { + super.initState(); + + for(final key in vm.textFieldKeys) { + _controllers[key] = TextEditingController(); + + _controllers[key]!.text = vm.getTextValue(key); + _controllers[key]!.addListener(() { + final text = _controllers[key]!.text; + vm.updateViewModelFromText(key, text); + }); + } + + _setupReactions(); + } + + @override + void dispose() { + for (final controller in _controllers.values) { + controller.dispose(); + } + super.dispose(); + } + + final NodeCreateOrEditViewModel vm; + final _formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return Observer( + builder: (_)=>Form( + key: _formKey, + child: NewListSections( + sections: { + 'main': [ + ListItemTextField( + keyValue: vm.nodeLabelUIKey, + label: 'Node label', + initialValue: vm.label, + ), + ListItemTextField( + keyValue: vm.nodeAddressUIKey, + label: S.current.node_address, + initialValue: vm.address, + validator: vm.walletType == WalletType.decred ? NodeAddressValidatorDecredBlankException() : NodeAddressValidator(), ), - ) - ]), - const SizedBox(height: 10), - if (nodeViewModel.hasPathSupport) ...[ - Row(children: [ - Expanded( - child: BaseTextFormField( - controller: _pathController, - hintText: "/path", + if (vm.hasPathSupport) + ListItemTextField( + keyValue: vm.nodePathUIKey, + label: '/path', + initialValue: vm.path, validator: NodePathValidator(), ), - ) - ]), - const SizedBox(height: 10), - ], - Row(children: [ - Expanded( - child: BaseTextFormField( - controller: _portController, - hintText: S.of(context).node_port, - keyboardType: TextInputType.numberWithOptions(signed: false, decimal: false), + ListItemTextField( + keyValue: vm.nodePortUIKey, + label: S.current.node_port, + initialValue: '', + //keyboardType: TextInputType.numberWithOptions(signed: false, decimal: false), validator: NodePortValidator(), ), - ) - ]), - const SizedBox(height: 10), - Padding( - padding: const EdgeInsets.only(top: 20), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - Observer( - builder: (_) => StandardCheckbox( - value: nodeViewModel.useSSL, - borderColor: Theme.of(context).colorScheme.surfaceContainerHighest, - iconColor: Theme.of(context).colorScheme.primary, - onChanged: (value) => nodeViewModel.useSSL = value, - caption: S.of(context).use_ssl, - ), - ) - ], - ), - ), - const SizedBox(height: 10), - Padding( - padding: const EdgeInsets.only(top: 20), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - Observer( - builder: (_) => StandardCheckbox( - value: nodeViewModel.isEnabledForAutoSwitching, - borderColor: Theme.of(context).colorScheme.surfaceContainerHighest, - iconColor: Theme.of(context).colorScheme.primary, - onChanged: (value) => nodeViewModel.isEnabledForAutoSwitching = value, - caption: S.current.enable_for_auto_switching, - ), + if (vm.hasAuthCredentials) ...[ + ListItemTextField( + keyValue: vm.nodeUsernameUIKey, + label: S.current.login, + initialValue: vm.login, ), - ], - ), - ), - const SizedBox(height: 10), - if (nodeViewModel.hasAuthCredentials) ...[ - Row(children: [ - Expanded( - child: BaseTextFormField( - controller: _loginController, - hintText: S.of(context).login, + ListItemTextField( + keyValue: vm.nodePasswordUIKey, + label: S.current.password, + initialValue: vm.password, ), - ) - ]), - const SizedBox(height: 10), - Row(children: [ - Expanded( - child: BaseTextFormField( - controller: _passwordController, - hintText: S.of(context).password, - ), - ) - ]), - Padding( - padding: const EdgeInsets.only(top: 20), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - Observer( - builder: (_) => StandardCheckbox( - value: nodeViewModel.trusted, - borderColor: Theme.of(context).colorScheme.surfaceContainerHighest, - iconColor: Theme.of(context).colorScheme.primary, - onChanged: (value) => nodeViewModel.trusted = value, - caption: S.of(context).trusted, - ), - ), - ], + ] + ], + 'advanced': [ + ListItemCheckbox( + keyValue: vm.useSSLUIKey, + label: S.current.use_ssl, + value: vm.useSSL, + onChanged: (value) => vm.useSSL = value), + ListItemCheckbox( + keyValue: vm.nodeTrustedUIKey, + label: S.current.trusted, + value: vm.trusted, + onChanged: (value) => vm.trusted = value, ), - ), - Observer( - builder: (_) => Column(children: [ - if (nodeViewModel.usesEmbeddedProxy) ...[ - Padding( - padding: const EdgeInsets.only(top: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - StandardCheckbox( - value: nodeViewModel.usesEmbeddedProxy, - gradientBackground: false, - borderColor: Theme.of(context).dividerColor, - iconColor: Theme.of(context).colorScheme.primary, - onChanged: null, - caption: 'Embedded Tor SOCKS Proxy', - ), - ], - ), - ), - ], - Padding( - padding: const EdgeInsets.only(top: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - StandardCheckbox( - value: nodeViewModel.useSocksProxy, - borderColor: Theme.of(context).colorScheme.surfaceContainerHighest, - iconColor: Theme.of(context).colorScheme.primary, - onChanged: (value) { - if (!value) _socksAddressController.text = ''; - nodeViewModel.useSocksProxy = value; - }, - caption: 'SOCKS Proxy', - ), - ], - ), + if (vm.usesEmbeddedProxy) + ListItemCheckbox( + keyValue: vm.nodeEmbeddedTorProxyUIKey, + label: 'Embedded Tor SOCKS Proxy', + value: vm.usesEmbeddedProxy, + onChanged: (_) {}, ), - if (nodeViewModel.useSocksProxy) ...[ - const SizedBox(height: 10), - Row(children: [ - Expanded( - child: BaseTextFormField( - controller: _socksAddressController, - hintText: '[:]', - validator: SocksProxyNodeAddressValidator(), - ), - ) - ]), - ] - ]), - ), - ] - ]), + ListItemCheckbox( + keyValue: vm.useSocksProxyUIKey, + label: 'Use SOCKS Proxy', + value: vm.useSocksProxy, + onChanged: (value) { + vm.socksProxyAddress = ''; + vm.useSocksProxy = value; + }, + ), + if (vm.useSocksProxy) + ListItemTextField( + keyValue: vm.socksProxyAddressUIKey, + label: 'host:port', + initialValue: vm.socksProxyAddress, + validator: SocksProxyNodeAddressValidator(), + ), + ListItemCheckbox( + keyValue: vm.autoSwitchingUIKey, + label: S.current.enable_for_auto_switching, + value: vm.isEnabledForAutoSwitching, + onChanged: (value) => vm.isEnabledForAutoSwitching = value), + ], + }, + controllers: _controllers, + getCheckboxValue: vm.getCheckboxValue, + updateCheckboxValue: vm.updateCheckboxValue, + tapHandlers: tapHandlers + ), + ), + ); + } + + Map get tapHandlers => { + 'node_regular_with_drill_in_row_key': () => _showToast('Regular with drill-in row tapped'), + 'node_tall_row_key': () => _showToast('Tall row tapped'), + 'node_regular_with_trailing_row_key': () => _showToast('Regular with trailing row tapped'), + 'node_item_selector_row_key': () => _showToast('Item selector row tapped'), + }; + + void _showToast(String msg) async { + try { + await Fluttertoast.showToast( + msg: msg, + backgroundColor: Color.fromRGBO(0, 0, 0, 0.85), ); + } catch (_) {} + } + + + void _setupReactions() { + _bindController(() => vm.label, vm.nodeLabelUIKey); + _bindController(() => vm.address, vm.nodeAddressUIKey); + _bindController(() => vm.port, vm.nodePortUIKey); + _bindController(() => vm.path, vm.nodePathUIKey); + + if (vm.hasAuthCredentials) { + _bindController(() => vm.login, vm.nodeUsernameUIKey); + _bindController(() => vm.password, vm.nodePasswordUIKey); + } + + if (vm.hasPathSupport) { + _bindController(() => vm.path, vm.nodePathUIKey); + } + + _bindController( + () => vm.socksProxyAddress, + vm.socksProxyAddressUIKey, + ); + } + + void _bindController( + T Function() observe, + String uiKey, + ) { + reaction((_) => observe(), (value) { + final controller = _controllers[uiKey]; + if (controller != null && controller.text != value.toString()) { + controller.text = value.toString(); + } + }); + } + + bool validate() => _formKey.currentState?.validate() ?? false; } diff --git a/lib/src/screens/nodes/widgets/node_list_row.dart b/lib/src/screens/nodes/widgets/node_list_row.dart index 4f442a471..25d47775d 100644 --- a/lib/src/screens/nodes/widgets/node_list_row.dart +++ b/lib/src/screens/nodes/widgets/node_list_row.dart @@ -21,24 +21,26 @@ class NodeListRow extends StandardListRow { final leading = buildLeading(context); final trailing = buildTrailing(context); return Container( - height: 56, - padding: EdgeInsets.only(left: 12, right: 12, top: 2, bottom: 2), + padding: EdgeInsets.only(left: 18, right: 18, top: 2, bottom: 2), margin: EdgeInsets.only(top: 2, bottom: 2), child: FilledButton( onPressed: () => onTap?.call(context), style: FilledButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.surfaceContainer, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(10)), + borderRadius: BorderRadius.all(Radius.circular(20)), ), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - leading, - buildCenter(context, hasLeftOffset: true), - trailing, - ], + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + leading, + buildCenter(context, hasLeftOffset: true), + trailing, + ], + ), ), ), ); diff --git a/lib/src/screens/pin_code/pin_code_widget.dart b/lib/src/screens/pin_code/pin_code_widget.dart index d89fe3d99..9fb04c566 100644 --- a/lib/src/screens/pin_code/pin_code_widget.dart +++ b/lib/src/screens/pin_code/pin_code_widget.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:another_flushbar/flushbar.dart'; @@ -141,7 +142,7 @@ class PinCodeState extends State { }, child: Container( color: Theme.of(context).colorScheme.surface, - padding: EdgeInsets.only(left: 40.0, right: 40.0, bottom: 60.0), + padding: EdgeInsets.only(left: 44.0, right: 44.0, bottom: 60.0), child: Column( children: [ Spacer(flex: 2), @@ -153,7 +154,7 @@ class PinCodeState extends State { color: Theme.of(context).colorScheme.onSurface, ), ), - Spacer(flex: 3), + Spacer(flex: 2), Container( width: 180, child: Row( @@ -206,11 +207,11 @@ class PinCodeState extends State { child: GridView.count( shrinkWrap: true, crossAxisCount: 3, - childAspectRatio: _aspectRatio, + childAspectRatio: DeviceInfo.instance.isDesktop ? _aspectRatio : 1.1, physics: const NeverScrollableScrollPhysics(), children: List.generate(12, (index) { - const double marginRight = 8; - const double marginLeft = 8; + const double marginRight = 10; + const double marginLeft = 10; if (index == 9) { // Empty container @@ -228,7 +229,7 @@ class PinCodeState extends State { button: true, onTap: () => _pop(), child: TextButton( - onPressed: () => _pop(), + onPressed: () => {_pop(), HapticFeedback.mediumImpact()}, style: TextButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.surfaceContainer, shape: CircleBorder(), @@ -246,7 +247,7 @@ class PinCodeState extends State { margin: EdgeInsets.only(left: marginLeft, right: marginRight), child: TextButton( key: ValueKey('pin_code_button_${index}_key'), - onPressed: () => _push(index), + onPressed: () => {_push(index), HapticFeedback.mediumImpact()}, style: TextButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.surfaceContainer, shape: CircleBorder(), diff --git a/lib/src/screens/receive/address_list_page.dart b/lib/src/screens/receive/address_list_page.dart index 5f6794715..abcf9e1e1 100644 --- a/lib/src/screens/receive/address_list_page.dart +++ b/lib/src/screens/receive/address_list_page.dart @@ -1,11 +1,13 @@ +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; import 'package:cake_wallet/src/screens/receive/widgets/address_list.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; -class AddressListPage extends BasePage { +class AddressListPage extends StatelessWidget { AddressListPage(this.addressListViewModel); final WalletAddressListViewModel addressListViewModel; @@ -14,18 +16,30 @@ class AddressListPage extends BasePage { String get title => S.current.accounts_subaddresses; @override - Widget body(BuildContext context) { - return SingleChildScrollView( - child: Column( - children: [ - AddressList( - addressListViewModel: addressListViewModel, - onSelect: (String address) async { - Navigator.of(context).pop(address); - }, + Widget build(BuildContext context) { + return Column( + children: [ + ModalTopBar( + title: S.of(context).accounts_subaddresses, + leadingIcon: Icon(Icons.arrow_back_ios_new), + onLeadingPressed: Navigator.of(context).pop, + ), + Expanded( + child: SingleChildScrollView( + controller: ModalScrollController.of(context), + child: Column( + children: [ + AddressList( + addressListViewModel: addressListViewModel, + onSelect: (String address) async { + Navigator.of(context).pop(address); + }, + ), + ], + ), ), - ], - ), + ), + ], ); } } diff --git a/lib/src/screens/receive/anonpay_invoice_page.dart b/lib/src/screens/receive/anonpay_invoice_page.dart index b883d5fe0..f71488b4d 100644 --- a/lib/src/screens/receive/anonpay_invoice_page.dart +++ b/lib/src/screens/receive/anonpay_invoice_page.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart'; +import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/present_receive_option_picker.dart'; @@ -21,7 +22,7 @@ import 'package:cake_wallet/src/widgets/trail_button.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -53,10 +54,13 @@ class AnonPayInvoicePage extends BasePage { bool get extendBodyBehindAppBar => true; @override - AppBarStyle get appBarStyle => AppBarStyle.transparent; + AppBarStyle get appBarStyle => + FeatureFlag.hasNewUiExtraPages ? AppBarStyle.completelyTransparent : AppBarStyle.transparent; @override - void onClose(BuildContext context) => Navigator.popUntil(context, (route) => route.isFirst); + void onClose(BuildContext context) => FeatureFlag.hasNewUiExtraPages + ? Navigator.of(context, rootNavigator: true).pop() + : Navigator.popUntil(context, (route) => route.isFirst); @override Widget middle(BuildContext context) => PresentReceiveOptionPicker( diff --git a/lib/src/screens/receive/widgets/copy_link_item.dart b/lib/src/screens/receive/widgets/copy_link_item.dart index 47b6dddd2..be36f73e8 100644 --- a/lib/src/screens/receive/widgets/copy_link_item.dart +++ b/lib/src/screens/receive/widgets/copy_link_item.dart @@ -1,8 +1,8 @@ import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/utils/share_util.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:share_plus/share_plus.dart'; class CopyLinkItem extends StatelessWidget { const CopyLinkItem({super.key, required this.url, required this.title}); @@ -40,7 +40,7 @@ class CopyLinkItem extends StatelessWidget { highlightColor: Colors.transparent, splashColor: Colors.transparent, iconSize: 25, - onPressed: () => Share.share(url), + onPressed: () => ShareUtil.share(text: url, context: context), icon: Icon( Icons.share, color: Theme.of(context).colorScheme.primary, diff --git a/lib/src/screens/receive/widgets/currency_input_field.dart b/lib/src/screens/receive/widgets/currency_input_field.dart index 2a061c4d0..a5b0800e2 100644 --- a/lib/src/screens/receive/widgets/currency_input_field.dart +++ b/lib/src/screens/receive/widgets/currency_input_field.dart @@ -184,7 +184,7 @@ class CurrencyAmountTextField extends StatelessWidget { inputFormatters: [ FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]')), ], - hintText: hintText ?? '0.0000', + hintText: hintText ?? (selectedCurrencyDecimals == 0 ? '0' : '0.0000'), fillColor: fillColor, textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( fontSize: 16, @@ -198,8 +198,13 @@ class CurrencyAmountTextField extends StatelessWidget { ), validator: isAmountEditable ? currencyValueValidator : null, onChanged: (value) { - final sanitized = + var sanitized = value.replaceAll(',', '.').withMaxDecimals(selectedCurrencyDecimals); + + if (selectedCurrencyDecimals == 0) { + sanitized = sanitized.replaceAll('.', ''); + } + if (sanitized != amountController.text) { // Update text while preserving a sane cursor position to avoid auto-selection amountController.value = amountController.value.copyWith( diff --git a/lib/src/screens/receive/widgets/qr_widget.dart b/lib/src/screens/receive/widgets/qr_widget.dart index 6a40850b6..5c2868c69 100644 --- a/lib/src/screens/receive/widgets/qr_widget.dart +++ b/lib/src/screens/receive/widgets/qr_widget.dart @@ -1,4 +1,3 @@ -import 'package:cake_wallet/core/payment_uris.dart'; import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/routes.dart'; @@ -15,6 +14,7 @@ import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -108,6 +108,7 @@ class QRWidget extends StatelessWidget { ), if (addressListViewModel.isPayjoinUnavailable && !addressListViewModel.isSilentPayments && + !addressListViewModel.isLightning && !addressListViewModel.isBitcoinViewOnly) ...[ GestureDetector( onTap: () => _onPayjoinInactivePressed(context), @@ -155,8 +156,7 @@ class QRWidget extends StatelessWidget { ), ), ], - if (addressListViewModel.payjoinEndpoint.isNotEmpty && - !addressListViewModel.isSilentPayments) ...[ + if (addressListViewModel.isPayjoinAvailable) ...[ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -204,7 +204,7 @@ class QRWidget extends StatelessWidget { borderWidth: 0.0, selectedCurrency: _currencyName, selectedCurrencyDecimals: - addressListViewModel.selectedCurrency.decimals, + addressListViewModel.selectedCurrencyDecimals, amountFocusNode: amountTextFieldFocusNode, amountController: amountController, padding: EdgeInsets.only(top: 20, left: _width / 4), @@ -255,8 +255,7 @@ class QRWidget extends StatelessWidget { ), Observer( builder: (_) => Offstage( - offstage: addressListViewModel.payjoinEndpoint.isEmpty || - addressListViewModel.isSilentPayments, + offstage: !addressListViewModel.isPayjoinAvailable, child: Padding( padding: EdgeInsets.only(top: 12), child: PrimaryImageButton( @@ -283,6 +282,7 @@ class QRWidget extends StatelessWidget { String get _currencyName { if (addressListViewModel.selectedCurrency is CryptoCurrency) { + if (addressListViewModel.useSatoshi) return "SATS"; return (addressListViewModel.selectedCurrency as CryptoCurrency).title.toUpperCase(); } return addressListViewModel.selectedCurrency.name.toUpperCase(); diff --git a/lib/src/screens/rescan/rescan_page.dart b/lib/src/screens/rescan/rescan_page.dart index 5c60a7d23..78fd248b8 100644 --- a/lib/src/screens/rescan/rescan_page.dart +++ b/lib/src/screens/rescan/rescan_page.dart @@ -1,66 +1,76 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/main.dart'; +import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/view_model/rescan_view_model.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/blockchain_height_widget.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; -class RescanPage extends BasePage { - RescanPage(this._rescanViewModel) - : _blockchainHeightWidgetKey = GlobalKey(); +class RescanPage extends StatefulWidget { + RescanPage(this._rescanViewModel); - @override - String get title => - _rescanViewModel.isSilentPaymentsScan ? S.current.silent_payments_scanning : S.current.rescan; - final GlobalKey _blockchainHeightWidgetKey; final RescanViewModel _rescanViewModel; @override - Widget body(BuildContext context) { + State createState() => _RescanPageState(); +} + +class _RescanPageState extends State { + final TextEditingController _heightController = TextEditingController(); + + + @override + Widget build(BuildContext context) { Widget child; - if (_rescanViewModel.wallet.type != WalletType.decred) { + if (widget._rescanViewModel.wallet.type != WalletType.decred) { child = Padding( padding: EdgeInsets.only(left: 24, right: 24, bottom: 24), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Observer( - builder: (_) => BlockchainHeightWidget( - key: _blockchainHeightWidgetKey, - onHeightOrDateEntered: (value) => _rescanViewModel.isButtonEnabled = value, - isSilentPaymentsScan: _rescanViewModel.isSilentPaymentsScan, - isMwebScan: _rescanViewModel.isMwebScan, - doSingleScan: _rescanViewModel.doSingleScan, - hasDatePicker: !_rescanViewModel.isMwebScan, // disable date picker for mweb for now - toggleSingleScan: () => - _rescanViewModel.doSingleScan = !_rescanViewModel.doSingleScan, - walletType: _rescanViewModel.wallet.type, - bitcoinMempoolAPIEnabled: _rescanViewModel.isBitcoinMempoolAPIEnabled, + builder: (_) => SingleChildScrollView( + controller: ModalScrollController.of(context), + child: BlockchainHeightWidget( + // key: _blockchainHeightWidgetKey, + onHeightOrDateEntered: (value) => widget._rescanViewModel.isButtonEnabled = value, + isSilentPaymentsScan: widget._rescanViewModel.isSilentPaymentsScan, + isMwebScan: widget._rescanViewModel.isMwebScan, + doSingleScan: widget._rescanViewModel.doSingleScan, + hasDatePicker: !widget._rescanViewModel.isMwebScan, + // disable date picker for mweb for now + toggleSingleScan: () => + widget._rescanViewModel.doSingleScan = !widget._rescanViewModel.doSingleScan, + walletType: widget._rescanViewModel.wallet.type, + heightController: _heightController, + bitcoinMempoolAPIEnabled: widget._rescanViewModel.isBitcoinMempoolAPIEnabled, + ), ), ), Observer( builder: (_) => LoadingPrimaryButton( - isLoading: _rescanViewModel.state == RescanWalletState.rescaning, + isLoading: widget._rescanViewModel.state == RescanWalletState.rescaning, text: S.of(context).rescan, onPressed: () async { - if (_rescanViewModel.isSilentPaymentsScan) { + if (widget._rescanViewModel.isSilentPaymentsScan) { return _toggleSilentPaymentsScanning(context); } - _rescanViewModel.rescanCurrentWallet( - restoreHeight: _blockchainHeightWidgetKey.currentState!.height); + widget._rescanViewModel.rescanCurrentWallet( + restoreHeight: int.tryParse(_heightController.text) ?? 0); Navigator.of(context).pop(); }, color: Theme.of(context).colorScheme.primary, textColor: Theme.of(context).colorScheme.onPrimary, - isDisabled: !_rescanViewModel.isButtonEnabled, + isDisabled: !widget._rescanViewModel.isButtonEnabled, ), ) ], @@ -76,10 +86,10 @@ class RescanPage extends BasePage { Spacer(), Observer( builder: (_) => LoadingPrimaryButton( - isLoading: _rescanViewModel.state == RescanWalletState.rescaning, + isLoading: widget._rescanViewModel.state == RescanWalletState.rescaning, text: S.of(context).rescan, onPressed: () async { - await _rescanViewModel.rescanCurrentWallet(restoreHeight: 0); + await widget._rescanViewModel.rescanCurrentWallet(restoreHeight: 0); Navigator.of(context).pop(); }, color: Theme.of(context).colorScheme.primary, @@ -93,18 +103,39 @@ class RescanPage extends BasePage { } return GestureDetector( behavior: HitTestBehavior.opaque, - onTap: () => FocusScope.of(context).unfocus(), - child: child, + // onTap: () => FocusScope.of(context).unfocus(), + child: Material( + color: Theme.of(context).colorScheme.surface, + child: KeyboardHideOverlay( + child: SafeArea( + child: Column( + children: [ + ModalTopBar( + title: widget._rescanViewModel.isSilentPaymentsScan + ? S.current.silent_payments_scanning + : S.current.rescan, + leadingIcon: Icon(Icons.arrow_back_ios_new), + onLeadingPressed: Navigator.of(context).pop), + Expanded( + child: Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: child, + )), + ], + ), + ), + ), + ), ); } Future _toggleSilentPaymentsScanning(BuildContext context) async { - final height = _blockchainHeightWidgetKey.currentState!.height; + final height = int.parse(_heightController.text); Navigator.of(context).pop(); final needsToSwitch = - await bitcoin!.getNodeIsElectrsSPEnabled(_rescanViewModel.wallet) == false; + await bitcoin!.getNodeIsElectrsSPEnabled(widget._rescanViewModel.wallet) == false; if (needsToSwitch) { return showPopUp( @@ -117,13 +148,13 @@ class RescanPage extends BasePage { actionRightButton: () async { Navigator.of(_dialogContext).pop(); - _rescanViewModel.rescanCurrentWallet(restoreHeight: height); + widget._rescanViewModel.rescanCurrentWallet(restoreHeight: height); }, actionLeftButton: () => Navigator.of(_dialogContext).pop(), ), ); } - _rescanViewModel.rescanCurrentWallet(restoreHeight: height); + widget._rescanViewModel.rescanCurrentWallet(restoreHeight: height); } } diff --git a/lib/src/screens/restore/restore_options_page.dart b/lib/src/screens/restore/restore_options_page.dart index c56220baf..f9a62a447 100644 --- a/lib/src/screens/restore/restore_options_page.dart +++ b/lib/src/screens/restore/restore_options_page.dart @@ -23,9 +23,9 @@ class RestoreOptionsPage extends BasePage { @override Widget body(BuildContext context) => _RestoreOptionsBody( - isNewInstall: isNewInstall, - themeType: currentTheme.type, - ); + isNewInstall: isNewInstall, + themeType: currentTheme.type, + ); } class _RestoreOptionsBody extends StatefulWidget { @@ -93,7 +93,6 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { image: imageRestoreCupcake, title: S.of(context).restore_title_from_cupcake, description: S.of(context).restore_description_from_cupcake, - tag: S.of(context).new_tag, ), ), Padding( @@ -144,21 +143,21 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.current.error, - alertContent: error, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.current.error, + alertContent: error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); } }); } Future _onScanQRCode(BuildContext context) async { final isCameraPermissionGranted = - await PermissionHandler.checkPermission(Permission.camera, context); + await PermissionHandler.checkPermission(Permission.camera, context); if (!isCameraPermissionGranted) return; try { @@ -181,4 +180,4 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> { _showQRScanError(context, e.toString()); } } -} +} \ No newline at end of file diff --git a/lib/src/screens/restore/wallet_restore_from_keys_form.dart b/lib/src/screens/restore/wallet_restore_from_keys_form.dart index bdc74377a..72b9ff6ae 100644 --- a/lib/src/screens/restore/wallet_restore_from_keys_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_keys_form.dart @@ -7,7 +7,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/blockchain_height_widget.dart'; import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/core/wallet_name_validator.dart'; -import 'package:cake_wallet/entities/generate_name.dart'; +import 'package:cw_core/generate_name.dart'; import 'package:flutter/services.dart'; class WalletRestoreFromKeysForm extends StatefulWidget { diff --git a/lib/src/screens/restore/wallet_restore_from_seed_form.dart b/lib/src/screens/restore/wallet_restore_from_seed_form.dart index 166a46cd1..c8a1f53f0 100644 --- a/lib/src/screens/restore/wallet_restore_from_seed_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_seed_form.dart @@ -1,5 +1,5 @@ import 'package:cake_wallet/core/wallet_name_validator.dart'; -import 'package:cake_wallet/entities/generate_name.dart'; +import 'package:cw_core/generate_name.dart'; import 'package:cake_wallet/entities/seed_type.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; @@ -171,7 +171,7 @@ class WalletRestoreFromSeedFormState extends State { icon: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6.0), + borderRadius: BorderRadius.circular(12.0), color: Theme.of(context).colorScheme.surface, ), width: 34, diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index 14e2d5a74..35c76ba3c 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -490,12 +490,10 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody> controller: _tabController, children: [ SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 8), child: _buildWalletRestoreFromSeedTab(), ), if (_hasKeysTab) SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 8), child: _buildWalletRestoreFromKeysTab(), ), ], diff --git a/lib/src/screens/seed/seed_verification/seed_verification_page.dart b/lib/src/screens/seed/seed_verification/seed_verification_page.dart index 1ff496d06..0daefc70d 100644 --- a/lib/src/screens/seed/seed_verification/seed_verification_page.dart +++ b/lib/src/screens/seed/seed_verification/seed_verification_page.dart @@ -32,6 +32,7 @@ class SeedVerificationPage extends BasePage { walletSeedViewModel.verificationIndices.isEmpty ? SeedVerificationSuccessView( key: ValueKey('seed_verification_success_view_page'), + walletType: walletSeedViewModel.walletType, currentTheme: currentTheme, ) : SeedVerificationStepView( diff --git a/lib/src/screens/seed/seed_verification/seed_verification_success_view.dart b/lib/src/screens/seed/seed_verification/seed_verification_success_view.dart index fa0dc8e3b..2c38f4add 100644 --- a/lib/src/screens/seed/seed_verification/seed_verification_success_view.dart +++ b/lib/src/screens/seed/seed_verification/seed_verification_success_view.dart @@ -1,13 +1,17 @@ import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/themes/core/material_base_theme.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; class SeedVerificationSuccessView extends StatelessWidget { - const SeedVerificationSuccessView({required this.currentTheme, super.key}); + const SeedVerificationSuccessView( + {required this.currentTheme, super.key, required this.walletType}); final MaterialThemeBase currentTheme; + final WalletType walletType; @override Widget build(BuildContext context) { @@ -18,7 +22,7 @@ class SeedVerificationSuccessView extends StatelessWidget { ConstrainedBox( constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.3), child: CakeImageWidget( - height: 200, + height: 128, imageUrl: currentTheme.isDark ? 'assets/images/seed_verified_dark.png' : 'assets/images/seed_verified_light.png', @@ -62,9 +66,13 @@ class SeedVerificationSuccessView extends StatelessWidget { PrimaryButton( key: ValueKey('wallet_seed_page_open_wallet_button_key'), onPressed: () { - Navigator.of(context).popUntil((route) => route.isFirst); + if (walletType == WalletType.bitcoin) { + Navigator.of(context).pushNamed(Routes.lightningUsernamePage, arguments: true); + } else { + Navigator.of(context).popUntil((route) => route.isFirst); + } }, - text: S.current.open_wallet, + text: (walletType == WalletType.bitcoin) ? S.current.continue_text : S.current.open_wallet, color: Theme.of(context).colorScheme.primary, textColor: Theme.of(context).colorScheme.onPrimary, ), diff --git a/lib/src/screens/seed/wallet_seed_page.dart b/lib/src/screens/seed/wallet_seed_page.dart index b73c13c14..f0794f98a 100644 --- a/lib/src/screens/seed/wallet_seed_page.dart +++ b/lib/src/screens/seed/wallet_seed_page.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/src/widgets/warning_box_widget.dart'; import 'package:cake_wallet/utils/clipboard_util.dart'; import 'package:cake_wallet/utils/share_util.dart'; import 'package:cake_wallet/utils/show_bar.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -20,7 +21,7 @@ class WalletSeedPage extends BasePage { WalletSeedPage(this.walletSeedViewModel, {required this.isNewWalletCreated}); @override - String get title => '${walletSeedViewModel.walletType} ${S.current.seed_title}'; + String get title => '${walletTypeToString(walletSeedViewModel.walletType)} ${S.current.seed_title}'; final bool isNewWalletCreated; final WalletSeedViewModel walletSeedViewModel; @@ -57,7 +58,8 @@ class WalletSeedPage extends BasePage { @override Widget body(BuildContext context) { return PopScope( - canPop: false, + //FIXME i can't figure out why this was here? it caused back gesture to break + canPop: true, child: Container( alignment: Alignment.center, child: Column( diff --git a/lib/src/screens/select_options_page.dart b/lib/src/screens/select_options_page.dart index bcfb9df98..630041655 100644 --- a/lib/src/screens/select_options_page.dart +++ b/lib/src/screens/select_options_page.dart @@ -2,7 +2,7 @@ import 'package:cake_wallet/core/selectable_option.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/provider_optoin_tile.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; import 'package:cake_wallet/themes/core/material_base_theme.dart'; import 'package:flutter/material.dart'; diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 68e59f5d7..fc98e9eca 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -1,13 +1,13 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/core/auth_service.dart'; -import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/entities/template.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; -import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; @@ -22,7 +22,7 @@ import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_shee import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/simple_checkbox.dart'; import 'package:cake_wallet/src/widgets/template_tile.dart'; import 'package:cake_wallet/src/widgets/trail_button.dart'; @@ -32,12 +32,12 @@ import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/payment/payment_view_model.dart'; import 'package:cake_wallet/view_model/send/output.dart'; -import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/view_model/send/send_view_model.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; +import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:keyboard_actions/keyboard_actions.dart'; @@ -98,8 +98,7 @@ class SendPage extends BasePage { size: 16, ); final _closeButton = currentTheme.isDark ? closeButtonImageDarkTheme : closeButtonImage; - - bool isMobileView = responsiveLayoutUtil.shouldRenderMobileUI; + final isMobileView = responsiveLayoutUtil.shouldRenderMobileUI; return MergeSemantics( child: SizedBox( @@ -150,27 +149,25 @@ class SendPage extends BasePage { @override Widget trailing(context) => Observer( - builder: (_) { - return sendViewModel.isBatchSending - ? TrailButton( - caption: S.of(context).remove, - onPressed: () { - var pageToJump = (controller.page?.round() ?? 0) - 1; - pageToJump = pageToJump > 0 ? pageToJump : 0; - final output = _defineCurrentOutput(); - sendViewModel.removeOutput(output); - controller.jumpToPage(pageToJump); - }, - ) - : TrailButton( - caption: S.of(context).clear, - onPressed: () { - final output = _defineCurrentOutput(); - _formKey.currentState?.reset(); - output.reset(); - }, - ); - }, + builder: (_) => sendViewModel.isBatchSending + ? TrailButton( + caption: S.of(context).remove, + onPressed: () { + var pageToJump = (controller.page?.round() ?? 0) - 1; + pageToJump = pageToJump > 0 ? pageToJump : 0; + final output = _defineCurrentOutput(); + sendViewModel.removeOutput(output); + controller.jumpToPage(pageToJump); + }, + ) + : TrailButton( + caption: S.of(context).clear, + onPressed: () { + final output = _defineCurrentOutput(); + _formKey.currentState?.reset(); + output.reset(); + }, + ), ); @override @@ -183,7 +180,7 @@ class SendPage extends BasePage { return Observer(builder: (_) { List sendCards = []; List keyboardActions = []; - for (var output in sendViewModel.outputs) { + for (final output in sendViewModel.outputs) { final isCurrent = sendViewModel.outputs.indexOf(output) == value; sendCards.add( SendCard( @@ -267,7 +264,7 @@ class SendPage extends BasePage { dotColor: Theme.of(context) .colorScheme .primary - .withOpacity(0.4), + .withAlpha(100), activeDotColor: Theme.of(context).colorScheme.primary, ), ), @@ -418,12 +415,11 @@ class SendPage extends BasePage { return; } - final notValidItems = sendViewModel.outputs - .where( - (item) => item.address.isEmpty || item.cryptoAmount.isEmpty) - .toList(); + final hasInvalidItems = sendViewModel.outputs.any((item) => + item.address.isEmpty || + (item.cryptoAmount.isEmpty && !item.sendAll)); - if (notValidItems.isNotEmpty) { + if (hasInvalidItems) { showErrorValidationAlert(context); return; } @@ -500,9 +496,7 @@ class SendPage extends BasePage { BuildContext? loadingBottomSheetContext; void _setEffects(BuildContext context) { - if (_effectsInstalled) { - return; - } + if (_effectsInstalled) return; if (sendViewModel.isElectrumWallet) { bitcoin!.updateFeeRates(sendViewModel.wallet); @@ -529,16 +523,14 @@ class SendPage extends BasePage { (_) { showPopUp( context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - key: ValueKey('send_page_send_failure_dialog_key'), - buttonKey: ValueKey('send_page_send_failure_dialog_button_key'), - alertTitle: S.of(context).error, - alertContent: state.error, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop(), - ); - }, + builder: (context) => AlertWithOneAction( + key: ValueKey('send_page_send_failure_dialog_key'), + buttonKey: ValueKey('send_page_send_failure_dialog_button_key'), + alertTitle: S.of(context).error, + alertContent: state.error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(), + ), ); }, ); @@ -557,7 +549,7 @@ class SendPage extends BasePage { showModalBottomSheet( context: context, isDismissible: false, - builder: (BuildContext context) { + builder: (context) { loadingBottomSheetContext = context; return LoadingBottomSheet( titleText: S.of(context).generating_transaction, @@ -577,34 +569,37 @@ class SendPage extends BasePage { isScrollControlled: true, builder: (BuildContext bottomSheetContext) { return Observer( - builder: (_) => ConfirmSendingBottomSheet( - key: ValueKey('send_page_confirm_sending_bottom_sheet_key'), - titleText: S.of(bottomSheetContext).confirm_transaction, - accessibleNavigationModeSlideActionButtonText: - S.of(bottomSheetContext).send, - footerType: FooterType.slideActionButton, - isSlideActionEnabled: sendViewModel.isReadyForSend, - walletType: sendViewModel.walletType, - titleIconPath: sendViewModel.selectedCryptoCurrency.iconPath, - currency: sendViewModel.selectedCryptoCurrency, - amount: S.of(bottomSheetContext).send_amount, - amountValue: sendViewModel.pendingTransaction!.amountFormatted, - fiatAmountValue: sendViewModel.pendingTransactionFiatAmountFormatted, - fee: isEVMCompatibleChain(sendViewModel.walletType) - ? S.of(bottomSheetContext).send_estimated_fee - : S.of(bottomSheetContext).send_fee, - feeValue: sendViewModel.pendingTransaction!.feeFormatted, - feeFiatAmount: - sendViewModel.pendingTransactionFeeFiatAmountFormatted, - outputs: sendViewModel.outputs, - onSlideActionComplete: () async { - Navigator.of(bottomSheetContext).pop(true); - sendViewModel.commitTransaction(context); - }, - change: sendViewModel.pendingTransaction!.change, - isOpenCryptoPay: sendViewModel.ocpRequest != null, - ), - ); + builder: (_) => ConfirmSendingBottomSheet( + key: ValueKey('send_page_confirm_sending_bottom_sheet_key'), + titleText: S.of(bottomSheetContext).confirm_transaction, + accessibleNavigationModeSlideActionButtonText: S.of(bottomSheetContext).send, + footerType: FooterType.slideActionButton, + isSlideActionEnabled: sendViewModel.isReadyForSend, + walletType: sendViewModel.walletType, + titleIconPath: sendViewModel.selectedCryptoCurrency.iconPath, + currency: sendViewModel.selectedCryptoCurrency, + amount: S.of(bottomSheetContext).send_amount, + amountValue: sendViewModel.amountParsingProxy.getDisplayCryptoAmount( + sendViewModel.pendingTransaction!.amountFormatted, + sendViewModel.selectedCryptoCurrency), + fiatAmountValue: sendViewModel.pendingTransactionFiatAmountFormatted, + fee: isEVMCompatibleChain(sendViewModel.walletType) + ? S.of(bottomSheetContext).send_estimated_fee + : S.of(bottomSheetContext).send_fee, + feeValue: "${sendViewModel.amountParsingProxy.getDisplayCryptoAmount( + sendViewModel.pendingTransaction!.feeFormattedValue, + sendViewModel.selectedCryptoCurrency)} ${sendViewModel.amountParsingProxy + .getCryptoSymbol(sendViewModel.wallet.currency)}", + feeFiatAmount: sendViewModel.pendingTransactionFeeFiatAmountFormatted, + outputs: sendViewModel.outputs, + onSlideActionComplete: () async { + Navigator.of(bottomSheetContext).pop(true); + sendViewModel.commitTransaction(context); + }, + change: sendViewModel.pendingTransaction!.change, + isOpenCryptoPay: sendViewModel.ocpRequest != null, + amountParsingProxy: sendViewModel.amountParsingProxy, + ),); }, ); @@ -615,9 +610,7 @@ class SendPage extends BasePage { if (state is TransactionCommitted) { WidgetsBinding.instance.addPostFrameCallback((_) async { - if (!context.mounted) { - return; - } + if (!context.mounted) return; newContactAddress = newContactAddress ?? sendViewModel.newContactAddress(); @@ -788,37 +781,34 @@ class SendPage extends BasePage { } Output _defineCurrentOutput() { - if (controller.page == null) { - throw Exception('Controller page is null'); - } + if (controller.page == null) throw Exception('Controller page is null'); final itemCount = controller.page!.round(); return sendViewModel.outputs[itemCount]; } - void showErrorValidationAlert(BuildContext context) async { - await showPopUp( + void showErrorValidationAlert(BuildContext context) => showPopUp( context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.of(context).error, - alertContent: 'Please, check receiver forms', - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); - } + builder: (context) => AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: 'Please, check receiver forms', + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(), + ), + ); bool isRegularElectrumAddress(String address) { final supportedTypes = [CryptoCurrency.btc, CryptoCurrency.ltc, CryptoCurrency.bch]; final excludedPatterns = [ RegExp(AddressValidator.silentPaymentAddressPatternMainnet), RegExp(AddressValidator.silentPaymentAddressPatternTestnet), - RegExp(AddressValidator.mWebAddressPattern) + RegExp(AddressValidator.mWebAddressPattern), + RegExp(AddressValidator.bolt11InvoiceMatcher), ]; final trimmed = address.trim(); bool isValid = false; - for (var type in supportedTypes) { + for (final type in supportedTypes) { final addressPattern = AddressValidator.getAddressFromStringPattern(type); if (addressPattern != null) { final regex = RegExp('^$addressPattern\$'); @@ -829,23 +819,16 @@ class SendPage extends BasePage { } } - for (var pattern in excludedPatterns) { - if (pattern.hasMatch(trimmed)) { - return false; - } + for (final pattern in excludedPatterns) { + if (pattern.hasMatch(trimmed)) return false; } return isValid; } String _sendButtonText(BuildContext context) { - if (!sendViewModel.isReadyForSend) { - return S.of(context).synchronizing; - } - if (sendViewModel.payjoinUri != null) { - return S.of(context).send_payjoin; - } else { - return S.of(context).send; - } + if (!sendViewModel.isReadyForSend) return S.of(context).synchronizing; + if (sendViewModel.payjoinUri != null) return S.of(context).send_payjoin; + return S.of(context).send; } } diff --git a/lib/src/screens/send/send_template_page.dart b/lib/src/screens/send/send_template_page.dart index 556d10d2e..20605c211 100644 --- a/lib/src/screens/send/send_template_page.dart +++ b/lib/src/screens/send/send_template_page.dart @@ -6,7 +6,7 @@ import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/view_model/send/send_template_view_model.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; import 'package:cake_wallet/src/screens/send/widgets/send_template_card.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart'; diff --git a/lib/src/screens/send/widgets/extract_address_from_parsed.dart b/lib/src/screens/send/widgets/extract_address_from_parsed.dart index b681e360c..ae08d5878 100644 --- a/lib/src/screens/send/widgets/extract_address_from_parsed.dart +++ b/lib/src/screens/send/widgets/extract_address_from_parsed.dart @@ -6,9 +6,9 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'choose_yat_address_alert.dart'; Future extractAddressFromParsed( - BuildContext context, + BuildContext? context, ParsedAddress parsedAddress) async { - if (!context.mounted) return parsedAddress.addresses.first; + if (context == null || !context.mounted) return parsedAddress.addresses.first; var title = ''; var content = ''; @@ -83,6 +83,11 @@ Future extractAddressFromParsed( content = S.of(context).extracted_address_content('${parsedAddress.name} (BIP-353)'); address = parsedAddress.addresses.first; break; + case ParseFrom.lnurlpay: + title = S.of(context).address_detected; + content = S.of(context).extracted_address_content('${parsedAddress.name} (Lightning)'); + address = parsedAddress.addresses.first; + break; case ParseFrom.yatRecord: if (parsedAddress.name.isEmpty) { title = S.of(context).yat_error; diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 77a6e795b..0472c0e52 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -155,6 +155,11 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin _handlePaymentFlow(String uri, PaymentRequest paymentRequest) async { if (uri.contains('@') || paymentRequest.address.contains('@')) return; + if (OpenCryptoPayService.isOpenCryptoPayQR(uri)) { + sendViewModel.createOpenCryptoPayTransaction(uri); + return; + } + try { final result = await paymentViewModel.processAddress(uri); @@ -588,15 +593,14 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin GestureDetector( key: ValueKey('send_page_select_fee_priority_button_key'), @@ -802,9 +806,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin Padding( padding: EdgeInsets.only(top: 14), @@ -942,8 +942,14 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin output.fiatAmount, (String amount) { @@ -987,8 +994,11 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin S.current.display_settings; final DisplaySettingsViewModel _displaySettingsViewModel; @override - Widget body(BuildContext context) { - return SingleChildScrollView( - child: Observer(builder: (_) { - return Container( - padding: EdgeInsets.only(top: 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SettingsSwitcherCell( - title: S.of(context).apps, - value: _displaySettingsViewModel.shouldShowMarketPlaceInDashboard, - onValueChange: (_, bool value) { - _displaySettingsViewModel.setShouldShowMarketPlaceInDashbaord(value); - }, - ), - SettingsSwitcherCell( - title: S.of(context).show_address_book_popup, - value: _displaySettingsViewModel.showAddressBookPopup, - onValueChange: (_, bool value) { - _displaySettingsViewModel.setShowAddressBookPopup(value); - }, - ), - if (getIt.get().wallet?.type == WalletType.zcash) - SettingsSwitcherCell( - title: S.of(context).show_zcash_card, - value: _displaySettingsViewModel.showZcashCard, - onValueChange: (_, bool value) { - _displaySettingsViewModel.setShowZcashCard(value); - }, - ), - SettingsPickerCell( - title: S.current.sync_status_display_mode, - items: SyncStatusDisplayMode.values.toList(), - selectedItem: _displaySettingsViewModel.syncStatusDisplayMode, - onItemSelected: (SyncStatusDisplayMode mode) => - _displaySettingsViewModel.setSyncStatusDisplayMode(mode), - displayItem: (SyncStatusDisplayMode mode) => mode.title, - isGridView: false, - ), - //if (!isHaven) it does not work correctly - if (!_displaySettingsViewModel.disabledFiatApiMode) - SettingsPickerCell( - title: S.of(context).settings_currency, - searchHintText: S.of(context).search_currency, - items: FiatCurrency.all, - selectedItem: _displaySettingsViewModel.fiatCurrency, - onItemSelected: (FiatCurrency currency) => - _displaySettingsViewModel.setFiatCurrency(currency), - images: FiatCurrency.all - .map((e) => Image.asset("assets/images/flags/${e.countryCode}.png")) - .toList(), - isGridView: true, - matchingCriteria: (FiatCurrency currency, String searchText) { - return currency.title.toLowerCase().contains(searchText) || - currency.fullName.toLowerCase().contains(searchText); - }, - ), - SettingsPickerCell( - title: S.of(context).settings_change_language, - searchHintText: S.of(context).search_language, - items: LanguageService.list.keys.toList(), - displayItem: (dynamic code) { - return LanguageService.list[code] ?? ''; - }, - selectedItem: _displaySettingsViewModel.languageCode, - onItemSelected: _displaySettingsViewModel.onLanguageSelected, - images: LanguageService.list.keys - .map((e) => Image.asset( - "assets/images/flags/${LanguageService.localeCountryCode[e]}.png")) - .toList(), - matchingCriteria: (String code, String searchText) { - return LanguageService.list[code]?.toLowerCase().contains(searchText) ?? false; - }, - ), - - if (FeatureFlag.customBackgroundEnabled) - StandardListRow( - title: "Custom background", - isSelected: false, - onTap: (_) => _pickImage(context), - ), - - if (responsiveLayoutUtil.shouldRenderMobileUI && DeviceInfo.instance.isMobile) ...[ - SizedBox(height: 24), - Padding( - padding: const EdgeInsets.only(left: 24), - child: Text( - S.current.appearance, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w400, - fontSize: 14, - height: 22 / 14, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ), - SizedBox(height: 12), - Container( - margin: EdgeInsets.symmetric(horizontal: 20), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainer, - borderRadius: BorderRadius.circular(16), - ), - child: Column( - children: [ - SettingsChoicesCell( - ChoicesListItem( - title: "", - items: ThemeMode.values, - selectedItem: _displaySettingsViewModel.themeMode, - onItemSelected: (ThemeMode themeMode) => - _displaySettingsViewModel.setThemeMode(themeMode), - displayItem: (ThemeMode themeMode) { - return themeMode.name[0].toUpperCase() + - themeMode.name.substring(1).toLowerCase(); + Widget build(BuildContext context) { + return Container( + color: Theme.of(context).colorScheme.surface, + child: SafeArea( + top: false, + child: Column( + children: [ + ModalTopBar(title: S.of(context).display_settings,leadingIcon: Icon(Icons.arrow_back_ios_new),onLeadingPressed: () => Navigator.of(context).pop()), + Expanded( + child: SingleChildScrollView( + controller: ModalScrollController.of(context), + physics: ClampingScrollPhysics(), + child: Observer(builder: (_) { + return Container( + padding: EdgeInsets.only(top: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SettingsSwitcherCell( + title: S.of(context).apps, + value: _displaySettingsViewModel.shouldShowMarketPlaceInDashboard, + onValueChange: (_, bool value) { + _displaySettingsViewModel.setShouldShowMarketPlaceInDashbaord(value); }, ), - useGenericColor: false, - padding: EdgeInsets.all(12), - ), - Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHigh, - borderRadius: BorderRadius.circular(16), + SettingsSwitcherCell( + title: S.of(context).show_address_book_popup, + value: _displaySettingsViewModel.showAddressBookPopup, + onValueChange: (_, bool value) { + _displaySettingsViewModel.setShowAddressBookPopup(value); + }, ), - child: Column( - children: [ - Semantics( - label: S.of(context).color_theme, - child: SettingsThemeChoicesCell(_displaySettingsViewModel), + if (_displaySettingsViewModel.showZcashCardSetting) + SettingsSwitcherCell( + title: S.of(context).show_zcash_card, + value: _displaySettingsViewModel.showZcashCard, + onValueChange: (_, bool value) { + _displaySettingsViewModel.setShowZcashCard(value); + }, + ), + SettingsPickerCell( + title: S.current.sync_status_display_mode, + items: SyncStatusDisplayMode.values.toList(), + selectedItem: _displaySettingsViewModel.syncStatusDisplayMode, + onItemSelected: (SyncStatusDisplayMode mode) => + _displaySettingsViewModel.setSyncStatusDisplayMode(mode), + displayItem: (SyncStatusDisplayMode mode) => mode.title, + isGridView: false, + ), + if (_displaySettingsViewModel.showDisplayAmountsInSatoshiSetting) + SettingsPickerCell( + title: S.of(context).bitcoin_amount_display, + items: BitcoinAmountDisplayMode.all, + selectedItem: _displaySettingsViewModel.displayAmountsInSatoshi, + onItemSelected: _displaySettingsViewModel.setDisplayAmountsInSatoshi, + displayItem: (mode) => mode.title, + isGridView: false, + ), + //if (!isHaven) it does not work correctly + if (!_displaySettingsViewModel.disabledFiatApiMode) + SettingsPickerCell( + title: S.of(context).settings_currency, + searchHintText: S.of(context).search_currency, + items: FiatCurrency.all, + selectedItem: _displaySettingsViewModel.fiatCurrency, + onItemSelected: (FiatCurrency currency) => + _displaySettingsViewModel.setFiatCurrency(currency), + images: FiatCurrency.all + .map((e) => Image.asset("assets/images/flags/${e.countryCode}.png")) + .toList(), + isGridView: true, + matchingCriteria: (FiatCurrency currency, String searchText) { + return currency.title.toLowerCase().contains(searchText) || + currency.fullName.toLowerCase().contains(searchText); + }, + ), + SettingsPickerCell( + title: S.of(context).settings_change_language, + searchHintText: S.of(context).search_language, + items: LanguageService.list.keys.toList(), + displayItem: (dynamic code) { + return LanguageService.list[code] ?? ''; + }, + selectedItem: _displaySettingsViewModel.languageCode, + onItemSelected: _displaySettingsViewModel.onLanguageSelected, + images: LanguageService.list.keys + .map((e) => Image.asset( + "assets/images/flags/${LanguageService.localeCountryCode[e]}.png")) + .toList(), + matchingCriteria: (String code, String searchText) { + return LanguageService.list[code]?.toLowerCase().contains(searchText) ?? false; + }, + ), + + if (FeatureFlag.customBackgroundEnabled) + StandardListRow( + title: "Custom background", + isSelected: false, + onTap: (_) => _pickImage(context), + ), + + if (responsiveLayoutUtil.shouldRenderMobileUI && DeviceInfo.instance.isMobile) ...[ + SizedBox(height: 24), + Padding( + padding: const EdgeInsets.only(left: 24), + child: Text( + S.current.appearance, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w400, + fontSize: 14, + height: 22 / 14, + color: Theme.of(context).colorScheme.onSurface, + ), ), - ], - ), - ), - ], - ), - ), - ], - ], - ), - ); - }), + ), + SizedBox(height: 12), + Container( + margin: EdgeInsets.symmetric(horizontal: 20), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + SettingsChoicesCell( + ChoicesListItem( + title: "", + items: ThemeMode.values, + selectedItem: _displaySettingsViewModel.themeMode, + onItemSelected: (ThemeMode themeMode) => + _displaySettingsViewModel.setThemeMode(themeMode), + displayItem: (ThemeMode themeMode) { + return themeMode.name[0].toUpperCase() + + themeMode.name.substring(1).toLowerCase(); + }, + ), + useGenericColor: false, + padding: EdgeInsets.all(12), + ), + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + Semantics( + label: S.of(context).color_theme, + child: SettingsThemeChoicesCell(_displaySettingsViewModel), + ), + ], + ), + ), + ], + ), + ), + ], + ], + ), + ); + }), + ), + ), + ], + ), + ), ); } diff --git a/lib/src/screens/settings/manage_nodes_page.dart b/lib/src/screens/settings/manage_nodes_page.dart index 42313daaf..a0d2f0844 100644 --- a/lib/src/screens/settings/manage_nodes_page.dart +++ b/lib/src/screens/settings/manage_nodes_page.dart @@ -11,6 +11,7 @@ import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart'; import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; class ManageNodesPage extends BasePage { ManageNodesPage(this.isPow, {this.nodeListViewModel, this.powNodeListViewModel}) @@ -60,6 +61,7 @@ class ManageNodesPage extends BasePage { nodeListViewModel?.nodes.length ?? powNodeListViewModel!.nodes.length; return Flexible( child: SectionStandardList( + scrollController: ModalScrollController.of(context), sectionCount: 1, dividerPadding: EdgeInsets.symmetric(horizontal: 24), itemCounter: (int sectionIndex) => itemsCount, diff --git a/lib/src/screens/settings/mweb_settings.dart b/lib/src/screens/settings/mweb_settings.dart index e78fdf596..ffe6184b7 100644 --- a/lib/src/screens/settings/mweb_settings.dart +++ b/lib/src/screens/settings/mweb_settings.dart @@ -23,13 +23,13 @@ class MwebSettingsPage extends BasePage { padding: EdgeInsets.only(top: 10), child: Column( children: [ - SettingsSwitcherCell( - title: S.current.litecoin_mweb_display_card, - value: _mwebSettingsViewModel.mwebCardDisplay, - onValueChange: (_, bool value) { - _mwebSettingsViewModel.setMwebCardDisplay(value); - }, - ), + // SettingsSwitcherCell( + // title: S.current.litecoin_mweb_display, + // value: _mwebSettingsViewModel.mwebCardDisplay, + // onValueChange: (_, bool value) { + // _mwebSettingsViewModel.setMwebCardDisplay(value); + // }, + // ), SettingsSwitcherCell( title: S.current.litecoin_mweb_enable, value: _mwebSettingsViewModel.mwebEnabled, diff --git a/lib/src/screens/settings/other_settings_page.dart b/lib/src/screens/settings/other_settings_page.dart index 850f566b1..fb1059ecc 100644 --- a/lib/src/screens/settings/other_settings_page.dart +++ b/lib/src/screens/settings/other_settings_page.dart @@ -9,17 +9,13 @@ import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arro import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_version_cell.dart'; import 'package:cake_wallet/utils/feature_flag.dart'; +import 'package:cake_wallet/utils/share_util.dart'; import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart'; -import 'package:cw_core/node.dart'; -import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/db/sqlite.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:get_it/get_it.dart'; -import 'package:hive/hive.dart'; -import 'package:path/path.dart' as p; class OtherSettingsPage extends BasePage { OtherSettingsPage(this._otherSettingsViewModel) { @@ -84,6 +80,16 @@ class OtherSettingsPage extends BasePage { }, }), ), + if (_otherSettingsViewModel.walletType == WalletType.bitcoin) ...[ + SettingsCellWithArrow( + title: S.of(context).export_lightning_logs, + handler: onExportLNLog, + ), + SettingsCellWithArrow( + title: S.of(context).export_payjoin_logs, + handler: onExportPJLog, + ), + ], SettingsCellWithArrow( title: S.current.settings_terms_and_conditions, handler: (context) => Navigator.of(context).pushNamed(Routes.readDisclaimer), @@ -176,4 +182,20 @@ class OtherSettingsPage extends BasePage { ), ), ); + + Future onExportLNLog(BuildContext context) async { + final file = await _otherSettingsViewModel.getLightningLog(); + + if (file != null) { + await ShareUtil.shareFile(filePath: file.path, fileName: "Lightning.log", context: context); + } + } + + Future onExportPJLog(BuildContext context) async { + final file = await _otherSettingsViewModel.getPayjoinLog(); + + if (file != null) { + await ShareUtil.shareFile(filePath: file.path, fileName: "Payjoin.log", context: context); + } + } } diff --git a/lib/src/screens/settings/privacy_page.dart b/lib/src/screens/settings/privacy_page.dart index 73e069bee..8eb1f7851 100644 --- a/lib/src/screens/settings/privacy_page.dart +++ b/lib/src/screens/settings/privacy_page.dart @@ -49,6 +49,12 @@ class PrivacyPage extends BasePage { _privacySettingsViewModel.setExchangeApiMode(mode), ), ), + if (_privacySettingsViewModel.canUseLightning) + SettingsSwitcherCell( + title: S.of(context).enable_lightning, + value: _privacySettingsViewModel.useLightning, + onValueChange: (_, value) => _privacySettingsViewModel.setUseLightning(value), + ), if (_privacySettingsViewModel.canUsePayjoin) SettingsSwitcherCell( title: S.of(context).use_payjoin, @@ -95,13 +101,13 @@ class PrivacyPage extends BasePage { _privacySettingsViewModel.setDisableAutomaticExchangeStatusUpdates(value); }, ), - SettingsSwitcherCell( - title: S.current.disable_bulletin, - value: _privacySettingsViewModel.disableBulletin, - onValueChange: (BuildContext _, bool value) { - _privacySettingsViewModel.setDisableBulletin(value); - }, - ), + // SettingsSwitcherCell( + // title: S.current.disable_bulletin, + // value: _privacySettingsViewModel.disableBulletin, + // onValueChange: (BuildContext _, bool value) { + // _privacySettingsViewModel.setDisableBulletin(value); + // }, + // ), if (_privacySettingsViewModel.canUseBlinkProtection) SettingsSwitcherCell( title: S.current.use_blink_protection, @@ -189,10 +195,15 @@ class PrivacyPage extends BasePage { title: S.current.domain_looks_up, handler: (context) => Navigator.of(context).pushNamed(Routes.domainLookupsPage), ), - SettingsCellWithArrow( - title: 'Trocador providers', - handler: (context) => Navigator.of(context).pushNamed(Routes.trocadorProvidersPage), - ), + if(_privacySettingsViewModel.hasCoinControl) + SettingsCellWithArrow( + title: "Coin Control", + handler: (context) => Navigator.of(context).pushNamed(Routes.unspentCoinsList), + ), + // SettingsCellWithArrow( + // title: 'Trocador providers', + // handler: (context) => Navigator.of(context).pushNamed(Routes.trocadorProvidersPage), + // ), ], ); }), diff --git a/lib/src/screens/settings/security_backup_page.dart b/lib/src/screens/settings/security_backup_page.dart index 45abc835f..714b69535 100644 --- a/lib/src/screens/settings/security_backup_page.dart +++ b/lib/src/screens/settings/security_backup_page.dart @@ -1,16 +1,18 @@ import 'dart:io'; import 'package:cake_wallet/core/auth_service.dart'; +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart'; +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_toggle.dart'; import 'package:cake_wallet/entities/pin_code_required_duration.dart'; +import 'package:cake_wallet/new-ui/widgets/modal_header.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; -import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart'; -import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart'; -import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart'; +import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/feature_flag.dart'; @@ -26,7 +28,7 @@ class SecurityBackupPage extends BasePage { final AuthService _authService; @override - String get title => S.current.security_and_backup; + String get title => S.current.security; final SecuritySettingsViewModel _securitySettingsViewModel; @@ -36,144 +38,168 @@ class SecurityBackupPage extends BasePage { Widget body(BuildContext context) { return Container( padding: EdgeInsets.only(top: 10), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (DeviceInfo.instance.isMobile || Platform.isMacOS || Platform.isLinux) - Observer(builder: (_) { - return SettingsSwitcherCell( - key: ValueKey('security_backup_page_allow_biometrics_button_key'), - title: S.current.settings_allow_biometrical_authentication, - value: _securitySettingsViewModel.allowBiometricalAuthentication, - onValueChange: (BuildContext context, bool value) { - if (value) { - _authService.authenticateAction( - context, - onAuthSuccess: (isAuthenticatedSuccessfully) async { - if (isAuthenticatedSuccessfully) { - if (await _securitySettingsViewModel.biometricAuthenticated()) { - _securitySettingsViewModel - .setAllowBiometricalAuthentication(isAuthenticatedSuccessfully); - } - } else { - _securitySettingsViewModel - .setAllowBiometricalAuthentication(isAuthenticatedSuccessfully); - } - }, - conditionToDetermineIfToUse2FA: _securitySettingsViewModel - .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, - ); - } else { - _securitySettingsViewModel.setAllowBiometricalAuthentication(value); - } - }); - }), - if (FeatureFlag.duressPinEnabled) - Observer(builder: (_) { - return SettingsSwitcherCell( - key: ValueKey('security_backup_page_duress_pin_button_key'), - title: 'Duress PIN', - value: _securitySettingsViewModel.enableDuressPin, - onValueChange: (BuildContext context, bool value) { - _authService.authenticateAction(context, route: Routes.securityBackupDuressPin, - onAuthSuccess: (isAuthenticatedSuccessfully) async { - if (isAuthenticatedSuccessfully) { - if (!value) { - _securitySettingsViewModel.setEnableDuressPin(value); - _securitySettingsViewModel.clearDuressPin(); - return; - } - final res = await _showDuressPinDescription(context); - if (res) { - final confirmation = await _showDuressPinConfirmation(context); - - if (confirmation) { - Navigator.of(context).pushNamed( - Routes.setupDuressPin, - arguments: (PinCodeState pinCtx, String _) async { - pinCtx.close(); - _securitySettingsViewModel.setEnableDuressPin(true); + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + spacing: 16, + mainAxisSize: MainAxisSize.min, + children: [ + ModalHeader( + iconPath: "assets/new-ui/settings_row_icons/security.svg", + message: S.of(context).privacy_and_security_desc, + title: S.of(context).security), + Observer( + builder: (_) => NewListSections(sections: { + "": [ + if (DeviceInfo.instance.isMobile || Platform.isMacOS || Platform.isLinux) + ListItemToggle( + keyValue: "security_backup_page_allow_biometrics_button_key", + label: S.current.settings_allow_biometrical_authentication, + value: _securitySettingsViewModel.allowBiometricalAuthentication, + onChanged: (bool value) { + if (value) { + _authService.authenticateAction( + context, + onAuthSuccess: (isAuthenticatedSuccessfully) async { + if (isAuthenticatedSuccessfully) { + if (await _securitySettingsViewModel.biometricAuthenticated()) { + _securitySettingsViewModel.setAllowBiometricalAuthentication( + isAuthenticatedSuccessfully); + } + } else { + _securitySettingsViewModel.setAllowBiometricalAuthentication( + isAuthenticatedSuccessfully); + } }, + conditionToDetermineIfToUse2FA: _securitySettingsViewModel + .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, ); + } else { + _securitySettingsViewModel.setAllowBiometricalAuthentication(value); } - } - } - }, - conditionToDetermineIfToUse2FA: _securitySettingsViewModel - .shouldRequireTOTP2FAForAllSecurityAndBackupSettings); - }); - }), - Observer(builder: (_) { - return SettingsPickerCell( - key: ValueKey('security_backup_page_require_pin_after_button_key'), - title: S.current.require_pin_after, - items: PinCodeRequiredDuration.values, - selectedItem: _securitySettingsViewModel.pinCodeRequiredDuration, - onItemSelected: (PinCodeRequiredDuration code) { - _securitySettingsViewModel.setPinCodeRequiredDuration(code); - }, - ); - }), - if (!_isHardwareWallet) - SettingsCellWithArrow( - key: ValueKey('security_backup_page_show_keys_button_key'), - title: S.current.show_keys, - handler: (_) => _authService.authenticateAction( - context, - route: Routes.showKeys, - conditionToDetermineIfToUse2FA: - _securitySettingsViewModel.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, - ), + }), + if (FeatureFlag.duressPinEnabled) + ListItemToggle( + keyValue: "security_backup_page_duress_pin_button_key", + label: "Duress PIN", + value: _securitySettingsViewModel.enableDuressPin, + onChanged: (bool value) { + _authService + .authenticateAction(context, route: Routes.securityBackupDuressPin, + onAuthSuccess: (isAuthenticatedSuccessfully) async { + if (isAuthenticatedSuccessfully) { + if (!value) { + _securitySettingsViewModel.setEnableDuressPin(value); + _securitySettingsViewModel.clearDuressPin(); + return; + } + final res = await _showDuressPinDescription(context); + if (res) { + final confirmation = await _showDuressPinConfirmation(context); + + if (confirmation) { + Navigator.of(context).pushNamed( + Routes.setupDuressPin, + arguments: + (PinCodeState pinCtx, String _) async { + pinCtx.close(); + _securitySettingsViewModel.setEnableDuressPin(true); + }, + ); + } + } + } + }, + conditionToDetermineIfToUse2FA: _securitySettingsViewModel + .shouldRequireTOTP2FAForAllSecurityAndBackupSettings); + }), + ListItemRegularRow( + keyValue: "security_backup_page_require_pin_after_button_key", + label: S.current.require_pin_after, + trailingText: _securitySettingsViewModel.pinCodeRequiredDuration.toString(), + onTap: () async { + final items = PinCodeRequiredDuration.values; + + final selectedAtIndex = + items.indexOf(_securitySettingsViewModel.pinCodeRequiredDuration); + + await showPopUp( + context: context, + builder: (_) => Picker( + items: items, + selectedAtIndex: selectedAtIndex, + mainAxisAlignment: MainAxisAlignment.start, + onItemSelected: (PinCodeRequiredDuration item) { + _securitySettingsViewModel.setPinCodeRequiredDuration(item); + }, + isSeparated: false, + ), + ); + }), + // if (!_isHardwareWallet) + // ListItemRegularRow( + // keyValue: "security_backup_page_show_keys_button_key", + // label: S.current.show_keys, + // onTap: () { + // _authService.authenticateAction( + // context, + // route: Routes.showKeys, + // conditionToDetermineIfToUse2FA: _securitySettingsViewModel + // .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + // ); + // }), + // if (!SettingsStoreBase.walletPasswordDirectInput) + // ListItemRegularRow( + // keyValue: "security_backup_page_change_password_button_key", + // label: S.current.create_backup, + // onTap: () { + // _authService.authenticateAction( + // context, + // route: Routes.backup, + // conditionToDetermineIfToUse2FA: _securitySettingsViewModel + // .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + // ); + // }), + ListItemRegularRow( + keyValue: "security_backup_page_change_pin_button_key", + label: S.current.settings_change_pin, + onTap: () { + _authService.authenticateAction( + context, + route: Routes.setupPin, + arguments: (PinCodeState setupPinContext, String _) { + setupPinContext.close(); + }, + conditionToDetermineIfToUse2FA: _securitySettingsViewModel + .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + ); + }), + ListItemRegularRow( + keyValue: "security_backup_page_sign_and_verify", + label: S.current.sign_verify_title, + onTap: () { + Navigator.of(context).pushNamed(Routes.signPage); + }), + ListItemRegularRow( + keyValue: "security_backup_page_totp_2fa_button_key", + label: _securitySettingsViewModel.useTotp2FA + ? S.current.modify_2fa + : S.current.setup_2fa, + onTap: () { + _authService.authenticateAction( + context, + route: _securitySettingsViewModel.useTotp2FA + ? Routes.modify2FAPage + : Routes.setup2faInfoPage, + conditionToDetermineIfToUse2FA: _securitySettingsViewModel + .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + ); + }) + ] + }), ), - if (!SettingsStoreBase.walletPasswordDirectInput) - SettingsCellWithArrow( - key: ValueKey('security_backup_page_create_backup_button_key'), - title: S.current.create_backup, - handler: (_) => _authService.authenticateAction( - context, - route: Routes.backup, - conditionToDetermineIfToUse2FA: - _securitySettingsViewModel.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, - ), - ), - SettingsCellWithArrow( - key: ValueKey('security_backup_page_change_pin_button_key'), - title: S.current.settings_change_pin, - handler: (_) => _authService.authenticateAction( - context, - route: Routes.setupPin, - arguments: (PinCodeState setupPinContext, String _) { - setupPinContext.close(); - }, - conditionToDetermineIfToUse2FA: - _securitySettingsViewModel.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, - ), - ), - SettingsCellWithArrow( - key: ValueKey('security_backup_page_sign_and_verify'), - title: S.current.sign_verify_title, - handler: (_) => Navigator.of(context).pushNamed(Routes.signPage) - //_securitySettingsViewModel.pinCodeRequiredDuration, - ), - Observer( - builder: (context) { - return SettingsCellWithArrow( - key: ValueKey('security_backup_page_totp_2fa_button_key'), - title: _securitySettingsViewModel.useTotp2FA - ? S.current.modify_2fa - : S.current.setup_2fa, - handler: (_) => _authService.authenticateAction( - context, - route: _securitySettingsViewModel.useTotp2FA - ? Routes.modify2FAPage - : Routes.setup2faInfoPage, - conditionToDetermineIfToUse2FA: _securitySettingsViewModel - .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, - ), - ); - }, - ), - ], + ], + ), ), ); } diff --git a/lib/src/screens/settings/silent_payments_logs_page.dart b/lib/src/screens/settings/silent_payments_logs_page.dart index 590f2fb40..baea684b6 100644 --- a/lib/src/screens/settings/silent_payments_logs_page.dart +++ b/lib/src/screens/settings/silent_payments_logs_page.dart @@ -9,6 +9,7 @@ import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/settings/silent_payments_settings_view_model.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'package:path_provider/path_provider.dart'; class SilentPaymentsLogPage extends BasePage { @@ -33,6 +34,7 @@ class SilentPaymentsLogPage extends BasePage { return Center(child: Text('No logs found')); } else { return SingleChildScrollView( + controller: ModalScrollController.of(context), child: Padding( padding: EdgeInsets.all(16.0), child: Text( diff --git a/lib/src/screens/settings/silent_payments_settings.dart b/lib/src/screens/settings/silent_payments_settings.dart index 82bdf018b..023c8f649 100644 --- a/lib/src/screens/settings/silent_payments_settings.dart +++ b/lib/src/screens/settings/silent_payments_settings.dart @@ -1,55 +1,64 @@ import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; +import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:cake_wallet/view_model/settings/silent_payments_settings_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -class SilentPaymentsSettingsPage extends BasePage { +class SilentPaymentsSettingsPage extends StatelessWidget { SilentPaymentsSettingsPage(this._silentPaymentsSettingsViewModel); - @override - String get title => S.current.silent_payments_settings; - final SilentPaymentsSettingsViewModel _silentPaymentsSettingsViewModel; @override - Widget body(BuildContext context) { - return SingleChildScrollView( - child: Observer(builder: (_) { - return Container( - padding: EdgeInsets.only(top: 10), - child: Column( - children: [ - SettingsSwitcherCell( - title: S.current.silent_payments_display_card, - value: _silentPaymentsSettingsViewModel.silentPaymentsCardDisplay, - onValueChange: (_, bool value) { - _silentPaymentsSettingsViewModel.setSilentPaymentsCardDisplay(value); - }, - ), - SettingsSwitcherCell( - title: S.current.silent_payments_always_scan, - value: _silentPaymentsSettingsViewModel.silentPaymentsAlwaysScan, - onValueChange: (_, bool value) { - _silentPaymentsSettingsViewModel.setSilentPaymentsAlwaysScan(value); - }, - ), - SettingsCellWithArrow( - title: S.current.silent_payments_scanning, - handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.rescan), - ), - SettingsCellWithArrow( - title: S.current.silent_payments_logs, - handler: (BuildContext context) => - Navigator.of(context).pushNamed(Routes.silentPaymentsLogs), - ), - ], + Widget build(BuildContext context) { + return Container( + color: Theme.of(context).colorScheme.surface, + child: Column( + children: [ + ModalTopBar(title: S.current.silent_payments_settings,leadingIcon: Icon(Icons.arrow_back_ios_new),onLeadingPressed: Navigator.of(context).pop,), + Expanded( + child: SingleChildScrollView( + child: Observer(builder: (_) { + return Container( + padding: EdgeInsets.only(top: 10), + child: Column( + children: [ + if(!FeatureFlag.hasNewUi) + SettingsSwitcherCell( + title: S.current.silent_payments_display_card, + value: _silentPaymentsSettingsViewModel.silentPaymentsCardDisplay, + onValueChange: (_, bool value) { + _silentPaymentsSettingsViewModel.setSilentPaymentsCardDisplay(value); + }, + ), + SettingsSwitcherCell( + title: S.current.silent_payments_always_scan, + value: _silentPaymentsSettingsViewModel.silentPaymentsAlwaysScan, + onValueChange: (_, bool value) { + _silentPaymentsSettingsViewModel.setSilentPaymentsAlwaysScan(value); + }, + ), + SettingsCellWithArrow( + title: S.current.silent_payments_scanning, + handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.rescan), + ), + SettingsCellWithArrow( + title: S.current.silent_payments_logs, + handler: (BuildContext context) => + Navigator.of(context).pushNamed(Routes.silentPaymentsLogs), + ), + ], + ), + ); + }), + ), ), - ); - }), + ], + ), ); } } diff --git a/lib/src/screens/setup_2fa/modify_2fa_page.dart b/lib/src/screens/setup_2fa/modify_2fa_page.dart index 601d084e7..d9414fb98 100644 --- a/lib/src/screens/setup_2fa/modify_2fa_page.dart +++ b/lib/src/screens/setup_2fa/modify_2fa_page.dart @@ -11,6 +11,7 @@ import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arro import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart'; import 'package:cake_wallet/src/widgets/standard_list.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import '../../../routes.dart'; @@ -25,6 +26,7 @@ class Modify2FAPage extends BasePage { @override Widget body(BuildContext context) { return SingleChildScrollView( + controller: ModalScrollController.of(context), child: _2FAControlsWidget(setup2FAViewModel: setup2FAViewModel), ); } diff --git a/lib/src/screens/setup_2fa/setup_2fa_enter_code_page.dart b/lib/src/screens/setup_2fa/setup_2fa_enter_code_page.dart index 5e7577f0b..4ecfc6fd4 100644 --- a/lib/src/screens/setup_2fa/setup_2fa_enter_code_page.dart +++ b/lib/src/screens/setup_2fa/setup_2fa_enter_code_page.dart @@ -1,6 +1,7 @@ import 'package:another_flushbar/flushbar.dart'; import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/core/totp_request_details.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/view_model/auth_state.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -92,7 +93,6 @@ class TotpAuthCodePageState extends State { Widget build(BuildContext context) { return Scaffold( key: _key, - resizeToAvoidBottomInset: false, body: TOTPEnterCode( setup2FAViewModel: widget.setup2FAViewModel, isForSetup: widget.totpArguments.isForSetup ?? false, @@ -108,99 +108,120 @@ class TotpAuthCodePageState extends State { } } -class TOTPEnterCode extends BasePage { - TOTPEnterCode({ +class TOTPEnterCode extends StatefulWidget { + const TOTPEnterCode({ required this.setup2FAViewModel, required this.isForSetup, required this.isClosable, - }) : totpController = TextEditingController() { - totpController.addListener(() { - setup2FAViewModel.enteredOTPCode = totpController.text; - }); - } + }); - @override - String get title => isForSetup ? S.current.setup_2fa : S.current.verify_with_2fa; - Widget? leading(BuildContext context) { - return isClosable ? super.leading(context) : null; - } - - final TextEditingController totpController; final Setup2FAViewModel setup2FAViewModel; final bool isForSetup; final bool isClosable; @override - Widget body(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 24, - ), - child: Column( - children: [ - BaseTextFormField( - textAlign: TextAlign.left, - hintText: S.current.totp_code, - controller: totpController, - keyboardType: TextInputType.number, - placeholderTextStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontSize: 16, - ), - ), - SizedBox(height: 16), - Text( - S.current.please_fill_totp, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - height: 1.2, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - textAlign: TextAlign.center, - ), - Spacer(), - Observer( - builder: (context) { - return PrimaryButton( - isDisabled: setup2FAViewModel.enteredOTPCode.length != 8, - onPressed: () async { - final result = - await setup2FAViewModel.totp2FAAuth(totpController.text, isForSetup); - final bannedState = setup2FAViewModel.state is AuthenticationBanned; + State createState() => _TOTPEnterCodeState(); +} - await showPopUp( - context: context, - builder: (BuildContext context) { - return PopUpCancellableAlertDialog( - contentText: _textDisplayedInPopupOnResult(result, bannedState, context), - actionButtonText: S.of(context).ok, - buttonAction: () { - result ? setup2FAViewModel.success() : null; - if (isForSetup && result) { - Navigator.pop(context); - // Navigator.of(context) - // .popAndPushNamed(Routes.modify2FAPage); - } else { - Navigator.of(context).pop(result); - } - }, - ); - }, - ); - if (isForSetup && result) { - if (context.mounted) { - Navigator.pushReplacementNamed(context, Routes.modify2FAPage); - } - } - }, - text: S.of(context).continue_text, - color: Theme.of(context).colorScheme.primary, - textColor: Theme.of(context).colorScheme.onPrimary, - ); - }, - ), - SizedBox(height: 24), - ], +class _TOTPEnterCodeState extends State { + final totpController = TextEditingController(); + + @override + void initState() { + super.initState(); + totpController.addListener(() { + widget.setup2FAViewModel.enteredOTPCode = totpController.text; + }); + } + + @override + Widget build(BuildContext context) { + return Container( + color: Theme.of(context).colorScheme.surface, + child: SafeArea( + child: Column( + children: [ + ModalTopBar( + title: widget.isForSetup ? S.current.setup_2fa : S.current.verify_with_2fa, + leadingIcon: Icon(Icons.arrow_back_ios_new), + onLeadingPressed: Navigator.of(context).pop, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24, + ), + child: Column( + children: [ + + BaseTextFormField( + textAlign: TextAlign.left, + hintText: S.current.totp_code, + controller: totpController, + keyboardType: TextInputType.number, + placeholderTextStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 16, + ), + ), + SizedBox(height: 16), + Text( + S.current.please_fill_totp, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + height: 1.2, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + Spacer(), + Observer( + builder: (context) { + return PrimaryButton( + isDisabled: widget.setup2FAViewModel.enteredOTPCode.length != 8, + onPressed: () async { + final result = + await widget.setup2FAViewModel.totp2FAAuth(totpController.text, widget.isForSetup); + final bannedState = widget.setup2FAViewModel.state is AuthenticationBanned; + + await showPopUp( + context: context, + builder: (BuildContext context) { + return PopUpCancellableAlertDialog( + contentText: _textDisplayedInPopupOnResult(result, bannedState, context), + actionButtonText: S.of(context).ok, + buttonAction: () { + result ? widget.setup2FAViewModel.success() : null; + if (widget.isForSetup && result) { + Navigator.pop(context); + // Navigator.of(context) + // .popAndPushNamed(Routes.modify2FAPage); + } else { + Navigator.of(context).pop(result); + } + }, + ); + }, + ); + if (widget.isForSetup && result) { + if (context.mounted) { + Navigator.pushReplacementNamed(context, Routes.modify2FAPage); + } + } + }, + text: S.of(context).continue_text, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + ); + }, + ), + SizedBox(height: 24), + ], + ), + ), + ), + ], + ), ), ); } @@ -208,10 +229,10 @@ class TOTPEnterCode extends BasePage { String _textDisplayedInPopupOnResult(bool result, bool bannedState, BuildContext context) { switch (result) { case true: - return isForSetup ? S.current.totp_2fa_success : S.current.totp_verification_success; + return widget.isForSetup ? S.current.totp_2fa_success : S.current.totp_verification_success; case false: if (bannedState) { - final state = setup2FAViewModel.state as AuthenticationBanned; + final state = widget.setup2FAViewModel.state as AuthenticationBanned; return S.of(context).failed_authentication(state.error); } else { return S.current.totp_2fa_failure; diff --git a/lib/src/screens/setup_2fa/setup_2fa_qr_page.dart b/lib/src/screens/setup_2fa/setup_2fa_qr_page.dart index 4213fd8d2..a7525ea25 100644 --- a/lib/src/screens/setup_2fa/setup_2fa_qr_page.dart +++ b/lib/src/screens/setup_2fa/setup_2fa_qr_page.dart @@ -41,7 +41,7 @@ class Setup2FAQRPage extends BasePage { ), SizedBox(height: 10), ConstrainedBox( - constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.4), + constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.3), child: AspectRatio( aspectRatio: 1.0, child: Container( diff --git a/lib/src/screens/support/support_page.dart b/lib/src/screens/support/support_page.dart index 7437430cb..c61004f9e 100644 --- a/lib/src/screens/support/support_page.dart +++ b/lib/src/screens/support/support_page.dart @@ -34,35 +34,27 @@ class SupportPage extends BasePage { @override Widget body(BuildContext context) => Center( child: Container( - padding: const EdgeInsets.only(left: 24, right: 24), + padding: const EdgeInsets.only(left: 16, right: 16, top: 16), child: Column( + spacing: 16, children: [ - Padding( - padding: const EdgeInsets.only(top: 20), - child: OptionTile( - image: Image.asset(_imageSupportChat, width: 55, height: 55), - title: S.of(context).support_title_live_chat, - description: S.of(context).support_description_live_chat, - onPressed: () => _onPressedSupportChat(context), - ), + OptionTile( + image: Image.asset(_imageSupportChat, width: 55, height: 55), + title: S.of(context).support_title_live_chat, + description: S.of(context).support_description_live_chat, + onPressed: () => _onPressedSupportChat(context), ), - Padding( - padding: const EdgeInsets.only(top: 20), - child: OptionTile( - image: Image.asset(_imageSupportDocs, width: 55, height: 55), - title: S.of(context).support_title_guides, - description: S.of(context).support_description_guides, - onPressed: () => _launchUrl(supportViewModel.docsUrl), - ), + OptionTile( + image: Image.asset(_imageSupportDocs, width: 55, height: 55), + title: S.of(context).support_title_guides, + description: S.of(context).support_description_guides, + onPressed: () => _launchUrl(supportViewModel.docsUrl), ), - Padding( - padding: const EdgeInsets.only(top: 20), - child: OptionTile( - image: Image.asset(_imageSupportLinks, width: 55, height: 55), - title: S.of(context).support_title_other_links, - description: S.of(context).support_description_other_links, - onPressed: () => Navigator.pushNamed(context, Routes.supportOtherLinks), - ), + OptionTile( + image: Image.asset(_imageSupportLinks, width: 55, height: 55), + title: S.of(context).support_title_other_links, + description: S.of(context).support_description_other_links, + onPressed: () => Navigator.pushNamed(context, Routes.supportOtherLinks), ), ], ), @@ -73,7 +65,7 @@ class SupportPage extends BasePage { if (DeviceInfo.instance.isDesktop) { _launchUrl(supportViewModel.fetchUrl()); } else { - Navigator.pushNamed(context, Routes.supportLiveChat); + Navigator.of(context, rootNavigator: true).pushNamed(Routes.supportLiveChat); } } diff --git a/lib/src/screens/support_chat/support_chat_page.dart b/lib/src/screens/support_chat/support_chat_page.dart index 2efbf84ee..f15b248a4 100644 --- a/lib/src/screens/support_chat/support_chat_page.dart +++ b/lib/src/screens/support_chat/support_chat_page.dart @@ -1,39 +1,52 @@ import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; import 'package:cake_wallet/src/screens/support_chat/widgets/chatwoot_widget.dart'; import 'package:cake_wallet/view_model/support_view_model.dart'; import 'package:flutter/material.dart'; -class SupportChatPage extends BasePage { +class SupportChatPage extends StatelessWidget { SupportChatPage(this.supportViewModel, {required this.secureStorage}); final SupportViewModel supportViewModel; final SecureStorage secureStorage; - @override - String get title => S.current.settings_support; @override - AppBarStyle get appBarStyle => AppBarStyle.regular; - - @override - Widget body(BuildContext context) => FutureBuilder( - future: getCookie(), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) - return ChatwootWidget( - secureStorage, - supportUrl: supportViewModel.fetchUrl(authToken: snapshot.data!), - appVersion: supportViewModel.appVersion, - fiatApiMode: supportViewModel.fiatApiMode, - walletType: supportViewModel.walletType, - walletSyncState: supportViewModel.walletSyncState, - builtInTorState: supportViewModel.builtInTorState, - ); - return Container(); - }, - ); + Widget build(BuildContext context) => Container( + color: Theme.of(context).colorScheme.surface, + child: SafeArea( + child: Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: Column( + children: [ + ModalTopBar( + title: S.current.settings_support, + leadingIcon: Icon(Icons.arrow_back_ios_new), + onLeadingPressed: Navigator.of(context).pop), + FutureBuilder( + future: getCookie(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData) + return Expanded( + child: ChatwootWidget( + secureStorage, + supportUrl: supportViewModel.fetchUrl(authToken: snapshot.data!), + appVersion: supportViewModel.appVersion, + fiatApiMode: supportViewModel.fiatApiMode, + walletType: supportViewModel.walletType, + walletSyncState: supportViewModel.walletSyncState, + builtInTorState: supportViewModel.builtInTorState, + ), + ); + return Container(); + }, + ), + ], + ), + ), + ), + ); Future getCookie() async => await secureStorage.read(key: COOKIE_KEY) ?? ""; diff --git a/lib/src/screens/support_other_links/support_other_links_page.dart b/lib/src/screens/support_other_links/support_other_links_page.dart index fe83dfd73..7c82d2ed5 100644 --- a/lib/src/screens/support_other_links/support_other_links_page.dart +++ b/lib/src/screens/support_other_links/support_other_links_page.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/view_model/support_view_model.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; class SupportOtherLinksPage extends BasePage { SupportOtherLinksPage(this.supportViewModel); @@ -29,6 +30,8 @@ class SupportOtherLinksPage extends BasePage { child: ConstrainedBox( constraints: BoxConstraints(maxWidth: 500), child: SectionStandardList( + scrollController: ModalScrollController.of(context), + physics: ClampingScrollPhysics(), sectionCount: 1, itemCounter: (int _) => supportViewModel.items.length, itemBuilder: (_, index) { diff --git a/lib/src/screens/transaction_details/rbf_details_page.dart b/lib/src/screens/transaction_details/rbf_details_page.dart index 66866bf2a..30de3892c 100644 --- a/lib/src/screens/transaction_details/rbf_details_page.dart +++ b/lib/src/screens/transaction_details/rbf_details_page.dart @@ -216,6 +216,7 @@ class RBFDetailsPage extends BasePage { } catch (_) {} }, change: transactionDetailsViewModel.sendViewModel.pendingTransaction!.change, + amountParsingProxy: transactionDetailsViewModel.sendViewModel.amountParsingProxy, ); }, ); diff --git a/lib/src/screens/ur/animated_ur_page.dart b/lib/src/screens/ur/animated_ur_page.dart index 0a8c950f6..df410d4b0 100644 --- a/lib/src/screens/ur/animated_ur_page.dart +++ b/lib/src/screens/ur/animated_ur_page.dart @@ -57,7 +57,7 @@ class AnimatedURPage extends BasePage { mainAxisSize: MainAxisSize.min, children: [ Padding( - padding: const EdgeInsets.all(32.0), + padding: const EdgeInsets.symmetric(horizontal: 32.0), child: URQR( urqr: urQr, walletType: animatedURmodel.wallet.type, @@ -66,7 +66,6 @@ class AnimatedURPage extends BasePage { ), if (["ur:xmr-txunsigned", "ur:xmr-output", "ur:psbt", BBQR.header] .contains(urQrType)) ...{ - Spacer(), Padding( padding: const EdgeInsets.all(16.0), child: SizedBox( diff --git a/lib/src/screens/wallet_connect/utils/string_parsing.dart b/lib/src/screens/wallet_connect/utils/string_parsing.dart index bb36e1879..c52f59dab 100644 --- a/lib/src/screens/wallet_connect/utils/string_parsing.dart +++ b/lib/src/screens/wallet_connect/utils/string_parsing.dart @@ -2,6 +2,6 @@ import 'dart:math'; extension StringParsing on String { String safeSubString(int start, int end) { - return this.substring(0, min(this.toString().length, 12)); + return this.substring(start, min(this.toString().length, end)); } } diff --git a/lib/src/screens/wallet_keys/wallet_keys_page.dart b/lib/src/screens/wallet_keys/wallet_keys_page.dart index 1a250ef47..c2b736d1b 100644 --- a/lib/src/screens/wallet_keys/wallet_keys_page.dart +++ b/lib/src/screens/wallet_keys/wallet_keys_page.dart @@ -14,6 +14,7 @@ import 'package:cake_wallet/view_model/wallet_keys_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'package:qr_flutter/qr_flutter.dart'; class WalletKeysPage extends BasePage { @@ -214,6 +215,7 @@ class _WalletKeysPageBodyState extends State children: [ Expanded( child: ListView.separated( + controller: ModalScrollController.of(context), shrinkWrap: true, itemCount: items.length, itemBuilder: (context, index) { @@ -237,7 +239,7 @@ class _WalletKeysPageBodyState extends State return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 14), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(18), color: Theme.of(context).colorScheme.surfaceContainer, ), child: Row( @@ -294,7 +296,7 @@ class _WalletKeysPageBodyState extends State return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 14), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(18), color: Theme.of(context).colorScheme.surfaceContainer, ), child: Row( diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index aa689c7c7..08b1d7b44 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -17,9 +17,12 @@ import 'package:cake_wallet/src/screens/wallet_list/edit_wallet_button_widget.da import 'package:cake_wallet/src/screens/wallet_list/filtered_list.dart'; import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; +import 'package:cake_wallet/src/widgets/gradient_background.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; +import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; @@ -28,6 +31,7 @@ import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; @@ -40,6 +44,9 @@ class WalletListPage extends BasePage { this.onWalletLoaded, }); + @override + bool get gradientBackground => true; + final WalletListViewModel walletListViewModel; final AuthService authService; final Future Function(BuildContext)? onWalletLoaded; @@ -99,7 +106,7 @@ class WalletListPage extends BasePage { ), child: Image.asset( 'assets/images/filter_icon.png', - color: Theme.of(context).colorScheme.onSurfaceVariant, + color: Theme.of(context).colorScheme.primary, ), ), ), @@ -135,204 +142,207 @@ class WalletListBodyState extends State { @override Widget build(BuildContext context) { - return Container( - height: double.infinity, - padding: EdgeInsets.only(top: 16), - child: Stack( - alignment: Alignment.bottomCenter, - fit: StackFit.expand, - children: [ - SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.walletListViewModel.multiWalletGroups.isNotEmpty) ...{ - Padding( - padding: const EdgeInsets.only(left: 24), - child: Text( - S.current.shared_seed_wallet_groups, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 18, - fontWeight: FontWeight.w700, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ), - SizedBox(height: 16), - Container( - child: Padding( - padding: EdgeInsets.only(left: 20, right: 20), - child: Observer( - builder: (_) => FilteredList( - shrinkWrap: true, - list: widget.walletListViewModel.multiWalletGroups, - updateFunction: widget.walletListViewModel.reorderAccordingToWalletList, - itemBuilder: (context, index) { - final group = widget.walletListViewModel.multiWalletGroups[index]; - final groupName = - group.groupName ?? '${S.current.wallet_group} ${index + 1}'; - - widget.walletListViewModel.updateTileState( - index, - widget.walletListViewModel.expansionTileStateTrack[index] ?? false, - ); - - return GroupedWalletExpansionTile( - onExpansionChanged: (value) { - widget.walletListViewModel.updateTileState(index, value); - setState(() {}); - }, - shouldShowCurrentWalletPointer: true, - borderRadius: BorderRadius.all(Radius.circular(16)), - title: groupName, - tileKey: ValueKey('group_wallets_expansion_tile_widget_$index'), - leadingWidget: Icon( - Icons.account_balance_wallet_outlined, - size: 28, - ), - trailingWidget: EditWalletButtonWidget( - width: 88, - isGroup: true, - isExpanded: - widget.walletListViewModel.expansionTileStateTrack[index]!, - onTap: () { - final wallet = widget.walletListViewModel - .convertWalletInfoToWalletListItem(group.wallets.first); - Navigator.of(context).pushNamed( - Routes.walletEdit, - arguments: WalletEditPageArguments( - walletListViewModel: widget.walletListViewModel, - editingWallet: wallet, - isWalletGroup: true, - groupName: groupName, - walletGroupKey: group.groupKey, - ), - ); - }, - ), - childWallets: group.wallets.map((walletInfo) { - return widget.walletListViewModel - .convertWalletInfoToWalletListItem(walletInfo); - }).toList(), - isSelected: false, - onChildItemTapped: (wallet) => - wallet.isCurrent ? null : _loadWallet(wallet), - childTrailingWidget: (item) { - return item.isCurrent - ? SizedBox.shrink() - : EditWalletButtonWidget( - width: 64, - onTap: () => Navigator.of(context).pushNamed( - Routes.walletEdit, - arguments: WalletEditPageArguments( - walletListViewModel: widget.walletListViewModel, - editingWallet: item, - ), - ), - ); - }, - ); - }, - ), + return GradientBackground( + scaffold: Container( + height: double.infinity, + padding: EdgeInsets.only(top: 16), + child: Stack( + alignment: Alignment.bottomCenter, + fit: StackFit.expand, + children: [ + SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.walletListViewModel.multiWalletGroups.isNotEmpty) ...{ + Padding( + padding: const EdgeInsets.only(left: 24), + child: Text( + S.current.shared_seed_wallets, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 18, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.onSurface, + ), ), ), - ), - SizedBox(height: 24), - }, - if (widget.walletListViewModel.singleWalletsList.isNotEmpty) ...{ - Padding( - padding: const EdgeInsets.only(left: 24), - child: Text( - S.current.single_seed_wallets_group, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 18, - fontWeight: FontWeight.w700, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ), - SizedBox(height: 16), - Padding( - padding: EdgeInsets.only(left: 20, right: 20), - child: Container( - child: Observer( - builder: (_) => FilteredList( - shrinkWrap: true, - list: widget.walletListViewModel.singleWalletsList, - updateFunction: widget.walletListViewModel.reorderAccordingToWalletList, - itemBuilder: (context, index) { - final wallet = widget.walletListViewModel.singleWalletsList[index]; - final currentColor = wallet.isCurrent - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.surface; - return GroupedWalletExpansionTile( - tileKey: ValueKey('single_wallets_expansion_tile_widget_$index'), - isCurrentlySelectedWallet: wallet.isCurrent, - leadingWidget: SizedBox( - width: wallet.isCurrent ? 56 : 40, - child: Row( - children: [ - wallet.isCurrent - ? Container( - height: 35, - width: 6, - margin: EdgeInsets.only(right: 16), - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - topRight: Radius.circular(16), - bottomRight: Radius.circular(16), - ), - color: currentColor, - ), - ) - : SizedBox(width: 6), - Image.asset( - getCryptoCurrencyForWalletListItem( - wallet.type, - ).iconPath!, - width: 32, - height: 32, - ), - ], + SizedBox(height: 16), + Container( + child: Padding( + padding: EdgeInsets.only(left: 20, right: 20), + child: Observer( + builder: (_) => FilteredList( + shrinkWrap: true, + list: widget.walletListViewModel.multiWalletGroups, + updateFunction: widget.walletListViewModel.reorderAccordingToWalletList, + itemBuilder: (context, index) { + final group = widget.walletListViewModel.multiWalletGroups[index]; + final groupName = + group.groupName ?? '${S.current.wallet_group} ${index + 1}'; + + widget.walletListViewModel.updateTileState( + index, + widget.walletListViewModel.expansionTileStateTrack[index] ?? false, + ); + + return GroupedWalletExpansionTile( + onExpansionChanged: (value) { + widget.walletListViewModel.updateTileState(index, value); + setState(() {}); + }, + shouldShowCurrentWalletPointer: true, + borderRadius: BorderRadius.all(Radius.circular(18)), + title: groupName, + tileKey: ValueKey('group_wallets_expansion_tile_widget_$index'), + leadingWidget: CakeImageWidget( + imageUrl: "assets/new-ui/navbar/wallets.svg", + width: 28, + color: Theme.of(context).colorScheme.onSurfaceVariant, ), - ), - title: wallet.name, - isSelected: false, - borderRadius: BorderRadius.all(Radius.circular(16)), - margin: EdgeInsets.only(left: 20, right: 20, bottom: 12), - onTitleTapped: () => wallet.isCurrent ? null : _loadWallet(wallet), - trailingWidget: wallet.isCurrent - ? null - : EditWalletButtonWidget( - width: 64, - onTap: () { - Navigator.of(context).pushNamed( - Routes.walletEdit, - arguments: WalletEditPageArguments( - walletListViewModel: widget.walletListViewModel, - editingWallet: wallet, + trailingWidget: EditWalletButtonWidget( + width: 88, + isGroup: true, + isExpanded: + widget.walletListViewModel.expansionTileStateTrack[index]!, + onTap: () { + final wallet = widget.walletListViewModel + .convertWalletInfoToWalletListItem(group.wallets.first); + Navigator.of(context).pushNamed( + Routes.walletEdit, + arguments: WalletEditPageArguments( + walletListViewModel: widget.walletListViewModel, + editingWallet: wallet, + isWalletGroup: true, + groupName: groupName, + walletGroupKey: group.groupKey, + ), + ); + }, + ), + childWallets: group.wallets.map((walletInfo) { + return widget.walletListViewModel + .convertWalletInfoToWalletListItem(walletInfo); + }).toList(), + isSelected: false, + onChildItemTapped: (wallet) => + wallet.isCurrent ? null : _loadWallet(wallet), + childTrailingWidget: (item) { + return item.isCurrent + ? SizedBox.shrink() + : EditWalletButtonWidget( + width: 64, + onTap: () => Navigator.of(context).pushNamed( + Routes.walletEdit, + arguments: WalletEditPageArguments( + walletListViewModel: widget.walletListViewModel, + editingWallet: item, + ), ), ); - }, - ), - ); - }, + }, + ); + }, + ), ), ), ), - ), - SizedBox(height: 150), - }, - ], + SizedBox(height: 24), + }, + if (widget.walletListViewModel.singleWalletsList.isNotEmpty) ...{ + Padding( + padding: const EdgeInsets.only(left: 24), + child: Text( + S.current.single_seed_wallets_group, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 18, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + SizedBox(height: 16), + Padding( + padding: EdgeInsets.only(left: 20, right: 20), + child: Container( + child: Observer( + builder: (_) => FilteredList( + shrinkWrap: true, + list: widget.walletListViewModel.singleWalletsList, + updateFunction: widget.walletListViewModel.reorderAccordingToWalletList, + itemBuilder: (context, index) { + final wallet = widget.walletListViewModel.singleWalletsList[index]; + final currentColor = wallet.isCurrent + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.surface; + return GroupedWalletExpansionTile( + tileKey: ValueKey('single_wallets_expansion_tile_widget_$index'), + isCurrentlySelectedWallet: wallet.isCurrent, + leadingWidget: SizedBox( + width: wallet.isCurrent ? 56 : 40, + child: Row( + children: [ + wallet.isCurrent + ? Container( + height: 35, + width: 6, + margin: EdgeInsets.only(right: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topRight: Radius.circular(18), + bottomRight: Radius.circular(18), + ), + color: currentColor, + ), + ) + : SizedBox(width: 6), + Image.asset( + getCryptoCurrencyIconForWalletListItem( + wallet.type, + ), + width: 32, + height: 32, + ), + ], + ), + ), + title: wallet.name, + isSelected: false, + borderRadius: BorderRadius.all(Radius.circular(18)), + margin: EdgeInsets.only(left: 20, right: 20, bottom: 12), + onTitleTapped: () => wallet.isCurrent ? null : _loadWallet(wallet), + trailingWidget: wallet.isCurrent + ? null + : EditWalletButtonWidget( + width: 64, + onTap: () { + Navigator.of(context).pushNamed( + Routes.walletEdit, + arguments: WalletEditPageArguments( + walletListViewModel: widget.walletListViewModel, + editingWallet: wallet, + ), + ); + }, + ), + ); + }, + ), + ), + ), + ), + }, + SizedBox(height: 250), + ], + ), ), - ), - Positioned( - child: Stack( + Stack( alignment: Alignment.bottomCenter, children: [ - IgnorePointer( + !FeatureFlag.hasNewUi + ? IgnorePointer( child: Container( - alignment: Alignment.bottomCenter, + alignment: Alignment.bottomCenter, height: 185, decoration: BoxDecoration( gradient: LinearGradient( @@ -347,9 +357,29 @@ class WalletListBodyState extends State { ), ), ), + ) + : IgnorePointer( + child: Container( + height: 275, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Theme.of(context).colorScheme.surfaceDim.withAlpha(10), + Theme.of(context).colorScheme.surfaceDim.withAlpha(150), + Theme.of(context).colorScheme.surfaceDim.withAlpha(255), + Theme.of(context).colorScheme.surfaceDim.withAlpha(255), + Theme.of(context).colorScheme.surfaceDim.withAlpha(255), + Theme.of(context).colorScheme.surfaceDim.withAlpha(255), + Theme.of(context).colorScheme.surfaceDim.withAlpha(255) + ], + ), + ), + ), ), Container( - height: 120, + height: 240, width: MediaQuery.of(context).size.width, margin: EdgeInsets.only(bottom: 24), padding: EdgeInsets.only(left: 16, right: 16), @@ -434,19 +464,24 @@ class WalletListBodyState extends State { color: Theme.of(context).colorScheme.primary, textColor: Theme.of(context).colorScheme.onPrimary, ), + if(FeatureFlag.hasNewUi) + SizedBox(height:52.0) ], ), ), ], ), - ), - ], + ], + ), ), ); } Future _loadWallet(WalletListItem wallet) async { - if (_loadingWallet) return; + if (_loadingWallet) { + printV("_loadWallet abandoned because _loadingWallet"); + return; + } _loadingWallet = true; @@ -469,6 +504,7 @@ class WalletListBodyState extends State { context, onAuthSuccess: (isAuthenticatedSuccessfully) async { if (!isAuthenticatedSuccessfully) { + printV("!isAuthenticatedSuccessfully"); _loadingWallet = false; return; } @@ -510,16 +546,16 @@ class WalletListBodyState extends State { await widget.walletListViewModel.loadWallet(wallet); // only pop the wallets route in mobile as it will go back to dashboard page // in desktop platforms the navigation tree is different + unawaited(hideProgressText()); if (responsiveLayoutUtil.shouldRenderMobileUI) { // await Future.delayed(Duration(seconds: 1)); - // if (!this.mounted) return; + if (!this.mounted) return; if (!context.mounted) return; if (requireHardwareWalletConnection) { Navigator.of(context).pop(); } await widget.onWalletLoaded.call(context); } - unawaited(hideProgressText()); } catch (e) { await ExceptionHandler.resetLastPopupDate(); final err = e.toString(); @@ -534,16 +570,25 @@ class WalletListBodyState extends State { conditionToDetermineIfToUse2FA: widget.walletListViewModel.shouldRequireTOTP2FAForAccessingWallet, ); + _loadingWallet = false; } void changeProcessText(String text) { - _progressBar = createBar(text, context, duration: null)..show(context); + try { + if (_progressBar != null) { + _progressBar!.dismiss(); + } + _progressBar = createBar(text, context, duration: null) + ..show(context); + }catch(e){} } Future hideProgressText() async { await Future.delayed(Duration(milliseconds: 50), () { - _progressBar?.dismiss(); - _progressBar = null; + try { + _progressBar?.dismiss(); + _progressBar = null; + } catch (e) {} }); } } diff --git a/lib/src/screens/welcome/create_pin_welcome_page.dart b/lib/src/screens/welcome/create_pin_welcome_page.dart index 7637d7abf..f8312fc79 100644 --- a/lib/src/screens/welcome/create_pin_welcome_page.dart +++ b/lib/src/screens/welcome/create_pin_welcome_page.dart @@ -1,5 +1,7 @@ +import 'dart:io'; + import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/routes.dart'; @@ -86,150 +88,154 @@ class CreatePinWelcomePage extends BasePage { return PopScope( canPop: false, - child: ScrollableWithBottomSection( - contentPadding: EdgeInsets.zero, - content: Container( - alignment: Alignment.center, - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: imageMaxWidth), - child: Stack( - alignment: Alignment.center, - clipBehavior: Clip.none, - children: [ - AspectRatio( - aspectRatio: imageAspectRatio, - child: FittedBox( - child: CakeImageWidget( - imageUrl: welcomeImage, + child: SafeArea( + top: false, + bottom: Platform.isAndroid, + child: ScrollableWithBottomSection( + contentPadding: EdgeInsets.zero, + content: Container( + alignment: Alignment.center, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: imageMaxWidth), + child: Stack( + alignment: Alignment.center, + clipBehavior: Clip.none, + children: [ + AspectRatio( + aspectRatio: imageAspectRatio, + child: FittedBox( + child: CakeImageWidget( + imageUrl: welcomeImage, + fit: BoxFit.contain, + ), fit: BoxFit.contain, ), - fit: BoxFit.contain, ), - ), - Positioned( - top: (imageMaxWidth / imageAspectRatio) * 0.57, - child: Column( - children: [ - Text( - S.current.welcome, - style: Theme.of(context).textTheme.displayLarge?.copyWith( - fontWeight: FontWeight.w700, - fontSize: 60, - letterSpacing: 2.5, - color: Theme.of(context).colorScheme.primary, - ), - textAlign: TextAlign.center, - ), - SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - S.current.to.toLowerCase(), - style: Theme.of(context).textTheme.titleMedium, - textAlign: TextAlign.center, - ), - SizedBox(width: 8), - if (!isMoneroOnly) ...[ - SizedBox(width: 8), - CakeImageWidget( - height: 40, - imageUrl: cakeLogoThemed, - fit: BoxFit.contain, - ), - SizedBox(width: 8), - ], - Text( - appTitle(context), - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.w600, - ), - textAlign: TextAlign.center, - ), - ], - ), - SizedBox(height: 48), - if (isMoneroOnly) + Positioned( + top: (imageMaxWidth / imageAspectRatio) * 0.57, + child: Column( + children: [ Text( - appDescription(context), - style: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith(fontWeight: FontWeight.w500), + S.current.welcome, + style: Theme.of(context).textTheme.displayLarge?.copyWith( + fontWeight: FontWeight.w700, + fontSize: 60, + letterSpacing: 2.5, + color: Theme.of(context).colorScheme.primary, + ), textAlign: TextAlign.center, ), - if (!isMoneroOnly) + SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, children: [ - AnimatedTypingText( - words: [S.current.payments, S.current.privacy, S.current.security], - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.primary, - ), - cursorColor: Theme.of(context).colorScheme.primary, - cursorHeight: 28, - cursorWidth: 4, - ), - SizedBox(width: 4), Text( - S.current.made_easy, + S.current.to.toLowerCase(), + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + SizedBox(width: 8), + if (!isMoneroOnly) ...[ + SizedBox(width: 8), + CakeImageWidget( + height: 40, + imageUrl: cakeLogoThemed, + fit: BoxFit.contain, + ), + SizedBox(width: 8), + ], + Text( + appTitle(context), style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w600, ), + textAlign: TextAlign.center, ), ], ), - ], - ), - ), - ], - ), - ), - ), - bottomSection: Column( - children: [ - RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, + SizedBox(height: 48), + if (isMoneroOnly) + Text( + appDescription(context), + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith(fontWeight: FontWeight.w500), + textAlign: TextAlign.center, + ), + if (!isMoneroOnly) + Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedTypingText( + words: [S.current.payments, S.current.privacy, S.current.security], + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.primary, + ), + cursorColor: Theme.of(context).colorScheme.primary, + cursorHeight: 28, + cursorWidth: 4, + ), + SizedBox(width: 4), + Text( + S.current.made_easy, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ], ), - children: [ - TextSpan( - text: 'By continuing, you agree to our ', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontWeight: FontWeight.w500, - ), - ), - TextSpan( - text: 'Terms of Service', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - decoration: TextDecoration.underline, - fontWeight: FontWeight.w700, - ), - recognizer: TapGestureRecognizer() - ..onTap = () => Navigator.pushNamed(context, Routes.readDisclaimer), ), ], ), ), - Padding( - padding: EdgeInsets.only(top: 24), - child: PrimaryButton( - key: ValueKey('create_pin_welcome_page_create_a_pin_button_key'), - onPressed: () => Navigator.pushNamed(context, Routes.welcomeWallet), - text: isWalletPasswordDirectInput ? S.current.set_up_a_wallet : S.current.set_a_pin, - color: Theme.of(context).colorScheme.primary, - textColor: Theme.of(context).colorScheme.onPrimary, + ), + bottomSection: Column( + children: [ + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + children: [ + TextSpan( + text: 'By continuing, you agree to our ', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w500, + ), + ), + TextSpan( + text: 'Terms of Service', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + decoration: TextDecoration.underline, + fontWeight: FontWeight.w700, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => Navigator.pushNamed(context, Routes.readDisclaimer), + ), + ], + ), ), - ), - SizedBox(height: 16), - ], + Padding( + padding: EdgeInsets.only(top: 24), + child: PrimaryButton( + key: ValueKey('create_pin_welcome_page_create_a_pin_button_key'), + onPressed: () => Navigator.pushNamed(context, Routes.welcomeWallet), + text: isWalletPasswordDirectInput ? S.current.set_up_a_wallet : S.current.set_a_pin, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + ), + ), + SizedBox(height: 16), + ], + ), ), ), ); diff --git a/lib/src/screens/welcome/welcome_page.dart b/lib/src/screens/welcome/welcome_page.dart index 023bafbe2..ff21b6610 100644 --- a/lib/src/screens/welcome/welcome_page.dart +++ b/lib/src/screens/welcome/welcome_page.dart @@ -1,5 +1,5 @@ import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/routes.dart'; diff --git a/lib/src/screens/yat/widgets/first_introduction.dart b/lib/src/screens/yat/widgets/first_introduction.dart index b7e11f860..a007b408d 100644 --- a/lib/src/screens/yat/widgets/first_introduction.dart +++ b/lib/src/screens/yat/widgets/first_introduction.dart @@ -1,7 +1,7 @@ import 'package:cake_wallet/src/screens/yat/widgets/yat_bar.dart'; import 'package:cake_wallet/src/screens/yat/widgets/yat_page_indicator.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; import 'package:flutter/material.dart'; import 'package:lottie/lottie.dart'; import 'package:cake_wallet/generated/i18n.dart'; diff --git a/lib/src/screens/yat/widgets/second_introduction.dart b/lib/src/screens/yat/widgets/second_introduction.dart index 6d2f2959a..4482e5f05 100644 --- a/lib/src/screens/yat/widgets/second_introduction.dart +++ b/lib/src/screens/yat/widgets/second_introduction.dart @@ -1,7 +1,7 @@ import 'package:cake_wallet/src/screens/yat/widgets/yat_bar.dart'; import 'package:cake_wallet/src/screens/yat/widgets/yat_page_indicator.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; import 'package:flutter/material.dart'; import 'package:lottie/lottie.dart'; import 'package:cake_wallet/generated/i18n.dart'; diff --git a/lib/src/screens/yat/widgets/third_introduction.dart b/lib/src/screens/yat/widgets/third_introduction.dart index 061e82208..63fcdcd18 100644 --- a/lib/src/screens/yat/widgets/third_introduction.dart +++ b/lib/src/screens/yat/widgets/third_introduction.dart @@ -1,7 +1,7 @@ import 'package:cake_wallet/src/screens/yat/widgets/yat_bar.dart'; import 'package:cake_wallet/src/screens/yat/widgets/yat_page_indicator.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:lottie/lottie.dart'; diff --git a/lib/src/widgets/address_text_field.dart b/lib/src/widgets/address_text_field.dart index 6a0084652..c5ef63874 100644 --- a/lib/src/widgets/address_text_field.dart +++ b/lib/src/widgets/address_text_field.dart @@ -29,6 +29,7 @@ class AddressTextField extends StatelessWidget { this.onPushPasteButton, this.onPushAddressBookButton, this.onPushAddressPickerButton, + this.onEditingComplete, this.onSelectedContact, this.selectedCurrency, this.addressKey, @@ -49,6 +50,7 @@ class AddressTextField extends StatelessWidget { final Function(Uri)? onURIScanned; final List options; final FormFieldValidator? validator; + final VoidCallback? onEditingComplete; final Color? buttonColor; final Color? fillColor; @@ -77,6 +79,7 @@ class AddressTextField extends StatelessWidget { borderWidth: borderWidth, hasUnderlineBorder: hasUnderlineBorder, key: addressKey, + onEditingComplete: onEditingComplete, enableIMEPersonalizedLearning: false, keyboardType: TextInputType.visiblePassword, onFieldSubmitted: (_) => FocusScope.of(context).unfocus(), @@ -242,7 +245,7 @@ class AddressTextField extends StatelessWidget { Future _presetAddressBookPicker(BuildContext context) async { final contact = await Navigator.of(context) - .pushNamed(Routes.pickerAddressBook, arguments: selectedCurrency); + .pushNamed(Routes.pickerAddressBook, arguments: [selectedCurrency, false]); if (contact is ContactBase) { controller?.text = contact.address; diff --git a/lib/src/widgets/base_text_form_field.dart b/lib/src/widgets/base_text_form_field.dart index fbff5a338..e605a83f2 100644 --- a/lib/src/widgets/base_text_form_field.dart +++ b/lib/src/widgets/base_text_form_field.dart @@ -47,6 +47,7 @@ class BaseTextFormField extends StatelessWidget { this.suffixIconConstraints, super.key, this.suffixText, + this.borderRadius = const BorderRadius.all(Radius.circular(18)), this.onEditingComplete, }); final TextEditingController? controller; @@ -82,6 +83,7 @@ class BaseTextFormField extends StatelessWidget { final bool? autocorrect; final bool? enableSuggestions; final void Function(String)? onChanged; + final VoidCallback? onEditingComplete; final EdgeInsetsGeometry? contentPadding; final bool? alignLabelWithHint; final FloatingLabelBehavior? floatingLabelBehavior; @@ -92,6 +94,7 @@ class BaseTextFormField extends StatelessWidget { final void Function(String)? onFieldSubmitted; final bool hasUnderlineBorder; final double borderWidth; + final BorderRadius borderRadius; @override Widget build(BuildContext context) { @@ -110,6 +113,7 @@ class BaseTextFormField extends StatelessWidget { controller: controller, keyboardType: keyboardType, textInputAction: textInputAction, + onEditingComplete: onEditingComplete, textAlign: textAlign, autovalidateMode: autovalidateMode, obscureText: obscureText, @@ -147,44 +151,49 @@ class BaseTextFormField extends StatelessWidget { labelStyle: !hasUnderlineBorder ? placeholderTextStyle : null, border: hasUnderlineBorder ? UnderlineInputBorder( + borderRadius: borderRadius, borderSide: BorderSide( color: Theme.of(context).colorScheme.outlineVariant, style: borderWidth == 0.0 ? BorderStyle.none : BorderStyle.solid, width: borderWidth, ), ) - : null, + : OutlineInputBorder(borderRadius: borderRadius, borderSide: BorderSide.none), focusedBorder: hasUnderlineBorder ? UnderlineInputBorder( + borderRadius: borderRadius, borderSide: BorderSide( color: Theme.of(context).colorScheme.outlineVariant, style: borderWidth == 0.0 ? BorderStyle.none : BorderStyle.solid, width: borderWidth, ), ) - : null, + : OutlineInputBorder(borderRadius: borderRadius, borderSide: BorderSide.none), disabledBorder: hasUnderlineBorder ? UnderlineInputBorder( + borderRadius: borderRadius, borderSide: BorderSide( color: Theme.of(context).colorScheme.outlineVariant, width: borderWidth, ), ) - : null, + : OutlineInputBorder(borderRadius: borderRadius, borderSide: BorderSide.none), enabledBorder: hasUnderlineBorder ? UnderlineInputBorder( + borderRadius: borderRadius, borderSide: BorderSide( color: Theme.of(context).colorScheme.outlineVariant, style: borderWidth == 0.0 ? BorderStyle.none : BorderStyle.solid, width: borderWidth, ), ) - : null, + : OutlineInputBorder(borderRadius: borderRadius, borderSide: BorderSide.none), errorBorder: hasUnderlineBorder ? UnderlineInputBorder( + borderRadius: borderRadius, borderSide: BorderSide(color: Theme.of(context).colorScheme.error), ) - : null, + : OutlineInputBorder(borderRadius: borderRadius, borderSide: BorderSide.none), ), validator: validator, ); diff --git a/lib/src/widgets/blockchain_height_widget.dart b/lib/src/widgets/blockchain_height_widget.dart index 6473891d2..876b9904d 100644 --- a/lib/src/widgets/blockchain_height_widget.dart +++ b/lib/src/widgets/blockchain_height_widget.dart @@ -25,6 +25,7 @@ class BlockchainHeightWidget extends StatefulWidget { this.bitcoinMempoolAPIEnabled, required this.walletType, this.blockHeightTextFieldKey, + this.heightController, }) : super(key: key); final Function(int)? onHeightChange; @@ -33,6 +34,7 @@ class BlockchainHeightWidget extends StatefulWidget { final bool hasDatePicker; final bool isSilentPaymentsScan; final bool isMwebScan; + final TextEditingController? heightController; final bool doSingleScan; final Future? bitcoinMempoolAPIEnabled; final Function()? toggleSingleScan; @@ -44,13 +46,15 @@ class BlockchainHeightWidget extends StatefulWidget { class BlockchainHeightState extends State { final dateController = TextEditingController(); - final restoreHeightController = TextEditingController(); + late final TextEditingController restoreHeightController; int get height => _height; int _height = 0; @override void initState() { + restoreHeightController = widget.heightController ?? TextEditingController(); + restoreHeightController.addListener(() { if (restoreHeightController.text.isNotEmpty) { widget.onHeightOrDateEntered?.call(true); @@ -71,100 +75,96 @@ class BlockchainHeightState extends State { @override Widget build(BuildContext context) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => FocusScope.of(context).unfocus(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Flexible( + child: Container( + padding: EdgeInsets.only(top: 20.0, bottom: 10.0), + child: BaseTextFormField( + key: widget.blockHeightTextFieldKey, + focusNode: widget.focusNode, + controller: restoreHeightController, + keyboardType: TextInputType.numberWithOptions( + signed: false, + decimal: false, + ), + hintText: widget.isSilentPaymentsScan + ? S.of(context).silent_payments_scan_from_height + : S.of(context).widgets_restore_from_blockheight, + ), + ), + ) + ], + ), + if (widget.hasDatePicker) ...[ + Padding( + padding: EdgeInsets.only(top: 15, bottom: 15), + child: Text( + S.of(context).widgets_or, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), Row( children: [ Flexible( - child: Container( - padding: EdgeInsets.only(top: 20.0, bottom: 10.0), - child: BaseTextFormField( - key: widget.blockHeightTextFieldKey, - focusNode: widget.focusNode, - controller: restoreHeightController, - keyboardType: TextInputType.numberWithOptions( - signed: false, - decimal: false, + child: Container( + child: InkWell( + onTap: () => _selectDate(context), + child: IgnorePointer( + child: BaseTextFormField( + controller: dateController, + hintText: widget.isSilentPaymentsScan + ? S.of(context).silent_payments_scan_from_date + : S.of(context).widgets_restore_from_date, ), - hintText: widget.isSilentPaymentsScan - ? S.of(context).silent_payments_scan_from_height - : S.of(context).widgets_restore_from_blockheight, ), ), - ) + )) ], ), - if (widget.hasDatePicker) ...[ + if (widget.isSilentPaymentsScan) Padding( - padding: EdgeInsets.only(top: 15, bottom: 15), - child: Text( - S.of(context).widgets_or, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ), - Row( - children: [ - Flexible( - child: Container( - child: InkWell( - onTap: () => _selectDate(context), - child: IgnorePointer( - child: BaseTextFormField( - controller: dateController, - hintText: widget.isSilentPaymentsScan - ? S.of(context).silent_payments_scan_from_date - : S.of(context).widgets_restore_from_date, - ), - ), + padding: EdgeInsets.only(top: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + S.of(context).scan_one_block, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), - )) - ], + Padding( + padding: const EdgeInsets.only(right: 8), + child: StandardSwitch( + value: widget.doSingleScan, + onTapped: () => widget.toggleSingleScan?.call(), + ), + ) + ], + ), ), - if (widget.isSilentPaymentsScan) - Padding( - padding: EdgeInsets.only(top: 24), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - S.of(context).scan_one_block, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - Padding( - padding: const EdgeInsets.only(right: 8), - child: StandardSwitch( - value: widget.doSingleScan, - onTapped: () => widget.toggleSingleScan?.call(), - ), - ) - ], - ), - ), - Padding( - padding: EdgeInsets.only(left: 40, right: 40, top: 24), - child: Text( - widget.isSilentPaymentsScan - ? S.of(context).silent_payments_scan_from_date_or_blockheight - : S.of(context).restore_from_date_or_blockheight, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ) - ] - ], - ), + Padding( + padding: EdgeInsets.only(left: 40, right: 40, top: 24), + child: Text( + widget.isSilentPaymentsScan + ? S.of(context).silent_payments_scan_from_date_or_blockheight + : S.of(context).restore_from_date_or_blockheight, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ) + ] + ], ); } diff --git a/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart b/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart index 1f9b09d52..e78b4985c 100644 --- a/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart +++ b/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart @@ -1,3 +1,5 @@ +import 'package:cake_wallet/core/amount_parsing_proxy.dart'; +import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/rounded_icon_button.dart'; import 'package:cake_wallet/themes/core/theme_extension.dart'; @@ -35,6 +37,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { required this.feeFiatAmount, required this.outputs, required this.walletType, + this.amountParsingProxy, this.change, this.explanation, this.isOpenCryptoPay = false, @@ -71,10 +74,14 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { final CakePayBuyCardViewModel? cakePayBuyCardViewModel; final String? quantity; final String? explanation; + final AmountParsingProxy? amountParsingProxy; final bool showScrollbar; final ScrollController scrollController = ScrollController(); + bool get showAddress => !outputs + .any((e) => RegExp(AddressValidator.bolt11InvoiceMatcher).hasMatch(e.address.toLowerCase())); + @override Widget contentWidget(BuildContext context) { final itemTitleTextStyle = Theme.of(context).textTheme.bodyMedium!.copyWith( @@ -113,7 +120,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { itemSubTitleTextStyle: itemSubTitleTextStyle, tileBackgroundColor: tileBackgroundColor, applyAddressFormatting: false, - copyButton: true, + copyButton: true, )), ), if (explanation != null) @@ -127,7 +134,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { ), StandardTile( itemTitle: amount, - itemValue: '$amountValue ${currency.title}', + itemValue: '$amountValue ${amountParsingProxy?.getCryptoSymbol(currency) ?? currency.title}', itemTitleTextStyle: itemTitleTextStyle, itemSubTitle: fiatAmountValue, itemSubTitleTextStyle: itemSubTitleTextStyle, @@ -145,48 +152,49 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { const SizedBox(height: 8), Column( children: [ - ListView.separated( - padding: const EdgeInsets.only(top: 0), - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: outputs.length, - separatorBuilder: (context, index) => const SizedBox(height: 8), - itemBuilder: (context, index) { - final bool isBatchSending = outputs.length > 1; - final item = outputs[index]; - final contactName = item.parsedAddress.name; - final isCakePayName = contactName == 'Cake Pay'; - final batchContactTitle = - '${index + 1}/${outputs.length} - ${contactName.isEmpty ? 'Address' : contactName}'; - final _address = item.isParsedAddress ? item.extractedAddress : item.address; - final _amount = item.cryptoAmount.replaceAll(',', '.') + ' ${currency.title}'; - return isBatchSending || (contactName.isNotEmpty && !isCakePayName) - ? ExpansionAddressTile( - contactType: isOpenCryptoPay ? 'Open CryptoPay' : S.of(context).contact, - name: isBatchSending ? batchContactTitle : contactName, - address: _address, - amount: _amount, - walletType: walletType, - isBatchSending: isBatchSending, - itemTitleTextStyle: itemTitleTextStyle, - itemSubTitleTextStyle: itemSubTitleTextStyle, - tileBackgroundColor: tileBackgroundColor, - ) - : AddressTile( - itemTitle: isCakePayName - ? item.parsedAddress.profileName - : S.of(context).address, - imagePath: isCakePayName ? item.parsedAddress.profileImageUrl : null, - itemTitleTextStyle: itemTitleTextStyle, - walletType: walletType, - amount: isCakePayName ? item.fiatAmount : _amount, - address: _address, - itemSubTitle: isCakePayName ? quantity : null, - itemSubTitleTextStyle: itemSubTitleTextStyle, - tileBackgroundColor: tileBackgroundColor, - ); - }, - ), + if (showAddress) + ListView.separated( + padding: const EdgeInsets.only(top: 0), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: outputs.length, + separatorBuilder: (_, __) => const SizedBox(height: 8), + itemBuilder: (context, index) { + final isBatchSending = outputs.length > 1; + final item = outputs[index]; + final contactName = item.parsedAddress.name; + final isCakePayName = contactName == 'Cake Pay'; + final batchContactTitle = + '${index + 1}/${outputs.length} - ${contactName.isEmpty ? 'Address' : contactName}'; + final _address = item.isParsedAddress ? item.extractedAddress : item.address; + final _amount = '${item.cryptoAmount.replaceAll(',', '.')} ${amountParsingProxy?.getCryptoSymbol(currency) ?? currency.title}'; + return isBatchSending || (contactName.isNotEmpty && !isCakePayName) + ? ExpansionAddressTile( + contactType: isOpenCryptoPay ? 'Open CryptoPay' : S.of(context).contact, + name: isBatchSending ? batchContactTitle : contactName, + address: _address, + amount: _amount, + walletType: walletType, + isBatchSending: isBatchSending, + itemTitleTextStyle: itemTitleTextStyle, + itemSubTitleTextStyle: itemSubTitleTextStyle, + tileBackgroundColor: tileBackgroundColor, + ) + : AddressTile( + itemTitle: isCakePayName + ? item.parsedAddress.profileName + : S.of(context).address, + imagePath: isCakePayName ? item.parsedAddress.profileImageUrl : null, + itemTitleTextStyle: itemTitleTextStyle, + walletType: walletType, + amount: isCakePayName ? item.fiatAmount : _amount, + address: _address, + itemSubTitle: isCakePayName ? quantity : null, + itemSubTitleTextStyle: itemSubTitleTextStyle, + tileBackgroundColor: tileBackgroundColor, + ); + }, + ), if (change != null) Padding( padding: const EdgeInsets.only(top: 8), @@ -194,7 +202,8 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { contactType: 'Change', name: S.of(context).send_change_to_you, address: change!.address, - amount: change!.amount + ' ${currency.title}', + amount: + '${amountParsingProxy?.getDisplayCryptoString(change!.amount.toInt(), currency)} ${amountParsingProxy?.getCryptoSymbol(currency) ?? currency.title}', isBatchSending: true, walletType: walletType, itemTitleTextStyle: itemTitleTextStyle, diff --git a/lib/src/widgets/bottom_sheet/payment_confirmation_bottom_sheet.dart b/lib/src/widgets/bottom_sheet/payment_confirmation_bottom_sheet.dart index ca7cf6d1a..9540566c9 100644 --- a/lib/src/widgets/bottom_sheet/payment_confirmation_bottom_sheet.dart +++ b/lib/src/widgets/bottom_sheet/payment_confirmation_bottom_sheet.dart @@ -1,8 +1,8 @@ -import 'package:cake_wallet/entities/generate_name.dart'; import 'package:cake_wallet/evm/evm.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; +import 'package:cw_core/generate_name.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; @@ -153,10 +153,10 @@ class _PaymentConfirmationContent extends StatelessWidget { bottom: 0, right: 0, child: Image.asset( - getCryptoCurrencyForWalletListItem( + getCryptoCurrencyIconForWalletListItem( paymentViewModel.detectedWalletType!, chainId: paymentViewModel.detectedChainId, - ).iconPath!, + ), width: 32, height: 32, errorBuilder: (context, error, stackTrace) => Icon( @@ -192,10 +192,10 @@ class _PaymentConfirmationContent extends StatelessWidget { ), ] else ...[ Image.asset( - getCryptoCurrencyForWalletListItem( + getCryptoCurrencyIconForWalletListItem( paymentViewModel.detectedWalletType!, chainId: paymentViewModel.detectedChainId, - ).iconPath!, + ), width: 118, height: 118, ), diff --git a/lib/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart b/lib/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart index 614a33949..cd69285bc 100644 --- a/lib/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart +++ b/lib/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart @@ -172,10 +172,10 @@ class SwapConfirmationContentState extends State { bottom: -4, right: -4, child: CakeImageWidget( - imageUrl: getCryptoCurrencyForWalletListItem( + imageUrl: getCryptoCurrencyIconForWalletListItem( widget.paymentFlowResult.walletType!, chainId: widget.paymentFlowResult.chainId, - ).iconPath!, + ), width: 16, height: 16, ), @@ -212,6 +212,7 @@ class SwapConfirmationContentState extends State { return AmountValidator( isAutovalidate: true, currency: widget.exchangeViewModel.receiveCurrency, + amountParsingProxy: widget.exchangeViewModel.amountParsingProxy, minValue: widget.exchangeViewModel.limits.min.toString(), maxValue: widget.exchangeViewModel.limits.max.toString(), ).call(value); @@ -286,7 +287,7 @@ class SwapConfirmationContentState extends State { SwapConfirmationTextfield( key: ValueKey('swap_confirmation_bottomsheet_address_textfield_key'), isAddress: true, - walletType: cryptoCurrencyToWalletType(widget.exchangeViewModel.receiveCurrency), + walletType: cryptoCurrencyOrTokenToWalletType(widget.exchangeViewModel.receiveCurrency), hintText: 'Destination Address', focusNode: _addressFocus, controller: _addressController, @@ -439,7 +440,7 @@ class SwapConfirmationContentState extends State { exchangeViewModel.receiveAddress = _addressController.text; exchangeViewModel.depositAddress = exchangeViewModel.wallet.walletAddresses.addressForExchange; - exchangeViewModel.receiveAmount = _amountController.text; + exchangeViewModel.setCanonicalReceiveAmount(_amountController.text); _amountFiatController.text = exchangeViewModel.receiveAmountFiatFormatted; exchangeViewModel.isReceiveAmountEntered = true; exchangeViewModel.isFixedRateMode = true; diff --git a/lib/src/widgets/bottom_sheet/swap_details_bottom_sheet.dart b/lib/src/widgets/bottom_sheet/swap_details_bottom_sheet.dart index 717fd80eb..9545f7757 100644 --- a/lib/src/widgets/bottom_sheet/swap_details_bottom_sheet.dart +++ b/lib/src/widgets/bottom_sheet/swap_details_bottom_sheet.dart @@ -112,6 +112,9 @@ class _SwapDetailsBottomSheetState extends State { buttonAction: () { _showingFailureDialog = false; Navigator.of(popupContext).pop(); + if(mounted) { + Navigator.of(context, rootNavigator: true).pop(); + } }, ); }, @@ -172,7 +175,7 @@ class _SwapDetailsBottomSheetState extends State { onSingleActionButtonPressed: () { Navigator.of(bottomSheetContext).pop(); if (mounted) { - Navigator.of(context).pop(); + Navigator.of(context, rootNavigator: true).pop(); } ; }, @@ -348,7 +351,7 @@ class _SwapDetailsContent extends StatelessWidget { const SizedBox(height: 4), AddressFormatter.buildSegmentedAddress( address: trade.payoutAddress ?? '', - walletType: trade.to != null ? cryptoCurrencyToWalletType(trade.to!) : null, + walletType: trade.to != null ? cryptoCurrencyOrTokenToWalletType(trade.to!) : null, evenTextStyle: Theme.of(context) .textTheme .bodyMedium! diff --git a/lib/src/widgets/bottom_sheet/wallet_switcher_bottom_sheet.dart b/lib/src/widgets/bottom_sheet/wallet_switcher_bottom_sheet.dart index b29da64b2..f017ae67d 100644 --- a/lib/src/widgets/bottom_sheet/wallet_switcher_bottom_sheet.dart +++ b/lib/src/widgets/bottom_sheet/wallet_switcher_bottom_sheet.dart @@ -97,9 +97,9 @@ class _WalletSwitcherContent extends StatelessWidget { child: Row( children: [ Image.asset( - getCryptoCurrencyForWalletListItem( + getCryptoCurrencyIconForWalletListItem( wallet.type, - ).iconPath!, + ), width: 32, height: 32, ), diff --git a/lib/src/widgets/cake_image_widget.dart b/lib/src/widgets/cake_image_widget.dart index 5c80083a3..27c56e60e 100644 --- a/lib/src/widgets/cake_image_widget.dart +++ b/lib/src/widgets/cake_image_widget.dart @@ -12,6 +12,7 @@ class CakeImageWidget extends StatelessWidget { this.errorWidget, this.color, this.borderRadius = 24.0, + this.colorFilter, }); final String? imageUrl; @@ -22,6 +23,7 @@ class CakeImageWidget extends StatelessWidget { final Widget? errorWidget; final Color? color; final double borderRadius; + final ColorFilter? colorFilter; @override Widget build(BuildContext context) { @@ -31,6 +33,8 @@ class CakeImageWidget extends StatelessWidget { final isSvg = imageUrl!.toLowerCase().endsWith('.svg'); final isAsset = imageUrl!.startsWith('assets/'); + final effectiveColorFilter = colorFilter ?? + (color != null ? ColorFilter.mode(color!, BlendMode.srcIn) : null); Widget imageWidget; if (isAsset) { @@ -39,7 +43,7 @@ class CakeImageWidget extends StatelessWidget { imageUrl!, height: height, width: width, - color: color, + colorFilter: effectiveColorFilter, fit: fit ?? BoxFit.contain, ) : Image.asset( @@ -55,7 +59,7 @@ class CakeImageWidget extends StatelessWidget { imageUrl!, height: height, width: width, - color: color, + colorFilter: effectiveColorFilter, fit: fit ?? BoxFit.contain, placeholderBuilder: (_) { return loadingWidget ?? const Center(child: CircularProgressIndicator()); diff --git a/lib/src/widgets/new_list_row/list_Item_style_wrapper.dart b/lib/src/widgets/new_list_row/list_Item_style_wrapper.dart new file mode 100644 index 000000000..a56137568 --- /dev/null +++ b/lib/src/widgets/new_list_row/list_Item_style_wrapper.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; + +class ListItemStyleWrapper extends StatelessWidget { + const ListItemStyleWrapper({ + super.key, + required this.isFirstInSection, + required this.isLastInSection, + required this.builder, + this.hasImage, + this.onTap, + this.height = 50, + }); + + final bool isFirstInSection; + final bool isLastInSection; + final bool ?hasImage; + final double height; + final VoidCallback? onTap; + final Widget Function(BuildContext context, TextStyle textStyle, TextStyle labelStyle) builder; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + final textStyle = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + fontFamily: 'Wix Madefor Text', + color: theme.colorScheme.onSurface, + ); + + final labelStyle = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + fontFamily: 'Wix Madefor Text', + color: theme.colorScheme.onSurfaceVariant, + ); + + final radius = BorderRadius.vertical( + top: Radius.circular(isFirstInSection ? 18 : 0), + bottom: Radius.circular(isLastInSection ? 18 : 0), + ); + + return ClipRSuperellipse( + borderRadius: radius, + child: Column( + children: [ + Container( + height: height, + decoration: ShapeDecoration( + shape: RoundedSuperellipseBorder( + borderRadius: radius, + ), + color: theme.colorScheme.surfaceContainer, + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: builder(context, textStyle, labelStyle))))), + if(hasImage == true && isLastInSection == false) Container( + color: theme.colorScheme.surfaceContainer, + child: Padding( + padding: const EdgeInsets.only(left: 48, right: 13), + child: Container(height: 1, color: theme.colorScheme.outlineVariant), + ), + ) + else if(!isLastInSection) Container( + color: theme.colorScheme.surfaceContainer, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 13), + child: Container(height: 1, color: theme.colorScheme.outlineVariant), + ), + ) + ], + ), + ); + } +} diff --git a/lib/src/widgets/new_list_row/list_item_checkbox_widget.dart b/lib/src/widgets/new_list_row/list_item_checkbox_widget.dart new file mode 100644 index 000000000..6853d69f7 --- /dev/null +++ b/lib/src/widgets/new_list_row/list_item_checkbox_widget.dart @@ -0,0 +1,101 @@ +import 'package:cake_wallet/src/widgets/new_list_row/list_Item_style_wrapper.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/new_simple_checkbox.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class ListItemCheckboxWidget extends StatefulWidget { + const ListItemCheckboxWidget({ + super.key, + required this.keyValue, + required this.label, + required this.value, + required this.onChanged, + this.onTap, + this.isFirstInSection = false, + this.isLastInSection = false, this.subtitle, this.iconPath, this.showArrow = false, + }); + + final String keyValue; + final String label; + final String? subtitle; + final String? iconPath; + final bool showArrow; + final bool value; + final VoidCallback? onTap; + final ValueChanged onChanged; + final bool isFirstInSection; + final bool isLastInSection; + + @override + State createState() => _ListItemCheckboxWidgetState(); +} + +class _ListItemCheckboxWidgetState extends State { + + + @override + Widget build(BuildContext context) { + return ListItemStyleWrapper( + onTap: widget.onTap ?? () { + widget.onChanged(!widget.value); + }, + isFirstInSection: widget.isFirstInSection, + height: widget.subtitle != null ? 64 : 50, + isLastInSection: widget.isLastInSection, + builder: (context, textStyle, labelStyle) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + spacing: 12, + children: [ + if (widget.iconPath != null) + widget.iconPath!.toLowerCase().endsWith("svg") + ? SvgPicture.asset(widget.iconPath!, height: 24, width: 24) + : Image.asset( + widget.iconPath!, + width: 24, + height: 24, + ), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + Flexible(child: Text(widget.label)), + if (widget.showArrow) + Icon( + Icons.chevron_right, + size: 18, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ) + ], + ), + if (widget.subtitle != null) + Text( + widget.subtitle!, + style: TextStyle( + fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), + ) + ], + ), + ), + ], + ), + ), + NewSimpleCheckbox( + value: widget.value, + onChanged: (newValue) { + widget.onChanged(newValue); + }, + ), + ], + ); + }, + ); + } +} diff --git a/lib/src/widgets/new_list_row/list_item_dropdown_widget.dart b/lib/src/widgets/new_list_row/list_item_dropdown_widget.dart new file mode 100644 index 000000000..011f90ea9 --- /dev/null +++ b/lib/src/widgets/new_list_row/list_item_dropdown_widget.dart @@ -0,0 +1,61 @@ +import 'package:cake_wallet/src/widgets/new_list_row/list_Item_style_wrapper.dart'; +import 'package:flutter/material.dart'; + +class ListItemDropdownWidget extends StatelessWidget { + const ListItemDropdownWidget({ + super.key, + required this.keyValue, + required this.label, + this.trailingText, + required this.onTap, + this.isFirstInSection = false, + this.isLastInSection = false, + }); + + final String keyValue; + final String label; + final String? trailingText; + final VoidCallback onTap; + final bool isFirstInSection; + final bool isLastInSection; + + BorderRadius get radius => BorderRadius.vertical( + top: Radius.circular(isFirstInSection ? 16 : 0), + bottom: Radius.circular(isLastInSection ? 16 : 0), + ); + + @override + Widget build(BuildContext context) { + return ListItemStyleWrapper( + isFirstInSection: isFirstInSection, + isLastInSection: isLastInSection, + builder: (context, textStyle, labelStyle) { + return InkWell( + onTap: onTap, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: textStyle), + Row( + children: [ + if (trailingText != null) + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text( + trailingText!, + style: labelStyle, + ), + ), + Icon( + Icons.keyboard_arrow_down, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ], + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/src/widgets/new_list_row/list_item_regular_row_widget.dart b/lib/src/widgets/new_list_row/list_item_regular_row_widget.dart new file mode 100644 index 000000000..3bded57f3 --- /dev/null +++ b/lib/src/widgets/new_list_row/list_item_regular_row_widget.dart @@ -0,0 +1,122 @@ +import 'package:cake_wallet/src/widgets/new_list_row/list_Item_style_wrapper.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class ListItemRegularRowWidget extends StatelessWidget { + const ListItemRegularRowWidget({ + super.key, + required this.keyValue, + required this.label, + this.subtitle, + this.trailingText, + this.iconPath, + this.onTap, + this.hasImage, + this.isFirstInSection = false, + this.isLastInSection = false, + this.showArrow = true, + this.trailingIconPath, + this.truncateTrailingText = false, + this.foregroundColor, + this.trailingIconSize, + }); + + final String keyValue; + final String label; + final String? subtitle; + final String? trailingText; + final String? iconPath; + final VoidCallback? onTap; + final bool? hasImage; + final bool isFirstInSection; + final bool isLastInSection; + final bool showArrow; + final String? trailingIconPath; + final bool truncateTrailingText; + final Color? foregroundColor; + final double? trailingIconSize; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final trailingTextToShow = truncateTrailingText && trailingText != null && trailingText!.length > 20 + ? "${trailingText!.substring(0, 17)}..." + : trailingText; + + return ListItemStyleWrapper( + onTap: onTap, + hasImage: iconPath != null ? true : false, + isFirstInSection: isFirstInSection, + isLastInSection: isLastInSection, + height: subtitle != null ? 64 : 50, + builder: (context, textStyle, labelStyle) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + if(iconPath != null) + Padding( + padding: const EdgeInsets.only(right: 12.0), + child: iconPath!.split(".").last.toLowerCase() == "svg" + ? SvgPicture.asset( + iconPath!, + width: 24, + height: 24, + ) + : Image.asset( + iconPath!, + width: 24, + height: 24, + ), + ), + Flexible( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: foregroundColor == null ? textStyle : textStyle.copyWith(color: foregroundColor)), + if (subtitle != null) + Text( + subtitle!, + style: labelStyle.copyWith(fontSize: 12), + ), + ], + ), + ), + ], + ), + ), + + Row( + children: [ + if (trailingTextToShow != null) + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text( + trailingTextToShow, + style: labelStyle, + ), + ), + if(trailingIconPath != null) + SvgPicture.asset( + trailingIconPath!, + height: trailingIconSize ?? 18, + width:trailingIconSize ?? 18, + colorFilter: ColorFilter.mode(foregroundColor ?? Theme.of(context).colorScheme.onSurfaceVariant,BlendMode.srcIn), + ) + else if(showArrow) + SvgPicture.asset( + "assets/new-ui/arrow_forward.svg", + height: 14, + color: theme.colorScheme.onSurfaceVariant + ) + ], + ), + ], + ); + } + ); + } +} diff --git a/lib/src/widgets/new_list_row/list_item_selector_widget.dart b/lib/src/widgets/new_list_row/list_item_selector_widget.dart new file mode 100644 index 000000000..3a4a28526 --- /dev/null +++ b/lib/src/widgets/new_list_row/list_item_selector_widget.dart @@ -0,0 +1,56 @@ +import 'package:cake_wallet/src/widgets/new_list_row/list_Item_style_wrapper.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class ListItemSelectorWidget extends StatelessWidget { + const ListItemSelectorWidget({ + super.key, + required this.keyValue, + required this.label, + required this.options, + required this.selectedIndex, + required this.onChanged, + this.isFirstInSection = false, + this.isLastInSection = false, this.onTap, + }); + + final String keyValue; + final String label; + final List options; + final int selectedIndex; + final ValueChanged onChanged; + final bool isFirstInSection; + final bool isLastInSection; + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return ListItemStyleWrapper( + onTap: onTap, + isFirstInSection: isFirstInSection, + isLastInSection: isLastInSection, + builder: (context, textStyle, labelStyle) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible(child: Text(label, style: textStyle)), + Row( + children: [ + Text( + options[selectedIndex], + style: labelStyle, + ), + SvgPicture.asset( + "assets/new-ui/chooser.svg", + colorFilter: + ColorFilter.mode(theme.colorScheme.onSurfaceVariant, BlendMode.srcIn), + ), + SizedBox(width: 8) + ], + ), + ], + ); + }); + } +} diff --git a/lib/src/widgets/new_list_row/list_item_text_field_widget.dart b/lib/src/widgets/new_list_row/list_item_text_field_widget.dart new file mode 100644 index 000000000..9b248c2dc --- /dev/null +++ b/lib/src/widgets/new_list_row/list_item_text_field_widget.dart @@ -0,0 +1,66 @@ +import 'package:cake_wallet/src/widgets/new_list_row/list_Item_style_wrapper.dart'; +import 'package:flutter/material.dart'; + +class ListItemTextFieldWidget extends StatefulWidget { + const ListItemTextFieldWidget({ + super.key, + required this.keyValue, + required this.label, + required this.controller, + this.validator, + this.onChanged, + this.onFieldSubmitted, + this.focusNode, + this.isFirstInSection = false, + this.isLastInSection = false, + }); + + final String keyValue; + final String label; + final TextEditingController controller; + final FormFieldValidator? validator; + final ValueChanged? onChanged; + final ValueChanged? onFieldSubmitted; + final FocusNode? focusNode; + final bool isFirstInSection; + final bool isLastInSection; + + @override + State createState() => + _ListItemTextFieldWidgetState(); +} + +class _ListItemTextFieldWidgetState extends State { + @override + Widget build(BuildContext context) { + return ListItemStyleWrapper( + isFirstInSection: widget.isFirstInSection, + isLastInSection: widget.isLastInSection, + builder: (context, textStyle, labelStyle) { + return Row( + children: [ + Expanded( + child: TextFormField( + controller: widget.controller, + validator: widget.validator, + onChanged: widget.onChanged, + onFieldSubmitted: widget.onFieldSubmitted, + focusNode: widget.focusNode, + style: textStyle, + decoration: InputDecoration( + labelText: widget.label, + labelStyle: labelStyle, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + disabledBorder: InputBorder.none, + isDense: true, + contentPadding: const EdgeInsets.symmetric(vertical: 7), + ), + ), + ), + ], + ); + }); + } +} diff --git a/lib/src/widgets/new_list_row/list_item_toggle_widget.dart b/lib/src/widgets/new_list_row/list_item_toggle_widget.dart new file mode 100644 index 000000000..899fcd246 --- /dev/null +++ b/lib/src/widgets/new_list_row/list_item_toggle_widget.dart @@ -0,0 +1,69 @@ +import 'package:cake_wallet/src/widgets/new_list_row/list_Item_style_wrapper.dart'; +import 'package:cake_wallet/src/widgets/standard_switch.dart'; +import 'package:flutter/material.dart'; + +class ListItemToggleWidget extends StatefulWidget { + const ListItemToggleWidget({ + super.key, + required this.keyValue, + required this.label, + required this.value, + required this.onChanged, + this.leadingEndWidget, + this.isFirstInSection = false, + this.isLastInSection = false, + }); + + final String keyValue; + final String label; + final bool value; + final ValueChanged onChanged; + final Widget? leadingEndWidget; + final bool isFirstInSection; + final bool isLastInSection; + + @override + State createState() => _ListItemToggleWidgetState(); +} + +class _ListItemToggleWidgetState extends State { + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return ListItemStyleWrapper( + isFirstInSection: widget.isFirstInSection, + isLastInSection: widget.isLastInSection, + onTap: () { + widget.onChanged(!widget.value); + }, + builder: (context, textStyle, labelStyle) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + spacing: 8, + children: [ + Flexible( + child: Text(widget.label, style: textStyle, softWrap: true), + ), + if(widget.leadingEndWidget != null) widget.leadingEndWidget! + ], + ), + ), + StandardSwitch( + value: widget.value, + onTapped: () { + widget.onChanged(!widget.value); + }, + ), + ], + ); + }); + } +} diff --git a/lib/src/widgets/new_list_row/new_list_section.dart b/lib/src/widgets/new_list_row/new_list_section.dart new file mode 100644 index 000000000..b36fc5164 --- /dev/null +++ b/lib/src/widgets/new_list_row/new_list_section.dart @@ -0,0 +1,163 @@ +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_Item_checkbox.dart'; +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart'; +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_dropdown.dart'; +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart'; +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_selector.dart'; +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_text_field.dart'; +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_toggle.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/list_item_checkbox_widget.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/list_item_dropdown_widget.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/list_item_regular_row_widget.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/list_item_selector_widget.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/list_item_text_field_widget.dart'; +import 'package:cake_wallet/src/widgets/new_list_row/list_item_toggle_widget.dart'; +import 'package:flutter/material.dart'; + +class NewListSections extends StatelessWidget { + const NewListSections({ + super.key, + required this.sections, + this.controllers = const {}, + this.tapHandlers = const {}, + this.getCheckboxValue, + this.updateCheckboxValue, + this.showHeader = false + }); + + final Map> sections; + final Map controllers; + final bool Function(String key)? getCheckboxValue; + final void Function(String key, bool value)? updateCheckboxValue; + final Map tapHandlers; + final bool showHeader; + + static const double sectionSpacing = 20.0; + + @override + Widget build(BuildContext context) { + final entries = sections.entries.toList(); + + return Column( + children: [ + for (int i = 0; i < entries.length; i++) ...[ + if (i > 0) const SizedBox(height: sectionSpacing), + _buildSection(entries[i].key, entries[i].value, context), + ], + ], + ); + } + + Widget _buildSection(String headerText, List items, BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (showHeader && headerText.isNotEmpty && items.length > 0) ...[ + Text( + headerText, + style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + SizedBox(height: 12) + ], + for (int index = 0; index < items.length; index++) + _withSectionFlags(items[index], index, items.length), + ], + ); + } + + Widget _withSectionFlags(ListItem item, int index, int length) { + final isFirst = index == 0; + final isLast = index == length - 1; + + if (item is ListItemTextField) { + final controller = controllers[item.keyValue]; + + assert( + controller != null, + 'No controller provided for key ${item.keyValue}. ' + 'Please provide a TextEditingController for this key.'); + + return ListItemTextFieldWidget( + keyValue: item.keyValue, + label: item.label, + controller: controllers[item.keyValue]!, + validator: item.validator, + isFirstInSection: isFirst, + isLastInSection: isLast, + onChanged: item.onChanged, + onFieldSubmitted: item.onFieldSubmitted, + focusNode: item.focusNode, + ); + } + + if (item is ListItemRegularRow) { + return ListItemRegularRowWidget( + keyValue: item.keyValue, + label: item.label, + subtitle: item.subtitle, + trailingText: item.trailingText, + iconPath: item.iconPath, + trailingIconPath: item.trailingIconPath, + onTap: tapHandlers[item.keyValue] ?? item.onTap, + isFirstInSection: isFirst, + isLastInSection: isLast, + showArrow: item.showArrow, + truncateTrailingText: item.truncateTrailingText, + foregroundColor: item.foregroundColor, + trailingIconSize: item.trailingIconSize, + ); + } + + if (item is ListItemToggle) { + return ListItemToggleWidget( + keyValue: item.keyValue, + label: item.label, + leadingEndWidget: item.leadingEndWidget, + value: item.value, + onChanged: item.onChanged, + isFirstInSection: isFirst, + isLastInSection: isLast, + ); + } + + if (item is ListItemCheckbox) { + return ListItemCheckboxWidget( + keyValue: item.keyValue, + label: item.label, + subtitle: item.subtitle, + iconPath: item.iconPath, + showArrow: item.showArrow, + onTap: item.onTap, + value: item.value, + onChanged: item.onChanged, + isFirstInSection: isFirst, + isLastInSection: isLast, + ); + } + + if (item is ListItemDropdown) { + return ListItemDropdownWidget( + keyValue: item.keyValue, + label: item.label, + trailingText: item.trailingText, + onTap: tapHandlers[item.keyValue] ?? item.onTap, + isFirstInSection: isFirst, + isLastInSection: isLast, + ); + } + + if (item is ListItemSelector) { + return ListItemSelectorWidget( + keyValue: item.keyValue, + label: item.label, + options: item.options, + onTap: item.onTap, + selectedIndex: 0, + isFirstInSection: isFirst, + isLastInSection: isLast, + onChanged: (int value) {}, + ); + } + + return SizedBox.shrink(); + } +} diff --git a/lib/src/widgets/new_list_row/new_simple_checkbox.dart b/lib/src/widgets/new_list_row/new_simple_checkbox.dart new file mode 100644 index 000000000..b6eb7e8e6 --- /dev/null +++ b/lib/src/widgets/new_list_row/new_simple_checkbox.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +class NewSimpleCheckbox extends StatelessWidget { + const NewSimpleCheckbox({ + super.key, + required this.value, + required this.onChanged, + }); + + final bool value; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + onChanged(!value); + }, + child: Container( + decoration: BoxDecoration( + color: value + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: const BorderRadius.all(Radius.circular(50)), + ), + height: 24, + width: 24, + child: value + ? Icon( + Icons.check, + color: Theme.of(context).colorScheme.onPrimary, + size: 20, + ) + : const SizedBox.shrink(), + ), + ); + } +} diff --git a/lib/src/widgets/picker.dart b/lib/src/widgets/picker.dart index 352f3441e..e784169e7 100644 --- a/lib/src/widgets/picker.dart +++ b/lib/src/widgets/picker.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/entities/seed_type.dart'; import 'package:cake_wallet/src/widgets/search_bar_widget.dart'; import 'package:cake_wallet/themes/core/theme_extension.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:flutter/material.dart'; import 'package:cw_core/currency.dart'; @@ -343,7 +344,7 @@ class _PickerState extends State> { Flexible( child: Text( key: ValueKey('picker_items_index_${itemName}_text_key'), - widget.displayItem?.call(item) ?? item.toString(), + widget.displayItem?.call(item) ?? (item == CryptoCurrency.btcln ? "BTC (LN)" : item.toString()), softWrap: true, style: Theme.of(context).textTheme.bodyMedium!.copyWith( fontWeight: FontWeight.w600, @@ -352,7 +353,7 @@ class _PickerState extends State> { ), ), ), - if (tag != null) + if (tag != null && item != CryptoCurrency.btcln) Align( alignment: Alignment.topCenter, child: Container( diff --git a/lib/src/widgets/primary_button.dart b/lib/src/widgets/primary_button.dart index 750d4cb45..07915f867 100644 --- a/lib/src/widgets/primary_button.dart +++ b/lib/src/widgets/primary_button.dart @@ -43,7 +43,7 @@ class PrimaryButton extends StatelessWidget { ), shape: WidgetStateProperty.all( RoundedRectangleBorder( - borderRadius: borderRadius ?? BorderRadius.circular(12.0), + borderRadius: borderRadius ?? BorderRadius.circular(18.0), ), ), ), @@ -108,7 +108,7 @@ class LoadingPrimaryButton extends StatelessWidget { backgroundColor: WidgetStateProperty.all(isDisabled ? color.withOpacity(0.5) : color), shape: WidgetStateProperty.all( RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), + borderRadius: BorderRadius.circular(18.0), ), )), child: isLoading @@ -138,7 +138,7 @@ class PrimaryIconButton extends StatelessWidget { required this.iconBackgroundColor, required this.textColor, this.mainAxisAlignment = MainAxisAlignment.start, - this.radius = 12, + this.radius = 18, super.key, }); @@ -233,7 +233,7 @@ class PrimaryImageButton extends StatelessWidget { backgroundColor: WidgetStateProperty.all(color), shape: WidgetStateProperty.all( RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), + borderRadius: BorderRadius.circular(18.0), ), )), child: Center( diff --git a/lib/src/widgets/scollable_with_bottom_section.dart b/lib/src/widgets/scrollable_with_bottom_section.dart similarity index 99% rename from lib/src/widgets/scollable_with_bottom_section.dart rename to lib/src/widgets/scrollable_with_bottom_section.dart index 07a070204..295e5ca2d 100644 --- a/lib/src/widgets/scollable_with_bottom_section.dart +++ b/lib/src/widgets/scrollable_with_bottom_section.dart @@ -52,4 +52,4 @@ class ScrollableWithBottomSectionState extends State { mainAxisAlignment: MainAxisAlignment.start, children: [ Container( - decoration: BoxDecoration( + decoration: ShapeDecoration( + shape: RoundedSuperellipseBorder(borderRadius: BorderRadius.circular(18)), color: Theme.of(context).colorScheme.surfaceContainer, - borderRadius: BorderRadius.circular(12), ), child: Stack( children: [ @@ -155,11 +155,9 @@ class SeedWidgetState extends State { onTap: () async => _pasteText(), child: Container( padding: EdgeInsets.all(6), - decoration: BoxDecoration( + decoration: ShapeDecoration( color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.all( - Radius.circular(6), - ), + shape: RoundedSuperellipseBorder(borderRadius: BorderRadius.circular(18)), ), child: Image.asset( 'assets/images/paste_ios.png', diff --git a/lib/src/widgets/seedphrase_grid_widget.dart b/lib/src/widgets/seedphrase_grid_widget.dart index 0d457a093..a32cf1c1c 100644 --- a/lib/src/widgets/seedphrase_grid_widget.dart +++ b/lib/src/widgets/seedphrase_grid_widget.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; class SeedPhraseGridWidget extends StatelessWidget { const SeedPhraseGridWidget({ @@ -28,6 +29,7 @@ class SeedPhraseGridWidget extends StatelessWidget { return GridView.builder( itemCount: list.length, + controller: ModalScrollController.of(context), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, childAspectRatio: 2.6, @@ -43,7 +45,7 @@ class SeedPhraseGridWidget extends StatelessWidget { alignment: Alignment.center, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), - color: Theme.of(context).colorScheme.surfaceContainerHighest, + color: Theme.of(context).colorScheme.surfaceContainerHigh ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, diff --git a/lib/src/widgets/setting_action_button.dart b/lib/src/widgets/setting_action_button.dart index ef2367313..afed4233b 100644 --- a/lib/src/widgets/setting_action_button.dart +++ b/lib/src/widgets/setting_action_button.dart @@ -42,7 +42,7 @@ class SettingActionButton extends StatelessWidget { ), shape: WidgetStateProperty.all( RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(18), ), ), ), diff --git a/lib/src/widgets/standard_list.dart b/lib/src/widgets/standard_list.dart index 8d5270b38..3e5fd1bd9 100644 --- a/lib/src/widgets/standard_list.dart +++ b/lib/src/widgets/standard_list.dart @@ -136,6 +136,8 @@ class SectionStandardList extends StatelessWidget { this.dividerPadding = const EdgeInsets.only(left: 24), this.sectionTitleBuilder, this.hasTopSeparator = false, + this.scrollController, + this.physics, }) : totalRows = []; final int sectionCount; @@ -146,6 +148,8 @@ class SectionStandardList extends StatelessWidget { final Widget Function(int sectionIndex)? sectionTitleBuilder; final List totalRows; final EdgeInsets dividerPadding; + final ScrollController? scrollController; + final ScrollPhysics? physics; List transform( bool hasTopSeparator, @@ -202,6 +206,8 @@ class SectionStandardList extends StatelessWidget { transform(hasTopSeparator, sectionCount, itemCounter, itemBuilder, sectionTitleBuilder)); return ListView.separated( + controller: scrollController, + physics: physics, separatorBuilder: (_, index) { final row = totalRows[index]; diff --git a/lib/src/widgets/text_info_box.dart b/lib/src/widgets/text_info_box.dart index 0a14af6d9..98eb5fca6 100644 --- a/lib/src/widgets/text_info_box.dart +++ b/lib/src/widgets/text_info_box.dart @@ -13,7 +13,7 @@ class TextInfoBox extends StatelessWidget { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(18), color: Theme.of(context).colorScheme.surfaceContainer, ), child: Column( diff --git a/lib/store/app_store.dart b/lib/store/app_store.dart index 183b723ea..466b235a6 100644 --- a/lib/store/app_store.dart +++ b/lib/store/app_store.dart @@ -1,4 +1,4 @@ - +import 'package:cake_wallet/core/amount_parsing_proxy.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; @@ -22,12 +22,18 @@ part 'app_store.g.dart'; class AppStore = AppStoreBase with _$AppStore; abstract class AppStoreBase with Store { - AppStoreBase( - {required this.authenticationStore, - required this.walletList, - required this.settingsStore, - required this.nodeListStore, - required this.themeStore}); + AppStoreBase({ + required this.authenticationStore, + required this.walletList, + required this.settingsStore, + required this.nodeListStore, + required this.themeStore, + }) : _amountParsingProxy = AmountParsingProxy(settingsStore.displayAmountsInSatoshi) { + reaction( + (_) => settingsStore.displayAmountsInSatoshi, + (value) => _amountParsingProxy = AmountParsingProxy(value), + ); + } AuthenticationStore authenticationStore; @@ -45,6 +51,12 @@ abstract class AppStoreBase with Store { ThemeStore themeStore; + @observable + AmountParsingProxy _amountParsingProxy; + + @computed + AmountParsingProxy get amountParsingProxy => _amountParsingProxy; + @action Future changeCurrentWallet( WalletBase, TransactionInfo> wallet) async { diff --git a/lib/store/dashboard/trades_store.dart b/lib/store/dashboard/trades_store.dart index d0a4592e3..b32ae1f88 100644 --- a/lib/store/dashboard/trades_store.dart +++ b/lib/store/dashboard/trades_store.dart @@ -1,17 +1,17 @@ import 'dart:async'; import 'package:cake_wallet/exchange/trade.dart'; +import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/store/settings_store.dart'; part 'trades_store.g.dart'; class TradesStore = TradesStoreBase with _$TradesStore; abstract class TradesStoreBase with Store { - TradesStoreBase({required this.tradesSource, required this.settingsStore}) + TradesStoreBase({required this.tradesSource, required this.appStore}) : trades = [] { _onTradesChanged = tradesSource.watch().listen((_) async => await updateTradeList()); updateTradeList(); @@ -19,7 +19,7 @@ abstract class TradesStoreBase with Store { Box tradesSource; StreamSubscription? _onTradesChanged; - SettingsStore settingsStore; + AppStore appStore; @observable List trades; @@ -34,7 +34,7 @@ abstract class TradesStoreBase with Store { Future updateTradeList() async => trades = tradesSource.values .map((trade) => TradeListItem( trade: trade, - settingsStore: settingsStore, + appStore: appStore, key: ValueKey('trade_list_item_${trade.id}_key'), )) .toList(); diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 6ae2909db..1b0d63f63 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -11,6 +11,7 @@ import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/action_list_display_mode.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; +import 'package:cake_wallet/entities/bitcoin_amount_display_mode.dart'; import 'package:cake_wallet/entities/cake_2fa_preset_options.dart'; import 'package:cake_wallet/entities/country.dart'; import 'package:cake_wallet/entities/default_settings_migration.dart'; @@ -30,6 +31,7 @@ import 'package:cake_wallet/evm/evm.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; +import 'package:cake_wallet/zcash/zcash.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; import 'package:cake_wallet/monero/monero.dart'; @@ -90,6 +92,7 @@ abstract class SettingsStoreBase with Store { required this.deviceName, required Map nodes, required Map powNodes, + required this.displayAmountsInSatoshi, required this.shouldShowYatPopup, required this.shouldShowDEuroDisclaimer, required this.shouldShowRepWarning, @@ -107,6 +110,8 @@ abstract class SettingsStoreBase with Store { required bool initialShouldRequireTOTP2FAForAddingContacts, required bool initialShouldRequireTOTP2FAForCreatingNewWallets, required bool initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings, + required this.forceDecentralizedExchanges, + required this.decentralizedExchangesPromptDismissed, required this.sortBalanceBy, required this.pinNativeTokenAtTop, required this.useEtherscan, @@ -137,6 +142,7 @@ abstract class SettingsStoreBase with Store { required this.mwebEnabled, required this.hasEnabledMwebBefore, required this.mwebNodeUri, + required this.mwebAdDismissed, required bool initialEnableAutomaticNodeSwitching, required String initialBackgroundImage, TransactionPriority? initialBitcoinTransactionPriority, @@ -152,6 +158,7 @@ abstract class SettingsStoreBase with Store { TransactionPriority? initialBitcoinCashTransactionPriority, TransactionPriority? initialZanoTransactionPriority, TransactionPriority? initialDecredTransactionPriority, + TransactionPriority? initialZcashTransactionPriority, Country? initialCakePayCountry}) : nodes = ObservableMap.of(nodes), powNodes = ObservableMap.of(powNodes), @@ -255,6 +262,9 @@ abstract class SettingsStoreBase with Store { if (initialDecredTransactionPriority != null) { priority[WalletType.decred] = initialDecredTransactionPriority; } + if (initialZcashTransactionPriority != null) { + priority[WalletType.zcash] = initialZcashTransactionPriority; + } if (initialCakePayCountry != null) { selectedCakePayCountry = initialCakePayCountry; @@ -284,6 +294,8 @@ abstract class SettingsStoreBase with Store { reaction((_) => shouldShowRepWarning, (bool val) => sharedPreferences.setBool(PreferencesKey.shouldShowRepWarning, val)); + reaction((_)=>mwebAdDismissed, (val)=>sharedPreferences.setBool(PreferencesKey.mwebAdDismissed, val)); + priority.observe((change) { final String? key; switch (change.key) { @@ -321,6 +333,9 @@ abstract class SettingsStoreBase with Store { case WalletType.decred: key = PreferencesKey.decredTransactionPriority; break; + case WalletType.zcash: + key = PreferencesKey.zcashTransactionPriority; + break; default: key = null; } @@ -443,6 +458,11 @@ abstract class SettingsStoreBase with Store { (BalanceDisplayMode mode) => sharedPreferences.setInt( PreferencesKey.currentBalanceDisplayModeKey, mode.serialize())); + reaction( + (_) => displayAmountsInSatoshi, + (BitcoinAmountDisplayMode displayAmountsInSatoshi) => sharedPreferences.setInt( + PreferencesKey.displayAmountsInSatoshi, displayAmountsInSatoshi.raw)); + reaction((_) => currentSyncMode, (SyncMode syncMode) { sharedPreferences.setInt(PreferencesKey.syncModeKey, syncMode.type.index); FlutterDaemon().startBackgroundSync(syncMode.frequency.inMinutes); @@ -689,6 +709,17 @@ abstract class SettingsStoreBase with Store { (String backgroundImage) => _sharedPreferences.setString(PreferencesKey.backgroundImage, backgroundImage)); + reaction( + (_) => forceDecentralizedExchanges, + (bool forceDecentralizedExchanges) => _sharedPreferences.setBool( + PreferencesKey.forceDecentralizedExchanges, forceDecentralizedExchanges)); + + reaction( + (_) => decentralizedExchangesPromptDismissed, + (bool forceDecentralizedExchanges) => _sharedPreferences.setBool( + PreferencesKey.decentralizedExchangesPromptDismissed, + decentralizedExchangesPromptDismissed)); + this.nodes.observe((change) { if (change.newValue != null && change.key != null) { _saveCurrentNode(change.newValue!, change.key!); @@ -742,6 +773,9 @@ abstract class SettingsStoreBase with Store { @observable BalanceDisplayMode balanceDisplayMode; + @observable + BitcoinAmountDisplayMode displayAmountsInSatoshi; + @observable FiatApiMode fiatApiMode; @@ -962,6 +996,15 @@ abstract class SettingsStoreBase with Store { @observable String backgroundImage; + @observable + bool forceDecentralizedExchanges; + + @observable + bool decentralizedExchangesPromptDismissed; + + @observable + bool mwebAdDismissed; + final SecureStorage _secureStorage; final SharedPreferences _sharedPreferences; @@ -1074,6 +1117,7 @@ abstract class SettingsStoreBase with Store { TransactionPriority? wowneroTransactionPriority; TransactionPriority? zanoTransactionPriority; TransactionPriority? decredTransactionPriority; + TransactionPriority? zcashTransactionPriority; if (sharedPreferences.getInt(PreferencesKey.havenTransactionPriority) != null) { havenTransactionPriority = monero?.deserializeMoneroTransactionPriority( @@ -1117,6 +1161,10 @@ abstract class SettingsStoreBase with Store { decredTransactionPriority = decred?.deserializeDecredTransactionPriority( sharedPreferences.getInt(PreferencesKey.decredTransactionPriority)!); } + if (sharedPreferences.getInt(PreferencesKey.zcashTransactionPriority) != null) { + zcashTransactionPriority = zcash?.deserializeZcashTransactionPriority( + raw: sharedPreferences.getInt(PreferencesKey.zcashTransactionPriority)!); + } moneroTransactionPriority ??= monero?.getDefaultTransactionPriority(); bitcoinTransactionPriority ??= bitcoin?.getMediumTransactionPriority(); @@ -1131,9 +1179,12 @@ abstract class SettingsStoreBase with Store { baseTransactionPriority ??= evm?.getDefaultTransactionPriority(); bscTransactionPriority ??= evm?.getDefaultTransactionPriority(); zanoTransactionPriority ??= zano?.getDefaultTransactionPriority(); + zcashTransactionPriority ??= zcash?.getDefaultTransactionPriority(); final currentBalanceDisplayMode = BalanceDisplayMode.deserialize( raw: sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey)!); + final displayAmountsInSatoshi = BitcoinAmountDisplayMode.deserialize( + raw: sharedPreferences.getInt(PreferencesKey.displayAmountsInSatoshi) ?? 0); // FIX-ME: Check for which default value we should have here final shouldSaveRecipientAddress = sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ?? false; @@ -1159,6 +1210,8 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getBool(PreferencesKey.shouldShowMarketPlaceInDashboard) ?? true; final showAddressBookPopupEnabled = sharedPreferences.getBool(PreferencesKey.showAddressBookPopupEnabled) ?? true; + final forceDecentralizedExchanges = await sharedPreferences.getBool(PreferencesKey.forceDecentralizedExchanges) ?? false; + final decentralizedExchangesPromptDismissed = await sharedPreferences.getBool(PreferencesKey.decentralizedExchangesPromptDismissed) ?? false; final syncStatusDisplayMode = SyncStatusDisplayModeExtension.fromString( sharedPreferences.getString(PreferencesKey.syncStatusDisplayMode) ?? SyncStatusDisplayMode.blocksRemaining.name); final exchangeStatus = ExchangeApiMode.deserialize( @@ -1497,6 +1550,9 @@ abstract class SettingsStoreBase with Store { ) ?? ''; + final mwebAdDismissed = + await sharedPreferences.getBool(PreferencesKey.mwebAdDismissed) ?? false; + return SettingsStore( secureStorage: secureStorage, sharedPreferences: sharedPreferences, @@ -1507,6 +1563,7 @@ abstract class SettingsStoreBase with Store { powNodes: powNodes, appVersion: packageInfo.version, deviceName: deviceName, + displayAmountsInSatoshi: displayAmountsInSatoshi, isBitcoinBuyEnabled: isBitcoinBuyEnabled, initialFiatCurrency: currentFiatCurrency, initialCakePayCountry: currentCakePayCountry, @@ -1568,6 +1625,8 @@ abstract class SettingsStoreBase with Store { mwebEnabled: mwebEnabled, mwebNodeUri: mwebNodeUri, hasEnabledMwebBefore: hasEnabledMwebBefore, + forceDecentralizedExchanges: forceDecentralizedExchanges, + decentralizedExchangesPromptDismissed: decentralizedExchangesPromptDismissed, initialEnableAutomaticNodeSwitching: enableAutomaticNodeSwitching, initialBackgroundImage: backgroundImage, initialMoneroTransactionPriority: moneroTransactionPriority, @@ -1578,6 +1637,7 @@ abstract class SettingsStoreBase with Store { initialLitecoinTransactionPriority: litecoinTransactionPriority, initialBitcoinCashTransactionPriority: bitcoinCashTransactionPriority, initialDecredTransactionPriority: decredTransactionPriority, + initialZcashTransactionPriority: zcashTransactionPriority, initialShouldRequireTOTP2FAForAccessingWallet: shouldRequireTOTP2FAForAccessingWallet, initialShouldRequireTOTP2FAForSendsToContact: shouldRequireTOTP2FAForSendsToContact, initialShouldRequireTOTP2FAForSendsToNonContact: shouldRequireTOTP2FAForSendsToNonContact, @@ -1602,6 +1662,7 @@ abstract class SettingsStoreBase with Store { shouldShowDEuroDisclaimer: shouldShowDEuroDisclaimer, shouldShowRepWarning: shouldShowRepWarning, initialBuiltinTor: builtinTor, + mwebAdDismissed: mwebAdDismissed, ); } @@ -1669,6 +1730,11 @@ abstract class SettingsStoreBase with Store { priority[WalletType.decred] = decred!.deserializeDecredTransactionPriority( sharedPreferences.getInt(PreferencesKey.decredTransactionPriority)!); } + if (zcash != null && + sharedPreferences.getInt(PreferencesKey.zcashTransactionPriority) != null) { + priority[WalletType.zcash] = zcash!.deserializeZcashTransactionPriority( + raw: sharedPreferences.getInt(PreferencesKey.zcashTransactionPriority)!); + } final generateSubaddresses = sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey); diff --git a/lib/themes/theme_classes/black_theme.dart b/lib/themes/theme_classes/black_theme.dart index 336e94240..ba181fc9e 100644 --- a/lib/themes/theme_classes/black_theme.dart +++ b/lib/themes/theme_classes/black_theme.dart @@ -66,6 +66,7 @@ class BlackTheme extends MaterialThemeBase { errorContainer: const Color(0xFFC53636), onErrorContainer: const Color(0xFFFFDAD6), surface: surfaceColor, + surfaceDim: const Color(0xFF000000), background: surfaceColor, onSurface: const Color(0xFFE6E1E3), onSurfaceVariant: const Color(0xFFB4B4B4), @@ -75,10 +76,10 @@ class BlackTheme extends MaterialThemeBase { surfaceContainerHigh: Color(0xFF2B292B), surfaceContainerHighest: Color(0xFF363435), outline: const Color(0xFF958F95), - outlineVariant: const Color(0xFF49454B), + outlineVariant: const Color(0xFF2F2D2F), ); - static const String fontFamily = 'Lato'; + static const String fontFamily = 'Wix Madefor Text'; @override TextTheme get textTheme => TextTheme( @@ -101,26 +102,26 @@ class BlackTheme extends MaterialThemeBase { color: colorScheme.onSurface, ), headlineLarge: TextStyle( - fontSize: 32, - fontWeight: FontWeight.w400, + fontSize: 22, + fontWeight: FontWeight.w600, letterSpacing: 0, color: colorScheme.onSurface, ), headlineMedium: TextStyle( - fontSize: 28, - fontWeight: FontWeight.w400, + fontSize: 18, + fontWeight: FontWeight.w600, letterSpacing: 0, color: colorScheme.onSurface, ), headlineSmall: TextStyle( - fontSize: 24, - fontWeight: FontWeight.w400, + fontSize: 14, + fontWeight: FontWeight.w600, letterSpacing: 0, color: colorScheme.onSurface, ), titleLarge: TextStyle( fontSize: 22, - fontWeight: FontWeight.w400, + fontWeight: FontWeight.w500, letterSpacing: 0, color: colorScheme.onSurface, ), @@ -202,7 +203,7 @@ class BlackTheme extends MaterialThemeBase { foregroundColor: colorScheme.onPrimary, elevation: 0, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(18), ), ), ), @@ -211,7 +212,7 @@ class BlackTheme extends MaterialThemeBase { foregroundColor: colorScheme.primary, side: BorderSide(color: colorScheme.outline), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(18), ), ), ), @@ -219,7 +220,7 @@ class BlackTheme extends MaterialThemeBase { style: TextButton.styleFrom( foregroundColor: colorScheme.primary, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(18), ), ), ), @@ -227,27 +228,27 @@ class BlackTheme extends MaterialThemeBase { filled: true, fillColor: colorScheme.surfaceContainer, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(18), borderSide: BorderSide.none, ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(18), borderSide: BorderSide.none, ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(18), borderSide: BorderSide(color: colorScheme.primary), ), errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(18), borderSide: BorderSide(color: colorScheme.error), ), focusedErrorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(18), borderSide: BorderSide(color: colorScheme.error), ), disabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(18), borderSide: BorderSide.none, ), ), diff --git a/lib/themes/theme_classes/dark_theme.dart b/lib/themes/theme_classes/dark_theme.dart index ade19e5e7..990508e31 100644 --- a/lib/themes/theme_classes/dark_theme.dart +++ b/lib/themes/theme_classes/dark_theme.dart @@ -45,6 +45,7 @@ class DarkTheme extends MaterialThemeBase { onErrorContainer: const Color(0xFFFFDAD6), surface: surfaceColor, onSurface: const Color(0xFFD7E2F7), + surfaceDim: const Color(0xFF0F1A36), onSurfaceVariant: const Color(0xFF8C9FBB), surfaceContainerLowest: Color(0xFF171C30), surfaceContainerLow: Color(0xFF2D385C), @@ -52,9 +53,9 @@ class DarkTheme extends MaterialThemeBase { surfaceContainerHigh: Color(0xFF2A3E73), surfaceContainerHighest: Color(0xFF334C8C), outline: const Color(0xFF9EACC1), - outlineVariant: const Color(0xFF3E5579), + outlineVariant: const Color(0xFF2A3E73), ); - static const String fontFamily = 'Lato'; + static const String fontFamily = 'Wix Madefor Text'; @override TextTheme get textTheme => TextTheme( displayLarge: TextStyle( @@ -76,26 +77,26 @@ class DarkTheme extends MaterialThemeBase { color: colorScheme.onSurface, ), headlineLarge: TextStyle( - fontSize: 32, - fontWeight: FontWeight.w400, + fontSize: 22, + fontWeight: FontWeight.w600, letterSpacing: 0, color: colorScheme.onSurface, ), headlineMedium: TextStyle( - fontSize: 28, - fontWeight: FontWeight.w400, + fontSize: 18, + fontWeight: FontWeight.w600, letterSpacing: 0, color: colorScheme.onSurface, ), headlineSmall: TextStyle( - fontSize: 24, - fontWeight: FontWeight.w400, + fontSize: 14, + fontWeight: FontWeight.w600, letterSpacing: 0, color: colorScheme.onSurface, ), titleLarge: TextStyle( fontSize: 22, - fontWeight: FontWeight.w400, + fontWeight: FontWeight.w500, letterSpacing: 0, color: colorScheme.onSurface, ), @@ -174,7 +175,7 @@ class DarkTheme extends MaterialThemeBase { foregroundColor: colorScheme.onPrimary, elevation: 0, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(18), ), ), ), @@ -183,7 +184,7 @@ class DarkTheme extends MaterialThemeBase { foregroundColor: colorScheme.primary, side: BorderSide(color: colorScheme.outline), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(18), ), ), ), @@ -191,7 +192,7 @@ class DarkTheme extends MaterialThemeBase { style: TextButton.styleFrom( foregroundColor: colorScheme.primary, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(18), ), ), ), @@ -199,27 +200,27 @@ class DarkTheme extends MaterialThemeBase { filled: true, fillColor: colorScheme.surfaceContainer, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(18), borderSide: BorderSide.none, ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(18), borderSide: BorderSide.none, ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(18), borderSide: BorderSide(color: colorScheme.primary), ), errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(18), borderSide: BorderSide(color: colorScheme.error), ), focusedErrorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(18), borderSide: BorderSide(color: colorScheme.error), ), disabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(18), borderSide: BorderSide.none, ), ), diff --git a/lib/themes/theme_classes/light_theme.dart b/lib/themes/theme_classes/light_theme.dart index e9b28f9e4..db1b6fdc2 100644 --- a/lib/themes/theme_classes/light_theme.dart +++ b/lib/themes/theme_classes/light_theme.dart @@ -44,6 +44,7 @@ class LightTheme extends MaterialThemeBase { errorContainer: const Color(0xFFFFBDBD), onErrorContainer: const Color(0xFFE43D3D), surface: surfaceColor, + surfaceDim: const Color(0xFFE7E7FD), onSurface: const Color(0xFF312938), onSurfaceVariant: const Color(0xFF6C6772), surfaceContainerLowest: Color(0xFFE4E4E4), @@ -51,8 +52,9 @@ class LightTheme extends MaterialThemeBase { surfaceContainer: Color(0xFFFBFBFD), surfaceContainerHigh: Color(0xFFFDFDFE), surfaceContainerHighest: Color(0xFFFFFFFF), + onInverseSurface: Color(0xFFFAFAFA), outline: const Color(0xFF7B757C), - outlineVariant: const Color(0xFFCBC4CB), + outlineVariant: const Color(0xFFEDEDF6), ); @override @@ -76,26 +78,26 @@ class LightTheme extends MaterialThemeBase { color: colorScheme.onSurface, ), headlineLarge: TextStyle( - fontSize: 32, - fontWeight: FontWeight.w400, + fontSize: 22, + fontWeight: FontWeight.w600, letterSpacing: 0, color: colorScheme.onSurface, ), headlineMedium: TextStyle( - fontSize: 28, - fontWeight: FontWeight.w400, + fontSize: 18, + fontWeight: FontWeight.w600, letterSpacing: 0, color: colorScheme.onSurface, ), headlineSmall: TextStyle( - fontSize: 24, - fontWeight: FontWeight.w400, + fontSize: 14, + fontWeight: FontWeight.w600, letterSpacing: 0, color: colorScheme.onSurface, ), titleLarge: TextStyle( fontSize: 22, - fontWeight: FontWeight.w400, + fontWeight: FontWeight.w500, letterSpacing: 0, color: colorScheme.onSurface, ), @@ -149,7 +151,7 @@ class LightTheme extends MaterialThemeBase { ), ); - static const String fontFamily = 'Lato'; + static const String fontFamily = 'Wix Madefor Text'; @override ThemeData get themeData => ThemeData( useMaterial3: true, @@ -175,7 +177,7 @@ class LightTheme extends MaterialThemeBase { foregroundColor: colorScheme.onPrimary, elevation: 0, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(18), ), ), ), @@ -184,7 +186,7 @@ class LightTheme extends MaterialThemeBase { foregroundColor: colorScheme.primary, side: BorderSide(color: colorScheme.outline), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(18), ), ), ), @@ -192,7 +194,7 @@ class LightTheme extends MaterialThemeBase { style: TextButton.styleFrom( foregroundColor: colorScheme.primary, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(18), ), ), ), @@ -200,23 +202,23 @@ class LightTheme extends MaterialThemeBase { filled: true, fillColor: colorScheme.surfaceContainer, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(18), borderSide: BorderSide.none, ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(18), borderSide: BorderSide.none, ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(18), borderSide: BorderSide(color: colorScheme.primary), ), errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(18), borderSide: BorderSide(color: colorScheme.error), ), focusedErrorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(18), borderSide: BorderSide(color: colorScheme.error), ), ), diff --git a/lib/typography.dart b/lib/typography.dart index 816f116b4..c08a98e16 100644 --- a/lib/typography.dart +++ b/lib/typography.dart @@ -1,6 +1,8 @@ +import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:flutter/material.dart'; const latoFont = "Lato"; +const wixFont = "Wix Madefor Text"; TextStyle textXxSmall({Color? color}) => _cakeRegular(10, color); @@ -54,7 +56,7 @@ TextStyle _textStyle({ Color? color, }) => TextStyle( - fontFamily: latoFont, + fontFamily: FeatureFlag.hasNewUi ? wixFont : latoFont, fontSize: size, fontWeight: fontWeight, color: color ?? Colors.white, diff --git a/lib/utils/address_formatter.dart b/lib/utils/address_formatter.dart index f2083c772..10382a814 100644 --- a/lib/utils/address_formatter.dart +++ b/lib/utils/address_formatter.dart @@ -15,6 +15,11 @@ class AddressFormatter { final cleanAddress = address.replaceAll('bitcoincash:', ''); final isMWEB = address.startsWith('ltcmweb'); final chunkSize = walletType != null ? _getChunkSize(walletType) : 4; + final isHumanReadable = address.contains("@") || address.startsWith("Error:"); + + if (isHumanReadable) { + return Text(address, style: evenTextStyle, textAlign: textAlign ?? TextAlign.start); + } if (shouldTruncate) { return _buildTruncatedAddress( @@ -158,4 +163,4 @@ class AddressFormatter { return 4; } } -} \ No newline at end of file +} diff --git a/lib/utils/date_formatter.dart b/lib/utils/date_formatter.dart index 58e59966a..c02b44a43 100644 --- a/lib/utils/date_formatter.dart +++ b/lib/utils/date_formatter.dart @@ -5,10 +5,10 @@ import 'package:cake_wallet/store/settings_store.dart'; class DateFormatter { static String currentLocalFormat({bool hasTime = true, bool reverse = false}) { - final isUSA = getIt.get().languageCode.toLowerCase() == 'en'; - final format = isUSA ? usaStyleFormat(hasTime, reverse) : regularStyleFormat(hasTime, reverse); + // final isUSA = getIt.get().languageCode.toLowerCase() == 'en'; + // final format = isUSA ? usaStyleFormat(hasTime, reverse) : regularStyleFormat(hasTime, reverse); - return format; + return hasTime ? (reverse ? "HH:mm MMMM d, yyyy" : "MMMM d, yyyy, HH:mm") : "MMMM d, yyyy"; } static DateFormat withCurrentLocal({bool hasTime = true, bool reverse = false}) => DateFormat( diff --git a/lib/utils/date_picker.dart b/lib/utils/date_picker.dart index 99274d8e0..8e04325d4 100644 --- a/lib/utils/date_picker.dart +++ b/lib/utils/date_picker.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:cake_wallet/main.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -35,7 +36,7 @@ Future _buildCupertinoDataPicker( DateTime lastDate) async { DateTime? date; await showModalBottomSheet( - context: context, + context: navigatorKey.currentState!.context, builder: (_) { return Container( height: MediaQuery.of(context).size.height / 3, diff --git a/lib/utils/feature_flag.dart b/lib/utils/feature_flag.dart index 340eb0d1f..c692c90d2 100644 --- a/lib/utils/feature_flag.dart +++ b/lib/utils/feature_flag.dart @@ -16,4 +16,6 @@ class FeatureFlag { static const bool duressPinEnabled = true; static const bool isEVMChainSwitcherEnabled = false; static const bool isAutomaticNodeSwitchingEnabled = false; + static const bool hasNewUi = true; + static const bool hasNewUiExtraPages = true; } diff --git a/lib/utils/payment_request.dart b/lib/utils/payment_request.dart index b574ab260..c0d747080 100644 --- a/lib/utils/payment_request.dart +++ b/lib/utils/payment_request.dart @@ -1,10 +1,26 @@ -import 'package:cake_wallet/core/payment_uris.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/lnurl.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cake_wallet/nano/nano.dart'; class PaymentRequest { PaymentRequest(this.address, this.amount, this.note, this.scheme, this.pjUri, {this.callbackUrl, this.callbackMessage, this.contractAddress}); + factory PaymentRequest.fromString(String input) { + try { + return PaymentRequest.fromBolt11(input); + } catch (_) { + return PaymentRequest.fromUri(Uri.parse(input)); + } + } + + factory PaymentRequest.fromBolt11(String invoice) { + final amountRaw = getBolt11Amount(invoice) ?? 0; + final amount = CryptoCurrency.btcln.formatAmount(BigInt.from(amountRaw)); + return PaymentRequest(invoice, amount, '', 'lightning', null); + } + factory PaymentRequest.fromUri(Uri? uri) { var address = ""; var amount = ""; @@ -22,7 +38,16 @@ class PaymentRequest { } address = uri.queryParameters['address'] ?? uri.path; - amount = uri.queryParameters['tx_amount'] ?? uri.queryParameters['amount'] ?? ""; + try { + final lnAmount = + CryptoCurrency.btcln.formatAmount(BigInt.from(getBolt11Amount(uri.path) ?? 0)); + if (lnAmount != 0) { + amount = lnAmount; + } + } catch (_) {} + if (amount.isEmpty) { + amount = uri.queryParameters['tx_amount'] ?? uri.queryParameters['amount'] ?? ""; + } note = uri.queryParameters['tx_description'] ?? uri.queryParameters['message'] ?? ""; scheme = uri.scheme; callbackUrl = uri.queryParameters['callback']; diff --git a/lib/utils/token_utilities.dart b/lib/utils/token_utilities.dart index 616c874c0..d38816fe7 100644 --- a/lib/utils/token_utilities.dart +++ b/lib/utils/token_utilities.dart @@ -388,7 +388,7 @@ class TokenUtilities { } static Future> _getUserTokensForNetwork(CryptoCurrency baseCurrency) async { - final walletType = cryptoCurrencyToWalletType(baseCurrency); + final walletType = cryptoCurrencyOrTokenToWalletType(baseCurrency); if (walletType == null) return []; if (isEVMCompatibleChain(walletType)) { diff --git a/lib/view_model/buy/buy_sell_view_model.dart b/lib/view_model/buy/buy_sell_view_model.dart index 6dc15b178..d7a4ad326 100644 --- a/lib/view_model/buy/buy_sell_view_model.dart +++ b/lib/view_model/buy/buy_sell_view_model.dart @@ -12,9 +12,9 @@ import 'package:cake_wallet/entities/provider_types.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/store/app_store.dart'; +import 'package:cw_core/crypto_amount_format.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:flutter/cupertino.dart'; -import 'package:intl/intl.dart'; import 'package:mobx/mobx.dart'; part 'buy_sell_view_model.g.dart'; @@ -24,8 +24,7 @@ class BuySellViewModel = BuySellViewModelBase with _$BuySellViewModel; abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with Store { BuySellViewModelBase( AppStore appStore, - ) : _cryptoNumberFormat = NumberFormat(), - cryptoAmount = '', + ) : _cryptoAmount = '', fiatAmount = '', cryptoCurrencyAddress = '', isCryptoCurrencyAddressEnabled = false, @@ -54,7 +53,6 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency); } - final NumberFormat _cryptoNumberFormat; late Timer bestRateSync; List get availableBuyProviders { @@ -79,11 +77,12 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S } double get amount { - final formattedFiatAmount = double.tryParse(fiatAmount) ?? 200.0; - final formattedCryptoAmount = - double.tryParse(cryptoAmount) ?? (cryptoCurrency == CryptoCurrency.btc ? 0.001 : 1); + final formattedFiatAmount = double.tryParse(fiatAmount); + final formattedCryptoAmount = double.tryParse(_cryptoAmount); - return isBuyAction ? formattedFiatAmount : formattedCryptoAmount; + return isBuyAction + ? formattedFiatAmount ?? 200.0 + : formattedCryptoAmount ?? (cryptoCurrency == CryptoCurrency.btc ? 0.001 : 1); } final AppStore _appStore; @@ -120,7 +119,11 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S CryptoCurrency cryptoCurrency; @observable - String cryptoAmount; + String _cryptoAmount; + + @computed + String get cryptoAmount => + _appStore.amountParsingProxy.getDisplayCryptoAmount(_cryptoAmount, cryptoCurrency); @observable String fiatAmount; @@ -164,6 +167,9 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S ? (buySellQuotState as BuySellQuotFailed).errorMessage : null; + @computed + bool get useSatoshi => _appStore.amountParsingProxy.useSatoshi(cryptoCurrency); + @action void reset() { cryptoCurrency = wallet.currency; @@ -200,26 +206,23 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S if (amount.isEmpty) { fiatAmount = ''; - cryptoAmount = ''; + _cryptoAmount = ''; return; } - final enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0; - if (!isReadyToTrade && !isBuySellQuoteFailed) { - cryptoAmount = S.current.fetching; + _cryptoAmount = S.current.fetching; return; } else if (isBuySellQuoteFailed) { - cryptoAmount = ''; + _cryptoAmount = ''; return; } if (bestRateQuote != null) { - _cryptoNumberFormat.maximumFractionDigits = cryptoCurrency.decimals; - cryptoAmount = _cryptoNumberFormat - .format(enteredAmount / bestRateQuote!.rate) - .toString() - .replaceAll(RegExp('\\,'), ''); + final enteredAmount = double.tryParse(fiatAmount.replaceAll(',', '.')) ?? 0; + final amount = enteredAmount / bestRateQuote!.rate; + + _cryptoAmount = amount.toString().withMaxDecimals(cryptoCurrency.decimals); } else { await calculateBestRate(); } @@ -227,16 +230,14 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S @action Future changeCryptoAmount({required String amount}) async { - cryptoAmount = amount; + _cryptoAmount = _appStore.amountParsingProxy.getCanonicalCryptoAmount(amount, cryptoCurrency); if (amount.isEmpty) { fiatAmount = ''; - cryptoAmount = ''; + _cryptoAmount = ''; return; } - final enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0; - if (!isReadyToTrade && !isBuySellQuoteFailed) { fiatAmount = S.current.fetching; return; @@ -246,10 +247,10 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S } if (bestRateQuote != null) { - fiatAmount = _cryptoNumberFormat - .format(enteredAmount * bestRateQuote!.rate) - .toString() - .replaceAll(RegExp('\\,'), ''); + final enteredAmount = double.tryParse(_cryptoAmount.replaceAll(',', '.')) ?? 0; + + fiatAmount = + (enteredAmount * bestRateQuote!.rate).toString().withMaxDecimals(fiatCurrency.decimals); } else { await calculateBestRate(); } @@ -338,7 +339,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S Future _initialize() async { _setProviders(); - cryptoAmount = ''; + _cryptoAmount = ''; fiatAmount = ''; cryptoCurrencyAddress = _getInitialCryptoCurrencyAddress(); paymentMethodState = InitialPaymentMethod(); @@ -348,19 +349,25 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S } String _getInitialCryptoCurrencyAddress() { - return cryptoCurrency == wallet.currency ? wallet.walletAddresses.address : ''; + if (cryptoCurrency == wallet.currency) { + if ([CryptoCurrency.zec, CryptoCurrency.btc].contains(cryptoCurrency)) { + return wallet.walletAddresses.addressForBuy; + } + + return wallet.walletAddresses.address; + } + return ''; } @action Future _getAvailablePaymentTypes() async { paymentMethodState = PaymentMethodLoading(); selectedPaymentMethod = null; - final result = await Future.wait(providerList.map((element) => element - .getAvailablePaymentTypes(fiatCurrency.title, cryptoCurrency, isBuyAction) - .timeout( - Duration(seconds: 10), - onTimeout: () => [], - ))); + final result = await Future.wait(providerList.map((element) => + element.getAvailablePaymentTypes(fiatCurrency.title, cryptoCurrency, isBuyAction).timeout( + Duration(seconds: 10), + onTimeout: () => [], + ))); final List tempPaymentMethods = []; @@ -394,11 +401,11 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S final List validProviders = providerList.where((provider) { if (isBuyAction) { - return provider.supportedCryptoList.any((pair) => - pair.from == cryptoCurrency && pair.to == fiatCurrency); + return provider.supportedCryptoList + .any((pair) => pair.from == cryptoCurrency && pair.to == fiatCurrency); } else { - return provider.supportedFiatList.any((pair) => - pair.from == fiatCurrency && pair.to == cryptoCurrency); + return provider.supportedFiatList + .any((pair) => pair.from == fiatCurrency && pair.to == cryptoCurrency); } }).toList(); @@ -441,7 +448,6 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S validQuotes.sort((a, b) => a.payout.compareTo(b.payout)); } - final Set addedProviders = {}; final List uniqueProviderQuotes = validQuotes.where((element) { if (addedProviders.contains(element.provider.title)) return false; @@ -449,10 +455,11 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S return true; }).toList(); - final List successRateQuotes = validQuotes.where((element) => - element.provider is OnRamperBuyProvider && - element.recommendations.contains(ProviderRecommendation.successRate) - ).toList(); + final List successRateQuotes = validQuotes + .where((element) => + element.provider is OnRamperBuyProvider && + element.recommendations.contains(ProviderRecommendation.successRate)) + .toList(); for (final quote in successRateQuotes) { if (!uniqueProviderQuotes.contains(quote)) { @@ -507,7 +514,8 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S if (e.toString().contains("403")) { buySellQuotState = BuySellQuotFailed(errorMessage: "Using Tor is not supported"); } else { - buySellQuotState = BuySellQuotFailed(errorMessage: "Something went wrong please try again later"); + buySellQuotState = + BuySellQuotFailed(errorMessage: "Something went wrong please try again later"); } } } diff --git a/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart b/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart index 8148e647c..ca12de3c5 100644 --- a/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart +++ b/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart @@ -107,7 +107,10 @@ abstract class CakePayBuyCardViewModelBase with Store { List get availableMethods { switch (walletType) { case WalletType.bitcoin: - return [CakePayPaymentMethod.BTC]; + return [ + CakePayPaymentMethod.BTC, + if (sendViewModel.wallet.isSoftwareWallet) CakePayPaymentMethod.BTC_LN + ]; case WalletType.litecoin: return [ CakePayPaymentMethod.LTC, diff --git a/lib/view_model/contact_list/contact_view_model.dart b/lib/view_model/contact_list/contact_view_model.dart index 2030cf03e..c808f966c 100644 --- a/lib/view_model/contact_list/contact_view_model.dart +++ b/lib/view_model/contact_list/contact_view_model.dart @@ -1,4 +1,9 @@ +import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/contact_record.dart'; +import 'package:cake_wallet/entities/parse_address_from_domain.dart'; +import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/core/execution_state.dart'; @@ -17,6 +22,7 @@ abstract class ContactViewModelBase with Store { _contact = contact, name = contact?.name ?? '', address = contact?.address ?? '', + displayName = contact?.displayName ?? '', currency = contact?.type, lastChange = contact?.lastChange; @@ -30,6 +36,9 @@ abstract class ContactViewModelBase with Store { @observable String address; + @observable + String displayName; + @observable CryptoCurrency? currency; @@ -52,6 +61,15 @@ abstract class ContactViewModelBase with Store { currency = null; } + Future extractParsedAddress(BuildContext context) async{ + if(currency == null) return; + final parsedAddress = await getIt.get().resolve(context, address, currency!); + if(parsedAddress.name.isNotEmpty) { + displayName = parsedAddress.name; + } + address = await extractAddressFromParsed(null, parsedAddress); + } + Future save() async { try { state = IsExecutingState(); @@ -70,11 +88,12 @@ abstract class ContactViewModelBase with Store { _contact.name = name; _contact.address = address; _contact.type = currency!; + _contact.displayName = displayName; _contact.lastChange = now; await _contact.save(); } else { await _contacts - .add(Contact(name: name, address: address, type: currency!, lastChange: now)); + .add(Contact(name: name, address: address, type: currency!, lastChange: now, displayName: displayName)); } lastChange = now; diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 8bdd78cba..8dc57f4fb 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -1,39 +1,42 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/entities/balance_display_mode.dart'; +import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/sort_balance_types.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/evm/evm.dart'; +import 'package:cw_core/crypto_amount_format.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/wallet_base.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/entities/balance_display_mode.dart'; -import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; -import 'package:cake_wallet/store/app_store.dart'; -import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:mobx/mobx.dart'; part 'balance_view_model.g.dart'; class BalanceRecord { - const BalanceRecord( - {required this.availableBalance, - required this.additionalBalance, - required this.secondAvailableBalance, - required this.secondAdditionalBalance, - required this.frozenBalance, - required this.fiatAvailableBalance, - required this.fiatAdditionalBalance, - required this.fiatFrozenBalance, - required this.fiatSecondAvailableBalance, - required this.fiatSecondAdditionalBalance, - required this.asset, - required this.formattedAssetTitle}); + const BalanceRecord({ + required this.availableBalance, + required this.additionalBalance, + required this.secondAvailableBalance, + required this.secondAdditionalBalance, + required this.frozenBalance, + required this.fiatAvailableBalance, + required this.fiatAdditionalBalance, + required this.fiatFrozenBalance, + required this.fiatSecondAvailableBalance, + required this.fiatSecondAdditionalBalance, + required this.asset, + required this.secondAsset, + required this.formattedAssetTitle, + }); final String fiatAdditionalBalance; final String fiatAvailableBalance; @@ -46,14 +49,26 @@ class BalanceRecord { final String fiatSecondAdditionalBalance; final String fiatSecondAvailableBalance; final CryptoCurrency asset; + final CryptoCurrency secondAsset; final String formattedAssetTitle; + + String get combinedAvailableBalance => + ((double.tryParse(availableBalance) ?? 0) + (double.tryParse(secondAvailableBalance) ?? 0)) + .toString().withMaxDecimals(8); + + String get combinedFiatAvailableBalance => + fiatAvailableBalance.split(" ").first + + " " + + ((double.tryParse(fiatAvailableBalance.split(" ").last) ?? 0) + + (double.tryParse(fiatSecondAvailableBalance.split(" ").last) ?? 0)) + .toString().withMaxDecimals(8); } class BalanceViewModel = BalanceViewModelBase with _$BalanceViewModel; abstract class BalanceViewModelBase with Store { BalanceViewModelBase( - {required this.appStore, required this.settingsStore, required this.fiatConvertationStore}) + {required this.appStore, required this.settingsStore, required this.fiatConversionStore}) : isReversing = false, isShowCard = appStore.wallet?.walletInfo.isShowIntroCakePayCard ?? false, wallet = appStore.wallet! { @@ -64,9 +79,7 @@ abstract class BalanceViewModelBase with Store { _checkMweb(); - reaction((_) => settingsStore.mwebAlwaysScan, (bool value) { - _checkMweb(); - }); + reaction((_) => settingsStore.mwebAlwaysScan, (_) => _checkMweb()); } void _checkMweb() { @@ -77,9 +90,7 @@ abstract class BalanceViewModelBase with Store { final AppStore appStore; final SettingsStore settingsStore; - final FiatConversionStore fiatConvertationStore; - - bool get canReverse => false; + final FiatConversionStore fiatConversionStore; @observable bool isReversing; @@ -87,17 +98,12 @@ abstract class BalanceViewModelBase with Store { @observable WalletBase, TransactionInfo> wallet; - @computed - bool get hasSilentPayments => wallet.type == WalletType.bitcoin && !wallet.isHardwareWallet; - @computed double get price { - final price = fiatConvertationStore.prices[appStore.wallet!.currency]; + final price = fiatConversionStore.prices[appStore.wallet!.currency]; - if (price == null) { - // price should update on next fetch: - return 0; - } + // price should update on next fetch: + if (price == null) return 0; return price; } @@ -111,15 +117,13 @@ abstract class BalanceViewModelBase with Store { @computed bool get isHomeScreenSettingsEnabled => isEVMCompatibleChain(wallet.type) || - wallet.type == WalletType.solana || - wallet.type == WalletType.tron || - wallet.type == WalletType.zano; + [WalletType.solana, WalletType.tron, WalletType.zano].contains(wallet.type); @computed bool get isEVMCompatible => isEVMCompatibleChain(wallet.type); @computed - bool get hasAccounts => wallet.type == WalletType.monero || wallet.type == WalletType.wownero; + bool get hasAccounts => [WalletType.monero, WalletType.wownero].contains(wallet.type); @computed SortBalanceBy get sortBalanceBy => settingsStore.sortBalanceBy; @@ -213,16 +217,16 @@ abstract class BalanceViewModelBase with Store { String additionalBalance(CryptoCurrency cryptoCurrency) { final balance = _currencyBalance(cryptoCurrency); - if (displayMode == BalanceDisplayMode.hiddenBalance) { - return '0.0'; - } + if (displayMode == BalanceDisplayMode.hiddenBalance || balance.additional == BigInt.zero) return '0.0'; - return balance.formattedAdditionalBalance; + return cryptoCurrency.formatAmount(balance.additional); } @computed Map get balances { return wallet.balance.map((key, value) { + var secondAsset = key == CryptoCurrency.ltc ? CryptoCurrency.ltcmweb : key; + if (displayMode == BalanceDisplayMode.hiddenBalance) { final fiatCurrency = settingsStore.fiatCurrency; return MapEntry( @@ -241,60 +245,62 @@ abstract class BalanceViewModelBase with Store { fiatSecondAdditionalBalance: isFiatDisabled ? '' : '${fiatCurrency.toString()} ●●●●●', asset: key, + secondAsset: secondAsset, formattedAssetTitle: _formatterAsset(key))); } final fiatCurrency = settingsStore.fiatCurrency; - final price = key.isPotentialScam ? 0.0 : fiatConvertationStore.prices[key] ?? 0; + final price = key.isPotentialScam + ? 0.0 + : fiatConversionStore.prices[key == CryptoCurrency.btcln ? CryptoCurrency.btc : key] ?? 0; // if (price == null) { // throw Exception('Price is null for: $key'); // } - final additionalFiatBalance = isFiatDisabled - ? '' - : (fiatCurrency.toString() + - ' ' + - _getFiatBalance(price: price, cryptoAmount: value.formattedAdditionalBalance)); + final available = evm?.getERC20AvailableBalance(value) ?? + (value.fullAvailableBalance - (value.secondAvailable ?? BigInt.zero)); + final additional = evm?.getERC20AvailableBalance(value) ?? + (value.additional - (value.secondAdditional ?? BigInt.zero)); final availableFiatBalance = isFiatDisabled ? '' - : (fiatCurrency.toString() + - ' ' + - _getFiatBalance(price: price, cryptoAmount: value.formattedAvailableBalance)); + : '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: key.formatAmount(available))}'; + + final additionalFiatBalance = isFiatDisabled + ? '' + : '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: key.formatAmount(additional))}'; final frozenFiatBalance = isFiatDisabled ? '' - : (fiatCurrency.toString() + - ' ' + - _getFiatBalance(price: price, cryptoAmount: getFormattedFrozenBalance(value))); - - final secondAdditionalFiatBalance = isFiatDisabled - ? '' - : (fiatCurrency.toString() + - ' ' + - _getFiatBalance(price: price, cryptoAmount: value.formattedSecondAdditionalBalance)); + : '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: value.frozen != null ? key.formatAmount(value.frozen!) : null)}'; final secondAvailableFiatBalance = isFiatDisabled ? '' - : (fiatCurrency.toString() + - ' ' + - _getFiatBalance(price: price, cryptoAmount: value.formattedSecondAvailableBalance)); + : '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: value.secondAvailable != null ? key.formatAmount(value.secondAvailable!) : null)}'; + + final secondAdditionalFiatBalance = isFiatDisabled + ? '' + : '$fiatCurrency ${_getFiatBalance(price: price, cryptoAmount: value.secondAdditional != null ? key.formatAmount(value.secondAdditional!) : null)}'; return MapEntry( - key, - BalanceRecord( - availableBalance: value.formattedAvailableBalance, - additionalBalance: value.formattedAdditionalBalance, - frozenBalance: getFormattedFrozenBalance(value), - secondAvailableBalance: value.formattedSecondAvailableBalance, - secondAdditionalBalance: value.formattedSecondAdditionalBalance, - fiatAdditionalBalance: additionalFiatBalance, - fiatAvailableBalance: availableFiatBalance, - fiatFrozenBalance: frozenFiatBalance, - fiatSecondAvailableBalance: secondAvailableFiatBalance, - fiatSecondAdditionalBalance: secondAdditionalFiatBalance, - asset: key, - formattedAssetTitle: _formatterAsset(key))); + key, + BalanceRecord( + availableBalance: _getFormattedCryptoAmount(key, available), + fiatAvailableBalance: availableFiatBalance, + additionalBalance: _getFormattedCryptoAmount(key, additional), + fiatAdditionalBalance: additionalFiatBalance, + frozenBalance: + (value.frozen ?? BigInt.zero) > BigInt.zero ? _getFormattedCryptoAmount(key, value.frozen??BigInt.zero) : '', + fiatFrozenBalance: frozenFiatBalance, + secondAvailableBalance: _getFormattedCryptoAmount(secondAsset, value.secondAvailable??BigInt.zero), + fiatSecondAvailableBalance: secondAvailableFiatBalance, + secondAdditionalBalance: _getFormattedCryptoAmount(secondAsset, value.secondAdditional??BigInt.zero), + fiatSecondAdditionalBalance: secondAdditionalFiatBalance, + asset: key, + secondAsset: secondAsset, + formattedAssetTitle: _formatterAsset(key), + ), + ); }); } @@ -302,47 +308,48 @@ abstract class BalanceViewModelBase with Store { bool mwebEnabled = false; bool hasAdditionalBalance(CryptoCurrency currency) { - bool isWalletTypeActivated = _hasAdditionalBalanceForWalletType(wallet.type); - bool isNotZeroAmount = additionalBalance(currency) != "0.0"; + final isWalletTypeActivated = _hasAdditionalBalanceForWalletType(wallet.type); + final isNotZeroAmount = additionalBalance(currency) != "0.0"; return isWalletTypeActivated && isNotZeroAmount; } @computed - bool get hasSecondAdditionalBalance => - mwebEnabled && _hasSecondAdditionalBalanceForWalletType(wallet.type); + bool get hasSecondAdditionalBalance { + if (wallet.type == WalletType.litecoin && mwebEnabled) { + return (wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) != 0; + } else if (wallet.type == WalletType.bitcoin) { + return (wallet.balance[CryptoCurrency.btc]?.secondAdditional ?? 0) != 0; + } + return false; + } @computed - bool get hasSecondAvailableBalance => - mwebEnabled && _hasSecondAvailableBalanceForWalletType(wallet.type); - - bool _hasAdditionalBalanceForWalletType(WalletType type) { - switch (type) { - case WalletType.monero: - case WalletType.wownero: - case WalletType.zano: - case WalletType.decred: - case WalletType.zcash: + bool get hasSecondAvailableBalance { + switch (wallet.type) { + case WalletType.bitcoin: return true; + case WalletType.litecoin: + return mwebEnabled; default: return false; } } - bool _hasSecondAdditionalBalanceForWalletType(WalletType type) { - if (wallet.type == WalletType.litecoin) { - if ((wallet.balance[CryptoCurrency.ltc]?.secondAdditional ?? 0) != 0) { - return true; - } - } - return false; - } + bool _hasAdditionalBalanceForWalletType(WalletType type) => [ + WalletType.monero, + WalletType.wownero, + WalletType.zano, + WalletType.decred, + WalletType.zcash + ].contains(type); - bool _hasSecondAvailableBalanceForWalletType(WalletType type) { - if (wallet.type == WalletType.litecoin) { - return true; - } - return false; + String _getFormattedCryptoAmount(CryptoCurrency cryptoCurrency, BigInt? amount) { + if (amount == null) return ""; + + return appStore.amountParsingProxy + .getDisplayCryptoStringFromBigInt(amount, cryptoCurrency) + .withMaxDecimals(8).withLocalSeperator(settingsStore.languageCode); } @computed @@ -351,25 +358,15 @@ abstract class BalanceViewModelBase with Store { balance.sort((BalanceRecord a, BalanceRecord b) { if (wallet.currency == CryptoCurrency.xhv) { - if (b.asset == CryptoCurrency.xhv) { - return 1; - } + if (b.asset == CryptoCurrency.xhv) return 1; if (b.asset == CryptoCurrency.xusd) { - if (a.asset == CryptoCurrency.xhv) { - return -1; - } - + if (a.asset == CryptoCurrency.xhv) return -1; return 1; } - if (b.asset == CryptoCurrency.xbtc) { - return 1; - } - - if (b.asset == CryptoCurrency.xeur) { - return 1; - } + if (b.asset == CryptoCurrency.xbtc) return 1; + if (b.asset == CryptoCurrency.xeur) return 1; return 0; } @@ -396,9 +393,9 @@ abstract class BalanceViewModelBase with Store { switch (sortBalanceBy) { case SortBalanceBy.FiatBalance: final aFiatBalance = _getFiatBalance( - price: fiatConvertationStore.prices[a.asset] ?? 0, cryptoAmount: a.availableBalance); + price: fiatConversionStore.prices[a.asset] ?? 0, cryptoAmount: a.availableBalance); final bFiatBalance = _getFiatBalance( - price: fiatConvertationStore.prices[b.asset] ?? 0, cryptoAmount: b.availableBalance); + price: fiatConversionStore.prices[b.asset] ?? 0, cryptoAmount: b.availableBalance); return (double.tryParse(bFiatBalance) ?? 0) .compareTo((double.tryParse(aFiatBalance)) ?? 0); @@ -416,9 +413,7 @@ abstract class BalanceViewModelBase with Store { Balance _currencyBalance(CryptoCurrency cryptoCurrency) { final balance = wallet.balance[cryptoCurrency]; - if (balance == null) { - throw Exception('No balance for ${wallet.currency}'); - } + if (balance == null) throw Exception('No balance for ${wallet.currency}'); return balance; } @@ -431,9 +426,7 @@ abstract class BalanceViewModelBase with Store { @action void _onWalletChange( WalletBase, TransactionInfo>? wallet) { - if (wallet == null) { - return; - } + if (wallet == null) return; this.wallet = wallet; _onCurrentWalletChangeReaction?.reaction.dispose(); @@ -459,27 +452,21 @@ abstract class BalanceViewModelBase with Store { String _getFiatBalance({required double price, String? cryptoAmount}) { if (cryptoAmount == null || cryptoAmount.isEmpty || double.tryParse(cryptoAmount) == null) { - return '0.00'; + return '0.00'.withLocalSeperator(settingsStore.languageCode); } - return calculateFiatAmount(price: price, cryptoAmount: cryptoAmount); + return calculateFiatAmount(price: price, cryptoAmount: cryptoAmount) + .withLocalSeperator(settingsStore.languageCode); } String _formatterAsset(CryptoCurrency asset) { - switch (wallet.type) { - case WalletType.haven: - final assetStringified = asset.toString(); - - if (asset != CryptoCurrency.xhv && assetStringified[0].toUpperCase() == 'X') { - return assetStringified.replaceFirst('X', 'x'); - } - - return asset.toString(); - default: - return asset.toString(); + final assetString = asset.toString(); + if (wallet.type == WalletType.haven && + asset != CryptoCurrency.xhv && + assetString[0].toUpperCase() == 'X') { + return assetString.replaceFirst('X', 'x'); } - } - String getFormattedFrozenBalance(Balance walletBalance) => - walletBalance.formattedUnAvailableBalance; + return appStore.amountParsingProxy.getCryptoSymbol(asset); + } } diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index bc58a8969..4bb20c86d 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -5,6 +5,8 @@ import 'dart:io' show Platform; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/key_service.dart'; +import "package:cw_core/balance_card_style_settings.dart"; +import 'package:cake_wallet/core/trade_monitor.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; @@ -14,6 +16,7 @@ import 'package:cake_wallet/entities/sync_status_display_mode.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/order/order_provider_description.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/store/dashboard/order_filter_store.dart'; @@ -23,7 +26,6 @@ import 'package:cake_wallet/zcash/zcash.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cake_wallet/utils/tor.dart'; import 'package:cake_wallet/wownero/wownero.dart' as wow; -import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/orders_store.dart'; @@ -45,6 +47,8 @@ import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart'; import 'package:cake_wallet/view_model/settings/sync_mode.dart'; import 'package:cryptography/cryptography.dart'; import 'package:cw_core/balance.dart'; +import 'package:cw_core/card_design.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_history.dart'; @@ -62,7 +66,6 @@ import 'package:mobx/mobx.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:cake_wallet/core/trade_monitor.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/evm/evm.dart'; @@ -200,6 +203,8 @@ abstract class DashboardViewModelBase with Store { name = appStore.wallet!.name, type = appStore.wallet!.type, transactions = ObservableList(), + cardDesigns = ObservableList(), + cardOrder = ObservableMap(), wallet = appStore.wallet! { showDecredInfoCard = wallet.type == WalletType.decred && (sharedPreferences.getBool(PreferencesKey.showDecredInfoCard) ?? true); @@ -238,7 +243,7 @@ abstract class DashboardViewModelBase with Store { (transaction) => TransactionListItem( transaction: transaction, balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore, + appStore: appStore, key: ValueKey('monero_transaction_history_item_${transaction.id}_key'), ), ), @@ -268,7 +273,7 @@ abstract class DashboardViewModelBase with Store { (transaction) => TransactionListItem( transaction: transaction, balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore, + appStore: appStore, key: ValueKey('wownero_transaction_history_item_${transaction.id}_key'), ), ), @@ -282,7 +287,7 @@ abstract class DashboardViewModelBase with Store { (transaction) => TransactionListItem( transaction: transaction, balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore, + appStore: appStore, key: ValueKey('${_wallet.type.name}_transaction_history_item_${transaction.id}_key'), ), ), @@ -294,9 +299,11 @@ abstract class DashboardViewModelBase with Store { // subname = nano!.getCurrentAccount(_wallet).label; // } - reaction((_) => appStore.wallet, (wallet) { + _walletChangeDisposer?.reaction.dispose(); + _walletChangeDisposer = reaction((_) => appStore.wallet, (wallet) { _onWalletChange(wallet); _checkMweb(); + loadCardDesigns(); showDecredInfoCard = wallet?.type == WalletType.decred && sharedPreferences.getBool(PreferencesKey.showDecredInfoCard) != false; @@ -314,7 +321,9 @@ abstract class DashboardViewModelBase with Store { if (![WalletType.solana, WalletType.tron].contains(wallet.type)) { try { confirmations = - appStore.wallet!.transactionHistory.transactions.values.first.confirmations + 1; + appStore.wallet!.transactionHistory.transactions.values.first.confirmations + + appStore.wallet!.transactionHistory.transactions.values.last.confirmations + + 1; } catch (_) {} } return length * confirmations; @@ -328,6 +337,8 @@ abstract class DashboardViewModelBase with Store { }); } + loadCardDesigns(); + _checkMweb(); reaction((_) => settingsStore.mwebAlwaysScan, (bool value) => _checkMweb()); @@ -351,13 +362,97 @@ abstract class DashboardViewModelBase with Store { (transaction) => TransactionListItem( transaction: transaction, balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore, + appStore: appStore, key: ValueKey('${wallet.type.name}_transaction_history_item_${transaction.id}_key'), ), ), ); } + @computed + bool get isSyncHeavy { + if ([WalletType.monero, WalletType.wownero, WalletType.decred, WalletType.zcash, WalletType.zano].contains(wallet.type)) { + return true; + } + + if (wallet.type == WalletType.bitcoin && silentPaymentsScanningActive && hasSilentPayments) { + return true; + } + + if (wallet.type == WalletType.litecoin && mwebEnabled && hasMweb) { + return true; + } + + return false; + } + + @action + Future loadCardDesigns() async { + final accountStyleSettings = + await BalanceCardStyleSettings.getAll(wallet.walletInfo.internalId); + + late final int numAccounts; + if (wallet.type == WalletType.monero) { + numAccounts = monero!.getAccountList(wallet).accounts.length; + } else if (wallet.type == WalletType.wownero) { + numAccounts = wow.wownero!.getAccountList(wallet).accounts.length; + } else if (wallet.type == WalletType.bitcoin) { + // bitcoin and lightning + numAccounts = 2; + } else { + numAccounts = 1; + } + cardDesigns.clear(); + Map newOrder = {}; + + for (int i = 0; i < numAccounts; i++) { + late final int index; + if(balanceViewModel.hasAccounts) { + index = i; + } else if(wallet.type == WalletType.bitcoin && i == 1) { + index = 0; + } else { + index = -1; + } + + + final setting = accountStyleSettings + .where((e) => e.accountIndex == index) + .firstOrNull; + + + late final CryptoCurrency curr; + if(wallet.type == WalletType.bitcoin && i == 1) { + curr = CryptoCurrency.btcln; + } else { + curr = wallet.currency; + } + + + cardDesigns.add(CardDesign.fromStyleSettings(setting, curr)); + if(setting?.cardOrder != null) { + newOrder[setting!.cardOrder] = i; + } + } + + // making sure ALL accounts have numbers, even the ones that existed before this feature was a thing + for (int i = 0; i < numAccounts; i++) { + if (!newOrder.containsKey(i) && !(wallet.type != WalletType.bitcoin && i == 1)) { + int free = 0; + while (newOrder.containsValue(free)) { + free++; + } + if(wallet.type == WalletType.bitcoin) { + newOrder[free] = 0; + } else { + newOrder[free] = i; + } + + } + } + cardOrder = newOrder.asObservable(); + } + void _transactionDisposerCallback(int _) async { // Simple check to prevent the callback from being called multiple times in the same frame if (_isTransactionDisposerCallbackRunning) return; @@ -390,7 +485,7 @@ abstract class DashboardViewModelBase with Store { transactions.addAll(relevantTxs.map((tx) => TransactionListItem( transaction: tx, balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore, + appStore: appStore, key: ValueKey('${wallet.type.name}_transaction_history_item_${tx.id}_key'), ))); } finally { @@ -426,8 +521,15 @@ abstract class DashboardViewModelBase with Store { @observable bool isShowThirdYatIntroduction; + @observable + ObservableList cardDesigns; + + @observable + ObservableMap cardOrder; + @computed bool get isDarkTheme => appStore.themeStore.currentTheme.isDark; + @computed String get address => wallet.walletAddresses.address; @@ -437,6 +539,24 @@ abstract class DashboardViewModelBase with Store { @computed SyncStatus get status => wallet.syncStatus; + @computed + bool get shouldShowMwebAd { + if(wallet.type != WalletType.litecoin) return false; + + if(mwebEnabled) return false; + + if(settingsStore.mwebAdDismissed) return false; + + return true; + } + + @action + void dismissMwebAd(bool enableMweb) { + if(enableMweb) setMwebEnabled(); + + settingsStore.mwebAdDismissed = true; + } + @computed String get syncStatusText { var statusText = ''; @@ -456,13 +576,40 @@ abstract class DashboardViewModelBase with Store { return statusText; } + @computed + double get confirmationProgress { + int received = 0; + int needed = 0; + + for (final transaction in transactions) { + if (transaction.neededConfirmations == 0) { + continue; + } + + if(transaction.transaction.confirmations >= transaction.neededConfirmations) { + continue; + } + + received += transaction.transaction.confirmations; + needed += transaction.neededConfirmations; + } + if (needed == 0) { + return 1; + } + return received / needed; + } + @computed BalanceDisplayMode get balanceDisplayMode => appStore.settingsStore.balanceDisplayMode; @computed + @Deprecated("Replaced by showApps") bool get shouldShowMarketPlaceInDashboard => appStore.settingsStore.shouldShowMarketPlaceInDashboard; + @computed + bool get showApps => appStore.settingsStore.shouldShowMarketPlaceInDashboard; + @computed List get trades => tradesStore.trades.where((trade) { @@ -520,6 +667,10 @@ abstract class DashboardViewModelBase with Store { @observable WalletBase, TransactionInfo> wallet; + @computed + bool get hasLightning => + wallet.type == WalletType.bitcoin && wallet.isSoftwareWallet && bitcoin!.useLightning(wallet); + @computed bool get isTestnet => wallet.type == WalletType.bitcoin && bitcoin!.isTestnet(wallet); @@ -581,7 +732,7 @@ abstract class DashboardViewModelBase with Store { ]; return errors; } - + @computed bool get showZcashMissingFundsCard { if (wallet.type != WalletType.zcash) return false; @@ -706,8 +857,11 @@ abstract class DashboardViewModelBase with Store { } bool get hasBgsyncNetworkConstraints => Platform.isAndroid; + bool get hasBgsyncBatteryNotLowConstraints => Platform.isAndroid; + bool get hasBgsyncChargingConstraints => Platform.isAndroid; + bool get hasBgsyncDeviceIdleConstraints => Platform.isAndroid; @observable @@ -860,12 +1014,12 @@ abstract class DashboardViewModelBase with Store { mwebEnabled = false; bitcoin!.setMwebEnabled(wallet, false); } - - @action + + @action Future rescanInternalChangeZcash() async { await zcash!.rescanInternalChange(wallet); } - + @action void dismissZcash() { settingsStore.showZcashMissingFundsCard = false; @@ -946,6 +1100,8 @@ abstract class DashboardViewModelBase with Store { ReactionDisposer? _transactionDisposer; + ReactionDisposer? _walletChangeDisposer; + ReactionDisposer? _chainChangeDisposer; @computed @@ -1086,7 +1242,9 @@ abstract class DashboardViewModelBase with Store { if (![WalletType.solana, WalletType.tron].contains(wallet.type)) { try { confirmations = - appStore.wallet!.transactionHistory.transactions.values.first.confirmations + 1; + appStore.wallet!.transactionHistory.transactions.values.first.confirmations + + appStore.wallet!.transactionHistory.transactions.values.last.confirmations + + 1; } catch (_) {} } return length * confirmations; @@ -1120,7 +1278,7 @@ abstract class DashboardViewModelBase with Store { (transaction) => TransactionListItem( transaction: transaction, balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore, + appStore: appStore, key: ValueKey('monero_transaction_history_item_${transaction.id}_key'), ), ), @@ -1140,7 +1298,7 @@ abstract class DashboardViewModelBase with Store { (transaction) => TransactionListItem( transaction: transaction, balanceViewModel: balanceViewModel, - settingsStore: appStore.settingsStore, + appStore: appStore, key: ValueKey('wownero_transaction_history_item_${transaction.id}_key'), ), ), @@ -1304,7 +1462,7 @@ abstract class DashboardViewModelBase with Store { if (wallet.chainId == 1 && tx.evmSignatureName == 'approval') return ' (${tx.evmSignatureName})'; - + return ''; } diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart index 177d01603..1192bbba9 100644 --- a/lib/view_model/dashboard/home_settings_view_model.dart +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -405,7 +405,7 @@ abstract class HomeSettingsViewModelBase with Store { void _updateFiatPrices(CryptoCurrency token) async { if (token.isPotentialScam) return; // don't fetch price data for potential scam tokens try { - _balanceViewModel.fiatConvertationStore.prices[token] = + _balanceViewModel.fiatConversionStore.prices[token] = await FiatConversionService.fetchPrice( crypto: token, fiat: _settingsStore.fiatCurrency, diff --git a/lib/view_model/dashboard/nft_view_model.dart b/lib/view_model/dashboard/nft_view_model.dart index 25c08bdea..5b7a2fdcc 100644 --- a/lib/view_model/dashboard/nft_view_model.dart +++ b/lib/view_model/dashboard/nft_view_model.dart @@ -52,7 +52,7 @@ abstract class NFTViewModelBase with Store { Future getNFTAssetByWallet() async { final wallet = appStore.wallet!; - if (!isNFTACtivatedChain(wallet.type, wallet.chainId)) return; + if (!isNFTACtivatedChain(wallet.type, chainId: wallet.chainId)) return; final walletAddress = wallet.walletInfo.address; log('Fetching wallet NFTs for $walletAddress'); diff --git a/lib/view_model/dashboard/receive_option_view_model.dart b/lib/view_model/dashboard/receive_option_view_model.dart index c3d4f20ff..08ef3f361 100644 --- a/lib/view_model/dashboard/receive_option_view_model.dart +++ b/lib/view_model/dashboard/receive_option_view_model.dart @@ -12,67 +12,26 @@ class ReceiveOptionViewModel = ReceiveOptionViewModelBase with _$ReceiveOptionVi abstract class ReceiveOptionViewModelBase with Store { ReceiveOptionViewModelBase(this._wallet, this.initialPageOption) : selectedReceiveOption = initialPageOption ?? - (_wallet.type == WalletType.bitcoin || - _wallet.type == WalletType.litecoin + ([WalletType.bitcoin, WalletType.litecoin].contains(_wallet.type) ? bitcoin!.getSelectedAddressType(_wallet) - : (_wallet.type == WalletType.decred && _wallet.isTestnet) + : (_wallet.type == WalletType.decred && _wallet.isTestnet) ? ReceivePageOption.testnet : _wallet.type == WalletType.zcash ? zcash!.getSelectedAddressType(_wallet) - : ReceivePageOption.mainnet), - _options = [] { - final walletType = _wallet.type; - switch (walletType) { - case WalletType.bitcoin: - _options = [ - ...bitcoin!.getBitcoinReceivePageOptions(_wallet), - ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) - ]; - break; - case WalletType.litecoin: - _options = [ - ...bitcoin!.getLitecoinReceivePageOptions(_wallet), - ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) - ]; - break; - case WalletType.haven: - _options = [ReceivePageOption.mainnet]; - break; - case WalletType.decred: - if (_wallet.isTestnet) { - _options = [ - ReceivePageOption.testnet, - ...ReceivePageOptions.where( - (element) => element != ReceivePageOption.mainnet) - ]; - } else { - _options = ReceivePageOptions; - } - break; - case WalletType.zcash: - _options = [ - ...zcash!.getZcashReceivePageOptions(_wallet), - ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) - ]; - break; - default: - _options = ReceivePageOptions; - } - } + : ReceivePageOption.mainnet); final WalletBase _wallet; final ReceivePageOption? initialPageOption; - List _options; - @observable ReceivePageOption selectedReceiveOption; - List get options => _options; + List get options => _wallet.walletAddresses.receivePageOptions; + + String get walletTypeString => walletTypeToString(_wallet.type); + @action - void selectReceiveOption(ReceivePageOption option) { - selectedReceiveOption = option; - } + void selectReceiveOption(ReceivePageOption option) => selectedReceiveOption = option; } diff --git a/lib/view_model/dashboard/trade_list_item.dart b/lib/view_model/dashboard/trade_list_item.dart index 973f5b76f..e725afca3 100644 --- a/lib/view_model/dashboard/trade_list_item.dart +++ b/lib/view_model/dashboard/trade_list_item.dart @@ -1,26 +1,29 @@ -import 'package:cake_wallet/entities/balance_display_mode.dart'; -import 'package:cake_wallet/exchange/trade.dart'; -import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; +import "package:cake_wallet/entities/balance_display_mode.dart"; +import "package:cake_wallet/exchange/trade.dart"; +import "package:cake_wallet/store/app_store.dart"; +import "package:cake_wallet/view_model/dashboard/action_list_item.dart"; class TradeListItem extends ActionListItem { TradeListItem({ required this.trade, - required this.settingsStore, + required this.appStore, required super.key, }); final Trade trade; - final SettingsStore settingsStore; + final AppStore appStore; - BalanceDisplayMode get displayMode => settingsStore.balanceDisplayMode; + BalanceDisplayMode get displayMode => appStore.settingsStore.balanceDisplayMode; - String get tradeFormattedAmount => - displayMode == BalanceDisplayMode.hiddenBalance ? '---' : trade.amountFormatted(); + String get tradeFormattedAmount => displayMode == BalanceDisplayMode.hiddenBalance + ? "---" + : appStore.amountParsingProxy.getDisplayCryptoAmount(trade.amountFormatted(), trade.from!); + + String get tradeFormattedReceiveAmount => displayMode == BalanceDisplayMode.hiddenBalance + ? "---" + : appStore.amountParsingProxy + .getDisplayCryptoAmount(trade.receiveAmountFormatted(), trade.to!); - String get tradeFormattedReceiveAmount => - displayMode == BalanceDisplayMode.hiddenBalance ? '---' : trade.receiveAmountFormatted(); - @override DateTime get date => trade.createdAt!; } diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index d9c72f8cc..e765e88c5 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -6,14 +6,15 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/solana/solana.dart'; +import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; import 'package:cake_wallet/zcash/zcash.dart'; +import 'package:cw_core/crypto_amount_format.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; -import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; @@ -26,17 +27,17 @@ class TransactionListItem extends ActionListItem with Keyable { TransactionListItem({ required this.transaction, required this.balanceViewModel, - required this.settingsStore, + required AppStore appStore, required super.key, - }); + }) : _appStore = appStore; final TransactionInfo transaction; final BalanceViewModel balanceViewModel; - final SettingsStore settingsStore; + final AppStore _appStore; double get price => balanceViewModel.price; - FiatCurrency get fiatCurrency => settingsStore.fiatCurrency; + FiatCurrency get fiatCurrency => _appStore.settingsStore.fiatCurrency; BalanceDisplayMode get displayMode => balanceViewModel.displayMode; @@ -49,13 +50,33 @@ class TransactionListItem extends ActionListItem with Keyable { balanceViewModel.wallet.type == WalletType.tron; String get formattedCryptoAmount { - return displayMode == BalanceDisplayMode.hiddenBalance ? '---' : transaction.amountFormatted(); + if (displayMode == BalanceDisplayMode.hiddenBalance) return '---'; + if (balanceViewModel.wallet.type == WalletType.bitcoin) { + final isLightning = (transaction.additionalInfo["isLightning"] as bool?) ?? false; + final crypto = isLightning ? CryptoCurrency.btcln : CryptoCurrency.btc; + final amount = _appStore.amountParsingProxy + .getDisplayCryptoString(transaction.amount, crypto) + .withMaxDecimals(8) + .withLocalSeperator(_appStore.settingsStore.languageCode); + + return '$amount ${_appStore.amountParsingProxy.getCryptoSymbol(crypto)}'; + } + + return transaction.amountFormatted(); } String get formattedTitle { if (transaction.additionalInfo['autoShield'] == true) { return "Autoshield"; } + if (transaction.isPending) { + final status = formattedStatus; + final baseString = transaction.direction == TransactionDirection.incoming + ? S.current.receiving + : S.current.sending; + return status.isNotEmpty ? "$baseString $status" : "$baseString..."; + } + if (transaction.direction == TransactionDirection.incoming) { return S.current.received; } @@ -63,6 +84,25 @@ class TransactionListItem extends ActionListItem with Keyable { return S.current.sent; } + int get neededConfirmations { + switch (balanceViewModel.wallet.type) { + case WalletType.monero: + case WalletType.haven: + case WalletType.zano: + return 10; + case WalletType.wownero: + return 3; + case WalletType.litecoin: + bool isPegOut = (transaction.additionalInfo["isPegOut"] as bool?) ?? false; + bool fromPegOut = (transaction.additionalInfo["fromPegOut"] as bool?) ?? false; + if(isPegOut || fromPegOut) + return 6; + default: + return 0; + } + return 0; + } + String get formattedPendingStatus { switch (balanceViewModel.wallet.type) { case WalletType.monero: @@ -91,10 +131,10 @@ class TransactionListItem extends ActionListItem with Keyable { str = " (${transaction.confirmations}/6)"; } if (isPegIn) { - str += " (Peg In)"; + str += " (Mask)"; } if (isPegOut) { - str += " (Peg Out)"; + str += " (Unmask)"; } return str; default: @@ -115,7 +155,7 @@ class TransactionListItem extends ActionListItem with Keyable { return formattedPendingStatus; } - return transaction.isPending ? S.current.pending : ''; + return ""; } String get formattedType { @@ -154,21 +194,24 @@ class TransactionListItem extends ActionListItem with Keyable { switch (balanceViewModel.wallet.type) { case WalletType.monero: amount = calculateFiatAmountRaw( - cryptoAmount: monero!.formatterMoneroAmountToDouble(amount: transaction.amount), - price: price); + cryptoAmount: monero!.formatterMoneroAmountToDouble(amount: transaction.amount), + price: price, + ).withLocalSeperator(_appStore.settingsStore.languageCode); break; case WalletType.wownero: amount = calculateFiatAmountRaw( - cryptoAmount: wownero!.formatterWowneroAmountToDouble(amount: transaction.amount), - price: price); + cryptoAmount: wownero!.formatterWowneroAmountToDouble(amount: transaction.amount), + price: price, + ).withLocalSeperator(_appStore.settingsStore.languageCode); break; case WalletType.bitcoin: case WalletType.litecoin: case WalletType.bitcoinCash: case WalletType.dogecoin: amount = calculateFiatAmountRaw( - cryptoAmount: bitcoin!.formatterBitcoinAmountToDouble(amount: transaction.amount), - price: price); + cryptoAmount: bitcoin!.formatterBitcoinAmountToDouble(amount: transaction.amount), + price: price, + ).withLocalSeperator(_appStore.settingsStore.languageCode); break; case WalletType.ethereum: case WalletType.polygon: @@ -176,34 +219,35 @@ class TransactionListItem extends ActionListItem with Keyable { case WalletType.arbitrum: case WalletType.bsc: final asset = evm!.assetOfTransaction(balanceViewModel.wallet, transaction); - final price = balanceViewModel.fiatConvertationStore.prices[asset]; + final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( cryptoAmount: evm!.formatterEVMAmountToDouble(transaction: transaction), price: price, - ); + ).withLocalSeperator(_appStore.settingsStore.languageCode); break; case WalletType.nano: amount = calculateFiatAmountRaw( - cryptoAmount: double.parse(nanoUtil!.getRawAsUsableString( - nano!.getTransactionAmountRaw(transaction).toString(), nanoUtil!.rawPerNano)), - price: price); + cryptoAmount: double.parse(nanoUtil!.getRawAsUsableString( + nano!.getTransactionAmountRaw(transaction).toString(), nanoUtil!.rawPerNano)), + price: price, + ).withLocalSeperator(_appStore.settingsStore.languageCode); break; case WalletType.solana: final asset = solana!.assetOfTransaction(balanceViewModel.wallet, transaction); - final price = balanceViewModel.fiatConvertationStore.prices[asset]; + final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( cryptoAmount: solana!.getTransactionAmountRaw(transaction), price: price, - ); + ).withLocalSeperator(_appStore.settingsStore.languageCode); break; case WalletType.tron: final asset = tron!.assetOfTransaction(balanceViewModel.wallet, transaction); - final price = balanceViewModel.fiatConvertationStore.prices[asset]; + final price = balanceViewModel.fiatConversionStore.prices[asset]; final cryptoAmount = tron!.getTransactionAmountRaw(transaction); amount = calculateFiatAmountRaw( cryptoAmount: cryptoAmount, price: price, - ); + ).withLocalSeperator(_appStore.settingsStore.languageCode); break; case WalletType.zano: final asset = zano!.assetOfTransaction(balanceViewModel.wallet, transaction); @@ -211,22 +255,26 @@ class TransactionListItem extends ActionListItem with Keyable { amount = "0.00"; break; } - final price = balanceViewModel.fiatConvertationStore.prices[asset]; + final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( - cryptoAmount: zano!.formatterIntAmountToDouble( - amount: transaction.amount, currency: asset, forFee: false), - price: price); + cryptoAmount: zano!.formatterIntAmountToDouble( + amount: transaction.amount, currency: asset, forFee: false), + price: price, + ).withLocalSeperator(_appStore.settingsStore.languageCode); break; case WalletType.decred: amount = calculateFiatAmountRaw( - cryptoAmount: decred!.formatterDecredAmountToDouble(amount: transaction.amount), - price: price); + cryptoAmount: decred!.formatterDecredAmountToDouble(amount: transaction.amount), + price: price, + ).withLocalSeperator(_appStore.settingsStore.languageCode); break; case WalletType.zcash: amount = calculateFiatAmountRaw( - cryptoAmount: zcash!.formatterZcashAmountToDouble(amount: BigInt.from(transaction.amount)), - price: price); - + cryptoAmount: + zcash!.formatterZcashAmountToDouble(amount: BigInt.from(transaction.amount)), + price: price, + ).withLocalSeperator(_appStore.settingsStore.languageCode); + case WalletType.none: case WalletType.banano: case WalletType.haven: diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 39a063575..04180e7d7 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:cake_wallet/core/payment_uris.dart'; +import 'package:cake_wallet/core/amount_parsing_proxy.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; @@ -30,6 +30,7 @@ import 'package:cake_wallet/view_model/send/fees_view_model.dart'; import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cake_wallet/view_model/send/send_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/payment_uris.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; @@ -50,6 +51,7 @@ abstract class ExchangeTradeViewModelBase with Store { required this.fiatConversionStore, }) : trade = tradesStore.trade!, isSendable = _checkIfCanSend(tradesStore, wallet), + isSwapsXYZCanSendFromExternal = _checkIfSwapsXYZCanSendFromExternal(tradesStore.trade!, wallet), items = ObservableList() { setUpOutput(); switch (trade.provider) { @@ -111,16 +113,16 @@ abstract class ExchangeTradeViewModelBase with Store { late Output output; + AmountParsingProxy get _amountParsingProxy => sendViewModel.amountParsingProxy; + @observable Trade trade; @observable bool isSendable; - bool get isSwapsXyzSendingEVMTokenSwap => - (_provider is SwapsXyzExchangeProvider) && - isEVMCompatibleChain(wallet.type) && - wallet.currency != trade.from; + + bool isSwapsXYZCanSendFromExternal; /// Providers that should hide the "send from external" button static const List _providersThatHideExternalSend = [ @@ -130,24 +132,27 @@ abstract class ExchangeTradeViewModelBase with Store { /// Returns true if the current provider should hide the external send button bool get shouldHideExternalSendButton { if (_provider == null) return false; + + if (!isSwapsXYZCanSendFromExternal) return true; + return _providersThatHideExternalSend.any( (providerType) => _provider.runtimeType == providerType, ); } String get extraInfo => trade.extraId != null && trade.extraId!.isNotEmpty - ? '\n\n' + S.current.exchange_extra_info - : ''; + ? "\n\n${S.current.exchange_extra_info}" + : ""; @computed String get pendingTransactionFiatAmountValueFormatted => sendViewModel.isFiatDisabled - ? '' - : sendViewModel.pendingTransactionFiatAmount + ' ' + sendViewModel.fiat.title; + ? "" + : "${sendViewModel.pendingTransactionFiatAmount} ${sendViewModel.fiat.title}"; @computed String get pendingTransactionFeeFiatAmountFormatted => sendViewModel.isFiatDisabled - ? '' - : sendViewModel.pendingTransactionFeeFiatAmount + ' ' + sendViewModel.fiat.title; + ? "" + : "${sendViewModel.pendingTransactionFeeFiatAmount} ${sendViewModel.fiat.title}"; @observable ObservableList items; @@ -204,7 +209,7 @@ abstract class ExchangeTradeViewModelBase with Store { output = sendViewModel.outputs.first; output.address = trade.inputAddress ?? ''; output.setCryptoAmount(trade.amount); - if (_provider is ThorChainExchangeProvider) output.memo = trade.memo; + if (_provider is ThorChainExchangeProvider) output.memo = trade.memo ?? ""; if (trade.isSendAll == true) output.sendAll = true; } @@ -264,12 +269,12 @@ abstract class ExchangeTradeViewModelBase with Store { void _updateItems() { final trade = tradesStore.trade!; - final tradeFrom = trade.fromRaw >= 0 ? trade.from : trade.userCurrencyFrom; + final tradeFrom = trade.fromRaw >= 0 ? trade.from : trade.userCurrencyFrom; final tradeTo = trade.toRaw >= 0 ? trade.to : trade.userCurrencyTo; - final tagFrom = tradeFrom?.tag != null ? '${tradeFrom!.tag}' + ' ' : ''; - final tagTo = tradeTo?.tag != null ? '${tradeTo!.tag}' + ' ' : ''; + final tagFrom = tradeFrom?.tag != null ? "${tradeFrom!.tag} " : ""; + final tagTo = tradeTo?.tag != null ? "${tradeTo!.tag} " : ""; items.clear(); @@ -277,7 +282,7 @@ abstract class ExchangeTradeViewModelBase with Store { items.add( ExchangeTradeItem( title: "${trade.provider.title} ${S.current.id}", - data: '${trade.id}', + data: "${trade.id}", isCopied: true, isReceiveDetail: true, isExternalSendDetail: false, @@ -288,26 +293,45 @@ abstract class ExchangeTradeViewModelBase with Store { items.addAll([ ExchangeTradeItem( title: S.current.amount, - data: '${trade.amount} ${tradeFrom}', + data: + "${_amountParsingProxy.getDisplayCryptoAmount(trade.amount, tradeFrom!)} ${_amountParsingProxy.getCryptoSymbol(tradeFrom)}", isCopied: false, isReceiveDetail: false, isExternalSendDetail: true, ), ExchangeTradeItem( - title: S.current.you_will_receive_estimated_amount + ':', - data: '${tradesStore.trade?.receiveAmount} ${tradeTo}', + title: "${S.current.you_will_receive_estimated_amount}:", + data: + "${_amountParsingProxy.getDisplayCryptoAmount(tradesStore.trade?.receiveAmount ?? "0", tradeTo!)} ${_amountParsingProxy.getCryptoSymbol(tradeTo)}", isCopied: true, isReceiveDetail: true, isExternalSendDetail: false, ), ExchangeTradeItem( - title: S.current.send_to_this_address('${tradeFrom}', tagFrom) + ':', - data: trade.inputAddress ?? '', + title: "${S.current.send_to_this_address("${tradeFrom}", tagFrom)}:", + data: trade.inputAddress ?? "", isCopied: false, isReceiveDetail: false, isExternalSendDetail: true, ), ]); + + items.add( + isSwapsXYZCanSendFromExternal + ? ExchangeTradeItem( + title: S.current.send_to_this_address('${tradeFrom}', tagFrom) + + ':', + data: trade.inputAddress ?? '', + isCopied: false, + isReceiveDetail: false, + isExternalSendDetail: true) + : ExchangeTradeItem( + title: 'Smart contract call (no address required)', + data: 'Wallet will execute a contract call. On-chain transaction', + isCopied: false, + isReceiveDetail: false, + isExternalSendDetail: true), + ); } final isExtraIdExist = trade.extraId != null && trade.extraId!.isNotEmpty; @@ -315,24 +339,25 @@ abstract class ExchangeTradeViewModelBase with Store { if (isExtraIdExist) { final title = tradeFrom == CryptoCurrency.xrp ? S.current.destination_tag - : tradeFrom == CryptoCurrency.xlm || tradeFrom == CryptoCurrency.ton + : [CryptoCurrency.xlm, CryptoCurrency.ton].contains(tradeFrom) ? S.current.memo : S.current.extra_id; items.add( ExchangeTradeItem( - title: title, - data: trade.extraId ?? '', - isCopied: true, - isReceiveDetail: !isExtraIdExist, - isExternalSendDetail: isExtraIdExist), + title: title, + data: trade.extraId ?? "", + isCopied: true, + isReceiveDetail: !isExtraIdExist, + isExternalSendDetail: isExtraIdExist, + ), ); } items.add( ExchangeTradeItem( - title: S.current.arrive_in_this_address('${tradeTo}', tagTo) + ':', - data: trade.payoutAddress ?? '', + title: "${S.current.arrive_in_this_address("${tradeTo}", tagTo)}:", + data: trade.payoutAddress ?? "", isCopied: true, isReceiveDetail: true, isExternalSendDetail: false, @@ -369,6 +394,7 @@ abstract class ExchangeTradeViewModelBase with Store { wallet.currency == CryptoCurrency.bnb && tradeFrom?.tag == CryptoCurrency.bnb.tag; return tradeFrom == wallet.currency || + tradeFrom == CryptoCurrency.btcln && wallet.currency == CryptoCurrency.btc || tradesStore.trade!.provider == ExchangeProviderDescription.xmrto || _isEthToken() || _isPolygonToken() || @@ -379,6 +405,20 @@ abstract class ExchangeTradeViewModelBase with Store { _isBscToken(); } + static bool _checkIfSwapsXYZCanSendFromExternal(Trade trade, WalletBase wallet) { + final provider = trade.provider; + + if (provider == ExchangeProviderDescription.swapsXyz && + isEVMCompatibleChain(wallet.type)) { + + if(trade.routerData != null && trade.routerData != '0x') { + return false; + } + + } + return true; + } + Future registerSwapsXyzTransaction() async { try { if (!(_provider is SwapsXyzExchangeProvider)) return; @@ -440,13 +480,11 @@ abstract class ExchangeTradeViewModelBase with Store { switch (wallet.type) { case WalletType.bitcoin: - return BitcoinURI(amount: amount, address: inputAddress); - case WalletType.litecoin: - return LitecoinURI(amount: amount, address: inputAddress); + return BitcoinURI(address: inputAddress, amount: amount); case WalletType.bitcoinCash: - return BitcoinCashURI(amount: amount, address: inputAddress); + return BitcoinCashURI(address: inputAddress, amount: amount); case WalletType.dogecoin: - return DogeURI(amount: amount, address: inputAddress); + return DogeURI(address: inputAddress, amount: amount); case WalletType.ethereum: return _createERC681URI(fromCurrency, inputAddress, amount); // TODO: Expand ERC681URI support to Polygon(modify decoding flow for QRs, pay anything, and deep link handling) @@ -463,17 +501,11 @@ abstract class ExchangeTradeViewModelBase with Store { case WalletType.tron: return TronURI(amount: amount, address: inputAddress); case WalletType.monero: - return MoneroURI(amount: amount, address: inputAddress); + return MoneroURI(address: inputAddress, amount: amount); case WalletType.wownero: - return WowneroURI(amount: amount, address: inputAddress); - case WalletType.zano: - return ZanoURI(amount: amount, address: inputAddress); - case WalletType.decred: - return DecredURI(amount: amount, address: inputAddress); - case WalletType.haven: - return HavenURI(amount: amount, address: inputAddress); - case WalletType.nano: - return NanoURI(amount: amount, address: inputAddress); + return MoneroURI( + address: inputAddress, + amount: amount); default: return null; } diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 929dde3e7..b2bd8916d 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -6,6 +6,7 @@ import 'dart:developer'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/core/amount_parsing_proxy.dart'; import 'package:cake_wallet/core/create_trade_result.dart'; import 'package:cake_wallet/core/fiat_conversion_service.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; @@ -28,13 +29,18 @@ import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/near_Intents_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cw_core/currency.dart'; +import "package:cw_core/wallet_info.dart"; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/dashboard/trades_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -56,8 +62,8 @@ import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; -import 'package:intl/intl.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -68,27 +74,26 @@ class ExchangeViewModel = ExchangeViewModelBase with _$ExchangeViewModel; abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with Store { @override void onWalletChange(wallet) { - receiveCurrency = wallet.currency; - depositCurrency = wallet.currency; + // FIXME this caused a bug when switching wallet, i can't figure out why it's here? + // receiveCurrency = wallet.currency; + // depositCurrency = wallet.currency; } ExchangeViewModelBase( - AppStore appStore, + this._appStore, this.trades, this._exchangeTemplateStore, this.tradesStore, - this._settingsStore, this.sharedPreferences, this.contactListViewModel, this.unspentCoinsListViewModel, this.feesViewModel, this.fiatConversionStore, - ) : _cryptoNumberFormat = NumberFormat(), - isSendAllEnabled = false, + ) : isSendAllEnabled = false, isFixedRateMode = false, isReceiveAmountEntered = false, - depositAmount = '', - receiveAmount = '', + _depositAmount = '', + _receiveAmount = '', receiveAddress = '', depositAddress = '', isDepositAddressEnabled = false, @@ -99,11 +104,11 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with limits = Limits(min: 0, max: 0), tradeState = ExchangeTradeStateInitial(), limitsState = LimitsInitialState(), - receiveCurrency = appStore.wallet!.currency, - depositCurrency = appStore.wallet!.currency, + receiveCurrency = _appStore.wallet!.currency, + depositCurrency = _appStore.wallet!.currency, providerList = [], selectedProviders = ObservableList(), - super(appStore: appStore) { + super(appStore: _appStore) { _useTorOnly = _settingsStore.exchangeStatus == ExchangeApiMode.torOnly; _setProviders(); const excludeDepositCurrencies = [CryptoCurrency.btt]; @@ -126,9 +131,11 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with /// if the provider is not in the user settings (user's first time or newly added provider) /// then use its default value decided by us selectedProviders = ObservableList.of(providerList - .where((element) => exchangeProvidersSelection[element.title] == null - ? element.isEnabled - : (exchangeProvidersSelection[element.title] as bool)) + .where((element) => + (!forceDecentralizedExchanges || !element.description.isCentralized) && + (exchangeProvidersSelection[element.title] == null + ? element.isEnabled + : (exchangeProvidersSelection[element.title] as bool))) .toList()); _setAvailableProviders(); @@ -150,12 +157,18 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with }); isDepositAddressEnabled = !useSameWalletAddress(depositCurrency); - depositAmount = ''; - receiveAmount = ''; + _depositAmount = ''; + _receiveAmount = ''; receiveAddress = ''; depositAddress = useSameWalletAddress(depositCurrency) ? wallet.walletAddresses.addressForExchange : ''; + reaction((_) => receiveAddress, (_) { + if(!(tradeState is TradeIsCreatedSuccessfully)) { + receiveAddressDisplayName = null; + } + }); + provider = providerList.firstOrNull; final initialProvider = provider; provider!.checkIsAvailable().then((bool isAvailable) { @@ -165,6 +178,13 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with _onPairChange(); } }); + + // providerDisplay is read by ui to display auto-selected provider. + // it's on a delay so it doesn't flicker. + reaction((_)=>bestRateProvider,(val) { + providerDisplay = val; + },delay: 300); + receiveCurrencies = CryptoCurrency.all .where((cryptoCurrency) => !excludeReceiveCurrencies.contains(cryptoCurrency)) .toList() @@ -180,28 +200,71 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with _defineIsReceiveAmountEditable(); loadLimits(); reaction((_) => isFixedRateMode, (Object _) { + bestRateProvider = null; bestRate = 0.0; loadLimits(); }); + reaction((_) => forceDecentralizedExchanges, (val) { + if (val && (bestRateProvider?.description.isCentralized ?? false)) { + bestRateProvider = null; + bestRate = 0.0; + loadLimits(); + } + }); + if (isElectrumWallet) { bitcoin!.updateFeeRates(wallet); } - reaction((_) => wallet.currency, (_) async { - // When currency changes (e.g., EVM chain switch), update currencies - await Future.delayed(const Duration(milliseconds: 100)); - receiveCurrency = wallet.currency; - depositCurrency = wallet.currency; - - // Only refresh ETH tokens for EVM wallets - if (isEVMCompatibleChain(wallet.type)) { - _injectUserEthTokensIntoCurrencyLists(); + fetchFiatPrice(receiveCurrency); + + updateDepositAvailableAmount(); + } + + Future updateDepositAvailableAmount() async { + if (depositCurrency == CryptoCurrency.btcln) { + final currency = depositCurrency; + Future.doWhile(() async { + await Future.delayed(const Duration(milliseconds: 400)); + if (depositCurrency != currency) { + return false; + } + final balance = wallet.balance[depositCurrency]; + if (balance != null) { + depositAvailableAmount = _appStore.amountParsingProxy + .getDisplayCryptoStringFromBigInt(balance.fullAvailableBalance, depositCurrency); + return false; + } + return true; + }); + } else { + final currency = depositCurrency; + final amount = _appStore.amountParsingProxy.getDisplayCryptoString( + (await unspentCoinsListViewModel.getSendingBalance(UnspentCoinType.any)), depositCurrency); + if(depositCurrency == currency) { + depositAvailableAmount = amount; } - }); + } + } + + void showFiatCurrencyPicker(BuildContext context) { + showPopUp( + context: context, + builder: (_) => CurrencyPicker( + key: ValueKey('send_page_currency_picker_dialog_button_key'), + selectedAtIndex: FiatCurrency.all.indexOf(fiat), + items: FiatCurrency.all, + hintText: S.of(context).search_currency, + onItemSelected: (Currency cur) async { + _settingsStore.fiatCurrency = (cur as FiatCurrency); + }, + ), + ); } bool useSameWalletAddress(CryptoCurrency currency) => currency == wallet.currency || + (currency == CryptoCurrency.btcln && wallet.currency == CryptoCurrency.btc && wallet.isSoftwareWallet) || (currency.tag != null && currency.tag == wallet.currency.tag) || currency.tag == wallet.currency.title; @@ -230,8 +293,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with LetsExchangeExchangeProvider(), StealthExExchangeProvider(), XOSwapExchangeProvider(), + SwapsXyzExchangeProvider(), JupiterExchangeProvider(), - // SwapsXyzExchangeProvider(), NearIntentsExchangeProvider(), TrocadorExchangeProvider( useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates), @@ -240,6 +303,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with @observable ExchangeProvider? provider; + @observable + ExchangeProvider? providerDisplay; + /// Maps in dart are not sorted by default /// SplayTreeMap is a map sorted by keys /// will use it to sort available providers @@ -274,10 +340,25 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with ExchangeTradeState tradeState; @observable - String depositAmount; + String _depositAmount; + + @computed + String get depositAmount => + _depositAmount == "..."? _depositAmount:amountParsingProxy.getDisplayCryptoAmount(_depositAmount, depositCurrency); + + @computed + String get depositAmountCanonical => _depositAmount; @observable - String receiveAmount; + String _receiveAmount; + + @computed + String get receiveAmount => + _receiveAmount == "..." ? _receiveAmount:amountParsingProxy.getDisplayCryptoAmount(_receiveAmount, receiveCurrency); + + @action + // only set canonical formated amounts here; + void setCanonicalReceiveAmount(String value) => _receiveAmount = value; @observable String depositAddress; @@ -285,6 +366,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with @observable String receiveAddress; + @observable + String? receiveAddressDisplayName; + @observable bool isDepositAddressEnabled; @@ -303,6 +387,12 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with @observable Limits limits; + @observable + bool tradeStarted = false; + + @observable + bool isSendFromExternal = false; + @computed SyncStatus get status => wallet.syncStatus; @@ -314,6 +404,49 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with .where((element) => element.type == receiveCurrency) .toList(); + @computed + List get refundWalletContactsToShow => contactListViewModel.walletContacts + .where((element) => element.type == depositCurrency) + .toList(); + + @computed + Future> get receiveWallets async { + WalletType? type; + type = cryptoCurrencyOrTokenToWalletType(receiveCurrency); + if (type == null) { + type = cryptoCurrencyOrTokenToWalletType(CryptoCurrency.fromString(receiveCurrency.tag ?? "")); + } + + return await WalletInfo.selectList("type = ?", [type!.index]); + } + + Future> addressesForWallet(WalletInfo wallet) async { + final List ret = []; + final addresses = await wallet.getAddressInfos(); + for (var list in addresses.values) { + // we only want the "primary" account addresses - those that contain account names. + ret.addAll(list.where((item) => item.label.split(" ").length > 1)); + } + return ret; + } + + @computed + Future> get depositWallets async { + WalletType? type; + type = cryptoCurrencyOrTokenToWalletType(depositCurrency); + if (type == null) { + try { + type = cryptoCurrencyOrTokenToWalletType(CryptoCurrency.fromString(depositCurrency.tag ?? "")); + } catch (_) {} + } + + if (type != null) { + return await WalletInfo.selectList("type = ?", [type.index]); + } + + return await WalletInfo.getAll(); + } + @action bool checkIfWalletIsAnInternalWallet(String address) { final walletContactList = @@ -330,6 +463,13 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with bool get shouldDisplayTOTP2FAForExchangesToExternalWallet => _settingsStore.shouldRequireTOTP2FAForExchangesToExternalWallets; + @computed + String? get balanceDisplay { + final bal = wallet.balance[depositCurrency]?.fullAvailableBalance; + if(bal == null) return null; + return amountParsingProxy.getDisplayCryptoStringFromBigInt(bal, depositCurrency); + } + //* Still open to further optimize these checks //* It works but can be made better @action @@ -343,6 +483,14 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with } } + @computed + bool get decentralizedExchangesPromptDismissed => _settingsStore.decentralizedExchangesPromptDismissed; + + @action + void dismissDecentralizedExchangesPrompt() { + _settingsStore.decentralizedExchangesPromptDismissed = true; + } + @computed TransactionPriority get transactionPriority { final priority = _settingsStore.getPriority(wallet.type, chainId: wallet.chainId); @@ -362,7 +510,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with WalletType.bitcoinCash, WalletType.dogecoin, ].contains(wallet.type) && - depositCurrency == wallet.currency; + (depositCurrency == wallet.currency); bool get isMoneroWallet => wallet.type == WalletType.monero; @@ -372,9 +520,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with @observable ObservableList depositCurrencies; - final NumberFormat _cryptoNumberFormat; - - final SettingsStore _settingsStore; + final AppStore _appStore; + SettingsStore get _settingsStore => _appStore.settingsStore; final ContactListViewModel contactListViewModel; @@ -385,6 +532,31 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with @observable double bestRate = 0.0; + @computed + bool get useSatoshiDeposit => _appStore.amountParsingProxy.useSatoshi(depositCurrency); + + @computed + bool get useSatoshisReceive => _appStore.amountParsingProxy.useSatoshi(receiveCurrency); + + @computed + AmountParsingProxy get amountParsingProxy => _appStore.amountParsingProxy; + + @observable + ExchangeProvider? bestRateProvider; + + @observable + ExchangeProvider? forcedProvider; + + @action + void setForcedProvider(ExchangeProvider? provider) { + forcedProvider = provider; + forcedProviderRate = 0.0; + calculateForcedProviderRate(); + } + + @observable + double forcedProviderRate = 0.0; + late Timer bestRateSync; final FiatConversionStore fiatConversionStore; @@ -411,12 +583,12 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with String get receiveAmountFiatFormatted { var amount = '0.00'; try { - if (receiveAmount.isNotEmpty) { + if (_receiveAmount.isNotEmpty) { if (fiatConversionStore.prices[receiveCurrency] == null) return ''; amount = calculateFiatAmount( price: fiatConversionStore.prices[receiveCurrency]!, - cryptoAmount: receiveAmount, + cryptoAmount: _receiveAmount, ); } } catch (_) { @@ -425,46 +597,143 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with return isFiatDisabled ? '' : '$amount'; } + @computed + String get depositAmountFiatFormatted { + var amount = '0.00'; + try { + if (_depositAmount.isNotEmpty) { + if (fiatConversionStore.prices[depositCurrency] == null) return ''; + + amount = calculateFiatAmount( + price: fiatConversionStore.prices[depositCurrency]!, + cryptoAmount: _depositAmount, + ); + } + } catch (_) { + log('Error calculating deposit amount fiat formatted: $_'); + } + return isFiatDisabled ? '' : '$amount'; + } + + @computed + String get receiveAmountFiat { + var amount = ''; + try { + if (_receiveAmount.isNotEmpty) { + if (fiatConversionStore.prices[receiveCurrency] == null) return ''; + + amount = calculateFiatAmount( + price: fiatConversionStore.prices[receiveCurrency]!, + cryptoAmount: _receiveAmount, + raw: true); + } + } catch (_) { + log('Error calculating receive amount fiat formatted: $_'); + } + return amount; + } + + @computed + String get depositAmountFiat { + var amount = ''; + try { + if (_depositAmount.isNotEmpty) { + if (fiatConversionStore.prices[depositCurrency] == null) return ''; + + amount = calculateFiatAmount( + price: fiatConversionStore.prices[depositCurrency]!, + cryptoAmount: _depositAmount, + raw: true); + } + } catch (_) { + log('Error calculating deposit amount fiat formatted: $_'); + } + return amount; + } + + String roundedDepositAmount(int digits) { + if (depositAmount.split(".").last.length <= digits) { + return depositAmount; + } + try { + return double.parse(depositAmount).toStringAsPrecision(digits); + } catch(e) { + return "0"; + } + } + + String roundedReceiveAmount(int digits) { + if (receiveAmount.split(".").last.length <= digits) { + return receiveAmount; + } + try { + return double.parse(receiveAmount).toStringAsPrecision(digits); + } catch(e) { + return "0"; + } + } + + String roundedReceiveAmountFiat(int digits) { + if (receiveAmountFiat.split(".").last.length <= digits) { + return receiveAmountFiat; + } + return double.parse(receiveAmountFiat).toStringAsPrecision(digits); + } + + String roundedDepositAmountFiat(int digits) { + if (depositAmountFiat.split(".").last.length <= digits) { + return depositAmountFiat; + } + return double.parse(depositAmountFiat).toStringAsPrecision(digits); + } + @action void changeDepositCurrency({required CryptoCurrency currency}) { depositCurrency = currency; isFixedRateMode = false; _onPairChange(); isDepositAddressEnabled = !useSameWalletAddress(depositCurrency); + fetchFiatPrice(currency); + updateDepositAvailableAmount(); } @action void changeReceiveCurrency({required CryptoCurrency currency}) { + if (!(currency.tag == receiveCurrency.tag || + currency.tag == receiveCurrency.title || + currency.title == receiveCurrency.tag || + currency == receiveCurrency)) { + receiveAddress = ""; + } receiveCurrency = currency; isFixedRateMode = false; _onPairChange(); isDepositAddressEnabled = !useSameWalletAddress(depositCurrency); + fetchFiatPrice(currency); } @action Future changeReceiveAmount({required String amount}) async { - receiveAmount = amount; + amount = amountParsingProxy.getCanonicalCryptoAmount(amount, receiveCurrency); + + _receiveAmount = amount; if (amount.isEmpty) { - depositAmount = ''; - receiveAmount = ''; + _depositAmount = ''; + _receiveAmount = ''; return; } - final _enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0; + final _enteredAmount = double.tryParse(_receiveAmount.replaceAll(',', '.')) ?? 0; if (bestRate == 0) { - depositAmount = S.current.fetching; + _depositAmount = "..."; await calculateBestRate(); } - _cryptoNumberFormat.maximumFractionDigits = depositMaxDigits; - - depositAmount = _cryptoNumberFormat - .format(_enteredAmount / bestRate) - .toString() - .replaceAll(RegExp('\\,'), ''); + final amount_ = _enteredAmount / (forcedProvider == null ? bestRate : forcedProviderRate); + _depositAmount = amount_.toString().withMaxDecimals(depositCurrency.decimals); } @action @@ -474,19 +743,33 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with if (price == null || price == 0.0) return; final crypto = _enteredAmount / price; - final receiveAmountTmp = _cryptoNumberFormat.format(crypto); - if (receiveAmount != receiveAmountTmp) { - changeReceiveAmount(amount: receiveAmountTmp.withMaxDecimals(receiveCurrency.decimals)); + final _receiveAmountTmp = crypto.toString().withMaxDecimals(receiveCurrency.decimals); + if (_receiveAmount != _receiveAmountTmp) { + changeReceiveAmount(amount: _receiveAmountTmp); } } @action - Future changeDepositAmount({required String amount}) async { - depositAmount = amount; + void setDepositAmountFromFiat({required String fiatAmount}) { + final _enteredAmount = double.tryParse(fiatAmount.replaceAll(',', '.')) ?? 0.0; + final price = fiatConversionStore.prices[depositCurrency]; + if (price == null || price == 0.0) return; + + final crypto = _enteredAmount / price; + final depositAmountTmp = crypto.toString().withMaxDecimals(receiveCurrency.decimals); + if (_depositAmount != depositAmountTmp) { + changeDepositAmount(amount: depositAmountTmp, isCanonical: true); + } + } + + @action + Future changeDepositAmount({required String amount, bool isCanonical = false}) async { + _depositAmount = isCanonical ? amount.replaceAll(',', '.') : _appStore.amountParsingProxy + .getCanonicalCryptoAmount(amount.replaceAll(',', '.'), depositCurrency); if (amount.isEmpty) { - depositAmount = ''; - receiveAmount = ''; + _depositAmount = ''; + _receiveAmount = ''; return; } @@ -494,21 +777,17 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with /// as it should remain exactly what the user set if (isFixedRateMode) return; - final _enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0; + final _enteredAmount = double.tryParse(_depositAmount) ?? 0; /// in case the best rate was not calculated yet if (bestRate == 0) { - receiveAmount = S.current.fetching; + _receiveAmount = "..."; await calculateBestRate(); } - _cryptoNumberFormat.maximumFractionDigits = receiveMaxDigits; - - receiveAmount = _cryptoNumberFormat - .format(bestRate * _enteredAmount) - .toString() - .replaceAll(RegExp('\\,'), ''); + final amount_ = _enteredAmount * (forcedProvider == null ? bestRate : forcedProviderRate); + _receiveAmount = amount_.toString().withMaxDecimals(receiveCurrency.decimals); } bool checkIfInputMeetsMinOrMaxCondition(String input) { @@ -523,12 +802,30 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with return true; } + Future calculateForcedProviderRate() async { + if (forcedProvider == null || depositCurrency == receiveCurrency) { + forcedProviderRate = 0.0; + return; + } + + final amount = double.tryParse(isFixedRateMode ? _receiveAmount : _depositAmount) ?? + initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency); + + forcedProviderRate = await forcedProvider!.fetchRate( + from: depositCurrency, + to: receiveCurrency, + amount: amount, + isFixedRateMode: isFixedRateMode, + isReceiveAmount: isFixedRateMode); + } + Future calculateBestRate() async { if (depositCurrency == receiveCurrency) { bestRate = 0.0; + bestRateProvider = null; return; } - final amount = double.tryParse(isFixedRateMode ? receiveAmount : depositAmount) ?? + final amount = double.tryParse(isFixedRateMode ? _receiveAmount : _depositAmount) ?? initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency); final validProvidersForAmount = _tradeAvailableProviders.where((provider) { @@ -581,7 +878,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with _sortedAvailableProviders.clear(); _sortedAvailableProviders.addAll(newSortedProviders); - if (_sortedAvailableProviders.isNotEmpty) bestRate = _sortedAvailableProviders.keys.first; + if (_sortedAvailableProviders.isNotEmpty) { + bestRate = _sortedAvailableProviders.keys.first; + bestRateProvider = _sortedAvailableProviders.values.first; + } } @action @@ -654,7 +954,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with Future createTrade() async { if (isSendAllEnabled) { await calculateDepositAllAmount(); - final amount = double.tryParse(depositAmount); + final amount = double.tryParse(_depositAmount); if (limits.min != null && amount != null && amount < limits.min!) { tradeState = TradeIsCreatedFailure( @@ -664,11 +964,34 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with } } + if (depositCurrency == CryptoCurrency.btcln && + depositAddress == wallet.walletAddresses.addressForExchange) { + final invoice = await bitcoin!.getLightningInvoice(wallet, BigInt.zero); + if (invoice != null) { + depositAddress = invoice; + } + } + + if (receiveCurrency == CryptoCurrency.btcln && + receiveAddress == wallet.walletAddresses.addressForExchange) { + final invoice = await bitcoin!.getLightningInvoice(wallet, BigInt.zero); + if (invoice != null) { + receiveAddress = invoice; + } + } + + late final Map providers; + if (forcedProvider != null) { + providers = {forcedProviderRate: forcedProvider!}; + } else { + providers = _sortedAvailableProviders; + } + // Ensure we have providers available before attempting to create trade - if (_sortedAvailableProviders.isEmpty) { + if (providers.isEmpty) { await calculateBestRate(); - if (_sortedAvailableProviders.isEmpty) { + if (providers.isEmpty) { tradeState = TradeIsCreatedFailure( title: S.current.trade_not_created, error: S.current.none_of_selected_providers_can_exchange); @@ -678,8 +1001,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with try { // snapshot of providers to avoid concurrent modification issues - final providersSnapshot = _sortedAvailableProviders.values.toList(); - final ratesSnapshot = _sortedAvailableProviders.keys.toList(); + final providersSnapshot = providers.values.toList(); + final ratesSnapshot = providers.keys.toList(); for (var i = 0; i < providersSnapshot.length; i++) { final provider = providersSnapshot[i]; @@ -688,13 +1011,15 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with if (!(await provider.checkIsAvailable())) continue; bestRate = providerRate; - await changeDepositAmount(amount: depositAmount); + bestRateProvider = provider; + + await changeDepositAmount(amount: _depositAmount, isCanonical: true); final request = TradeRequest( fromCurrency: depositCurrency, toCurrency: receiveCurrency, - fromAmount: depositAmount.replaceAll(',', '.'), - toAmount: receiveAmount.replaceAll(',', '.'), + fromAmount: _depositAmount.replaceAll(',', '.'), + toAmount: _receiveAmount.replaceAll(',', '.'), refundAddress: depositAddress, toAddress: receiveAddress, isFixedRate: isFixedRateMode, @@ -705,7 +1030,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with await wallet.walletAddresses.saveAddressesInBox(); } - var amount = isFixedRateMode ? receiveAmount : depositAmount; + var amount = isFixedRateMode ? _receiveAmount : _depositAmount; amount = amount.replaceAll(',', '.'); if (limitsState is LimitsLoadedSuccessfully) { @@ -729,11 +1054,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with final canCreateTrade = await isCanCreateTrade(trade); if (!canCreateTrade.result) { - tradeState = TradeIsCreatedFailure( - title: S.current.trade_not_created, - error: canCreateTrade.errorMessage ?? '', - ); - return; + continue; } tradesStore.setTrade(trade); @@ -767,8 +1088,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with void reset() { _initialPairBasedOnWallet(); isReceiveAmountEntered = false; - depositAmount = ''; - receiveAmount = ''; + _depositAmount = ''; + _receiveAmount = ''; depositAddress = depositCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : ''; receiveAddress = @@ -801,21 +1122,40 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with ].contains(wallet.type)) { final priority = _settingsStore.getPriority(wallet.type)!; - final amount = await bitcoin!.estimateFakeSendAllTxAmount( - wallet, - priority, - coinTypeToSpendFrom: - wallet.type == WalletType.litecoin ? UnspentCoinType.nonMweb : UnspentCoinType.any, - ); + final amount = depositCurrency == CryptoCurrency.btcln + // FIXME amount estimation is broken/impossible for ln, konsti suggested this + ? (amountParsingProxy + .parseCryptoString(depositAvailableAmount, depositCurrency).toInt() - 10) + : await bitcoin!.estimateFakeSendAllTxAmount( + wallet, + priority, + coinTypeToSpendFrom: wallet.type == WalletType.litecoin + ? UnspentCoinType.nonMweb + : depositCurrency == CryptoCurrency.btcln + ? UnspentCoinType.lightning + : UnspentCoinType.any, + ); - changeDepositAmount(amount: wallet.formatCryptoAmount(amount.toString())); + changeDepositAmount(amount: wallet.currency.formatAmount(BigInt.from(amount)), isCanonical: true); } else if (wallet.type == WalletType.monero) { final amount = await unspentCoinsListViewModel.getSendingBalance(UnspentCoinType.any); - changeDepositAmount(amount: wallet.formatCryptoAmount(amount.toString())); + changeDepositAmount(amount: wallet.currency.formatAmount(BigInt.from(amount)), isCanonical: true); } } + @observable + String depositAvailableAmount = ""; + + @action + void reverseSwapDirection() { + final tmpAmount = _receiveAmount; + final tmpCurrency = depositCurrency; + changeDepositCurrency(currency: receiveCurrency); + changeReceiveCurrency(currency: tmpCurrency); + _depositAmount = tmpAmount; + } + void updateTemplate() => _exchangeTemplateStore.update(); void addTemplate( @@ -841,9 +1181,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with _exchangeTemplateStore.remove(template: template); void _onPairChange() { - depositAmount = ''; - receiveAmount = ''; + _depositAmount = ''; + _receiveAmount = ''; bestRate = 0.0; + bestRateProvider = null; loadLimits(); _setAvailableProviders(); } @@ -953,7 +1294,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with @action void addExchangeProvider(ExchangeProvider provider) { - selectedProviders.add(provider); + if (provider.description.isCentralized && forceDecentralizedExchanges) return; + if (!selectedProviders.contains(provider)) selectedProviders.add(provider); if (providerList.contains(provider)) _tradeAvailableProviders.add(provider); } @@ -961,14 +1303,18 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with void removeExchangeProvider(ExchangeProvider provider) { selectedProviders.remove(provider); _tradeAvailableProviders.remove(provider); + if (forcedProvider == provider) { + setForcedProvider(null); + } } @action void saveSelectedProviders() { - depositAmount = ''; - receiveAmount = ''; + _depositAmount = ''; + _receiveAmount = ''; isFixedRateMode = false; _defineIsReceiveAmountEditable(); + bestRateProvider = null; bestRate = 0.0; loadLimits(); @@ -1002,6 +1348,29 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with .any((element) => element.isAvailable && providerList.contains(element)); } + @computed + bool get forceDecentralizedExchanges => _settingsStore.forceDecentralizedExchanges; + + @action + void toggleForceDecentralizedExchanges() { + _settingsStore.forceDecentralizedExchanges = !_settingsStore.forceDecentralizedExchanges; + if(forceDecentralizedExchanges) { + final providers = selectedProviders.toList(); + for (final provider in providers) { + if (forceDecentralizedExchanges && provider.description.isCentralized) { + removeExchangeProvider(provider); + } + } + } else { + for(final provider in providerList) { + if(!selectedProviders.contains(provider) && provider.description.isCentralized) { + addExchangeProvider(provider); + } + } + } + + } + void _setAvailableProviders() { _tradeAvailableProviders.clear(); @@ -1021,6 +1390,53 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with int get receiveMaxDigits => receiveCurrency.decimals; Future isCanCreateTrade(Trade trade) async { + + if (trade.provider == ExchangeProviderDescription.swapsXyz) { + + final tradeFrom = trade.fromRaw >= 0 ? trade.from : trade.userCurrencyFrom; + + if (tradeFrom == null) { + return CreateTradeResult( + result: false, + errorMessage: 'From currency is null', + ); + } + + final isNativeSupportedToken = walletTypes.contains(cryptoCurrencyOrTokenToWalletType(tradeFrom)); + + if (!isNativeSupportedToken) { + bool _isEthToken() => + wallet.currency == CryptoCurrency.eth && tradeFrom.tag == CryptoCurrency.eth.title; + + bool _isPolygonToken() => + wallet.currency == CryptoCurrency.maticpoly && + tradeFrom.tag == CryptoCurrency.maticpoly.tag; + + bool _isBaseToken() => + wallet.currency == CryptoCurrency.baseEth && tradeFrom.tag == CryptoCurrency.baseEth.tag; + + bool _isTronToken() => + wallet.currency == CryptoCurrency.trx && tradeFrom.tag == CryptoCurrency.trx.title; + + bool _isSplToken() => + wallet.currency == CryptoCurrency.sol && tradeFrom.tag == CryptoCurrency.sol.title; + + bool isArbitrumToken() => + wallet.currency == CryptoCurrency.arbEth && tradeFrom.tag == CryptoCurrency.arbEth.tag; + + bool isBscToken() => + wallet.currency == CryptoCurrency.bnb && tradeFrom.tag == CryptoCurrency.bnb.tag; + + if(!(_isEthToken() || _isPolygonToken() || _isBaseToken() || _isTronToken() || _isSplToken() || isArbitrumToken() || isBscToken())) { + return CreateTradeResult( + result: false, + errorMessage: 'This token isn’t supported on the current wallet/network for Swaps.xyz. Switch to a supported wallet or asset', + ); + } + } + + } + if (trade.provider == ExchangeProviderDescription.thorChain) { final payoutAddress = trade.payoutAddress ?? ''; final fromWalletAddress = trade.fromWalletAddress ?? ''; diff --git a/lib/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart b/lib/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart index 8eb034029..6481efb25 100644 --- a/lib/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart +++ b/lib/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart @@ -1,6 +1,11 @@ +import 'dart:math'; + import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cw_core/balance_card_style_settings.dart'; +import 'package:cw_core/card_design.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/monero/monero.dart'; @@ -33,7 +38,30 @@ abstract class MoneroAccountEditOrCreateViewModelBase with Store { final AccountListItem? _accountListItem; final WalletBase _wallet; + Future> _getUsableCardGradients() async { + final List ret = List.from(CardDesign.allGradients); + final designs = (await BalanceCardStyleSettings.getAll(_wallet.walletInfo.internalId)) + .map((item) => CardDesign.fromStyleSettings(item, _wallet.currency)); + for (final design in designs) { + ret.remove(design.gradient); + } + return ret.isNotEmpty ? ret : CardDesign.allGradients; + } + + Future _saveRandomCardDesign() async { + final gradients = await _getUsableCardGradients(); + + await BalanceCardStyleSettings.fromCardDesign( + _wallet.walletInfo.internalId, + _moneroAccountList.accounts.length, + _moneroAccountList.accounts.length, + CardDesign.specialDesignsForCurrencies[_wallet.currency]! + .withGradient(gradients[Random().nextInt(gradients.length)])) + .insert(); + } + Future save() async { + await _saveRandomCardDesign(); if (_wallet.type == WalletType.monero) { await saveMonero(); } diff --git a/lib/view_model/monero_account_list/monero_account_list_view_model.dart b/lib/view_model/monero_account_list/monero_account_list_view_model.dart index 5671a7fe0..9bfca0008 100644 --- a/lib/view_model/monero_account_list/monero_account_list_view_model.dart +++ b/lib/view_model/monero_account_list/monero_account_list_view_model.dart @@ -56,6 +56,12 @@ abstract class MoneroAccountListViewModelBase with Store { throw Exception('Unexpected wallet type: ${_wallet.type} for monero'); } + @computed + AccountListItem get selected { + final currentId = monero!.getCurrentAccount(_wallet).id; + return accounts.firstWhere((item) => item.id == currentId); + } + final WalletBase _wallet; void select(AccountListItem item) { diff --git a/lib/view_model/node_list/node_create_or_edit_view_model.dart b/lib/view_model/node_list/node_create_or_edit_view_model.dart index 18ec024f5..a238b0d14 100644 --- a/lib/view_model/node_list/node_create_or_edit_view_model.dart +++ b/lib/view_model/node_list/node_create_or_edit_view_model.dart @@ -1,38 +1,79 @@ import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart'; import 'package:cake_wallet/entities/qr_scanner.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/utils/permission_handler.dart'; +import 'package:collection/collection.dart'; +import 'package:cw_core/node.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/cupertino.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; -import 'package:cw_core/node.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:collection/collection.dart'; -import 'package:cake_wallet/utils/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart'; part 'node_create_or_edit_view_model.g.dart'; -class NodeCreateOrEditViewModel = NodeCreateOrEditViewModelBase with _$NodeCreateOrEditViewModel; +class NodeCreateOrEditViewModel = NodeCreateOrEditViewModelBase + with _$NodeCreateOrEditViewModel; abstract class NodeCreateOrEditViewModelBase with Store { - NodeCreateOrEditViewModelBase(this._nodeSource, this._walletType, this._settingsStore) + NodeCreateOrEditViewModelBase( + this._nodeSource, this.walletType, this.editingNode, this._settingsStore) : state = InitialExecutionState(), connectionState = InitialExecutionState(), - useSSL = false, - address = '', - path = '', - port = '', - login = '', - password = '', - trusted = false, - isEnabledForAutoSwitching = false, - useSocksProxy = false, - socksProxyAddress = ''; + label = editingNode?.label ?? '', + address = editingNode?.uri.host.toString() ?? '', + path = editingNode?.path.toString() ?? '', + port = (editingNode != null && editingNode.uri.hasPort) + ? editingNode.uri.port.toString() + : '', + login = editingNode?.login ?? '', + password = editingNode?.password ?? '', + socksProxyAddress = editingNode?.socksProxyAddress ?? '', + trusted = editingNode?.trusted ?? false, + isEnabledForAutoSwitching = + editingNode?.isEnabledForAutoSwitching ?? false, + useSocksProxy = editingNode?.socksProxyAddress != null && + editingNode!.socksProxyAddress!.isNotEmpty, + useSSL = editingNode?.useSSL ?? false { + } + + final nodeLabelUIKey = 'node_label_row_key'; + final nodeAddressUIKey = 'node_address_row_key'; + final nodePathUIKey = 'node_path_row_key'; + final nodeUsernameUIKey = 'node_username_row_key'; + final nodePasswordUIKey = 'node_password_row_key'; + final nodePortUIKey = 'node_port_row_key'; + final useSSLUIKey = 'node_use_ssl_row_key'; + final nodeTrustedUIKey = 'node_trusted_daemon_row_key'; + final nodeEmbeddedTorProxyUIKey = 'node_embedded_tor_proxy_row_key'; + final autoSwitchingUIKey = 'node_auto_switching_row_key'; + final useSocksProxyUIKey = 'node_use_socks_proxy_row_key'; + final socksProxyAddressUIKey = 'node_socks_proxy_address_row_key'; + + late final textFieldKeys = [ + nodeLabelUIKey, + nodeAddressUIKey, + nodePathUIKey, + nodePortUIKey, + nodeUsernameUIKey, + nodePasswordUIKey, + socksProxyAddressUIKey, + ]; + + Map> nodeFormItems = {}; + + //TODO: Remove example values when done testing + bool toggleExampleValue = false; + bool checkboxExampleValue = false; @observable ExecutionState state; + @observable + String label; + @observable String address; @@ -71,13 +112,16 @@ abstract class NodeCreateOrEditViewModelBase with Store { @computed bool get isReady => - (address.isNotEmpty) || _walletType == WalletType.decred; // Allow an empty address. + (address.isNotEmpty) || + walletType == WalletType.decred; // Allow an empty address. bool get hasAuthCredentials => - _walletType == WalletType.monero || _walletType == WalletType.wownero || _walletType == WalletType.haven; + walletType == WalletType.monero || + walletType == WalletType.wownero || + walletType == WalletType.haven; bool get hasPathSupport { - switch (_walletType) { + switch (walletType) { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: @@ -113,10 +157,48 @@ abstract class NodeCreateOrEditViewModelBase with Store { return uri; } - final WalletType _walletType; + final WalletType walletType; + final Node? editingNode; final Box _nodeSource; final SettingsStore _settingsStore; + void updateViewModelFromText(String key, String value) { + if (key == nodeLabelUIKey) setLabel(value); + if (key == nodeAddressUIKey) setAddress(value); + if (key == nodePortUIKey) setPort(value); + if (key == nodePathUIKey) setPath(value); + if (key == nodeUsernameUIKey) setLogin(value); + if (key == nodePasswordUIKey) setPassword(value); + if (key == socksProxyAddressUIKey) setSocksProxyAddress(value); + } + + void updateCheckboxValue(String key, bool value) { + if (key == useSSLUIKey) setSSL(value); + if (key == nodeTrustedUIKey) setTrusted(value); + if (key == useSocksProxyUIKey) setSocksProxy(value); + if (key == autoSwitchingUIKey) setIsEnabledForAutoSwitching(value); + } + + bool getCheckboxValue(String key) { + if (key == useSSLUIKey) return useSSL; + if (key == nodeTrustedUIKey) return trusted; + if (key == nodeEmbeddedTorProxyUIKey) return usesEmbeddedProxy; + if (key == useSocksProxyUIKey) return useSocksProxy; + if (key == autoSwitchingUIKey) return isEnabledForAutoSwitching; + return false; + } + + String getTextValue(String key) { + if (key == nodeLabelUIKey) return label; + if (key == nodeAddressUIKey) return address; + if (key == nodePortUIKey) return port; + if (key == nodePathUIKey) return path; + if (key == nodeUsernameUIKey) return login; + if (key == nodePasswordUIKey) return password; + if (key == socksProxyAddressUIKey) return socksProxyAddress; + return ''; + } + @action void reset() { address = ''; @@ -131,6 +213,9 @@ abstract class NodeCreateOrEditViewModelBase with Store { socksProxyAddress = ''; } + @action + void setLabel(String val) => label = val; + @action void setPort(String val) => port = val; @@ -153,7 +238,8 @@ abstract class NodeCreateOrEditViewModelBase with Store { void setTrusted(bool val) => trusted = val; @action - void setIsEnabledForAutoSwitching(bool val) => isEnabledForAutoSwitching = val; + void setIsEnabledForAutoSwitching(bool val) => + isEnabledForAutoSwitching = val; @action void setSocksProxy(bool val) => useSocksProxy = val; @@ -164,9 +250,10 @@ abstract class NodeCreateOrEditViewModelBase with Store { @action Future save({Node? editingNode, bool saveAsCurrent = false}) async { final node = Node( + label: label, uri: uri, path: path, - type: _walletType, + type: walletType, login: login, password: password, useSSL: useSSL, @@ -198,7 +285,7 @@ abstract class NodeCreateOrEditViewModelBase with Store { final node = Node( uri: uri, path: path, - type: _walletType, + type: walletType, login: login, password: password, useSSL: useSSL, @@ -225,7 +312,7 @@ abstract class NodeCreateOrEditViewModelBase with Store { } @action - void setAsCurrent(Node node) => _settingsStore.nodes[_walletType] = node; + void setAsCurrent(Node node) => _settingsStore.nodes[walletType] = node; @action Future scanQRCodeForNewNode(BuildContext context) async { @@ -240,7 +327,8 @@ abstract class NodeCreateOrEditViewModelBase with Store { throw Exception('Unexpected scan QR code value: value is empty'); } - if (code.startsWith("monero_node:")) code = code.replaceFirst("monero_node:", "tcp://"); + if (code.startsWith("monero_node:")) + code = code.replaceFirst("monero_node:", "tcp://"); if (!code.contains('://')) code = 'tcp://$code'; final uri = Uri.tryParse(code); diff --git a/lib/view_model/payment/payment_view_model.dart b/lib/view_model/payment/payment_view_model.dart index 394c7f82f..13fca7ee3 100644 --- a/lib/view_model/payment/payment_view_model.dart +++ b/lib/view_model/payment/payment_view_model.dart @@ -76,6 +76,12 @@ abstract class PaymentViewModelBase with Store { final currentWallet = appStore.wallet; + if (currentWallet != null && + currentWallet.type == detectedWalletType && + !isEVMCompatibleChain(detectedWalletType!)) { + return PaymentFlowResult.currentWalletCompatible(); + } + if (detectedWalletType == WalletType.solana) { final compatibleWallets = await getWalletsByType(WalletType.solana); return PaymentFlowResult.solanaTokenSelection( @@ -98,10 +104,6 @@ abstract class PaymentViewModelBase with Store { return PaymentFlowResult.evmNetworkSelection(detectionResult); } - if (currentWallet != null && currentWallet.type == detectedWalletType) { - return PaymentFlowResult.currentWalletCompatible(); - } - final compatibleWallets = await getWalletsByType(detectedWalletType!); switch (compatibleWallets.length) { diff --git a/lib/view_model/send/fees_view_model.dart b/lib/view_model/send/fees_view_model.dart index c9f0059cd..c9fb0b5d5 100644 --- a/lib/view_model/send/fees_view_model.dart +++ b/lib/view_model/send/fees_view_model.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; +import 'package:cake_wallet/core/amount_parsing_proxy.dart'; import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/dogecoin/dogecoin.dart'; import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; @@ -25,7 +26,7 @@ abstract class FeesViewModelBase extends WalletChangeListenerViewModel with Stor FeesViewModelBase( AppStore appStore, this.balanceViewModel, - ) : _settingsStore = appStore.settingsStore, + ) : _appStore = appStore, super(appStore: appStore) { final priority = _settingsStore.getPriority(wallet.type, chainId: wallet.chainId); @@ -39,6 +40,10 @@ abstract class FeesViewModelBase extends WalletChangeListenerViewModel with Stor } } + final AppStore _appStore; + SettingsStore get _settingsStore => _appStore.settingsStore; + AmountParsingProxy get amountParsingProxy => _appStore.amountParsingProxy; + @computed WalletType get walletType => wallet.type; CryptoCurrency get currency => wallet.currency; @@ -138,8 +143,6 @@ abstract class FeesViewModelBase extends WalletChangeListenerViewModel with Stor @computed FiatCurrency get fiatCurrency => _settingsStore.fiatCurrency; - final SettingsStore _settingsStore; - @action void setTransactionPriority(TransactionPriority priority) => _settingsStore.setPriority(wallet.type, priority, chainId: wallet.chainId); diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index 715c36e7e..80640f993 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -1,4 +1,4 @@ -import 'dart:math'; +import 'dart:math' show min; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/di.dart'; @@ -13,6 +13,7 @@ import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart'; +import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/tron/tron.dart'; @@ -20,7 +21,9 @@ import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; import 'package:cake_wallet/zcash/zcash.dart'; import 'package:cw_core/balance.dart'; +import 'package:cw_core/crypto_amount_format.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/currency_for_wallet_type.dart'; import 'package:cw_core/format_fixed.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/transaction_info.dart'; @@ -37,8 +40,7 @@ const String cryptoNumberPattern = '0.0'; class Output = OutputBase with _$Output; abstract class OutputBase with Store { - OutputBase( - this._wallet, this._settingsStore, this._fiatConversationStore, this.cryptoCurrencyHandler) + OutputBase(this._wallet, this._appStore, this._fiatConversationStore, this.cryptoCurrencyHandler) : key = UniqueKey(), sendAll = false, cryptoAmount = '', @@ -46,6 +48,7 @@ abstract class OutputBase with Store { fiatAmount = '', address = '', note = '', + memo = "", extractedAddress = '', estimatedFee = '0.0', parsedAddress = ParsedAddress(addresses: []) { @@ -59,12 +62,21 @@ abstract class OutputBase with Store { Key key; + bool get useSatoshi => _appStore.amountParsingProxy.useSatoshi(cryptoCurrencyHandler()); + @observable String fiatAmount; @observable String cryptoAmount; + @observable + String? displayName; + + @computed + String get displayCryptoAmount => + _appStore.amountParsingProxy.getDisplayCryptoAmount(cryptoAmount, cryptoCurrencyHandler()); + @observable String cryptoFullBalance; @@ -73,6 +85,9 @@ abstract class OutputBase with Store { @observable String note; + + @observable + String memo; @observable bool sendAll; @@ -83,12 +98,20 @@ abstract class OutputBase with Store { @observable String extractedAddress; - String? memo; @computed bool get isParsedAddress => parsedAddress.parseFrom != ParseFrom.notParsed && parsedAddress.name.isNotEmpty; + String roundedCryptoAmount(int digits) => displayCryptoAmount.withMaxDecimals(digits); + + String roundedFiatAmount(int digits) { + if (fiatAmount.split(".").last.length <= digits) { + return fiatAmount; + } + return double.parse(fiatAmount.replaceAll(",", "")).toStringAsPrecision(digits); + } + @observable String? stealthAddress; @@ -108,7 +131,7 @@ abstract class OutputBase with Store { case WalletType.litecoin: case WalletType.bitcoinCash: case WalletType.dogecoin: - _amount = bitcoin!.formatterStringDoubleToBitcoinAmount(_cryptoAmount); + _amount = cryptoCurrencyHandler().parseAmount(_cryptoAmount).toInt(); break; case WalletType.decred: _amount = decred!.formatterStringDoubleToDecredAmount(_cryptoAmount); @@ -171,37 +194,36 @@ abstract class OutputBase with Store { switch (_wallet.type) { case WalletType.monero: - estimatedFee = monero!.formatterMoneroAmountToDouble(amount: fee).toString(); + case WalletType.wownero: + case WalletType.litecoin: + case WalletType.bitcoinCash: + case WalletType.dogecoin: + case WalletType.decred: + estimatedFee = walletTypeToCryptoCurrency(_wallet.type).formatAmount(BigInt.from(fee)); break; case WalletType.bitcoin: + if (cryptoCurrencyHandler() == CryptoCurrency.btcln) { + estimatedFee = + _appStore.amountParsingProxy.getDisplayCryptoString(10, cryptoCurrencyHandler()); + break; + } if (_settingsStore.getPriority(_wallet.type) == bitcoin!.getBitcoinTransactionPriorityCustom()) { fee = bitcoin!.getEstimatedFeeWithFeeRate( _wallet, _settingsStore.customBitcoinFeeRate, formattedCryptoAmount); } - estimatedFee = bitcoin!.formatterBitcoinAmountToDouble(amount: fee).toString(); - break; - case WalletType.litecoin: - case WalletType.bitcoinCash: - case WalletType.dogecoin: - estimatedFee = bitcoin!.formatterBitcoinAmountToDouble(amount: fee).toString(); + estimatedFee = _appStore.amountParsingProxy.getDisplayCryptoString(fee, cryptoCurrencyHandler()); break; case WalletType.solana: estimatedFee = solana!.getEstimateFees(_wallet).toString(); break; - case WalletType.wownero: - estimatedFee = wownero!.formatterWowneroAmountToDouble(amount: fee).toString(); - break; case WalletType.zano: estimatedFee = zano! .formatterIntAmountToDouble( amount: fee, currency: cryptoCurrencyHandler(), forFee: true) .toString(); break; - case WalletType.decred: - estimatedFee = decred!.formatterDecredAmountToDouble(amount: fee).toString(); - break; case WalletType.tron: if (cryptoCurrencyHandler() == CryptoCurrency.trx) { estimatedFee = tron!.getTronNativeEstimatedFee(_wallet).toString(); @@ -209,7 +231,7 @@ abstract class OutputBase with Store { estimatedFee = tron!.getTronTRC20EstimatedFee(_wallet).toString(); } break; - + case WalletType.zcash: estimatedFee = zcash!.formatterZcashAmountToDouble(amount: BigInt.from(fee)).toString(); break; @@ -253,29 +275,35 @@ abstract class OutputBase with Store { try { final currency = (isEVMCompatibleChain(_wallet.type) || - _wallet.type == WalletType.solana || - _wallet.type == WalletType.tron) + [WalletType.solana, WalletType.tron].contains(_wallet.type)) ? _wallet.currency : cryptoCurrencyHandler(); - final fiat = calculateFiatAmountRaw( - price: _fiatConversationStore.prices[currency]!, - cryptoAmount: double.parse(estimatedFee)); - return fiat; + + final cryptoAmount = + double.parse(_appStore.amountParsingProxy.getCanonicalCryptoAmount(estimatedFee, currency)); + + return calculateFiatAmountRaw( + price: _fiatConversationStore.prices[currency]!, cryptoAmount: cryptoAmount); } catch (_) { return '0.00'; } } - WalletType get walletType => _wallet.type; - final CryptoCurrency Function() cryptoCurrencyHandler; @observable WalletBase, TransactionInfo> _wallet; - final SettingsStore _settingsStore; + + WalletType get walletType => _wallet.type; + + final CryptoCurrency Function() cryptoCurrencyHandler; final FiatConversionStore _fiatConversationStore; + final AppStore _appStore; + + SettingsStore get _settingsStore => _appStore.settingsStore; @action void setSendAll(String fullBalance) { - cryptoFullBalance = fullBalance; + cryptoFullBalance = + _appStore.amountParsingProxy.getCanonicalCryptoAmount(fullBalance, cryptoCurrencyHandler()); sendAll = true; _updateFiatAmount(); } @@ -293,20 +321,20 @@ abstract class OutputBase with Store { fiatAmount = ''; address = ''; note = ''; - memo = null; + memo = ""; resetParsedAddress(); } void resetParsedAddress() { + displayName = null; extractedAddress = ''; parsedAddress = ParsedAddress(addresses: []); } @action + /// [setCryptoAmount] always takes in the canonical representation eg. Bitcoin and not Sats void setCryptoAmount(String amount) { - if (amount.toUpperCase() != S.current.all) { - sendAll = false; - } + if (amount.toUpperCase() != S.current.all) sendAll = false; cryptoAmount = amount; _updateFiatAmount(); @@ -321,10 +349,15 @@ abstract class OutputBase with Store { @action void _updateFiatAmount() { try { + var cryptoAmount_ = + sendAll ? cryptoFullBalance.replaceAll(",", ".") : cryptoAmount.replaceAll(',', '.'); + + var cryptoCurrency = cryptoCurrencyHandler() == CryptoCurrency.btcln + ? CryptoCurrency.btc + : cryptoCurrencyHandler(); + final fiat = calculateFiatAmount( - price: _fiatConversationStore.prices[cryptoCurrencyHandler()]!, - cryptoAmount: - sendAll ? cryptoFullBalance.replaceAll(",", ".") : cryptoAmount.replaceAll(',', '.')); + price: _fiatConversationStore.prices[cryptoCurrency]!, cryptoAmount: cryptoAmount_); if (fiatAmount != fiat) { fiatAmount = fiat; } @@ -336,15 +369,18 @@ abstract class OutputBase with Store { @action void _updateCryptoAmount() { try { + var cryptoCurrency = cryptoCurrencyHandler() == CryptoCurrency.btcln + ? CryptoCurrency.btc + : cryptoCurrencyHandler(); + final decimals = min(20, cryptoCurrencyHandler().decimals); final crypto = (double.parse(fiatAmount.replaceAll(',', '.')) / - _fiatConversationStore.prices[cryptoCurrencyHandler()]!) + _fiatConversationStore.prices[cryptoCurrency]!) .toStringAsFixed(decimals); - if (cryptoAmount != crypto) { - cryptoAmount = crypto; - } + if (cryptoAmount != crypto) cryptoAmount = crypto; } catch (e) { + printV(e); cryptoAmount = ''; } } @@ -362,6 +398,9 @@ abstract class OutputBase with Store { final domain = address; final currency = cryptoCurrencyHandler(); parsedAddress = await getIt.get().resolve(context, domain, currency); + if(parsedAddress.name.isNotEmpty) { + displayName = parsedAddress.name; + } extractedAddress = await extractAddressFromParsed(context, parsedAddress); note = parsedAddress.description; } @@ -381,7 +420,7 @@ extension OutputCopyWith on Output { }) { final clone = Output( _wallet, - _settingsStore, + _appStore, _fiatConversationStore, cryptoCurrencyHandler, ); diff --git a/lib/view_model/send/send_template_view_model.dart b/lib/view_model/send/send_template_view_model.dart index f1b8a8546..02c7ae216 100644 --- a/lib/view_model/send/send_template_view_model.dart +++ b/lib/view_model/send/send_template_view_model.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/evm/evm.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; +import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/view_model/send/template_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; @@ -12,7 +13,6 @@ import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/core/amount_validator.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; -import 'package:cake_wallet/store/settings_store.dart'; part 'send_template_view_model.g.dart'; @@ -20,12 +20,12 @@ class SendTemplateViewModel = SendTemplateViewModelBase with _$SendTemplateViewM abstract class SendTemplateViewModelBase with Store { final WalletBase _wallet; - final SettingsStore _settingsStore; + final AppStore _appStore; final SendTemplateStore _sendTemplateStore; final FiatConversionStore _fiatConversationStore; SendTemplateViewModelBase( - this._wallet, this._settingsStore, this._sendTemplateStore, this._fiatConversationStore) + this._wallet, this._appStore, this._sendTemplateStore, this._fiatConversationStore) : recipients = ObservableList() { addRecipient(); } @@ -36,7 +36,7 @@ abstract class SendTemplateViewModelBase with Store { void addRecipient() { recipients.add(TemplateViewModel( wallet: _wallet, - settingsStore: _settingsStore, + appStore: _appStore, fiatConversationStore: _fiatConversationStore)); } @@ -46,10 +46,8 @@ abstract class SendTemplateViewModelBase with Store { } AmountValidator get amountValidator => AmountValidator( - currency: walletTypeToCryptoCurrency( - _wallet.type, - chainId: _wallet.chainId, - ), + currency: walletTypeToCryptoCurrency(_wallet.type, chainId: _wallet.chainId), + amountParsingProxy: _appStore.amountParsingProxy, ); AddressValidator get addressValidator => @@ -67,10 +65,10 @@ abstract class SendTemplateViewModelBase with Store { CryptoCurrency get cryptoCurrency => _wallet.currency; @computed - String get fiatCurrency => _settingsStore.fiatCurrency.title; + String get fiatCurrency => _appStore.settingsStore.fiatCurrency.title; @computed - int get fiatCurrencyDecimals => _settingsStore.fiatCurrency.decimals; + int get fiatCurrencyDecimals => _appStore.settingsStore.fiatCurrency.decimals; @computed ObservableList