Merge branch 'main' into zano-pr
14
.github/workflows/cache_dependencies.yml
vendored
|
@ -62,10 +62,22 @@ jobs:
|
||||||
/opt/android/cake_wallet/cw_haven/android/.cxx
|
/opt/android/cake_wallet/cw_haven/android/.cxx
|
||||||
/opt/android/cake_wallet/scripts/monero_c/release
|
/opt/android/cake_wallet/scripts/monero_c/release
|
||||||
key: ${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh' ,'**/cache_dependencies.yml') }}
|
key: ${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh' ,'**/cache_dependencies.yml') }}
|
||||||
|
|
||||||
- if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }}
|
- if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }}
|
||||||
name: Generate Externals
|
name: Generate Externals
|
||||||
run: |
|
run: |
|
||||||
cd /opt/android/cake_wallet/scripts/android/
|
cd /opt/android/cake_wallet/scripts/android/
|
||||||
source ./app_env.sh cakewallet
|
source ./app_env.sh cakewallet
|
||||||
./build_monero_all.sh
|
./build_monero_all.sh
|
||||||
|
|
||||||
|
- name: Cache Keystore
|
||||||
|
id: cache-keystore
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: /opt/android/cake_wallet/android/app/key.jks
|
||||||
|
key: $STORE_PASS
|
||||||
|
|
||||||
|
- if: ${{ steps.cache-keystore.outputs.cache-hit != 'true' }}
|
||||||
|
name: Generate KeyStore
|
||||||
|
run: |
|
||||||
|
cd /opt/android/cake_wallet/android/app
|
||||||
|
keytool -genkey -v -keystore key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias testKey -noprompt -dname "CN=CakeWallet, OU=CakeWallet, O=CakeWallet, L=Florida, S=America, C=USA" -storepass $STORE_PASS -keypass $KEY_PASS
|
||||||
|
|
48
.github/workflows/pr_test_build_android.yml
vendored
|
@ -116,6 +116,14 @@ jobs:
|
||||||
cd /opt/android/cake_wallet/scripts/android/
|
cd /opt/android/cake_wallet/scripts/android/
|
||||||
./build_mwebd.sh --dont-install
|
./build_mwebd.sh --dont-install
|
||||||
|
|
||||||
|
# - name: Cache Keystore
|
||||||
|
# id: cache-keystore
|
||||||
|
# uses: actions/cache@v3
|
||||||
|
# with:
|
||||||
|
# path: /opt/android/cake_wallet/android/app/key.jks
|
||||||
|
# key: $STORE_PASS
|
||||||
|
#
|
||||||
|
# - if: ${{ steps.cache-keystore.outputs.cache-hit != 'true' }}
|
||||||
- name: Generate KeyStore
|
- name: Generate KeyStore
|
||||||
run: |
|
run: |
|
||||||
cd /opt/android/cake_wallet/android/app
|
cd /opt/android/cake_wallet/android/app
|
||||||
|
@ -193,6 +201,8 @@ jobs:
|
||||||
echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
|
echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
|
||||||
echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
|
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 tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
|
||||||
|
echo "const meldTestApiKey = '${{ secrets.MELD_TEST_API_KEY }}';" >> lib/.secrets.g.dart
|
||||||
|
echo "const meldTestPublicKey = '${{ secrets.MELD_TEST_PUBLIC_KEY}}';" >> lib/.secrets.g.dar
|
||||||
echo "const letsExchangeBearerToken = '${{ secrets.LETS_EXCHANGE_TOKEN }}';" >> lib/.secrets.g.dart
|
echo "const letsExchangeBearerToken = '${{ secrets.LETS_EXCHANGE_TOKEN }}';" >> lib/.secrets.g.dart
|
||||||
echo "const letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart
|
echo "const letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart
|
||||||
echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart
|
echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart
|
||||||
|
@ -202,6 +212,36 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
echo -e "id=com.cakewallet.test_${{ env.PR_NUMBER }}\nname=${{ env.BRANCH_NAME }}" > /opt/android/cake_wallet/android/app.properties
|
echo -e "id=com.cakewallet.test_${{ env.PR_NUMBER }}\nname=${{ env.BRANCH_NAME }}" > /opt/android/cake_wallet/android/app.properties
|
||||||
|
|
||||||
|
# Step 3: Download previous build number
|
||||||
|
- name: Download previous build number
|
||||||
|
id: download-build-number
|
||||||
|
run: |
|
||||||
|
# Download the artifact if it exists
|
||||||
|
if [[ ! -f build_number.txt ]]; then
|
||||||
|
echo "1" > build_number.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 4: Read and Increment Build Number
|
||||||
|
- name: Increment Build Number
|
||||||
|
id: increment-build-number
|
||||||
|
run: |
|
||||||
|
# Read current build number from file
|
||||||
|
BUILD_NUMBER=$(cat build_number.txt)
|
||||||
|
BUILD_NUMBER=$((BUILD_NUMBER + 1))
|
||||||
|
echo "New build number: $BUILD_NUMBER"
|
||||||
|
|
||||||
|
# Save the incremented build number
|
||||||
|
echo "$BUILD_NUMBER" > build_number.txt
|
||||||
|
|
||||||
|
# Export the build number to use in later steps
|
||||||
|
echo "BUILD_NUMBER=$BUILD_NUMBER" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# Step 5: Update pubspec.yaml with new build number
|
||||||
|
- name: Update build number
|
||||||
|
run: |
|
||||||
|
cd /opt/android/cake_wallet
|
||||||
|
sed -i "s/^version: .*/version: 1.0.$BUILD_NUMBER/" pubspec.yaml
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
cd /opt/android/cake_wallet
|
cd /opt/android/cake_wallet
|
||||||
|
@ -232,6 +272,13 @@ jobs:
|
||||||
with:
|
with:
|
||||||
path: /opt/android/cake_wallet/build/app/outputs/flutter-apk/test-apk/
|
path: /opt/android/cake_wallet/build/app/outputs/flutter-apk/test-apk/
|
||||||
|
|
||||||
|
# Re-upload updated build number for the next run
|
||||||
|
- name: Upload updated build number
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: build_number
|
||||||
|
path: build_number.txt
|
||||||
|
|
||||||
- name: Send Test APK
|
- name: Send Test APK
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
uses: adrey/slack-file-upload-action@1.0.5
|
uses: adrey/slack-file-upload-action@1.0.5
|
||||||
|
@ -242,3 +289,4 @@ jobs:
|
||||||
title: "${{ env.BRANCH_NAME }}.apk"
|
title: "${{ env.BRANCH_NAME }}.apk"
|
||||||
filename: ${{ env.BRANCH_NAME }}.apk
|
filename: ${{ env.BRANCH_NAME }}.apk
|
||||||
initial_comment: ${{ github.event.head_commit.message }}
|
initial_comment: ${{ github.event.head_commit.message }}
|
||||||
|
|
||||||
|
|
2
.github/workflows/pr_test_build_linux.yml
vendored
|
@ -175,6 +175,8 @@ jobs:
|
||||||
echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
|
echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
|
||||||
echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
|
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 tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
|
||||||
|
echo "const meldTestApiKey = '${{ secrets.MELD_TEST_API_KEY }}';" >> lib/.secrets.g.dart
|
||||||
|
echo "const meldTestPublicKey = '${{ secrets.MELD_TEST_PUBLIC_KEY}}';" >> lib/.secrets.g.dar
|
||||||
echo "const letsExchangeBearerToken = '${{ secrets.LETS_EXCHANGE_TOKEN }}';" >> lib/.secrets.g.dart
|
echo "const letsExchangeBearerToken = '${{ secrets.LETS_EXCHANGE_TOKEN }}';" >> lib/.secrets.g.dart
|
||||||
echo "const letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart
|
echo "const letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart
|
||||||
echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart
|
echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart
|
||||||
|
|
|
@ -92,3 +92,8 @@ dependencies {
|
||||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||||
}
|
}
|
||||||
|
configurations {
|
||||||
|
implementation.exclude module:'proto-google-common-protos'
|
||||||
|
implementation.exclude module:'protolite-well-known-types'
|
||||||
|
implementation.exclude module:'protobuf-javalite'
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx3072M
|
||||||
android.enableR8=true
|
android.enableR8=true
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
|
BIN
assets/images/apple_pay_logo.png
Normal file
After Width: | Height: | Size: 55 KiB |
9
assets/images/apple_pay_round_dark.svg
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
|
||||||
|
|
||||||
|
<defs>
|
||||||
|
</defs>
|
||||||
|
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
|
||||||
|
<path d="M 24.144 34.985 c -0.77 0.912 -2.003 1.631 -3.236 1.528 c -0.154 -1.233 0.449 -2.542 1.156 -3.351 c 0.77 -0.937 2.119 -1.605 3.21 -1.656 C 25.402 32.789 24.902 34.047 24.144 34.985 M 25.261 36.757 c -1.785 -0.103 -3.313 1.014 -4.16 1.014 c -0.86 0 -2.157 -0.963 -3.57 -0.937 c -1.836 0.026 -3.544 1.066 -4.481 2.722 c -1.926 3.313 -0.501 8.218 1.361 10.914 c 0.912 1.335 2.003 2.799 3.441 2.748 c 1.361 -0.051 1.9 -0.886 3.544 -0.886 c 1.656 0 2.131 0.886 3.57 0.86 c 1.489 -0.026 2.427 -1.335 3.338 -2.671 c 1.04 -1.515 1.464 -2.992 1.489 -3.069 c -0.026 -0.026 -2.876 -1.117 -2.902 -4.404 c -0.026 -2.748 2.247 -4.057 2.35 -4.134 C 27.958 37.014 25.954 36.808 25.261 36.757 M 35.572 33.033 v 20.018 h 3.107 v -6.844 h 4.301 c 3.929 0 6.69 -2.696 6.69 -6.6 s -2.709 -6.574 -6.587 -6.574 H 35.572 L 35.572 33.033 z M 38.679 35.652 h 3.582 c 2.696 0 4.237 1.438 4.237 3.968 c 0 2.529 -1.541 3.98 -4.25 3.98 h -3.57 V 35.652 z M 55.345 53.205 c 1.952 0 3.762 -0.989 4.584 -2.555 h 0.064 v 2.401 h 2.876 v -9.964 c 0 -2.889 -2.311 -4.751 -5.868 -4.751 c -3.3 0 -5.739 1.887 -5.829 4.481 h 2.799 c 0.231 -1.233 1.374 -2.042 2.94 -2.042 c 1.9 0 2.966 0.886 2.966 2.517 v 1.104 L 56 44.628 c -3.608 0.218 -5.56 1.695 -5.56 4.263 C 50.44 51.484 52.456 53.205 55.345 53.205 z M 56.18 50.829 c -1.656 0 -2.709 -0.796 -2.709 -2.016 c 0 -1.258 1.014 -1.99 2.953 -2.106 l 3.454 -0.218 v 1.13 C 59.878 49.494 58.286 50.829 56.18 50.829 z M 66.709 58.495 c 3.03 0 4.455 -1.156 5.701 -4.661 l 5.457 -15.305 h -3.159 l -3.659 11.826 h -0.064 l -3.659 -11.826 h -3.249 l 5.264 14.573 l -0.282 0.886 c -0.475 1.502 -1.245 2.08 -2.619 2.08 c -0.244 0 -0.719 -0.026 -0.912 -0.051 v 2.401 C 65.707 58.469 66.478 58.495 66.709 58.495 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: white; fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||||
|
<path d="M 45 90 C 20.187 90 0 69.813 0 45 C 0 20.187 20.187 0 45 0 c 24.813 0 45 20.187 45 45 C 90 69.813 69.813 90 45 90 z M 45 3 C 21.841 3 3 21.841 3 45 c 0 23.159 18.841 42 42 42 c 23.159 0 42 -18.841 42 -42 C 87 21.841 68.159 3 45 3 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: white; fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.8 KiB |
9
assets/images/apple_pay_round_light.svg
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
|
||||||
|
|
||||||
|
<defs>
|
||||||
|
</defs>
|
||||||
|
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
|
||||||
|
<path d="M 24.144 34.985 c -0.77 0.912 -2.003 1.631 -3.236 1.528 c -0.154 -1.233 0.449 -2.542 1.156 -3.351 c 0.77 -0.937 2.119 -1.605 3.21 -1.656 C 25.402 32.789 24.902 34.047 24.144 34.985 M 25.261 36.757 c -1.785 -0.103 -3.313 1.014 -4.16 1.014 c -0.86 0 -2.157 -0.963 -3.57 -0.937 c -1.836 0.026 -3.544 1.066 -4.481 2.722 c -1.926 3.313 -0.501 8.218 1.361 10.914 c 0.912 1.335 2.003 2.799 3.441 2.748 c 1.361 -0.051 1.9 -0.886 3.544 -0.886 c 1.656 0 2.131 0.886 3.57 0.86 c 1.489 -0.026 2.427 -1.335 3.338 -2.671 c 1.04 -1.515 1.464 -2.992 1.489 -3.069 c -0.026 -0.026 -2.876 -1.117 -2.902 -4.404 c -0.026 -2.748 2.247 -4.057 2.35 -4.134 C 27.958 37.014 25.954 36.808 25.261 36.757 M 35.572 33.033 v 20.018 h 3.107 v -6.844 h 4.301 c 3.929 0 6.69 -2.696 6.69 -6.6 s -2.709 -6.574 -6.587 -6.574 H 35.572 L 35.572 33.033 z M 38.679 35.652 h 3.582 c 2.696 0 4.237 1.438 4.237 3.968 c 0 2.529 -1.541 3.98 -4.25 3.98 h -3.57 V 35.652 z M 55.345 53.205 c 1.952 0 3.762 -0.989 4.584 -2.555 h 0.064 v 2.401 h 2.876 v -9.964 c 0 -2.889 -2.311 -4.751 -5.868 -4.751 c -3.3 0 -5.739 1.887 -5.829 4.481 h 2.799 c 0.231 -1.233 1.374 -2.042 2.94 -2.042 c 1.9 0 2.966 0.886 2.966 2.517 v 1.104 L 56 44.628 c -3.608 0.218 -5.56 1.695 -5.56 4.263 C 50.44 51.484 52.456 53.205 55.345 53.205 z M 56.18 50.829 c -1.656 0 -2.709 -0.796 -2.709 -2.016 c 0 -1.258 1.014 -1.99 2.953 -2.106 l 3.454 -0.218 v 1.13 C 59.878 49.494 58.286 50.829 56.18 50.829 z M 66.709 58.495 c 3.03 0 4.455 -1.156 5.701 -4.661 l 5.457 -15.305 h -3.159 l -3.659 11.826 h -0.064 l -3.659 -11.826 h -3.249 l 5.264 14.573 l -0.282 0.886 c -0.475 1.502 -1.245 2.08 -2.619 2.08 c -0.244 0 -0.719 -0.026 -0.912 -0.051 v 2.401 C 65.707 58.469 66.478 58.495 66.709 58.495 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||||
|
<path d="M 45 90 C 20.187 90 0 69.813 0 45 C 0 20.187 20.187 0 45 0 c 24.813 0 45 20.187 45 45 C 90 69.813 69.813 90 45 90 z M 45 3 C 21.841 3 3 21.841 3 45 c 0 23.159 18.841 42 42 42 c 23.159 0 42 -18.841 42 -42 C 87 21.841 68.159 3 45 3 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.8 KiB |
BIN
assets/images/bank.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
8
assets/images/bank_dark.svg
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
|
||||||
|
|
||||||
|
<defs>
|
||||||
|
</defs>
|
||||||
|
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: white; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
|
||||||
|
<path d="M 84.668 38.004 v -6.27 H 90 V 20 L 45 3.034 L 0 20 v 11.734 h 5.332 v 6.27 h 4.818 v 30.892 H 5.332 v 6.271 H 0 v 11.8 h 90 v -11.8 h -5.332 v -6.271 H 79.85 V 38.004 H 84.668 z M 81.668 35.004 H 66.332 v -3.27 h 15.336 V 35.004 z M 63.332 68.896 v 6.271 h -7.664 v -6.271 H 50.85 V 38.004 h 4.818 v -6.27 h 7.664 v 6.27 h 4.818 v 30.892 H 63.332 z M 26.668 38.004 v -6.27 h 7.664 v 6.27 h 4.818 v 30.892 h -4.818 v 6.271 h -7.664 v -6.271 H 21.85 V 38.004 H 26.668 z M 42.15 68.896 V 38.004 h 5.7 v 30.892 H 42.15 z M 37.332 35.004 v -3.27 h 15.336 v 3.27 H 37.332 z M 37.332 71.896 h 15.336 v 3.271 H 37.332 V 71.896 z M 3 22.075 L 45 6.24 l 42 15.835 v 6.659 H 3 V 22.075 z M 8.332 31.734 h 15.336 v 3.27 H 8.332 V 31.734 z M 13.15 38.004 h 5.7 v 30.892 h -5.7 V 38.004 z M 8.332 71.896 h 15.336 v 3.271 H 8.332 V 71.896 z M 87 83.966 H 3 v -5.8 h 84 V 83.966 z M 81.668 75.166 H 66.332 v -3.271 h 15.336 V 75.166 z M 76.85 68.896 H 71.15 V 38.004 h 5.699 V 68.896 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: white; fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
8
assets/images/bank_light.svg
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
|
||||||
|
|
||||||
|
<defs>
|
||||||
|
</defs>
|
||||||
|
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: black; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
|
||||||
|
<path d="M 84.668 38.004 v -6.27 H 90 V 20 L 45 3.034 L 0 20 v 11.734 h 5.332 v 6.27 h 4.818 v 30.892 H 5.332 v 6.271 H 0 v 11.8 h 90 v -11.8 h -5.332 v -6.271 H 79.85 V 38.004 H 84.668 z M 81.668 35.004 H 66.332 v -3.27 h 15.336 V 35.004 z M 63.332 68.896 v 6.271 h -7.664 v -6.271 H 50.85 V 38.004 h 4.818 v -6.27 h 7.664 v 6.27 h 4.818 v 30.892 H 63.332 z M 26.668 38.004 v -6.27 h 7.664 v 6.27 h 4.818 v 30.892 h -4.818 v 6.271 h -7.664 v -6.271 H 21.85 V 38.004 H 26.668 z M 42.15 68.896 V 38.004 h 5.7 v 30.892 H 42.15 z M 37.332 35.004 v -3.27 h 15.336 v 3.27 H 37.332 z M 37.332 71.896 h 15.336 v 3.271 H 37.332 V 71.896 z M 3 22.075 L 45 6.24 l 42 15.835 v 6.659 H 3 V 22.075 z M 8.332 31.734 h 15.336 v 3.27 H 8.332 V 31.734 z M 13.15 38.004 h 5.7 v 30.892 h -5.7 V 38.004 z M 8.332 71.896 h 15.336 v 3.271 H 8.332 V 71.896 z M 87 83.966 H 3 v -5.8 h 84 V 83.966 z M 81.668 75.166 H 66.332 v -3.271 h 15.336 V 75.166 z M 76.85 68.896 H 71.15 V 38.004 h 5.699 V 68.896 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: black; fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
BIN
assets/images/buy_sell.png
Normal file
After Width: | Height: | Size: 9.7 KiB |
1
assets/images/card.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="iso-8859-1"?><!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 50 50" width="100px" height="100px"><path style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;" d="M43,40H7c-2.209,0-4-1.791-4-4V14c0-2.209,1.791-4,4-4h36c2.209,0,4,1.791,4,4v22C47,38.209,45.209,40,43,40z"/><rect x="3" y="16" width="44" height="5"/><line style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:10;" x1="9" y1="25" x2="25" y2="25"/></svg>
|
After Width: | Height: | Size: 633 B |
7
assets/images/card_dark.svg
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 50 50" width="100px" height="100px">
|
||||||
|
<path style="fill:none;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;" d="M43,40H7c-2.209,0-4-1.791-4-4V14c0-2.209,1.791-4,4-4h36c2.209,0,4,1.791,4,4v22C47,38.209,45.209,40,43,40z"/>
|
||||||
|
<rect x="3" y="16" width="44" height="5" style="fill:white;stroke:#ffffff;stroke-width:2;"/>
|
||||||
|
<line style="fill:none;stroke:#ffffff;stroke-width:2;stroke-miterlimit:10;" x1="9" y1="25" x2="25" y2="25"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 701 B |
12
assets/images/dollar_coin.svg
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" ?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="Outline">
|
||||||
|
<g data-name="Outline" id="Outline-2">
|
||||||
|
<path d="M39,36.852a6.8,6.8,0,0,0-6.793-6.793h-.319A3.716,3.716,0,1,1,35.6,26.344a1,1,0,0,0,2,0,5.725,5.725,0,0,0-4.561-5.6V18.09a1,1,0,0,0-2,0V20.7a5.712,5.712,0,0,0,.846,11.361h.319a4.793,4.793,0,1,1-4.793,4.793,1,1,0,0,0-2,0A6.8,6.8,0,0,0,31.451,43.6v2.947a1,1,0,0,0,2,0v-3c0-.008,0-.014,0-.021A6.8,6.8,0,0,0,39,36.852Z"/>
|
||||||
|
<path d="M32,2A30,30,0,1,0,62,32,30.034,30.034,0,0,0,32,2Zm0,58A28,28,0,1,1,60,32,28.032,28.032,0,0,1,32,60Z"/>
|
||||||
|
<path d="M49.655,16.793a3.172,3.172,0,1,0-3.172,3.172,3.137,3.137,0,0,0,1.263-.266A19.994,19.994,0,0,1,22.692,49.707a1,1,0,0,0-.933,1.769,21.986,21.986,0,0,0,27.47-33.124A3.141,3.141,0,0,0,49.655,16.793Zm-4.344,0a1.172,1.172,0,1,1,1.172,1.172A1.172,1.172,0,0,1,45.311,16.793Z"/>
|
||||||
|
<path d="M16.793,44.035a3.164,3.164,0,0,0-.692.081A19.779,19.779,0,0,1,12,32,20.023,20.023,0,0,1,32,12a19.811,19.811,0,0,1,8.463,1.874,1,1,0,0,0,.848-1.812A21.989,21.989,0,0,0,14.39,45.16a3.141,3.141,0,0,0-.769,2.047,3.172,3.172,0,1,0,3.172-3.172Zm0,4.344a1.172,1.172,0,1,1,1.172-1.172A1.172,1.172,0,0,1,16.793,48.379Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
BIN
assets/images/google_pay_icon.png
Normal file
After Width: | Height: | Size: 12 KiB |
30
assets/images/meld_logo.svg
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<svg width="40" height="42" viewBox="0 0 40 42" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path opacity="0.7" d="M19.9526 41.9076L7.3877 34.655V26.1226L19.9526 33.3751V41.9076Z" fill="url(#paint0_linear_2113_32117)"/>
|
||||||
|
<path opacity="0.7" d="M19.9521 41.9076L32.5171 34.655V26.1226L19.9521 33.3751V41.9076Z" fill="url(#paint1_linear_2113_32117)"/>
|
||||||
|
<path opacity="0.7" d="M39.9095 7.34521V21.8562L32.5166 26.1225V11.6114L39.9095 7.34521Z" fill="url(#paint2_linear_2113_32117)"/>
|
||||||
|
<path d="M39.9099 7.34536L27.345 0.0927734L19.9521 4.359L32.5171 11.6116L39.9099 7.34536Z" fill="url(#paint3_linear_2113_32117)"/>
|
||||||
|
<path d="M0 7.34536L12.5649 0.0927734L19.9519 4.359L7.387 11.6116L0 7.34536Z" fill="#F969D3"/>
|
||||||
|
<path opacity="0.7" d="M0 7.34521V21.8562L7.387 26.1225V11.6114L0 7.34521Z" fill="url(#paint4_linear_2113_32117)"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_2113_32117" x1="18.6099" y1="41.8335" x2="7.73529" y2="8.31842" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#E98ADA"/>
|
||||||
|
<stop offset="1" stop-color="#7E4DBD"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint1_linear_2113_32117" x1="26.2346" y1="26.1226" x2="26.2346" y2="41.9076" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#719DED"/>
|
||||||
|
<stop offset="1" stop-color="#2545BE"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint2_linear_2113_32117" x1="36.213" y1="7.34521" x2="36.213" y2="26.1225" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#93EBFF"/>
|
||||||
|
<stop offset="1" stop-color="#197DDB"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint3_linear_2113_32117" x1="29.931" y1="0.0927734" x2="38.2156" y2="14.8448" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#F969D3"/>
|
||||||
|
<stop offset="1" stop-color="#4F51C0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint4_linear_2113_32117" x1="18.1251" y1="44.2539" x2="-7.06792" y2="15.2763" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#E98ADA"/>
|
||||||
|
<stop offset="1" stop-color="#7E4DBD"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
BIN
assets/images/revolut.png
Normal file
After Width: | Height: | Size: 11 KiB |
15
assets/images/skrill.svg
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
|
||||||
|
|
||||||
|
<defs>
|
||||||
|
</defs>
|
||||||
|
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
|
||||||
|
<circle cx="45" cy="45" r="45" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(127,33,99); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) "/>
|
||||||
|
<polygon points="69.59,36.9 69.59,54.86 74.5,54.86 74.5,36.02 " style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(249,249,249); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) "/>
|
||||||
|
<polygon points="62.42,36.9 67.33,36.02 67.33,54.87 62.42,54.87 " style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(249,249,249); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) "/>
|
||||||
|
<rect x="55.43" y="41.08" rx="0" ry="0" width="4.91" height="13.78" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(249,249,249); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) "/>
|
||||||
|
<path d="M 57.879 39.76 c 1.332 0 2.425 -1.082 2.425 -2.415 s -1.082 -2.425 -2.425 -2.425 c -1.332 0 -2.415 1.082 -2.415 2.425 C 55.465 38.677 56.547 39.76 57.879 39.76 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(249,249,249); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||||
|
<path d="M 52.665 40.884 c -4.538 0.146 -6.838 2.186 -6.838 6.234 v 7.754 h 4.954 v -6.328 c 0 -2.425 0.312 -3.466 3.195 -3.559 v -4.038 C 53.477 40.853 52.665 40.884 52.665 40.884 L 52.665 40.884 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(249,249,249); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||||
|
<path d="M 39.344 41.071 c -0.104 0.271 -0.895 2.498 -2.8 4.798 v -9.845 l -5.068 0.999 v 17.838 h 5.068 v -5.516 c 1.467 2.206 2.196 5.516 2.196 5.516 h 6.068 c -0.604 -2.498 -3.226 -7.098 -3.226 -7.098 c 2.352 -2.987 3.393 -6.172 3.559 -6.702 h -5.797 L 39.344 41.071 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(249,249,249); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||||
|
<path d="M 22.973 43.028 c -0.624 -0.042 -2.061 -0.135 -2.061 -1.426 c 0 -1.561 2.071 -1.561 2.841 -1.561 c 1.363 0 3.133 0.406 4.392 0.781 c 0 0 0.708 0.25 1.301 0.5 l 0.052 0.01 v -4.267 l -0.073 -0.021 c -1.488 -0.52 -3.216 -1.02 -6.432 -1.02 c -5.537 0 -7.493 3.226 -7.493 5.984 c 0 1.592 0.687 5.339 7.025 5.776 c 0.541 0.031 1.967 0.114 1.967 1.457 c 0 1.103 -1.166 1.759 -3.133 1.759 c -2.154 0 -4.236 -0.552 -5.506 -1.072 v 4.402 c 1.894 0.5 4.038 0.749 6.546 0.749 c 5.412 0 7.837 -3.049 7.837 -6.078 C 30.237 45.567 27.531 43.34 22.973 43.028 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(249,249,249); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.7 KiB |
4
assets/images/usd_round_dark.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 24 24" width="512" height="512">
|
||||||
|
<path d="M12,0C5.383,0,0,5.383,0,12s5.383,12,12,12,12-5.383,12-12S18.617,0,12,0Zm0,23c-6.065,0-11-4.935-11-11S5.935,1,12,1s11,4.935,11,11-4.935,11-11,11Zm4-8.626c0,1.448-1.178,2.626-2.626,2.626h-.874v1.5c0,.276-.224,.5-.5,.5s-.5-.224-.5-.5v-1.5h-.926c-.979,0-1.893-.526-2.382-1.375-.139-.239-.057-.545,.183-.683,.238-.14,.544-.057,.683,.183,.312,.54,.894,.875,1.517,.875h2.8c.896,0,1.626-.729,1.626-1.626,0-.803-.575-1.478-1.368-1.605l-3.422-.55c-1.28-.206-2.21-1.296-2.21-2.593,0-1.448,1.178-2.626,2.626-2.626h.874v-1.5c0-.276,.224-.5,.5-.5s.5,.224,.5,.5v1.5h.926c.979,0,1.892,.527,2.382,1.375,.139,.239,.057,.545-.183,.683-.236,.136-.545,.057-.683-.183-.312-.54-.894-.875-1.517-.875h-2.8c-.896,0-1.626,.729-1.626,1.626,0,.803,.575,1.478,1.368,1.605l3.422,.55c1.28,.206,2.21,1.297,2.21,2.593Z" fill="white"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 978 B |
2
assets/images/usd_round_light.svg
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 24 24" width="512" height="512"><path d="M12,0C5.383,0,0,5.383,0,12s5.383,12,12,12,12-5.383,12-12S18.617,0,12,0Zm0,23c-6.065,0-11-4.935-11-11S5.935,1,12,1s11,4.935,11,11-4.935,11-11,11Zm4-8.626c0,1.448-1.178,2.626-2.626,2.626h-.874v1.5c0,.276-.224,.5-.5,.5s-.5-.224-.5-.5v-1.5h-.926c-.979,0-1.893-.526-2.382-1.375-.139-.239-.057-.545,.183-.683,.238-.14,.544-.057,.683,.183,.312,.54,.894,.875,1.517,.875h2.8c.896,0,1.626-.729,1.626-1.626,0-.803-.575-1.478-1.368-1.605l-3.422-.55c-1.28-.206-2.21-1.296-2.21-2.593,0-1.448,1.178-2.626,2.626-2.626h.874v-1.5c0-.276,.224-.5,.5-.5s.5,.224,.5,.5v1.5h.926c.979,0,1.892,.527,2.382,1.375,.139,.239,.057,.545-.183,.683-.236,.136-.545,.057-.683-.183-.312-.54-.894-.875-1.517-.875h-2.8c-.896,0-1.626,.729-1.626,1.626,0,.803,.575,1.478,1.368,1.605l3.422,.55c1.28,.206,2.21,1.297,2.21,2.593Z"/></svg>
|
After Width: | Height: | Size: 960 B |
BIN
assets/images/wallet_new.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
|
@ -1,6 +1,7 @@
|
||||||
-
|
-
|
||||||
uri: xmr-node.cakewallet.com:18081
|
uri: xmr-node.cakewallet.com:18081
|
||||||
is_default: true
|
is_default: true
|
||||||
|
trusted: true
|
||||||
-
|
-
|
||||||
uri: cakexmrl7bonq7ovjka5kuwuyd3f7qnkz6z6s6dmsy3uckwra7bvggyd.onion:18081
|
uri: cakexmrl7bonq7ovjka5kuwuyd3f7qnkz6z6s6dmsy3uckwra7bvggyd.onion:18081
|
||||||
is_default: false
|
is_default: false
|
||||||
|
|
|
@ -11,7 +11,6 @@ import 'package:blockchain_utils/blockchain_utils.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:cw_bitcoin/address_from_output.dart';
|
import 'package:cw_bitcoin/address_from_output.dart';
|
||||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||||
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
|
|
||||||
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
|
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
|
||||||
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
|
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
|
||||||
import 'package:cw_bitcoin/bitcoin_unspent.dart';
|
import 'package:cw_bitcoin/bitcoin_unspent.dart';
|
||||||
|
@ -597,8 +596,8 @@ abstract class ElectrumWalletBase
|
||||||
|
|
||||||
UtxoDetails _createUTXOS({
|
UtxoDetails _createUTXOS({
|
||||||
required bool sendAll,
|
required bool sendAll,
|
||||||
required int credentialsAmount,
|
|
||||||
required bool paysToSilentPayment,
|
required bool paysToSilentPayment,
|
||||||
|
int credentialsAmount = 0,
|
||||||
int? inputsCount,
|
int? inputsCount,
|
||||||
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
|
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
|
||||||
}) {
|
}) {
|
||||||
|
@ -732,13 +731,11 @@ abstract class ElectrumWalletBase
|
||||||
List<BitcoinOutput> outputs,
|
List<BitcoinOutput> outputs,
|
||||||
int feeRate, {
|
int feeRate, {
|
||||||
String? memo,
|
String? memo,
|
||||||
int credentialsAmount = 0,
|
|
||||||
bool hasSilentPayment = false,
|
bool hasSilentPayment = false,
|
||||||
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
|
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
|
||||||
}) async {
|
}) async {
|
||||||
final utxoDetails = _createUTXOS(
|
final utxoDetails = _createUTXOS(
|
||||||
sendAll: true,
|
sendAll: true,
|
||||||
credentialsAmount: credentialsAmount,
|
|
||||||
paysToSilentPayment: hasSilentPayment,
|
paysToSilentPayment: hasSilentPayment,
|
||||||
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
||||||
);
|
);
|
||||||
|
@ -764,23 +761,11 @@ abstract class ElectrumWalletBase
|
||||||
throw BitcoinTransactionWrongBalanceException(amount: utxoDetails.allInputsAmount + fee);
|
throw BitcoinTransactionWrongBalanceException(amount: utxoDetails.allInputsAmount + fee);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (amount <= 0) {
|
|
||||||
throw BitcoinTransactionWrongBalanceException();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempting to send less than the dust limit
|
// Attempting to send less than the dust limit
|
||||||
if (_isBelowDust(amount)) {
|
if (_isBelowDust(amount)) {
|
||||||
throw BitcoinTransactionNoDustException();
|
throw BitcoinTransactionNoDustException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (credentialsAmount > 0) {
|
|
||||||
final amountLeftForFee = amount - credentialsAmount;
|
|
||||||
if (amountLeftForFee > 0 && _isBelowDust(amountLeftForFee)) {
|
|
||||||
amount -= amountLeftForFee;
|
|
||||||
fee += amountLeftForFee;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (outputs.length == 1) {
|
if (outputs.length == 1) {
|
||||||
outputs[0] = BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount));
|
outputs[0] = BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount));
|
||||||
}
|
}
|
||||||
|
@ -810,6 +795,11 @@ abstract class ElectrumWalletBase
|
||||||
bool hasSilentPayment = false,
|
bool hasSilentPayment = false,
|
||||||
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
|
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
|
||||||
}) async {
|
}) async {
|
||||||
|
// Attempting to send less than the dust limit
|
||||||
|
if (_isBelowDust(credentialsAmount)) {
|
||||||
|
throw BitcoinTransactionNoDustException();
|
||||||
|
}
|
||||||
|
|
||||||
final utxoDetails = _createUTXOS(
|
final utxoDetails = _createUTXOS(
|
||||||
sendAll: false,
|
sendAll: false,
|
||||||
credentialsAmount: credentialsAmount,
|
credentialsAmount: credentialsAmount,
|
||||||
|
@ -894,7 +884,43 @@ abstract class ElectrumWalletBase
|
||||||
final lastOutput = updatedOutputs.last;
|
final lastOutput = updatedOutputs.last;
|
||||||
final amountLeftForChange = amountLeftForChangeAndFee - fee;
|
final amountLeftForChange = amountLeftForChangeAndFee - fee;
|
||||||
|
|
||||||
if (!_isBelowDust(amountLeftForChange)) {
|
if (_isBelowDust(amountLeftForChange)) {
|
||||||
|
// If has change that is lower than dust, will end up with tx rejected by network rules
|
||||||
|
// so remove the change amount
|
||||||
|
updatedOutputs.removeLast();
|
||||||
|
outputs.removeLast();
|
||||||
|
|
||||||
|
if (amountLeftForChange < 0) {
|
||||||
|
if (!spendingAllCoins) {
|
||||||
|
return estimateTxForAmount(
|
||||||
|
credentialsAmount,
|
||||||
|
outputs,
|
||||||
|
updatedOutputs,
|
||||||
|
feeRate,
|
||||||
|
inputsCount: utxoDetails.utxos.length + 1,
|
||||||
|
memo: memo,
|
||||||
|
useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins,
|
||||||
|
hasSilentPayment: hasSilentPayment,
|
||||||
|
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw BitcoinTransactionWrongBalanceException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return EstimatedTxResult(
|
||||||
|
utxos: utxoDetails.utxos,
|
||||||
|
inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
|
||||||
|
publicKeys: utxoDetails.publicKeys,
|
||||||
|
fee: fee,
|
||||||
|
amount: amount,
|
||||||
|
hasChange: false,
|
||||||
|
isSendAll: spendingAllCoins,
|
||||||
|
memo: memo,
|
||||||
|
spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
|
||||||
|
spendsSilentPayment: utxoDetails.spendsSilentPayment,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
// Here, lastOutput already is change, return the amount left without the fee to the user's address.
|
// Here, lastOutput already is change, return the amount left without the fee to the user's address.
|
||||||
updatedOutputs[updatedOutputs.length - 1] = BitcoinOutput(
|
updatedOutputs[updatedOutputs.length - 1] = BitcoinOutput(
|
||||||
address: lastOutput.address,
|
address: lastOutput.address,
|
||||||
|
@ -908,75 +934,6 @@ abstract class ElectrumWalletBase
|
||||||
isSilentPayment: lastOutput.isSilentPayment,
|
isSilentPayment: lastOutput.isSilentPayment,
|
||||||
isChange: true,
|
isChange: true,
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
// If has change that is lower than dust, will end up with tx rejected by network rules, so estimate again without the added change
|
|
||||||
updatedOutputs.removeLast();
|
|
||||||
outputs.removeLast();
|
|
||||||
|
|
||||||
// Still has inputs to spend before failing
|
|
||||||
if (!spendingAllCoins) {
|
|
||||||
return estimateTxForAmount(
|
|
||||||
credentialsAmount,
|
|
||||||
outputs,
|
|
||||||
updatedOutputs,
|
|
||||||
feeRate,
|
|
||||||
inputsCount: utxoDetails.utxos.length + 1,
|
|
||||||
memo: memo,
|
|
||||||
hasSilentPayment: hasSilentPayment,
|
|
||||||
useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins,
|
|
||||||
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final estimatedSendAll = await estimateSendAllTx(
|
|
||||||
updatedOutputs,
|
|
||||||
feeRate,
|
|
||||||
memo: memo,
|
|
||||||
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (estimatedSendAll.amount == credentialsAmount) {
|
|
||||||
return estimatedSendAll;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Estimate to user how much is needed to send to cover the fee
|
|
||||||
final maxAmountWithReturningChange = utxoDetails.allInputsAmount - _dustAmount - fee - 1;
|
|
||||||
throw BitcoinTransactionNoDustOnChangeException(
|
|
||||||
bitcoinAmountToString(amount: maxAmountWithReturningChange),
|
|
||||||
bitcoinAmountToString(amount: estimatedSendAll.amount),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempting to send less than the dust limit
|
|
||||||
if (_isBelowDust(amount)) {
|
|
||||||
throw BitcoinTransactionNoDustException();
|
|
||||||
}
|
|
||||||
|
|
||||||
final totalAmount = amount + fee;
|
|
||||||
|
|
||||||
if (totalAmount > (balance[currency]!.confirmed + balance[currency]!.secondConfirmed)) {
|
|
||||||
throw BitcoinTransactionWrongBalanceException();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalAmount > utxoDetails.allInputsAmount) {
|
|
||||||
if (spendingAllCoins) {
|
|
||||||
throw BitcoinTransactionWrongBalanceException();
|
|
||||||
} else {
|
|
||||||
updatedOutputs.removeLast();
|
|
||||||
outputs.removeLast();
|
|
||||||
return estimateTxForAmount(
|
|
||||||
credentialsAmount,
|
|
||||||
outputs,
|
|
||||||
updatedOutputs,
|
|
||||||
feeRate,
|
|
||||||
inputsCount: utxoDetails.utxos.length + 1,
|
|
||||||
memo: memo,
|
|
||||||
useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins,
|
|
||||||
hasSilentPayment: hasSilentPayment,
|
|
||||||
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return EstimatedTxResult(
|
return EstimatedTxResult(
|
||||||
utxos: utxoDetails.utxos,
|
utxos: utxoDetails.utxos,
|
||||||
|
@ -985,12 +942,13 @@ abstract class ElectrumWalletBase
|
||||||
fee: fee,
|
fee: fee,
|
||||||
amount: amount,
|
amount: amount,
|
||||||
hasChange: true,
|
hasChange: true,
|
||||||
isSendAll: false,
|
isSendAll: spendingAllCoins,
|
||||||
memo: memo,
|
memo: memo,
|
||||||
spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
|
spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
|
||||||
spendsSilentPayment: utxoDetails.spendsSilentPayment,
|
spendsSilentPayment: utxoDetails.spendsSilentPayment,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<int> calcFee({
|
Future<int> calcFee({
|
||||||
required List<UtxoWithAddress> utxos,
|
required List<UtxoWithAddress> utxos,
|
||||||
|
@ -1080,15 +1038,20 @@ abstract class ElectrumWalletBase
|
||||||
: feeRate(transactionCredentials.priority!);
|
: feeRate(transactionCredentials.priority!);
|
||||||
|
|
||||||
EstimatedTxResult estimatedTx;
|
EstimatedTxResult estimatedTx;
|
||||||
final updatedOutputs =
|
final updatedOutputs = outputs
|
||||||
outputs.map((e) => BitcoinOutput(address: e.address, value: e.value)).toList();
|
.map((e) => BitcoinOutput(
|
||||||
|
address: e.address,
|
||||||
|
value: e.value,
|
||||||
|
isSilentPayment: e.isSilentPayment,
|
||||||
|
isChange: e.isChange,
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
|
||||||
if (sendAll) {
|
if (sendAll) {
|
||||||
estimatedTx = await estimateSendAllTx(
|
estimatedTx = await estimateSendAllTx(
|
||||||
updatedOutputs,
|
updatedOutputs,
|
||||||
feeRateInt,
|
feeRateInt,
|
||||||
memo: memo,
|
memo: memo,
|
||||||
credentialsAmount: credentialsAmount,
|
|
||||||
hasSilentPayment: hasSilentPayment,
|
hasSilentPayment: hasSilentPayment,
|
||||||
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
||||||
);
|
);
|
||||||
|
|
|
@ -153,4 +153,9 @@ class PendingBitcoinTransaction with PendingTransaction {
|
||||||
inputAddresses: _tx.inputs.map((input) => input.txId).toList(),
|
inputAddresses: _tx.inputs.map((input) => input.txId).toList(),
|
||||||
outputAddresses: outputAddresses,
|
outputAddresses: outputAddresses,
|
||||||
fee: fee);
|
fee: fee);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> commitUR() {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,4 +85,8 @@ class PendingBitcoinCashTransaction with PendingTransaction {
|
||||||
fee: fee,
|
fee: fee,
|
||||||
isReplaced: false,
|
isReplaced: false,
|
||||||
);
|
);
|
||||||
|
@override
|
||||||
|
Future<String?> commitUR() {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,3 +38,34 @@ CryptoCurrency currencyForWalletType(WalletType type, {bool? isTestnet}) {
|
||||||
'Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType');
|
'Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WalletType? walletTypeForCurrency(CryptoCurrency currency) {
|
||||||
|
switch (currency) {
|
||||||
|
case CryptoCurrency.btc:
|
||||||
|
return WalletType.bitcoin;
|
||||||
|
case CryptoCurrency.xmr:
|
||||||
|
return WalletType.monero;
|
||||||
|
case CryptoCurrency.ltc:
|
||||||
|
return WalletType.litecoin;
|
||||||
|
case CryptoCurrency.xhv:
|
||||||
|
return WalletType.haven;
|
||||||
|
case CryptoCurrency.eth:
|
||||||
|
return WalletType.ethereum;
|
||||||
|
case CryptoCurrency.bch:
|
||||||
|
return WalletType.bitcoinCash;
|
||||||
|
case CryptoCurrency.nano:
|
||||||
|
return WalletType.nano;
|
||||||
|
case CryptoCurrency.banano:
|
||||||
|
return WalletType.banano;
|
||||||
|
case CryptoCurrency.maticpoly:
|
||||||
|
return WalletType.polygon;
|
||||||
|
case CryptoCurrency.sol:
|
||||||
|
return WalletType.solana;
|
||||||
|
case CryptoCurrency.trx:
|
||||||
|
return WalletType.tron;
|
||||||
|
case CryptoCurrency.wow:
|
||||||
|
return WalletType.wownero;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
class MoneroWalletKeys {
|
class MoneroWalletKeys {
|
||||||
const MoneroWalletKeys(
|
const MoneroWalletKeys(
|
||||||
{required this.privateSpendKey,
|
{required this.primaryAddress,
|
||||||
|
required this.privateSpendKey,
|
||||||
required this.privateViewKey,
|
required this.privateViewKey,
|
||||||
required this.publicSpendKey,
|
required this.publicSpendKey,
|
||||||
required this.publicViewKey});
|
required this.publicViewKey});
|
||||||
|
|
||||||
|
final String primaryAddress;
|
||||||
final String publicViewKey;
|
final String publicViewKey;
|
||||||
final String privateViewKey;
|
final String privateViewKey;
|
||||||
final String publicSpendKey;
|
final String publicSpendKey;
|
||||||
|
|
|
@ -14,5 +14,8 @@ mixin PendingTransaction {
|
||||||
int? get outputCount => null;
|
int? get outputCount => null;
|
||||||
PendingChange? change;
|
PendingChange? change;
|
||||||
|
|
||||||
|
bool shouldCommitUR() => false;
|
||||||
|
|
||||||
Future<void> commit();
|
Future<void> commit();
|
||||||
|
Future<String?> commitUR();
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,4 +47,9 @@ class PendingEVMChainTransaction with PendingTransaction {
|
||||||
|
|
||||||
return '0x${Hex.HEX.encode(txid)}';
|
return '0x${Hex.HEX.encode(txid)}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> commitUR() {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,4 +2,9 @@ class WalletRestoreFromKeysException implements Exception {
|
||||||
WalletRestoreFromKeysException({required this.message});
|
WalletRestoreFromKeysException({required this.message});
|
||||||
|
|
||||||
final String message;
|
final String message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -73,6 +73,7 @@ abstract class HavenWalletBase
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MoneroWalletKeys get keys => MoneroWalletKeys(
|
MoneroWalletKeys get keys => MoneroWalletKeys(
|
||||||
|
primaryAddress: haven_wallet.getAddress(accountIndex: 0, addressIndex: 0),
|
||||||
privateSpendKey: haven_wallet.getSecretSpendKey(),
|
privateSpendKey: haven_wallet.getSecretSpendKey(),
|
||||||
privateViewKey: haven_wallet.getSecretViewKey(),
|
privateViewKey: haven_wallet.getSecretViewKey(),
|
||||||
publicSpendKey: haven_wallet.getPublicSpendKey(),
|
publicSpendKey: haven_wallet.getPublicSpendKey(),
|
||||||
|
|
|
@ -48,4 +48,9 @@ class PendingHavenTransaction with PendingTransaction {
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> commitUR() {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'package:cw_monero/api/wallet.dart';
|
||||||
import 'package:monero/monero.dart' as monero;
|
import 'package:monero/monero.dart' as monero;
|
||||||
|
|
||||||
monero.wallet? wptr = null;
|
monero.wallet? wptr = null;
|
||||||
|
bool get isViewOnly => int.tryParse(monero.Wallet_secretSpendKey(wptr!)) == 0;
|
||||||
|
|
||||||
int _wlptrForW = 0;
|
int _wlptrForW = 0;
|
||||||
monero.WalletListener? _wlptr = null;
|
monero.WalletListener? _wlptr = null;
|
||||||
|
|
|
@ -13,7 +13,13 @@ import 'package:mutex/mutex.dart';
|
||||||
|
|
||||||
|
|
||||||
String getTxKey(String txId) {
|
String getTxKey(String txId) {
|
||||||
return monero.Wallet_getTxKey(wptr!, txid: txId);
|
final txKey = monero.Wallet_getTxKey(wptr!, txid: txId);
|
||||||
|
final status = monero.Wallet_status(wptr!);
|
||||||
|
if (status != 0) {
|
||||||
|
final error = monero.Wallet_errorString(wptr!);
|
||||||
|
return txId+"_"+error;
|
||||||
|
}
|
||||||
|
return txKey;
|
||||||
}
|
}
|
||||||
final txHistoryMutex = Mutex();
|
final txHistoryMutex = Mutex();
|
||||||
monero.TransactionHistory? txhistory;
|
monero.TransactionHistory? txhistory;
|
||||||
|
@ -178,12 +184,13 @@ PendingTransactionDescription createTransactionMultDestSync(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void commitTransactionFromPointerAddress({required int address}) =>
|
String? commitTransactionFromPointerAddress({required int address, required bool useUR}) =>
|
||||||
commitTransaction(transactionPointer: monero.PendingTransaction.fromAddress(address));
|
commitTransaction(transactionPointer: monero.PendingTransaction.fromAddress(address), useUR: useUR);
|
||||||
|
|
||||||
void commitTransaction({required monero.PendingTransaction transactionPointer}) {
|
String? commitTransaction({required monero.PendingTransaction transactionPointer, required bool useUR}) {
|
||||||
|
final txCommit = useUR
|
||||||
final txCommit = monero.PendingTransaction_commit(transactionPointer, filename: '', overwrite: false);
|
? monero.PendingTransaction_commitUR(transactionPointer, 120)
|
||||||
|
: monero.PendingTransaction_commit(transactionPointer, filename: '', overwrite: false);
|
||||||
|
|
||||||
final String? error = (() {
|
final String? error = (() {
|
||||||
final status = monero.PendingTransaction_status(transactionPointer.cast());
|
final status = monero.PendingTransaction_status(transactionPointer.cast());
|
||||||
|
@ -196,6 +203,11 @@ void commitTransaction({required monero.PendingTransaction transactionPointer})
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
throw CreationTransactionException(message: error);
|
throw CreationTransactionException(message: error);
|
||||||
}
|
}
|
||||||
|
if (useUR) {
|
||||||
|
return txCommit as String?;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<PendingTransactionDescription> _createTransactionSync(Map args) async {
|
Future<PendingTransactionDescription> _createTransactionSync(Map args) async {
|
||||||
|
|
|
@ -425,3 +425,5 @@ Future<void> restoreFromSpendKey(
|
||||||
});
|
});
|
||||||
|
|
||||||
bool isWalletExist({required String path}) => _isWalletExist(path);
|
bool isWalletExist({required String path}) => _isWalletExist(path);
|
||||||
|
|
||||||
|
bool isViewOnlyBySpendKey() => int.tryParse(monero.Wallet_secretSpendKey(wptr!)) == 0;
|
|
@ -19,6 +19,7 @@ import 'package:cw_core/transaction_direction.dart';
|
||||||
import 'package:cw_core/unspent_coins_info.dart';
|
import 'package:cw_core/unspent_coins_info.dart';
|
||||||
import 'package:cw_core/wallet_base.dart';
|
import 'package:cw_core/wallet_base.dart';
|
||||||
import 'package:cw_core/wallet_info.dart';
|
import 'package:cw_core/wallet_info.dart';
|
||||||
|
import 'package:cw_monero/api/account_list.dart';
|
||||||
import 'package:cw_monero/api/coins_info.dart';
|
import 'package:cw_monero/api/coins_info.dart';
|
||||||
import 'package:cw_monero/api/monero_output.dart';
|
import 'package:cw_monero/api/monero_output.dart';
|
||||||
import 'package:cw_monero/api/structs/pending_transaction.dart';
|
import 'package:cw_monero/api/structs/pending_transaction.dart';
|
||||||
|
@ -121,6 +122,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MoneroWalletKeys get keys => MoneroWalletKeys(
|
MoneroWalletKeys get keys => MoneroWalletKeys(
|
||||||
|
primaryAddress: monero_wallet.getAddress(accountIndex: 0, addressIndex: 0),
|
||||||
privateSpendKey: monero_wallet.getSecretSpendKey(),
|
privateSpendKey: monero_wallet.getSecretSpendKey(),
|
||||||
privateViewKey: monero_wallet.getSecretViewKey(),
|
privateViewKey: monero_wallet.getSecretViewKey(),
|
||||||
publicSpendKey: monero_wallet.getPublicSpendKey(),
|
publicSpendKey: monero_wallet.getPublicSpendKey(),
|
||||||
|
@ -230,6 +232,36 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> submitTransactionUR(String ur) async {
|
||||||
|
final retStatus = monero.Wallet_submitTransactionUR(wptr!, ur);
|
||||||
|
final status = monero.Wallet_status(wptr!);
|
||||||
|
if (status != 0) {
|
||||||
|
final err = monero.Wallet_errorString(wptr!);
|
||||||
|
throw MoneroTransactionCreationException("unable to broadcast signed transaction: $err");
|
||||||
|
}
|
||||||
|
return retStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool importKeyImagesUR(String ur) {
|
||||||
|
final retStatus = monero.Wallet_importKeyImagesUR(wptr!, ur);
|
||||||
|
final status = monero.Wallet_status(wptr!);
|
||||||
|
if (status != 0) {
|
||||||
|
final err = monero.Wallet_errorString(wptr!);
|
||||||
|
throw Exception("unable to import key images: $err");
|
||||||
|
}
|
||||||
|
return retStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
String exportOutputsUR(bool all) {
|
||||||
|
final str = monero.Wallet_exportOutputsUR(wptr!, all: all);
|
||||||
|
final status = monero.Wallet_status(wptr!);
|
||||||
|
if (status != 0) {
|
||||||
|
final err = monero.Wallet_errorString(wptr!);
|
||||||
|
throw MoneroTransactionCreationException("unable to export UR: $err");
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<PendingTransaction> createTransaction(Object credentials) async {
|
Future<PendingTransaction> createTransaction(Object credentials) async {
|
||||||
final _credentials = credentials as MoneroTransactionCreationCredentials;
|
final _credentials = credentials as MoneroTransactionCreationCredentials;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:cw_monero/api/account_list.dart';
|
||||||
import 'package:cw_monero/api/structs/pending_transaction.dart';
|
import 'package:cw_monero/api/structs/pending_transaction.dart';
|
||||||
import 'package:cw_monero/api/transaction_history.dart'
|
import 'package:cw_monero/api/transaction_history.dart'
|
||||||
as monero_transaction_history;
|
as monero_transaction_history;
|
||||||
|
@ -35,11 +36,32 @@ class PendingMoneroTransaction with PendingTransaction {
|
||||||
String get feeFormatted => AmountConverter.amountIntToString(
|
String get feeFormatted => AmountConverter.amountIntToString(
|
||||||
CryptoCurrency.xmr, pendingTransactionDescription.fee);
|
CryptoCurrency.xmr, pendingTransactionDescription.fee);
|
||||||
|
|
||||||
|
bool shouldCommitUR() => isViewOnly;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> commit() async {
|
Future<void> commit() async {
|
||||||
try {
|
try {
|
||||||
monero_transaction_history.commitTransactionFromPointerAddress(
|
monero_transaction_history.commitTransactionFromPointerAddress(
|
||||||
address: pendingTransactionDescription.pointerAddress);
|
address: pendingTransactionDescription.pointerAddress,
|
||||||
|
useUR: false);
|
||||||
|
} catch (e) {
|
||||||
|
final message = e.toString();
|
||||||
|
|
||||||
|
if (message.contains('Reason: double spend')) {
|
||||||
|
throw DoubleSpendException();
|
||||||
|
}
|
||||||
|
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> commitUR() async {
|
||||||
|
try {
|
||||||
|
final ret = monero_transaction_history.commitTransactionFromPointerAddress(
|
||||||
|
address: pendingTransactionDescription.pointerAddress,
|
||||||
|
useUR: true);
|
||||||
|
return ret;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
final message = e.toString();
|
final message = e.toString();
|
||||||
|
|
||||||
|
|
|
@ -37,4 +37,9 @@ class PendingNanoTransaction with PendingTransaction {
|
||||||
await nanoClient.processBlock(block, "send");
|
await nanoClient.processBlock(block, "send");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> commitUR() {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
189
cw_shared_external/pubspec.lock
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
# Generated by pub
|
||||||
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
|
packages:
|
||||||
|
async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: async
|
||||||
|
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.11.0"
|
||||||
|
boolean_selector:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: boolean_selector
|
||||||
|
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
|
characters:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: characters
|
||||||
|
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
|
clock:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: clock
|
||||||
|
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
|
collection:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: collection
|
||||||
|
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.18.0"
|
||||||
|
fake_async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fake_async
|
||||||
|
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.1"
|
||||||
|
flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
flutter_test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
leak_tracker:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker
|
||||||
|
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "10.0.4"
|
||||||
|
leak_tracker_flutter_testing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker_flutter_testing
|
||||||
|
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.3"
|
||||||
|
leak_tracker_testing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker_testing
|
||||||
|
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.1"
|
||||||
|
matcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: matcher
|
||||||
|
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.12.16+1"
|
||||||
|
material_color_utilities:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: material_color_utilities
|
||||||
|
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.0"
|
||||||
|
meta:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: meta
|
||||||
|
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.12.0"
|
||||||
|
path:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path
|
||||||
|
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.9.0"
|
||||||
|
sky_engine:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.99"
|
||||||
|
source_span:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_span
|
||||||
|
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.10.0"
|
||||||
|
stack_trace:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stack_trace
|
||||||
|
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.11.1"
|
||||||
|
stream_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_channel
|
||||||
|
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
string_scanner:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: string_scanner
|
||||||
|
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
|
term_glyph:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: term_glyph
|
||||||
|
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
|
test_api:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: test_api
|
||||||
|
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.0"
|
||||||
|
vector_math:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_math
|
||||||
|
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
vm_service:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vm_service
|
||||||
|
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "14.2.1"
|
||||||
|
sdks:
|
||||||
|
dart: ">=3.3.0 <4.0.0"
|
||||||
|
flutter: ">=3.18.0-18.0.pre.54"
|
|
@ -40,4 +40,9 @@ class PendingSolanaTransaction with PendingTransaction {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get id => '';
|
String get id => '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> commitUR() {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,4 +30,9 @@ class PendingTronTransaction with PendingTransaction {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get id => '';
|
String get id => '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> commitUR() {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,5 +3,6 @@ class WalletRestoreFromKeysException implements Exception {
|
||||||
|
|
||||||
final String message;
|
final String message;
|
||||||
|
|
||||||
|
@override
|
||||||
String toString() => message;
|
String toString() => message;
|
||||||
}
|
}
|
|
@ -50,4 +50,9 @@ class PendingWowneroTransaction with PendingTransaction {
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> commitUR() {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,6 +120,7 @@ abstract class WowneroWalletBase
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MoneroWalletKeys get keys => MoneroWalletKeys(
|
MoneroWalletKeys get keys => MoneroWalletKeys(
|
||||||
|
primaryAddress: wownero_wallet.getAddress(accountIndex: 0, addressIndex: 0),
|
||||||
privateSpendKey: wownero_wallet.getSecretSpendKey(),
|
privateSpendKey: wownero_wallet.getSecretSpendKey(),
|
||||||
privateViewKey: wownero_wallet.getSecretViewKey(),
|
privateViewKey: wownero_wallet.getSecretViewKey(),
|
||||||
publicSpendKey: wownero_wallet.getPublicSpendKey(),
|
publicSpendKey: wownero_wallet.getPublicSpendKey(),
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import 'package:cake_wallet/buy/buy_amount.dart';
|
import 'package:cake_wallet/buy/buy_amount.dart';
|
||||||
|
import 'package:cake_wallet/buy/buy_quote.dart';
|
||||||
import 'package:cake_wallet/buy/order.dart';
|
import 'package:cake_wallet/buy/order.dart';
|
||||||
|
import 'package:cake_wallet/buy/payment_method.dart';
|
||||||
|
import 'package:cake_wallet/entities/fiat_currency.dart';
|
||||||
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
|
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
|
||||||
|
import 'package:cw_core/crypto_currency.dart';
|
||||||
import 'package:cw_core/wallet_base.dart';
|
import 'package:cw_core/wallet_base.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
@ -23,14 +27,38 @@ abstract class BuyProvider {
|
||||||
|
|
||||||
String get darkIcon;
|
String get darkIcon;
|
||||||
|
|
||||||
|
bool get isAggregator;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => title;
|
String toString() => title;
|
||||||
|
|
||||||
Future<void> launchProvider(BuildContext context, bool? isBuyAction);
|
Future<void>? launchProvider(
|
||||||
|
{required BuildContext context,
|
||||||
|
required Quote quote,
|
||||||
|
required double amount,
|
||||||
|
required bool isBuyAction,
|
||||||
|
required String cryptoCurrencyAddress,
|
||||||
|
String? countryCode}) =>
|
||||||
|
null;
|
||||||
|
|
||||||
Future<String> requestUrl(String amount, String sourceCurrency) => throw UnimplementedError();
|
Future<String> requestUrl(String amount, String sourceCurrency) => throw UnimplementedError();
|
||||||
|
|
||||||
Future<Order> findOrderById(String id) => throw UnimplementedError();
|
Future<Order> findOrderById(String id) => throw UnimplementedError();
|
||||||
|
|
||||||
Future<BuyAmount> calculateAmount(String amount, String sourceCurrency) => throw UnimplementedError();
|
Future<BuyAmount> calculateAmount(String amount, String sourceCurrency) =>
|
||||||
|
throw UnimplementedError();
|
||||||
|
|
||||||
|
Future<List<PaymentMethod>> getAvailablePaymentTypes(
|
||||||
|
String fiatCurrency, String cryptoCurrency, bool isBuyAction) async =>
|
||||||
|
[];
|
||||||
|
|
||||||
|
Future<List<Quote>?> fetchQuote(
|
||||||
|
{required CryptoCurrency cryptoCurrency,
|
||||||
|
required FiatCurrency fiatCurrency,
|
||||||
|
required double amount,
|
||||||
|
required bool isBuyAction,
|
||||||
|
required String walletAddress,
|
||||||
|
PaymentType? paymentType,
|
||||||
|
String? countryCode}) async =>
|
||||||
|
null;
|
||||||
}
|
}
|
||||||
|
|
302
lib/buy/buy_quote.dart
Normal file
|
@ -0,0 +1,302 @@
|
||||||
|
import 'package:cake_wallet/buy/buy_provider.dart';
|
||||||
|
import 'package:cake_wallet/buy/payment_method.dart';
|
||||||
|
import 'package:cake_wallet/core/selectable_option.dart';
|
||||||
|
import 'package:cake_wallet/entities/fiat_currency.dart';
|
||||||
|
import 'package:cake_wallet/entities/provider_types.dart';
|
||||||
|
import 'package:cake_wallet/exchange/limits.dart';
|
||||||
|
import 'package:cw_core/crypto_currency.dart';
|
||||||
|
|
||||||
|
enum ProviderRecommendation { bestRate, lowKyc, successRate }
|
||||||
|
|
||||||
|
extension RecommendationTitle on ProviderRecommendation {
|
||||||
|
String get title {
|
||||||
|
switch (this) {
|
||||||
|
case ProviderRecommendation.bestRate:
|
||||||
|
return 'BEST RATE';
|
||||||
|
case ProviderRecommendation.lowKyc:
|
||||||
|
return 'LOW KYC';
|
||||||
|
case ProviderRecommendation.successRate:
|
||||||
|
return 'SUCCESS RATE';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ProviderRecommendation? getRecommendationFromString(String title) {
|
||||||
|
switch (title) {
|
||||||
|
case 'BEST RATE':
|
||||||
|
return ProviderRecommendation.bestRate;
|
||||||
|
case 'LowKyc':
|
||||||
|
return ProviderRecommendation.lowKyc;
|
||||||
|
case 'SuccessRate':
|
||||||
|
return ProviderRecommendation.successRate;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Quote extends SelectableOption {
|
||||||
|
Quote({
|
||||||
|
required this.rate,
|
||||||
|
required this.feeAmount,
|
||||||
|
required this.networkFee,
|
||||||
|
required this.transactionFee,
|
||||||
|
required this.payout,
|
||||||
|
required this.provider,
|
||||||
|
required this.paymentType,
|
||||||
|
required this.recommendations,
|
||||||
|
this.isBuyAction = true,
|
||||||
|
this.quoteId,
|
||||||
|
this.rampId,
|
||||||
|
this.rampName,
|
||||||
|
this.rampIconPath,
|
||||||
|
this.limits,
|
||||||
|
}) : super(title: provider.isAggregator ? rampName ?? '' : provider.title);
|
||||||
|
|
||||||
|
final double rate;
|
||||||
|
final double feeAmount;
|
||||||
|
final double networkFee;
|
||||||
|
final double transactionFee;
|
||||||
|
final double payout;
|
||||||
|
final PaymentType paymentType;
|
||||||
|
final BuyProvider provider;
|
||||||
|
final String? quoteId;
|
||||||
|
final List<ProviderRecommendation> recommendations;
|
||||||
|
String? rampId;
|
||||||
|
String? rampName;
|
||||||
|
String? rampIconPath;
|
||||||
|
bool _isSelected = false;
|
||||||
|
bool _isBestRate = false;
|
||||||
|
bool isBuyAction;
|
||||||
|
Limits? limits;
|
||||||
|
|
||||||
|
late FiatCurrency _fiatCurrency;
|
||||||
|
late CryptoCurrency _cryptoCurrency;
|
||||||
|
|
||||||
|
|
||||||
|
bool get isSelected => _isSelected;
|
||||||
|
bool get isBestRate => _isBestRate;
|
||||||
|
FiatCurrency get fiatCurrency => _fiatCurrency;
|
||||||
|
CryptoCurrency get cryptoCurrency => _cryptoCurrency;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isOptionSelected => this._isSelected;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lightIconPath =>
|
||||||
|
provider.isAggregator ? rampIconPath ?? provider.lightIcon : provider.lightIcon;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get darkIconPath =>
|
||||||
|
provider.isAggregator ? rampIconPath ?? provider.darkIcon : provider.darkIcon;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<String> get badges => recommendations.map((e) => e.title).toList();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get topLeftSubTitle =>
|
||||||
|
this.rate > 0 ? '1 $cryptoName = ${rate.toStringAsFixed(2)} $fiatName' : '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get bottomLeftSubTitle {
|
||||||
|
if (limits != null) {
|
||||||
|
final min = limits!.min;
|
||||||
|
final max = limits!.max;
|
||||||
|
return 'min: ${min} ${fiatCurrency.toString()} | max: ${max == double.infinity ? '' : '${max} ${fiatCurrency.toString()}'}';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get fiatName => isBuyAction ? fiatCurrency.toString() : cryptoCurrency.toString();
|
||||||
|
|
||||||
|
String get cryptoName => isBuyAction ? cryptoCurrency.toString() : fiatCurrency.toString();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get topRightSubTitle => '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get topRightSubTitleLightIconPath => provider.isAggregator ? provider.lightIcon : '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get topRightSubTitleDarkIconPath => provider.isAggregator ? provider.darkIcon : '';
|
||||||
|
|
||||||
|
String get quoteTitle => '${provider.title} - ${paymentType.name}';
|
||||||
|
|
||||||
|
String get formatedFee => '$feeAmount ${isBuyAction ? fiatCurrency : cryptoCurrency}';
|
||||||
|
|
||||||
|
set setIsSelected(bool isSelected) => _isSelected = isSelected;
|
||||||
|
set setIsBestRate(bool isBestRate) => _isBestRate = isBestRate;
|
||||||
|
set setFiatCurrency(FiatCurrency fiatCurrency) => _fiatCurrency = fiatCurrency;
|
||||||
|
set setCryptoCurrency(CryptoCurrency cryptoCurrency) => _cryptoCurrency = cryptoCurrency;
|
||||||
|
set setLimits(Limits limits) => this.limits = limits;
|
||||||
|
|
||||||
|
factory Quote.fromOnramperJson(Map<String, dynamic> json, bool isBuyAction,
|
||||||
|
Map<String, dynamic> metaData, PaymentType paymentType) {
|
||||||
|
final rate = _toDouble(json['rate']) ?? 0.0;
|
||||||
|
final networkFee = _toDouble(json['networkFee']) ?? 0.0;
|
||||||
|
final transactionFee = _toDouble(json['transactionFee']) ?? 0.0;
|
||||||
|
final feeAmount = double.parse((networkFee + transactionFee).toStringAsFixed(2));
|
||||||
|
|
||||||
|
final rampId = json['ramp'] as String? ?? '';
|
||||||
|
final rampData = metaData[rampId] ?? {};
|
||||||
|
final rampName = rampData['displayName'] as String? ?? '';
|
||||||
|
final rampIconPath = rampData['svg'] as String? ?? '';
|
||||||
|
|
||||||
|
final recommendations = json['recommendations'] != null
|
||||||
|
? List<String>.from(json['recommendations'] as List<dynamic>)
|
||||||
|
: <String>[];
|
||||||
|
|
||||||
|
final enumRecommendations = recommendations
|
||||||
|
.map((e) => getRecommendationFromString(e))
|
||||||
|
.whereType<ProviderRecommendation>()
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final availablePaymentMethods = json['availablePaymentMethods'] as List<dynamic>? ?? [];
|
||||||
|
double minLimit = 0.0;
|
||||||
|
double maxLimit = double.infinity;
|
||||||
|
|
||||||
|
for (var paymentMethod in availablePaymentMethods) {
|
||||||
|
if (paymentMethod is Map<String, dynamic>) {
|
||||||
|
final details = paymentMethod['details'] as Map<String, dynamic>?;
|
||||||
|
|
||||||
|
if (details != null) {
|
||||||
|
final limits = details['limits'] as Map<String, dynamic>?;
|
||||||
|
|
||||||
|
if (limits != null && limits.isNotEmpty) {
|
||||||
|
final firstLimitEntry = limits.values.first as Map<String, dynamic>?;
|
||||||
|
if (firstLimitEntry != null) {
|
||||||
|
minLimit = _toDouble(firstLimitEntry['min'])?.roundToDouble() ?? 0.0;
|
||||||
|
maxLimit = _toDouble(firstLimitEntry['max'])?.roundToDouble() ?? double.infinity;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Quote(
|
||||||
|
rate: rate,
|
||||||
|
feeAmount: feeAmount,
|
||||||
|
networkFee: networkFee,
|
||||||
|
transactionFee: transactionFee,
|
||||||
|
payout: json['payout'] as double? ?? 0.0,
|
||||||
|
rampId: rampId,
|
||||||
|
rampName: rampName,
|
||||||
|
rampIconPath: rampIconPath,
|
||||||
|
paymentType: paymentType,
|
||||||
|
quoteId: json['quoteId'] as String? ?? '',
|
||||||
|
recommendations: enumRecommendations,
|
||||||
|
provider: ProvidersHelper.getProviderByType(ProviderType.onramper)!,
|
||||||
|
isBuyAction: isBuyAction,
|
||||||
|
limits: Limits(min: minLimit, max: maxLimit),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory Quote.fromMoonPayJson(
|
||||||
|
Map<String, dynamic> json, bool isBuyAction, PaymentType paymentType) {
|
||||||
|
final rate = isBuyAction
|
||||||
|
? json['quoteCurrencyPrice'] as double? ?? 0.0
|
||||||
|
: json['baseCurrencyPrice'] as double? ?? 0.0;
|
||||||
|
final fee = _toDouble(json['feeAmount']) ?? 0.0;
|
||||||
|
final networkFee = _toDouble(json['networkFeeAmount']) ?? 0.0;
|
||||||
|
final transactionFee = _toDouble(json['extraFeeAmount']) ?? 0.0;
|
||||||
|
final feeAmount = double.parse((fee + networkFee + transactionFee).toStringAsFixed(2));
|
||||||
|
|
||||||
|
final baseCurrency = json['baseCurrency'] as Map<String, dynamic>?;
|
||||||
|
|
||||||
|
double minLimit = 0.0;
|
||||||
|
double maxLimit = double.infinity;
|
||||||
|
|
||||||
|
if (baseCurrency != null) {
|
||||||
|
minLimit = _toDouble(baseCurrency['minAmount']) ?? minLimit;
|
||||||
|
maxLimit = _toDouble(baseCurrency['maxAmount']) ?? maxLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Quote(
|
||||||
|
rate: rate,
|
||||||
|
feeAmount: feeAmount,
|
||||||
|
networkFee: networkFee,
|
||||||
|
transactionFee: transactionFee,
|
||||||
|
payout: _toDouble(json['quoteCurrencyAmount']) ?? 0.0,
|
||||||
|
paymentType: paymentType,
|
||||||
|
recommendations: [],
|
||||||
|
quoteId: json['signature'] as String? ?? '',
|
||||||
|
provider: ProvidersHelper.getProviderByType(ProviderType.moonpay)!,
|
||||||
|
isBuyAction: isBuyAction,
|
||||||
|
limits: Limits(min: minLimit, max: maxLimit),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory Quote.fromDFXJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
bool isBuyAction,
|
||||||
|
PaymentType paymentType,
|
||||||
|
) {
|
||||||
|
final rate = _toDouble(json['exchangeRate']) ?? 0.0;
|
||||||
|
final fees = json['fees'] as Map<String, dynamic>;
|
||||||
|
|
||||||
|
final minVolume = _toDouble(json['minVolume']) ?? 0.0;
|
||||||
|
final maxVolume = _toDouble(json['maxVolume']) ?? double.infinity;
|
||||||
|
|
||||||
|
return Quote(
|
||||||
|
rate: isBuyAction ? rate : 1 / rate,
|
||||||
|
feeAmount: _toDouble(json['feeAmount']) ?? 0.0,
|
||||||
|
networkFee: _toDouble(fees['network']) ?? 0.0,
|
||||||
|
transactionFee: _toDouble(fees['rate']) ?? 0.0,
|
||||||
|
payout: _toDouble(json['payout']) ?? 0.0,
|
||||||
|
paymentType: paymentType,
|
||||||
|
recommendations: [ProviderRecommendation.lowKyc],
|
||||||
|
provider: ProvidersHelper.getProviderByType(ProviderType.dfx)!,
|
||||||
|
isBuyAction: isBuyAction,
|
||||||
|
limits: Limits(min: minVolume, max: maxVolume),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory Quote.fromRobinhoodJson(
|
||||||
|
Map<String, dynamic> json, bool isBuyAction, PaymentType paymentType) {
|
||||||
|
final networkFee = json['networkFee'] as Map<String, dynamic>;
|
||||||
|
final processingFee = json['processingFee'] as Map<String, dynamic>;
|
||||||
|
final networkFeeAmount = _toDouble(networkFee['fiatAmount']) ?? 0.0;
|
||||||
|
final transactionFeeAmount = _toDouble(processingFee['fiatAmount']) ?? 0.0;
|
||||||
|
final feeAmount = double.parse((networkFeeAmount + transactionFeeAmount).toStringAsFixed(2));
|
||||||
|
|
||||||
|
return Quote(
|
||||||
|
rate: _toDouble(json['price']) ?? 0.0,
|
||||||
|
feeAmount: feeAmount,
|
||||||
|
networkFee: _toDouble(networkFee['fiatAmount']) ?? 0.0,
|
||||||
|
transactionFee: _toDouble(processingFee['fiatAmount']) ?? 0.0,
|
||||||
|
payout: _toDouble(json['cryptoAmount']) ?? 0.0,
|
||||||
|
paymentType: paymentType,
|
||||||
|
recommendations: [],
|
||||||
|
provider: ProvidersHelper.getProviderByType(ProviderType.robinhood)!,
|
||||||
|
isBuyAction: isBuyAction,
|
||||||
|
limits: Limits(min: 0.0, max: double.infinity),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory Quote.fromMeldJson(Map<String, dynamic> json, bool isBuyAction, PaymentType paymentType) {
|
||||||
|
final quotes = json['quotes'][0] as Map<String, dynamic>;
|
||||||
|
return Quote(
|
||||||
|
rate: quotes['exchangeRate'] as double? ?? 0.0,
|
||||||
|
feeAmount: quotes['totalFee'] as double? ?? 0.0,
|
||||||
|
networkFee: quotes['networkFee'] as double? ?? 0.0,
|
||||||
|
transactionFee: quotes['transactionFee'] as double? ?? 0.0,
|
||||||
|
payout: quotes['payout'] as double? ?? 0.0,
|
||||||
|
paymentType: paymentType,
|
||||||
|
recommendations: [],
|
||||||
|
provider: ProvidersHelper.getProviderByType(ProviderType.meld)!,
|
||||||
|
isBuyAction: isBuyAction,
|
||||||
|
limits: Limits(min: 0.0, max: double.infinity),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static double? _toDouble(dynamic value) {
|
||||||
|
if (value is int) {
|
||||||
|
return value.toDouble();
|
||||||
|
} else if (value is double) {
|
||||||
|
return value;
|
||||||
|
} else if (value is String) {
|
||||||
|
return double.tryParse(value);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,17 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:cake_wallet/buy/buy_provider.dart';
|
import 'package:cake_wallet/buy/buy_provider.dart';
|
||||||
|
import 'package:cake_wallet/buy/buy_quote.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/generated/i18n.dart';
|
||||||
import 'package:cake_wallet/routes.dart';
|
import 'package:cake_wallet/routes.dart';
|
||||||
import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart';
|
import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart';
|
||||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||||
import 'package:cake_wallet/utils/device_info.dart';
|
|
||||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||||
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
|
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
|
||||||
|
import 'package:cw_core/crypto_currency.dart';
|
||||||
import 'package:cw_core/wallet_base.dart';
|
import 'package:cw_core/wallet_base.dart';
|
||||||
import 'package:cw_core/wallet_type.dart';
|
import 'package:cw_core/wallet_type.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -15,10 +19,12 @@ import 'package:http/http.dart' as http;
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class DFXBuyProvider extends BuyProvider {
|
class DFXBuyProvider extends BuyProvider {
|
||||||
DFXBuyProvider({required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM})
|
DFXBuyProvider(
|
||||||
|
{required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM})
|
||||||
: super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: ledgerVM);
|
: super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: ledgerVM);
|
||||||
|
|
||||||
static const _baseUrl = 'api.dfx.swiss';
|
static const _baseUrl = 'api.dfx.swiss';
|
||||||
|
|
||||||
// static const _signMessagePath = '/v1/auth/signMessage';
|
// static const _signMessagePath = '/v1/auth/signMessage';
|
||||||
static const _authPath = '/v1/auth';
|
static const _authPath = '/v1/auth';
|
||||||
static const walletName = 'CakeWallet';
|
static const walletName = 'CakeWallet';
|
||||||
|
@ -35,24 +41,8 @@ class DFXBuyProvider extends BuyProvider {
|
||||||
@override
|
@override
|
||||||
String get darkIcon => 'assets/images/dfx_dark.png';
|
String get darkIcon => 'assets/images/dfx_dark.png';
|
||||||
|
|
||||||
String get assetOut {
|
@override
|
||||||
switch (wallet.type) {
|
bool get isAggregator => false;
|
||||||
case WalletType.bitcoin:
|
|
||||||
return 'BTC';
|
|
||||||
case WalletType.bitcoinCash:
|
|
||||||
return 'BCH';
|
|
||||||
case WalletType.litecoin:
|
|
||||||
return 'LTC';
|
|
||||||
case WalletType.monero:
|
|
||||||
return 'XMR';
|
|
||||||
case WalletType.ethereum:
|
|
||||||
return 'ETH';
|
|
||||||
case WalletType.polygon:
|
|
||||||
return 'MATIC';
|
|
||||||
default:
|
|
||||||
throw Exception("WalletType is not available for DFX ${wallet.type}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String get blockchain {
|
String get blockchain {
|
||||||
switch (wallet.type) {
|
switch (wallet.type) {
|
||||||
|
@ -60,21 +50,13 @@ class DFXBuyProvider extends BuyProvider {
|
||||||
case WalletType.bitcoinCash:
|
case WalletType.bitcoinCash:
|
||||||
case WalletType.litecoin:
|
case WalletType.litecoin:
|
||||||
return 'Bitcoin';
|
return 'Bitcoin';
|
||||||
case WalletType.monero:
|
|
||||||
return 'Monero';
|
|
||||||
case WalletType.ethereum:
|
|
||||||
return 'Ethereum';
|
|
||||||
case WalletType.polygon:
|
|
||||||
return 'Polygon';
|
|
||||||
default:
|
default:
|
||||||
throw Exception("WalletType is not available for DFX ${wallet.type}");
|
return walletTypeToString(wallet.type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String get walletAddress =>
|
|
||||||
wallet.walletAddresses.primaryAddress ?? wallet.walletAddresses.address;
|
|
||||||
|
|
||||||
Future<String> getSignMessage() async =>
|
Future<String> getSignMessage(String walletAddress) async =>
|
||||||
"By_signing_this_message,_you_confirm_that_you_are_the_sole_owner_of_the_provided_Blockchain_address._Your_ID:_$walletAddress";
|
"By_signing_this_message,_you_confirm_that_you_are_the_sole_owner_of_the_provided_Blockchain_address._Your_ID:_$walletAddress";
|
||||||
|
|
||||||
// // Lets keep this just in case, but we can avoid this API Call
|
// // Lets keep this just in case, but we can avoid this API Call
|
||||||
|
@ -92,8 +74,9 @@ class DFXBuyProvider extends BuyProvider {
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
Future<String> auth() async {
|
Future<String> auth(String walletAddress) async {
|
||||||
final signMessage = await getSignature(await getSignMessage());
|
final signMessage = await getSignature(
|
||||||
|
await getSignMessage(walletAddress), walletAddress);
|
||||||
|
|
||||||
final requestBody = jsonEncode({
|
final requestBody = jsonEncode({
|
||||||
'wallet': walletName,
|
'wallet': walletName,
|
||||||
|
@ -120,7 +103,7 @@ class DFXBuyProvider extends BuyProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> getSignature(String message) async {
|
Future<String> getSignature(String message, String walletAddress) async {
|
||||||
switch (wallet.type) {
|
switch (wallet.type) {
|
||||||
case WalletType.ethereum:
|
case WalletType.ethereum:
|
||||||
case WalletType.polygon:
|
case WalletType.polygon:
|
||||||
|
@ -135,8 +118,178 @@ class DFXBuyProvider extends BuyProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> fetchFiatCredentials(String fiatCurrency) async {
|
||||||
|
final url = Uri.https(_baseUrl, '/v1/fiat');
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await http.get(url, headers: {'accept': 'application/json'});
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final data = jsonDecode(response.body) as List<dynamic>;
|
||||||
|
for (final item in data) {
|
||||||
|
if (item['name'] == fiatCurrency) return item as Map<String, dynamic>;
|
||||||
|
}
|
||||||
|
log('DFX does not support fiat: $fiatCurrency');
|
||||||
|
return {};
|
||||||
|
} else {
|
||||||
|
log('DFX Failed to fetch fiat currencies: ${response.statusCode}');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('DFX Error fetching fiat currencies: $e');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> fetchAssetCredential(String assetsName) async {
|
||||||
|
final url = Uri.https(_baseUrl, '/v1/asset', {'blockchains': blockchain});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await http.get(url, headers: {'accept': 'application/json'});
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final responseData = jsonDecode(response.body);
|
||||||
|
|
||||||
|
if (responseData is List && responseData.isNotEmpty) {
|
||||||
|
return responseData.first as Map<String, dynamic>;
|
||||||
|
} else if (responseData is Map<String, dynamic>) {
|
||||||
|
return responseData;
|
||||||
|
} else {
|
||||||
|
log('DFX: Does not support this asset name : ${blockchain}');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('DFX: Failed to fetch assets: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log('DFX: Error fetching assets: $e');
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<PaymentMethod>> getAvailablePaymentTypes(
|
||||||
|
String fiatCurrency, String cryptoCurrency, bool isBuyAction) async {
|
||||||
|
final List<PaymentMethod> paymentMethods = [];
|
||||||
|
|
||||||
|
if (isBuyAction) {
|
||||||
|
final fiatBuyCredentials = await fetchFiatCredentials(fiatCurrency);
|
||||||
|
if (fiatBuyCredentials.isNotEmpty) {
|
||||||
|
fiatBuyCredentials.forEach((key, value) {
|
||||||
|
if (key == 'limits') {
|
||||||
|
final limits = value as Map<String, dynamic>;
|
||||||
|
limits.forEach((paymentMethodKey, paymentMethodValue) {
|
||||||
|
final min = _toDouble(paymentMethodValue['minVolume']);
|
||||||
|
final max = _toDouble(paymentMethodValue['maxVolume']);
|
||||||
|
if (min != null && max != null && min > 0 && max > 0) {
|
||||||
|
final paymentMethod = PaymentMethod.fromDFX(
|
||||||
|
paymentMethodKey, _getPaymentTypeByString(paymentMethodKey));
|
||||||
|
paymentMethods.add(paymentMethod);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final assetCredentials = await fetchAssetCredential(cryptoCurrency);
|
||||||
|
if (assetCredentials.isNotEmpty) {
|
||||||
|
if (assetCredentials['sellable'] == true) {
|
||||||
|
final availablePaymentTypes = [
|
||||||
|
PaymentType.bankTransfer,
|
||||||
|
PaymentType.creditCard,
|
||||||
|
PaymentType.sepa
|
||||||
|
];
|
||||||
|
availablePaymentTypes.forEach((element) {
|
||||||
|
final paymentMethod = PaymentMethod.fromDFX(normalizePaymentMethod(element)!, element);
|
||||||
|
paymentMethods.add(paymentMethod);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return paymentMethods;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
|
Future<List<Quote>?> fetchQuote(
|
||||||
|
{required CryptoCurrency cryptoCurrency,
|
||||||
|
required FiatCurrency fiatCurrency,
|
||||||
|
required double amount,
|
||||||
|
required bool isBuyAction,
|
||||||
|
required String walletAddress,
|
||||||
|
PaymentType? paymentType,
|
||||||
|
String? countryCode}) async {
|
||||||
|
/// if buying with any currency other than eur or chf then DFX is not supported
|
||||||
|
|
||||||
|
if (isBuyAction && (fiatCurrency != FiatCurrency.eur && fiatCurrency != FiatCurrency.chf)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? paymentMethod;
|
||||||
|
if (paymentType != null && paymentType != PaymentType.all) {
|
||||||
|
paymentMethod = normalizePaymentMethod(paymentType);
|
||||||
|
if (paymentMethod == null) paymentMethod = paymentType.name;
|
||||||
|
} else {
|
||||||
|
paymentMethod = 'Bank';
|
||||||
|
}
|
||||||
|
|
||||||
|
final action = isBuyAction ? 'buy' : 'sell';
|
||||||
|
|
||||||
|
if (isBuyAction && cryptoCurrency != wallet.currency) return null;
|
||||||
|
|
||||||
|
final fiatCredentials = await fetchFiatCredentials(fiatCurrency.name.toString());
|
||||||
|
if (fiatCredentials['id'] == null) return null;
|
||||||
|
|
||||||
|
final assetCredentials = await fetchAssetCredential(cryptoCurrency.title.toString());
|
||||||
|
if (assetCredentials['id'] == null) return null;
|
||||||
|
|
||||||
|
log('DFX: Fetching $action quote: ${isBuyAction ? cryptoCurrency : fiatCurrency} -> ${isBuyAction ? fiatCurrency : cryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod');
|
||||||
|
|
||||||
|
final url = Uri.https(_baseUrl, '/v1/$action/quote');
|
||||||
|
final headers = {'accept': 'application/json', 'Content-Type': 'application/json'};
|
||||||
|
final body = jsonEncode({
|
||||||
|
'currency': {'id': fiatCredentials['id'] as int},
|
||||||
|
'asset': {'id': assetCredentials['id']},
|
||||||
|
'amount': amount,
|
||||||
|
'targetAmount': 0,
|
||||||
|
'paymentMethod': paymentMethod,
|
||||||
|
'discountCode': ''
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await http.put(url, headers: headers, body: body);
|
||||||
|
final responseData = jsonDecode(response.body);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
if (responseData is Map<String, dynamic>) {
|
||||||
|
final paymentType = _getPaymentTypeByString(responseData['paymentMethod'] as String?);
|
||||||
|
final quote = Quote.fromDFXJson(responseData, isBuyAction, paymentType);
|
||||||
|
quote.setFiatCurrency = fiatCurrency;
|
||||||
|
quote.setCryptoCurrency = cryptoCurrency;
|
||||||
|
return [quote];
|
||||||
|
} else {
|
||||||
|
print('DFX: Unexpected data type: ${responseData.runtimeType}');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (responseData is Map<String, dynamic> && responseData.containsKey('message')) {
|
||||||
|
print('DFX Error: ${responseData['message']}');
|
||||||
|
} else {
|
||||||
|
print('DFX Failed to fetch buy quote: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('DFX Error fetching buy quote: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void>? launchProvider(
|
||||||
|
{required BuildContext context,
|
||||||
|
required Quote quote,
|
||||||
|
required double amount,
|
||||||
|
required bool isBuyAction,
|
||||||
|
required String cryptoCurrencyAddress,
|
||||||
|
String? countryCode}) async {
|
||||||
if (wallet.isHardwareWallet) {
|
if (wallet.isHardwareWallet) {
|
||||||
if (!ledgerVM!.isConnected) {
|
if (!ledgerVM!.isConnected) {
|
||||||
await Navigator.of(context).pushNamed(Routes.connectDevices,
|
await Navigator.of(context).pushNamed(Routes.connectDevices,
|
||||||
|
@ -152,26 +305,21 @@ class DFXBuyProvider extends BuyProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final assetOut = this.assetOut;
|
final actionType = isBuyAction ? '/buy' : '/sell';
|
||||||
final blockchain = this.blockchain;
|
|
||||||
final actionType = isBuyAction == true ? '/buy' : '/sell';
|
|
||||||
|
|
||||||
final accessToken = await auth();
|
final accessToken = await auth(cryptoCurrencyAddress);
|
||||||
|
|
||||||
final uri = Uri.https('services.dfx.swiss', actionType, {
|
final uri = Uri.https('services.dfx.swiss', actionType, {
|
||||||
'session': accessToken,
|
'session': accessToken,
|
||||||
'lang': 'en',
|
'lang': 'en',
|
||||||
'asset-out': assetOut,
|
'asset-out': isBuyAction ? quote.cryptoCurrency.toString() : quote.fiatCurrency.toString(),
|
||||||
'blockchain': blockchain,
|
'blockchain': blockchain,
|
||||||
'asset-in': 'EUR',
|
'asset-in': isBuyAction ? quote.fiatCurrency.toString() : quote.cryptoCurrency.toString(),
|
||||||
|
'amount': amount.toString() //TODO: Amount does not work
|
||||||
});
|
});
|
||||||
|
|
||||||
if (await canLaunchUrl(uri)) {
|
if (await canLaunchUrl(uri)) {
|
||||||
if (DeviceInfo.instance.isMobile) {
|
|
||||||
Navigator.of(context).pushNamed(Routes.webViewPage, arguments: [title, uri]);
|
|
||||||
} else {
|
|
||||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Could not launch URL');
|
throw Exception('Could not launch URL');
|
||||||
}
|
}
|
||||||
|
@ -187,4 +335,39 @@ class DFXBuyProvider extends BuyProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? normalizePaymentMethod(PaymentType paymentMethod) {
|
||||||
|
switch (paymentMethod) {
|
||||||
|
case PaymentType.bankTransfer:
|
||||||
|
return 'Bank';
|
||||||
|
case PaymentType.creditCard:
|
||||||
|
return 'Card';
|
||||||
|
case PaymentType.sepa:
|
||||||
|
return 'Instant';
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PaymentType _getPaymentTypeByString(String? paymentMethod) {
|
||||||
|
switch (paymentMethod) {
|
||||||
|
case 'Bank':
|
||||||
|
return PaymentType.bankTransfer;
|
||||||
|
case 'Card':
|
||||||
|
return PaymentType.creditCard;
|
||||||
|
case 'Instant':
|
||||||
|
return PaymentType.sepa;
|
||||||
|
default:
|
||||||
|
return PaymentType.all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double? _toDouble(dynamic value) {
|
||||||
|
if (value is int) {
|
||||||
|
return value.toDouble();
|
||||||
|
} else if (value is double) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
266
lib/buy/meld/meld_buy_provider.dart
Normal file
|
@ -0,0 +1,266 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
||||||
|
import 'package:cake_wallet/buy/buy_provider.dart';
|
||||||
|
import 'package:cake_wallet/buy/buy_quote.dart';
|
||||||
|
import 'package:cake_wallet/buy/payment_method.dart';
|
||||||
|
import 'package:cake_wallet/entities/fiat_currency.dart';
|
||||||
|
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/src/widgets/alert_with_one_action.dart';
|
||||||
|
import 'package:cake_wallet/utils/device_info.dart';
|
||||||
|
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||||
|
import 'package:cw_core/crypto_currency.dart';
|
||||||
|
import 'package:cw_core/currency.dart';
|
||||||
|
import 'package:cw_core/wallet_base.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'dart:developer';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
class MeldBuyProvider extends BuyProvider {
|
||||||
|
MeldBuyProvider({required WalletBase wallet, bool isTestEnvironment = false})
|
||||||
|
: super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: null);
|
||||||
|
|
||||||
|
static const _isProduction = false;
|
||||||
|
|
||||||
|
static const _baseUrl = _isProduction ? 'api.meld.io' : 'api-sb.meld.io';
|
||||||
|
static const _providersProperties = '/service-providers/properties';
|
||||||
|
static const _paymentMethodsPath = '/payment-methods';
|
||||||
|
static const _quotePath = '/payments/crypto/quote';
|
||||||
|
|
||||||
|
static const String sandboxUrl = 'sb.fluidmoney.xyz';
|
||||||
|
static const String productionUrl = 'fluidmoney.xyz';
|
||||||
|
|
||||||
|
static const String _baseWidgetUrl = _isProduction ? productionUrl : sandboxUrl;
|
||||||
|
|
||||||
|
static String get _testApiKey => secrets.meldTestApiKey;
|
||||||
|
|
||||||
|
static String get _testPublicKey => '' ; //secrets.meldTestPublicKey;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get title => 'Meld';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get providerDescription => 'Meld Buy Provider';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lightIcon => 'assets/images/meld_logo.svg';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get darkIcon => 'assets/images/meld_logo.svg';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isAggregator => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<PaymentMethod>> getAvailablePaymentTypes(
|
||||||
|
String fiatCurrency, String cryptoCurrency, bool isBuyAction) async {
|
||||||
|
final params = {'fiatCurrencies': fiatCurrency, 'statuses': 'LIVE,RECENTLY_ADDED,BUILDING'};
|
||||||
|
|
||||||
|
final path = '$_providersProperties$_paymentMethodsPath';
|
||||||
|
final url = Uri.https(_baseUrl, path, params);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await http.get(
|
||||||
|
url,
|
||||||
|
headers: {
|
||||||
|
'Authorization': _isProduction ? '' : _testApiKey,
|
||||||
|
'Meld-Version': '2023-12-19',
|
||||||
|
'accept': 'application/json',
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final data = jsonDecode(response.body) as List<dynamic>;
|
||||||
|
final paymentMethods =
|
||||||
|
data.map((e) => PaymentMethod.fromMeldJson(e as Map<String, dynamic>)).toList();
|
||||||
|
return paymentMethods;
|
||||||
|
} else {
|
||||||
|
print('Meld: Failed to fetch payment types');
|
||||||
|
return List<PaymentMethod>.empty();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Meld: Failed to fetch payment types: $e');
|
||||||
|
return List<PaymentMethod>.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Quote>?> fetchQuote(
|
||||||
|
{required CryptoCurrency cryptoCurrency,
|
||||||
|
required FiatCurrency fiatCurrency,
|
||||||
|
required double amount,
|
||||||
|
required bool isBuyAction,
|
||||||
|
required String walletAddress,
|
||||||
|
PaymentType? paymentType,
|
||||||
|
String? countryCode}) async {
|
||||||
|
String? paymentMethod;
|
||||||
|
if (paymentType != null && paymentType != PaymentType.all) {
|
||||||
|
paymentMethod = normalizePaymentMethod(paymentType);
|
||||||
|
if (paymentMethod == null) paymentMethod = paymentType.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
log('Meld: Fetching buy quote: ${isBuyAction ? cryptoCurrency : fiatCurrency} -> ${isBuyAction ? fiatCurrency : cryptoCurrency}, amount: $amount');
|
||||||
|
|
||||||
|
final url = Uri.https(_baseUrl, _quotePath);
|
||||||
|
final headers = {
|
||||||
|
'Authorization': _testApiKey,
|
||||||
|
'Meld-Version': '2023-12-19',
|
||||||
|
'accept': 'application/json',
|
||||||
|
'content-type': 'application/json',
|
||||||
|
};
|
||||||
|
final body = jsonEncode({
|
||||||
|
'countryCode': countryCode,
|
||||||
|
'destinationCurrencyCode': isBuyAction ? fiatCurrency.name : cryptoCurrency.title,
|
||||||
|
'sourceAmount': amount,
|
||||||
|
'sourceCurrencyCode': isBuyAction ? cryptoCurrency.title : fiatCurrency.name,
|
||||||
|
if (paymentMethod != null) 'paymentMethod': paymentMethod,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await http.post(url, headers: headers, body: body);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
final paymentType = _getPaymentTypeByString(data['paymentMethodType'] as String?);
|
||||||
|
final quote = Quote.fromMeldJson(data, isBuyAction, paymentType);
|
||||||
|
|
||||||
|
quote.setFiatCurrency = fiatCurrency;
|
||||||
|
quote.setCryptoCurrency = cryptoCurrency;
|
||||||
|
|
||||||
|
return [quote];
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error fetching buy quote: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void>? launchProvider(
|
||||||
|
{required BuildContext context,
|
||||||
|
required Quote quote,
|
||||||
|
required double amount,
|
||||||
|
required bool isBuyAction,
|
||||||
|
required String cryptoCurrencyAddress,
|
||||||
|
String? countryCode}) async {
|
||||||
|
final actionType = isBuyAction ? 'BUY' : 'SELL';
|
||||||
|
|
||||||
|
final params = {
|
||||||
|
'publicKey': _isProduction ? '' : _testPublicKey,
|
||||||
|
'countryCode': countryCode,
|
||||||
|
//'paymentMethodType': normalizePaymentMethod(paymentMethod.paymentMethodType),
|
||||||
|
'sourceAmount': amount.toString(),
|
||||||
|
'sourceCurrencyCode': quote.fiatCurrency,
|
||||||
|
'destinationCurrencyCode': quote.cryptoCurrency,
|
||||||
|
'walletAddress': cryptoCurrencyAddress,
|
||||||
|
'transactionType': actionType
|
||||||
|
};
|
||||||
|
|
||||||
|
final uri = Uri.https(_baseWidgetUrl, '', params);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (await canLaunchUrl(uri)) {
|
||||||
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
|
} else {
|
||||||
|
throw Exception('Could not launch URL');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
await showPopUp<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertWithOneAction(
|
||||||
|
alertTitle: "Meld",
|
||||||
|
alertContent: S.of(context).buy_provider_unavailable + ': $e',
|
||||||
|
buttonText: S.of(context).ok,
|
||||||
|
buttonAction: () => Navigator.of(context).pop());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? normalizePaymentMethod(PaymentType paymentType) {
|
||||||
|
switch (paymentType) {
|
||||||
|
case PaymentType.creditCard:
|
||||||
|
return 'CREDIT_DEBIT_CARD';
|
||||||
|
case PaymentType.applePay:
|
||||||
|
return 'APPLE_PAY';
|
||||||
|
case PaymentType.googlePay:
|
||||||
|
return 'GOOGLE_PAY';
|
||||||
|
case PaymentType.neteller:
|
||||||
|
return 'NETELLER';
|
||||||
|
case PaymentType.skrill:
|
||||||
|
return 'SKRILL';
|
||||||
|
case PaymentType.sepa:
|
||||||
|
return 'SEPA';
|
||||||
|
case PaymentType.sepaInstant:
|
||||||
|
return 'SEPA_INSTANT';
|
||||||
|
case PaymentType.ach:
|
||||||
|
return 'ACH';
|
||||||
|
case PaymentType.achInstant:
|
||||||
|
return 'INSTANT_ACH';
|
||||||
|
case PaymentType.Khipu:
|
||||||
|
return 'KHIPU';
|
||||||
|
case PaymentType.ovo:
|
||||||
|
return 'OVO';
|
||||||
|
case PaymentType.zaloPay:
|
||||||
|
return 'ZALOPAY';
|
||||||
|
case PaymentType.zaloBankTransfer:
|
||||||
|
return 'ZA_BANK_TRANSFER';
|
||||||
|
case PaymentType.gcash:
|
||||||
|
return 'GCASH';
|
||||||
|
case PaymentType.imps:
|
||||||
|
return 'IMPS';
|
||||||
|
case PaymentType.dana:
|
||||||
|
return 'DANA';
|
||||||
|
case PaymentType.ideal:
|
||||||
|
return 'IDEAL';
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PaymentType _getPaymentTypeByString(String? paymentMethod) {
|
||||||
|
switch (paymentMethod?.toUpperCase()) {
|
||||||
|
case 'CREDIT_DEBIT_CARD':
|
||||||
|
return PaymentType.creditCard;
|
||||||
|
case 'APPLE_PAY':
|
||||||
|
return PaymentType.applePay;
|
||||||
|
case 'GOOGLE_PAY':
|
||||||
|
return PaymentType.googlePay;
|
||||||
|
case 'NETELLER':
|
||||||
|
return PaymentType.neteller;
|
||||||
|
case 'SKRILL':
|
||||||
|
return PaymentType.skrill;
|
||||||
|
case 'SEPA':
|
||||||
|
return PaymentType.sepa;
|
||||||
|
case 'SEPA_INSTANT':
|
||||||
|
return PaymentType.sepaInstant;
|
||||||
|
case 'ACH':
|
||||||
|
return PaymentType.ach;
|
||||||
|
case 'INSTANT_ACH':
|
||||||
|
return PaymentType.achInstant;
|
||||||
|
case 'KHIPU':
|
||||||
|
return PaymentType.Khipu;
|
||||||
|
case 'OVO':
|
||||||
|
return PaymentType.ovo;
|
||||||
|
case 'ZALOPAY':
|
||||||
|
return PaymentType.zaloPay;
|
||||||
|
case 'ZA_BANK_TRANSFER':
|
||||||
|
return PaymentType.zaloBankTransfer;
|
||||||
|
case 'GCASH':
|
||||||
|
return PaymentType.gcash;
|
||||||
|
case 'IMPS':
|
||||||
|
return PaymentType.imps;
|
||||||
|
case 'DANA':
|
||||||
|
return PaymentType.dana;
|
||||||
|
case 'IDEAL':
|
||||||
|
return PaymentType.ideal;
|
||||||
|
default:
|
||||||
|
return PaymentType.all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,19 +1,20 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
||||||
import 'package:cake_wallet/buy/buy_amount.dart';
|
|
||||||
import 'package:cake_wallet/buy/buy_exception.dart';
|
import 'package:cake_wallet/buy/buy_exception.dart';
|
||||||
import 'package:cake_wallet/buy/buy_provider.dart';
|
import 'package:cake_wallet/buy/buy_provider.dart';
|
||||||
import 'package:cake_wallet/buy/buy_provider_description.dart';
|
import 'package:cake_wallet/buy/buy_provider_description.dart';
|
||||||
|
import 'package:cake_wallet/buy/buy_quote.dart';
|
||||||
import 'package:cake_wallet/buy/order.dart';
|
import 'package:cake_wallet/buy/order.dart';
|
||||||
|
import 'package:cake_wallet/buy/payment_method.dart';
|
||||||
|
import 'package:cake_wallet/entities/fiat_currency.dart';
|
||||||
import 'package:cake_wallet/exchange/trade_state.dart';
|
import 'package:cake_wallet/exchange/trade_state.dart';
|
||||||
import 'package:cake_wallet/generated/i18n.dart';
|
import 'package:cake_wallet/generated/i18n.dart';
|
||||||
import 'package:cake_wallet/palette.dart';
|
import 'package:cake_wallet/palette.dart';
|
||||||
import 'package:cake_wallet/routes.dart';
|
|
||||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||||
import 'package:cake_wallet/store/settings_store.dart';
|
import 'package:cake_wallet/store/settings_store.dart';
|
||||||
import 'package:cake_wallet/themes/theme_base.dart';
|
import 'package:cake_wallet/themes/theme_base.dart';
|
||||||
import 'package:cake_wallet/utils/device_info.dart';
|
|
||||||
import 'package:cw_core/crypto_currency.dart';
|
import 'package:cw_core/crypto_currency.dart';
|
||||||
import 'package:cw_core/wallet_base.dart';
|
import 'package:cw_core/wallet_base.dart';
|
||||||
import 'package:cw_core/wallet_type.dart';
|
import 'package:cw_core/wallet_type.dart';
|
||||||
|
@ -39,6 +40,15 @@ class MoonPayProvider extends BuyProvider {
|
||||||
static const _baseBuyProductUrl = 'buy.moonpay.com';
|
static const _baseBuyProductUrl = 'buy.moonpay.com';
|
||||||
static const _cIdBaseUrl = 'exchange-helper.cakewallet.com';
|
static const _cIdBaseUrl = 'exchange-helper.cakewallet.com';
|
||||||
static const _apiUrl = 'https://api.moonpay.com';
|
static const _apiUrl = 'https://api.moonpay.com';
|
||||||
|
static const _baseUrl = 'api.moonpay.com';
|
||||||
|
static const _currenciesPath = '/v3/currencies';
|
||||||
|
static const _buyQuote = '/buy_quote';
|
||||||
|
static const _sellQuote = '/sell_quote';
|
||||||
|
|
||||||
|
static const _transactionsSuffix = '/v1/transactions';
|
||||||
|
|
||||||
|
final String baseBuyUrl;
|
||||||
|
final String baseSellUrl;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get providerDescription =>
|
String get providerDescription =>
|
||||||
|
@ -53,6 +63,17 @@ class MoonPayProvider extends BuyProvider {
|
||||||
@override
|
@override
|
||||||
String get darkIcon => 'assets/images/moonpay_dark.png';
|
String get darkIcon => 'assets/images/moonpay_dark.png';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isAggregator => false;
|
||||||
|
|
||||||
|
static String get _apiKey => secrets.moonPayApiKey;
|
||||||
|
|
||||||
|
String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase();
|
||||||
|
|
||||||
|
String get trackUrl => baseBuyUrl + '/transaction_receipt?transactionId=';
|
||||||
|
|
||||||
|
static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey;
|
||||||
|
|
||||||
static String themeToMoonPayTheme(ThemeBase theme) {
|
static String themeToMoonPayTheme(ThemeBase theme) {
|
||||||
switch (theme.type) {
|
switch (theme.type) {
|
||||||
case ThemeType.bright:
|
case ThemeType.bright:
|
||||||
|
@ -63,28 +84,12 @@ class MoonPayProvider extends BuyProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static String get _apiKey => secrets.moonPayApiKey;
|
|
||||||
|
|
||||||
final String baseBuyUrl;
|
|
||||||
final String baseSellUrl;
|
|
||||||
|
|
||||||
String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase();
|
|
||||||
|
|
||||||
String get trackUrl => baseBuyUrl + '/transaction_receipt?transactionId=';
|
|
||||||
|
|
||||||
static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey;
|
|
||||||
|
|
||||||
Future<String> getMoonpaySignature(String query) async {
|
Future<String> getMoonpaySignature(String query) async {
|
||||||
final uri = Uri.https(_cIdBaseUrl, "/api/moonpay");
|
final uri = Uri.https(_cIdBaseUrl, "/api/moonpay");
|
||||||
|
|
||||||
final response = await post(
|
final response = await post(uri,
|
||||||
uri,
|
headers: {'Content-Type': 'application/json', 'x-api-key': _exchangeHelperApiKey},
|
||||||
headers: {
|
body: json.encode({'query': query}));
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'x-api-key': _exchangeHelperApiKey,
|
|
||||||
},
|
|
||||||
body: json.encode({'query': query}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return (jsonDecode(response.body) as Map<String, dynamic>)['signature'] as String;
|
return (jsonDecode(response.body) as Map<String, dynamic>)['signature'] as String;
|
||||||
|
@ -94,85 +99,195 @@ class MoonPayProvider extends BuyProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Uri> requestSellMoonPayUrl({
|
Future<Map<String, dynamic>> fetchFiatCredentials(
|
||||||
required CryptoCurrency currency,
|
String fiatCurrency, String cryptocurrency, String? paymentMethod) async {
|
||||||
required String refundWalletAddress,
|
final params = {'baseCurrencyCode': fiatCurrency.toLowerCase(), 'apiKey': _apiKey};
|
||||||
required SettingsStore settingsStore,
|
|
||||||
}) async {
|
if (paymentMethod != null) params['paymentMethod'] = paymentMethod;
|
||||||
|
|
||||||
|
final path = '$_currenciesPath/${cryptocurrency.toLowerCase()}/limits';
|
||||||
|
final url = Uri.https(_baseUrl, path, params);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await get(url, headers: {'accept': 'application/json'});
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
} else {
|
||||||
|
print('MoonPay does not support fiat: $fiatCurrency');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('MoonPay Error fetching fiat currencies: $e');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<PaymentMethod>> getAvailablePaymentTypes(
|
||||||
|
String fiatCurrency, String cryptoCurrency, bool isBuyAction) async {
|
||||||
|
final List<PaymentMethod> paymentMethods = [];
|
||||||
|
|
||||||
|
if (isBuyAction) {
|
||||||
|
final fiatBuyCredentials = await fetchFiatCredentials(fiatCurrency, cryptoCurrency, null);
|
||||||
|
if (fiatBuyCredentials.isNotEmpty) {
|
||||||
|
final paymentMethod = fiatBuyCredentials['paymentMethod'] as String?;
|
||||||
|
paymentMethods.add(PaymentMethod.fromMoonPayJson(
|
||||||
|
fiatBuyCredentials, _getPaymentTypeByString(paymentMethod)));
|
||||||
|
return paymentMethods;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return paymentMethods;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Quote>?> fetchQuote(
|
||||||
|
{required CryptoCurrency cryptoCurrency,
|
||||||
|
required FiatCurrency fiatCurrency,
|
||||||
|
required double amount,
|
||||||
|
required bool isBuyAction,
|
||||||
|
required String walletAddress,
|
||||||
|
PaymentType? paymentType,
|
||||||
|
String? countryCode}) async {
|
||||||
|
String? paymentMethod;
|
||||||
|
|
||||||
|
if (paymentType != null && paymentType != PaymentType.all) {
|
||||||
|
paymentMethod = normalizePaymentMethod(paymentType);
|
||||||
|
if (paymentMethod == null) paymentMethod = paymentType.name;
|
||||||
|
} else {
|
||||||
|
paymentMethod = 'credit_debit_card';
|
||||||
|
}
|
||||||
|
|
||||||
|
final action = isBuyAction ? 'buy' : 'sell';
|
||||||
|
|
||||||
|
final formattedCryptoCurrency = _normalizeCurrency(cryptoCurrency);
|
||||||
|
final baseCurrencyCode =
|
||||||
|
isBuyAction ? fiatCurrency.name.toLowerCase() : cryptoCurrency.title.toLowerCase();
|
||||||
|
|
||||||
final params = {
|
final params = {
|
||||||
'theme': themeToMoonPayTheme(settingsStore.currentTheme),
|
'baseCurrencyCode': baseCurrencyCode,
|
||||||
'language': settingsStore.languageCode,
|
'baseCurrencyAmount': amount.toString(),
|
||||||
'colorCode': settingsStore.currentTheme.type == ThemeType.dark
|
'amount': amount.toString(),
|
||||||
? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}'
|
'paymentMethod': paymentMethod,
|
||||||
: '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}',
|
'areFeesIncluded': 'false',
|
||||||
'defaultCurrencyCode': _normalizeCurrency(currency),
|
'apiKey': _apiKey
|
||||||
'refundWalletAddress': refundWalletAddress,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (_apiKey.isNotEmpty) {
|
log('MoonPay: Fetching $action quote: ${isBuyAction ? formattedCryptoCurrency : fiatCurrency.name.toLowerCase()} -> ${isBuyAction ? baseCurrencyCode : formattedCryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod');
|
||||||
params['apiKey'] = _apiKey;
|
|
||||||
|
final quotePath = isBuyAction ? _buyQuote : _sellQuote;
|
||||||
|
|
||||||
|
final path = '$_currenciesPath/$formattedCryptoCurrency$quotePath';
|
||||||
|
final url = Uri.https(_baseUrl, path, params);
|
||||||
|
try {
|
||||||
|
final response = await get(url);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
|
||||||
|
// Check if the response is for the correct fiat currency
|
||||||
|
if (isBuyAction) {
|
||||||
|
final fiatCurrencyCode = data['baseCurrencyCode'] as String?;
|
||||||
|
if (fiatCurrencyCode == null || fiatCurrencyCode != fiatCurrency.name.toLowerCase())
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
final quoteCurrency = data['quoteCurrency'] as Map<String, dynamic>?;
|
||||||
|
if (quoteCurrency == null || quoteCurrency['code'] != fiatCurrency.name.toLowerCase())
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final originalUri = Uri.https(
|
final paymentMethods = data['paymentMethod'] as String?;
|
||||||
baseSellUrl,
|
final quote =
|
||||||
'',
|
Quote.fromMoonPayJson(data, isBuyAction, _getPaymentTypeByString(paymentMethods));
|
||||||
params,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isTestEnvironment) {
|
quote.setFiatCurrency = fiatCurrency;
|
||||||
return originalUri;
|
quote.setCryptoCurrency = cryptoCurrency;
|
||||||
|
|
||||||
|
return [quote];
|
||||||
|
} else {
|
||||||
|
print('Moon Pay: Error fetching buy quote: ');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Moon Pay: Error fetching buy quote: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final signature = await getMoonpaySignature('?${originalUri.query}');
|
@override
|
||||||
|
Future<void>? launchProvider(
|
||||||
|
{required BuildContext context,
|
||||||
|
required Quote quote,
|
||||||
|
required double amount,
|
||||||
|
required bool isBuyAction,
|
||||||
|
required String cryptoCurrencyAddress,
|
||||||
|
String? countryCode}) async {
|
||||||
|
|
||||||
final query = Map<String, dynamic>.from(originalUri.queryParameters);
|
final Map<String, String> params = {
|
||||||
query['signature'] = signature;
|
'theme': themeToMoonPayTheme(_settingsStore.currentTheme),
|
||||||
final signedUri = originalUri.replace(queryParameters: query);
|
'language': _settingsStore.languageCode,
|
||||||
return signedUri;
|
'colorCode': _settingsStore.currentTheme.type == ThemeType.dark
|
||||||
}
|
|
||||||
|
|
||||||
// BUY:
|
|
||||||
static const _currenciesSuffix = '/v3/currencies';
|
|
||||||
static const _quoteSuffix = '/buy_quote';
|
|
||||||
static const _transactionsSuffix = '/v1/transactions';
|
|
||||||
static const _ipAddressSuffix = '/v4/ip_address';
|
|
||||||
|
|
||||||
Future<Uri> requestBuyMoonPayUrl({
|
|
||||||
required CryptoCurrency currency,
|
|
||||||
required SettingsStore settingsStore,
|
|
||||||
required String walletAddress,
|
|
||||||
String? amount,
|
|
||||||
}) async {
|
|
||||||
final params = {
|
|
||||||
'theme': themeToMoonPayTheme(settingsStore.currentTheme),
|
|
||||||
'language': settingsStore.languageCode,
|
|
||||||
'colorCode': settingsStore.currentTheme.type == ThemeType.dark
|
|
||||||
? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}'
|
? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}'
|
||||||
: '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}',
|
: '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}',
|
||||||
'baseCurrencyCode': settingsStore.fiatCurrency.title,
|
'baseCurrencyCode': isBuyAction ? quote.fiatCurrency.name : quote.cryptoCurrency.name,
|
||||||
'baseCurrencyAmount': amount ?? '0',
|
'baseCurrencyAmount': amount.toString(),
|
||||||
'currencyCode': _normalizeCurrency(currency),
|
'walletAddress': cryptoCurrencyAddress,
|
||||||
'walletAddress': walletAddress,
|
|
||||||
'lockAmount': 'false',
|
'lockAmount': 'false',
|
||||||
'showAllCurrencies': 'false',
|
'showAllCurrencies': 'false',
|
||||||
'showWalletAddressForm': 'false',
|
'showWalletAddressForm': 'false',
|
||||||
'enabledPaymentMethods':
|
if (isBuyAction)
|
||||||
|
'enabledPaymentMethods': normalizePaymentMethod(quote.paymentType) ??
|
||||||
'credit_debit_card,apple_pay,google_pay,samsung_pay,sepa_bank_transfer,gbp_bank_transfer,gbp_open_banking_payment',
|
'credit_debit_card,apple_pay,google_pay,samsung_pay,sepa_bank_transfer,gbp_bank_transfer,gbp_open_banking_payment',
|
||||||
|
if (!isBuyAction) 'refundWalletAddress': cryptoCurrencyAddress
|
||||||
};
|
};
|
||||||
|
|
||||||
if (_apiKey.isNotEmpty) {
|
if (isBuyAction) params['currencyCode'] = quote.cryptoCurrency.name;
|
||||||
params['apiKey'] = _apiKey;
|
if (!isBuyAction) params['quoteCurrencyCode'] = quote.cryptoCurrency.name;
|
||||||
}
|
|
||||||
|
|
||||||
final originalUri = Uri.https(
|
try {
|
||||||
baseBuyUrl,
|
{
|
||||||
'',
|
final uri = await requestMoonPayUrl(
|
||||||
params,
|
walletAddress: cryptoCurrencyAddress,
|
||||||
|
settingsStore: _settingsStore,
|
||||||
|
isBuyAction: isBuyAction,
|
||||||
|
amount: amount.toString(),
|
||||||
|
params: params);
|
||||||
|
|
||||||
|
if (await canLaunchUrl(uri)) {
|
||||||
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
|
} else {
|
||||||
|
throw Exception('Could not launch URL');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertWithOneAction(
|
||||||
|
alertTitle: 'MoonPay',
|
||||||
|
alertContent: 'The MoonPay service is currently unavailable: $e',
|
||||||
|
buttonText: S.of(context).ok,
|
||||||
|
buttonAction: () => Navigator.of(context).pop(),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isTestEnvironment) {
|
|
||||||
return originalUri;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Uri> requestMoonPayUrl({
|
||||||
|
required String walletAddress,
|
||||||
|
required SettingsStore settingsStore,
|
||||||
|
required bool isBuyAction,
|
||||||
|
required Map<String, String> params,
|
||||||
|
String? amount,
|
||||||
|
}) async {
|
||||||
|
if (_apiKey.isNotEmpty) params['apiKey'] = _apiKey;
|
||||||
|
|
||||||
|
final baseUrl = isBuyAction ? baseBuyUrl : baseSellUrl;
|
||||||
|
final originalUri = Uri.https(baseUrl, '', params);
|
||||||
|
|
||||||
|
if (isTestEnvironment) return originalUri;
|
||||||
|
|
||||||
final signature = await getMoonpaySignature('?${originalUri.query}');
|
final signature = await getMoonpaySignature('?${originalUri.query}');
|
||||||
final query = Map<String, dynamic>.from(originalUri.queryParameters);
|
final query = Map<String, dynamic>.from(originalUri.queryParameters);
|
||||||
|
@ -181,33 +296,6 @@ class MoonPayProvider extends BuyProvider {
|
||||||
return signedUri;
|
return signedUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<BuyAmount> calculateAmount(String amount, String sourceCurrency) async {
|
|
||||||
final url = _apiUrl +
|
|
||||||
_currenciesSuffix +
|
|
||||||
'/$currencyCode' +
|
|
||||||
_quoteSuffix +
|
|
||||||
'/?apiKey=' +
|
|
||||||
_apiKey +
|
|
||||||
'&baseCurrencyAmount=' +
|
|
||||||
amount +
|
|
||||||
'&baseCurrencyCode=' +
|
|
||||||
sourceCurrency.toLowerCase();
|
|
||||||
final uri = Uri.parse(url);
|
|
||||||
final response = await get(uri);
|
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
|
||||||
throw BuyException(title: providerDescription, content: 'Quote is not found!');
|
|
||||||
}
|
|
||||||
|
|
||||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
|
||||||
final sourceAmount = responseJSON['totalAmount'] as double;
|
|
||||||
final destAmount = responseJSON['quoteCurrencyAmount'] as double;
|
|
||||||
final minSourceAmount = responseJSON['baseCurrency']['minAmount'] as int;
|
|
||||||
|
|
||||||
return BuyAmount(
|
|
||||||
sourceAmount: sourceAmount, destAmount: destAmount, minAmount: minSourceAmount);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Order> findOrderById(String id) async {
|
Future<Order> findOrderById(String id) async {
|
||||||
final url = _apiUrl + _transactionsSuffix + '/$id' + '?apiKey=' + _apiKey;
|
final url = _apiUrl + _transactionsSuffix + '/$id' + '?apiKey=' + _apiKey;
|
||||||
final uri = Uri.parse(url);
|
final uri = Uri.parse(url);
|
||||||
|
@ -235,74 +323,83 @@ class MoonPayProvider extends BuyProvider {
|
||||||
walletId: wallet.id);
|
walletId: wallet.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<bool> onEnabled() async {
|
|
||||||
final url = _apiUrl + _ipAddressSuffix + '?apiKey=' + _apiKey;
|
|
||||||
var isBuyEnable = false;
|
|
||||||
final uri = Uri.parse(url);
|
|
||||||
final response = await get(uri);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
|
||||||
isBuyEnable = responseJSON['isBuyAllowed'] as bool;
|
|
||||||
} catch (e) {
|
|
||||||
isBuyEnable = false;
|
|
||||||
print(e.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
return isBuyEnable;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
|
|
||||||
try {
|
|
||||||
late final Uri uri;
|
|
||||||
if (isBuyAction ?? true) {
|
|
||||||
uri = await requestBuyMoonPayUrl(
|
|
||||||
currency: wallet.currency,
|
|
||||||
walletAddress: wallet.walletAddresses.address,
|
|
||||||
settingsStore: _settingsStore,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
uri = await requestSellMoonPayUrl(
|
|
||||||
currency: wallet.currency,
|
|
||||||
refundWalletAddress: wallet.walletAddresses.address,
|
|
||||||
settingsStore: _settingsStore,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await canLaunchUrl(uri)) {
|
|
||||||
if (DeviceInfo.instance.isMobile) {
|
|
||||||
Navigator.of(context).pushNamed(Routes.webViewPage, arguments: ['MoonPay', uri]);
|
|
||||||
} else {
|
|
||||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw Exception('Could not launch URL');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (context.mounted) {
|
|
||||||
await showDialog<void>(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return AlertWithOneAction(
|
|
||||||
alertTitle: 'MoonPay',
|
|
||||||
alertContent: 'The MoonPay service is currently unavailable: $e',
|
|
||||||
buttonText: S.of(context).ok,
|
|
||||||
buttonAction: () => Navigator.of(context).pop(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _normalizeCurrency(CryptoCurrency currency) {
|
String _normalizeCurrency(CryptoCurrency currency) {
|
||||||
if (currency == CryptoCurrency.maticpoly) {
|
if (currency.tag == 'POLY') {
|
||||||
return "POL_POLYGON";
|
return '${currency.title.toLowerCase()}_polygon';
|
||||||
} else if (currency == CryptoCurrency.matic) {
|
}
|
||||||
return "POL";
|
|
||||||
|
if (currency.tag == 'TRX') {
|
||||||
|
return '${currency.title.toLowerCase()}_trx';
|
||||||
}
|
}
|
||||||
|
|
||||||
return currency.toString().toLowerCase();
|
return currency.toString().toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? normalizePaymentMethod(PaymentType paymentMethod) {
|
||||||
|
switch (paymentMethod) {
|
||||||
|
case PaymentType.creditCard:
|
||||||
|
return 'credit_debit_card';
|
||||||
|
case PaymentType.debitCard:
|
||||||
|
return 'credit_debit_card';
|
||||||
|
case PaymentType.ach:
|
||||||
|
return 'ach_bank_transfer';
|
||||||
|
case PaymentType.applePay:
|
||||||
|
return 'apple_pay';
|
||||||
|
case PaymentType.googlePay:
|
||||||
|
return 'google_pay';
|
||||||
|
case PaymentType.sepa:
|
||||||
|
return 'sepa_bank_transfer';
|
||||||
|
case PaymentType.paypal:
|
||||||
|
return 'paypal';
|
||||||
|
case PaymentType.sepaOpenBankingPayment:
|
||||||
|
return 'sepa_open_banking_payment';
|
||||||
|
case PaymentType.gbpOpenBankingPayment:
|
||||||
|
return 'gbp_open_banking_payment';
|
||||||
|
case PaymentType.lowCostAch:
|
||||||
|
return 'low_cost_ach';
|
||||||
|
case PaymentType.mobileWallet:
|
||||||
|
return 'mobile_wallet';
|
||||||
|
case PaymentType.pixInstantPayment:
|
||||||
|
return 'pix_instant_payment';
|
||||||
|
case PaymentType.yellowCardBankTransfer:
|
||||||
|
return 'yellow_card_bank_transfer';
|
||||||
|
case PaymentType.fiatBalance:
|
||||||
|
return 'fiat_balance';
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PaymentType _getPaymentTypeByString(String? paymentMethod) {
|
||||||
|
switch (paymentMethod) {
|
||||||
|
case 'ach_bank_transfer':
|
||||||
|
return PaymentType.ach;
|
||||||
|
case 'apple_pay':
|
||||||
|
return PaymentType.applePay;
|
||||||
|
case 'credit_debit_card':
|
||||||
|
return PaymentType.creditCard;
|
||||||
|
case 'fiat_balance':
|
||||||
|
return PaymentType.fiatBalance;
|
||||||
|
case 'gbp_open_banking_payment':
|
||||||
|
return PaymentType.gbpOpenBankingPayment;
|
||||||
|
case 'google_pay':
|
||||||
|
return PaymentType.googlePay;
|
||||||
|
case 'low_cost_ach':
|
||||||
|
return PaymentType.lowCostAch;
|
||||||
|
case 'mobile_wallet':
|
||||||
|
return PaymentType.mobileWallet;
|
||||||
|
case 'paypal':
|
||||||
|
return PaymentType.paypal;
|
||||||
|
case 'pix_instant_payment':
|
||||||
|
return PaymentType.pixInstantPayment;
|
||||||
|
case 'sepa_bank_transfer':
|
||||||
|
return PaymentType.sepa;
|
||||||
|
case 'sepa_open_banking_payment':
|
||||||
|
return PaymentType.sepaOpenBankingPayment;
|
||||||
|
case 'yellow_card_bank_transfer':
|
||||||
|
return PaymentType.yellowCardBankTransfer;
|
||||||
|
default:
|
||||||
|
return PaymentType.all;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
||||||
import 'package:cake_wallet/buy/buy_provider.dart';
|
import 'package:cake_wallet/buy/buy_provider.dart';
|
||||||
|
import 'package:cake_wallet/buy/buy_quote.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/generated/i18n.dart';
|
||||||
import 'package:cake_wallet/routes.dart';
|
|
||||||
import 'package:cake_wallet/store/settings_store.dart';
|
import 'package:cake_wallet/store/settings_store.dart';
|
||||||
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
|
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
|
||||||
import 'package:cake_wallet/utils/device_info.dart';
|
|
||||||
import 'package:cw_core/crypto_currency.dart';
|
import 'package:cw_core/crypto_currency.dart';
|
||||||
|
import 'package:cw_core/currency.dart';
|
||||||
import 'package:cw_core/wallet_base.dart';
|
import 'package:cw_core/wallet_base.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class OnRamperBuyProvider extends BuyProvider {
|
class OnRamperBuyProvider extends BuyProvider {
|
||||||
|
@ -16,9 +22,15 @@ class OnRamperBuyProvider extends BuyProvider {
|
||||||
: super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: null);
|
: super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: null);
|
||||||
|
|
||||||
static const _baseUrl = 'buy.onramper.com';
|
static const _baseUrl = 'buy.onramper.com';
|
||||||
|
static const _baseApiUrl = 'api.onramper.com';
|
||||||
|
static const quotes = '/quotes';
|
||||||
|
static const paymentTypes = '/payment-types';
|
||||||
|
static const supported = '/supported';
|
||||||
|
|
||||||
final SettingsStore _settingsStore;
|
final SettingsStore _settingsStore;
|
||||||
|
|
||||||
|
String get _apiKey => secrets.onramperApiKey;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get title => 'Onramper';
|
String get title => 'Onramper';
|
||||||
|
|
||||||
|
@ -31,74 +43,327 @@ class OnRamperBuyProvider extends BuyProvider {
|
||||||
@override
|
@override
|
||||||
String get darkIcon => 'assets/images/onramper_dark.png';
|
String get darkIcon => 'assets/images/onramper_dark.png';
|
||||||
|
|
||||||
String get _apiKey => secrets.onramperApiKey;
|
@override
|
||||||
|
bool get isAggregator => true;
|
||||||
|
|
||||||
String get _normalizeCryptoCurrency {
|
Future<List<PaymentMethod>> getAvailablePaymentTypes(
|
||||||
switch (wallet.currency) {
|
String fiatCurrency, String cryptoCurrency, bool isBuyAction) async {
|
||||||
case CryptoCurrency.ltc:
|
final params = {
|
||||||
return "LTC_LITECOIN";
|
'fiatCurrency': fiatCurrency,
|
||||||
case CryptoCurrency.xmr:
|
'type': isBuyAction ? 'buy' : 'sell',
|
||||||
return "XMR_MONERO";
|
'isRecurringPayment': 'false'
|
||||||
case CryptoCurrency.bch:
|
};
|
||||||
return "BCH_BITCOINCASH";
|
|
||||||
case CryptoCurrency.nano:
|
final url = Uri.https(_baseApiUrl, '$supported$paymentTypes/$fiatCurrency', params);
|
||||||
return "XNO_NANO";
|
|
||||||
default:
|
try {
|
||||||
return wallet.currency.title;
|
final response =
|
||||||
|
await http.get(url, headers: {'Authorization': _apiKey, 'accept': 'application/json'});
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final Map<String, dynamic> data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
final List<dynamic> message = data['message'] as List<dynamic>;
|
||||||
|
return message
|
||||||
|
.map((item) => PaymentMethod.fromOnramperJson(item as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
} else {
|
||||||
|
print('Failed to fetch available payment types');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Failed to fetch available payment types: $e');
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String getColorStr(Color color) {
|
Future<Map<String, dynamic>> getOnrampMetadata() async {
|
||||||
return color.value.toRadixString(16).replaceAll(RegExp(r'^ff'), "");
|
final url = Uri.https(_baseApiUrl, '$supported/onramps/all');
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response =
|
||||||
|
await http.get(url, headers: {'Authorization': _apiKey, 'accept': 'application/json'});
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final Map<String, dynamic> data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
|
||||||
|
final List<dynamic> onramps = data['message'] as List<dynamic>;
|
||||||
|
|
||||||
|
final Map<String, dynamic> result = {
|
||||||
|
for (var onramp in onramps)
|
||||||
|
(onramp['id'] as String): {
|
||||||
|
'displayName': onramp['displayName'] as String,
|
||||||
|
'svg': onramp['icons']['svg'] as String
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
print('Failed to fetch onramp metadata');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error occurred: $e');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Uri requestOnramperUrl(BuildContext context, bool? isBuyAction) {
|
@override
|
||||||
String primaryColor,
|
Future<List<Quote>?> fetchQuote(
|
||||||
secondaryColor,
|
{required CryptoCurrency cryptoCurrency,
|
||||||
primaryTextColor,
|
required FiatCurrency fiatCurrency,
|
||||||
secondaryTextColor,
|
required double amount,
|
||||||
containerColor,
|
required bool isBuyAction,
|
||||||
cardColor;
|
required String walletAddress,
|
||||||
|
PaymentType? paymentType,
|
||||||
|
String? countryCode}) async {
|
||||||
|
String? paymentMethod;
|
||||||
|
|
||||||
primaryColor = getColorStr(Theme.of(context).primaryColor);
|
if (paymentType != null && paymentType != PaymentType.all) {
|
||||||
secondaryColor = getColorStr(Theme.of(context).colorScheme.background);
|
paymentMethod = normalizePaymentMethod(paymentType);
|
||||||
primaryTextColor =
|
if (paymentMethod == null) paymentMethod = paymentType.name;
|
||||||
getColorStr(Theme.of(context).extension<CakeTextTheme>()!.titleColor);
|
}
|
||||||
secondaryTextColor = getColorStr(
|
|
||||||
Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor);
|
final actionType = isBuyAction ? 'buy' : 'sell';
|
||||||
containerColor = getColorStr(Theme.of(context).colorScheme.background);
|
|
||||||
cardColor = getColorStr(Theme.of(context).cardColor);
|
final normalizedCryptoCurrency = _getNormalizeCryptoCurrency(cryptoCurrency);
|
||||||
|
|
||||||
|
final params = {
|
||||||
|
'amount': amount.toString(),
|
||||||
|
if (paymentMethod != null) 'paymentMethod': paymentMethod,
|
||||||
|
'clientName': 'CakeWallet',
|
||||||
|
'type': actionType,
|
||||||
|
'walletAddress': walletAddress,
|
||||||
|
'isRecurringPayment': 'false',
|
||||||
|
'input': 'source',
|
||||||
|
};
|
||||||
|
|
||||||
|
log('Onramper: Fetching $actionType quote: ${isBuyAction ? normalizedCryptoCurrency : fiatCurrency.name} -> ${isBuyAction ? fiatCurrency.name : normalizedCryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod');
|
||||||
|
|
||||||
|
final sourceCurrency = isBuyAction ? fiatCurrency.name : normalizedCryptoCurrency;
|
||||||
|
final destinationCurrency = isBuyAction ? normalizedCryptoCurrency : fiatCurrency.name;
|
||||||
|
|
||||||
|
final url = Uri.https(_baseApiUrl, '$quotes/${sourceCurrency}/${destinationCurrency}', params);
|
||||||
|
final headers = {'Authorization': _apiKey, 'accept': 'application/json'};
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await http.get(url, headers: headers);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final data = jsonDecode(response.body) as List<dynamic>;
|
||||||
|
if (data.isEmpty) return null;
|
||||||
|
|
||||||
|
List<Quote> validQuotes = [];
|
||||||
|
|
||||||
|
final onrampMetadata = await getOnrampMetadata();
|
||||||
|
|
||||||
|
for (var item in data) {
|
||||||
|
|
||||||
|
if (item['errors'] != null) continue;
|
||||||
|
|
||||||
|
final paymentMethod = (item as Map<String, dynamic>)['paymentMethod'] as String;
|
||||||
|
|
||||||
|
final rampId = item['ramp'] as String?;
|
||||||
|
final rampMetaData = onrampMetadata[rampId] as Map<String, dynamic>?;
|
||||||
|
|
||||||
|
if (rampMetaData == null) continue;
|
||||||
|
|
||||||
|
final quote = Quote.fromOnramperJson(
|
||||||
|
item, isBuyAction, onrampMetadata, _getPaymentTypeByString(paymentMethod));
|
||||||
|
quote.setFiatCurrency = fiatCurrency;
|
||||||
|
quote.setCryptoCurrency = cryptoCurrency;
|
||||||
|
validQuotes.add(quote);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validQuotes.isEmpty) return null;
|
||||||
|
|
||||||
|
return validQuotes;
|
||||||
|
} else {
|
||||||
|
print('Onramper: Failed to fetch rate');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Onramper: Failed to fetch rate $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void>? launchProvider(
|
||||||
|
{required BuildContext context,
|
||||||
|
required Quote quote,
|
||||||
|
required double amount,
|
||||||
|
required bool isBuyAction,
|
||||||
|
required String cryptoCurrencyAddress,
|
||||||
|
String? countryCode}) async {
|
||||||
|
final actionType = isBuyAction ? 'buy' : 'sell';
|
||||||
|
final prefix = actionType == 'sell' ? actionType + '_' : '';
|
||||||
|
|
||||||
|
final primaryColor = getColorStr(Theme.of(context).primaryColor);
|
||||||
|
final secondaryColor = getColorStr(Theme.of(context).colorScheme.background);
|
||||||
|
final primaryTextColor = getColorStr(Theme.of(context).extension<CakeTextTheme>()!.titleColor);
|
||||||
|
final secondaryTextColor =
|
||||||
|
getColorStr(Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor);
|
||||||
|
final containerColor = getColorStr(Theme.of(context).colorScheme.background);
|
||||||
|
var cardColor = getColorStr(Theme.of(context).cardColor);
|
||||||
|
|
||||||
if (_settingsStore.currentTheme.title == S.current.high_contrast_theme) {
|
if (_settingsStore.currentTheme.title == S.current.high_contrast_theme) {
|
||||||
cardColor = getColorStr(Colors.white);
|
cardColor = getColorStr(Colors.white);
|
||||||
}
|
}
|
||||||
|
|
||||||
final networkName =
|
final defaultCrypto = _getNormalizeCryptoCurrency(quote.cryptoCurrency);
|
||||||
wallet.currency.fullName?.toUpperCase().replaceAll(" ", "");
|
|
||||||
|
|
||||||
return Uri.https(_baseUrl, '', <String, dynamic>{
|
final paymentMethod = normalizePaymentMethod(quote.paymentType);
|
||||||
|
|
||||||
|
final uri = Uri.https(_baseUrl, '', {
|
||||||
'apiKey': _apiKey,
|
'apiKey': _apiKey,
|
||||||
'defaultCrypto': _normalizeCryptoCurrency,
|
'mode': actionType,
|
||||||
'sell_defaultCrypto': _normalizeCryptoCurrency,
|
'${prefix}defaultFiat': quote.fiatCurrency.name,
|
||||||
'networkWallets': '${networkName}:${wallet.walletAddresses.address}',
|
'${prefix}defaultCrypto': defaultCrypto,
|
||||||
|
'${prefix}defaultAmount': amount.toString(),
|
||||||
|
if (paymentMethod != null) '${prefix}defaultPaymentMethod': paymentMethod,
|
||||||
|
'onlyOnramps': quote.rampId,
|
||||||
|
'networkWallets': '$defaultCrypto:$cryptoCurrencyAddress',
|
||||||
|
'walletAddress': cryptoCurrencyAddress,
|
||||||
'supportSwap': "false",
|
'supportSwap': "false",
|
||||||
'primaryColor': primaryColor,
|
'primaryColor': primaryColor,
|
||||||
'secondaryColor': secondaryColor,
|
'secondaryColor': secondaryColor,
|
||||||
|
'containerColor': containerColor,
|
||||||
'primaryTextColor': primaryTextColor,
|
'primaryTextColor': primaryTextColor,
|
||||||
'secondaryTextColor': secondaryTextColor,
|
'secondaryTextColor': secondaryTextColor,
|
||||||
'containerColor': containerColor,
|
|
||||||
'cardColor': cardColor,
|
'cardColor': cardColor,
|
||||||
'mode': isBuyAction == true ? 'buy' : 'sell',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (await canLaunchUrl(uri)) {
|
||||||
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
|
} else {
|
||||||
|
throw Exception('Could not launch URL');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
|
List<CryptoCurrency> mainCurrency = [
|
||||||
final uri = requestOnramperUrl(context, isBuyAction);
|
CryptoCurrency.btc,
|
||||||
if (DeviceInfo.instance.isMobile) {
|
CryptoCurrency.eth,
|
||||||
Navigator.of(context)
|
CryptoCurrency.sol,
|
||||||
.pushNamed(Routes.webViewPage, arguments: [title, uri]);
|
];
|
||||||
} else {
|
|
||||||
await launchUrl(uri);
|
String _tagToNetwork(String tag) {
|
||||||
|
switch (tag) {
|
||||||
|
case 'OMNI':
|
||||||
|
return tag;
|
||||||
|
case 'POL':
|
||||||
|
return 'POLYGON';
|
||||||
|
default:
|
||||||
|
return CryptoCurrency.fromString(tag).fullName ?? tag;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _getNormalizeCryptoCurrency(Currency currency) {
|
||||||
|
if (currency is CryptoCurrency) {
|
||||||
|
if (!mainCurrency.contains(currency)) {
|
||||||
|
final network = currency.tag == null ? currency.fullName : _tagToNetwork(currency.tag!);
|
||||||
|
return '${currency.title}_${network?.replaceAll(' ', '')}'.toUpperCase();
|
||||||
|
}
|
||||||
|
return currency.title.toUpperCase();
|
||||||
|
}
|
||||||
|
return currency.name.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
String? normalizePaymentMethod(PaymentType paymentType) {
|
||||||
|
switch (paymentType) {
|
||||||
|
case PaymentType.bankTransfer:
|
||||||
|
return 'banktransfer';
|
||||||
|
case PaymentType.creditCard:
|
||||||
|
return 'creditcard';
|
||||||
|
case PaymentType.debitCard:
|
||||||
|
return 'debitcard';
|
||||||
|
case PaymentType.applePay:
|
||||||
|
return 'applepay';
|
||||||
|
case PaymentType.googlePay:
|
||||||
|
return 'googlepay';
|
||||||
|
case PaymentType.revolutPay:
|
||||||
|
return 'revolutpay';
|
||||||
|
case PaymentType.neteller:
|
||||||
|
return 'neteller';
|
||||||
|
case PaymentType.skrill:
|
||||||
|
return 'skrill';
|
||||||
|
case PaymentType.sepa:
|
||||||
|
return 'sepabanktransfer';
|
||||||
|
case PaymentType.sepaInstant:
|
||||||
|
return 'sepainstant';
|
||||||
|
case PaymentType.ach:
|
||||||
|
return 'ach';
|
||||||
|
case PaymentType.achInstant:
|
||||||
|
return 'iach';
|
||||||
|
case PaymentType.Khipu:
|
||||||
|
return 'khipu';
|
||||||
|
case PaymentType.palomaBanktTansfer:
|
||||||
|
return 'palomabanktransfer';
|
||||||
|
case PaymentType.ovo:
|
||||||
|
return 'ovo';
|
||||||
|
case PaymentType.zaloPay:
|
||||||
|
return 'zalopay';
|
||||||
|
case PaymentType.zaloBankTransfer:
|
||||||
|
return 'zalobanktransfer';
|
||||||
|
case PaymentType.gcash:
|
||||||
|
return 'gcash';
|
||||||
|
case PaymentType.imps:
|
||||||
|
return 'imps';
|
||||||
|
case PaymentType.dana:
|
||||||
|
return 'dana';
|
||||||
|
case PaymentType.ideal:
|
||||||
|
return 'ideal';
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PaymentType _getPaymentTypeByString(String paymentMethod) {
|
||||||
|
switch (paymentMethod.toLowerCase()) {
|
||||||
|
case 'banktransfer':
|
||||||
|
return PaymentType.bankTransfer;
|
||||||
|
case 'creditcard':
|
||||||
|
return PaymentType.creditCard;
|
||||||
|
case 'debitcard':
|
||||||
|
return PaymentType.debitCard;
|
||||||
|
case 'applepay':
|
||||||
|
return PaymentType.applePay;
|
||||||
|
case 'googlepay':
|
||||||
|
return PaymentType.googlePay;
|
||||||
|
case 'revolutpay':
|
||||||
|
return PaymentType.revolutPay;
|
||||||
|
case 'neteller':
|
||||||
|
return PaymentType.neteller;
|
||||||
|
case 'skrill':
|
||||||
|
return PaymentType.skrill;
|
||||||
|
case 'sepabanktransfer':
|
||||||
|
return PaymentType.sepa;
|
||||||
|
case 'sepainstant':
|
||||||
|
return PaymentType.sepaInstant;
|
||||||
|
case 'ach':
|
||||||
|
return PaymentType.ach;
|
||||||
|
case 'iach':
|
||||||
|
return PaymentType.achInstant;
|
||||||
|
case 'khipu':
|
||||||
|
return PaymentType.Khipu;
|
||||||
|
case 'palomabanktransfer':
|
||||||
|
return PaymentType.palomaBanktTansfer;
|
||||||
|
case 'ovo':
|
||||||
|
return PaymentType.ovo;
|
||||||
|
case 'zalopay':
|
||||||
|
return PaymentType.zaloPay;
|
||||||
|
case 'zalobanktransfer':
|
||||||
|
return PaymentType.zaloBankTransfer;
|
||||||
|
case 'gcash':
|
||||||
|
return PaymentType.gcash;
|
||||||
|
case 'imps':
|
||||||
|
return PaymentType.imps;
|
||||||
|
case 'dana':
|
||||||
|
return PaymentType.dana;
|
||||||
|
case 'ideal':
|
||||||
|
return PaymentType.ideal;
|
||||||
|
default:
|
||||||
|
return PaymentType.all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String getColorStr(Color color) => color.value.toRadixString(16).replaceAll(RegExp(r'^ff'), "");
|
||||||
}
|
}
|
||||||
|
|
287
lib/buy/payment_method.dart
Normal file
|
@ -0,0 +1,287 @@
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:cake_wallet/core/selectable_option.dart';
|
||||||
|
|
||||||
|
enum PaymentType {
|
||||||
|
all,
|
||||||
|
bankTransfer,
|
||||||
|
creditCard,
|
||||||
|
debitCard,
|
||||||
|
applePay,
|
||||||
|
googlePay,
|
||||||
|
revolutPay,
|
||||||
|
neteller,
|
||||||
|
skrill,
|
||||||
|
sepa,
|
||||||
|
sepaInstant,
|
||||||
|
ach,
|
||||||
|
achInstant,
|
||||||
|
Khipu,
|
||||||
|
palomaBanktTansfer,
|
||||||
|
ovo,
|
||||||
|
zaloPay,
|
||||||
|
zaloBankTransfer,
|
||||||
|
gcash,
|
||||||
|
imps,
|
||||||
|
dana,
|
||||||
|
ideal,
|
||||||
|
paypal,
|
||||||
|
sepaOpenBankingPayment,
|
||||||
|
gbpOpenBankingPayment,
|
||||||
|
lowCostAch,
|
||||||
|
mobileWallet,
|
||||||
|
pixInstantPayment,
|
||||||
|
yellowCardBankTransfer,
|
||||||
|
fiatBalance,
|
||||||
|
bancontact,
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PaymentTypeTitle on PaymentType {
|
||||||
|
String? get title {
|
||||||
|
switch (this) {
|
||||||
|
case PaymentType.all:
|
||||||
|
return 'All Payment Methods';
|
||||||
|
case PaymentType.bankTransfer:
|
||||||
|
return 'Bank Transfer';
|
||||||
|
case PaymentType.creditCard:
|
||||||
|
return 'Credit Card';
|
||||||
|
case PaymentType.debitCard:
|
||||||
|
return 'Debit Card';
|
||||||
|
case PaymentType.applePay:
|
||||||
|
return 'Apple Pay';
|
||||||
|
case PaymentType.googlePay:
|
||||||
|
return 'Google Pay';
|
||||||
|
case PaymentType.revolutPay:
|
||||||
|
return 'Revolut Pay';
|
||||||
|
case PaymentType.neteller:
|
||||||
|
return 'Neteller';
|
||||||
|
case PaymentType.skrill:
|
||||||
|
return 'Skrill';
|
||||||
|
case PaymentType.sepa:
|
||||||
|
return 'SEPA';
|
||||||
|
case PaymentType.sepaInstant:
|
||||||
|
return 'SEPA Instant';
|
||||||
|
case PaymentType.ach:
|
||||||
|
return 'ACH';
|
||||||
|
case PaymentType.achInstant:
|
||||||
|
return 'ACH Instant';
|
||||||
|
case PaymentType.Khipu:
|
||||||
|
return 'Khipu';
|
||||||
|
case PaymentType.palomaBanktTansfer:
|
||||||
|
return 'Paloma Bank Transfer';
|
||||||
|
case PaymentType.ovo:
|
||||||
|
return 'OVO';
|
||||||
|
case PaymentType.zaloPay:
|
||||||
|
return 'Zalo Pay';
|
||||||
|
case PaymentType.zaloBankTransfer:
|
||||||
|
return 'Zalo Bank Transfer';
|
||||||
|
case PaymentType.gcash:
|
||||||
|
return 'GCash';
|
||||||
|
case PaymentType.imps:
|
||||||
|
return 'IMPS';
|
||||||
|
case PaymentType.dana:
|
||||||
|
return 'DANA';
|
||||||
|
case PaymentType.ideal:
|
||||||
|
return 'iDEAL';
|
||||||
|
case PaymentType.paypal:
|
||||||
|
return 'PayPal';
|
||||||
|
case PaymentType.sepaOpenBankingPayment:
|
||||||
|
return 'SEPA Open Banking Payment';
|
||||||
|
case PaymentType.gbpOpenBankingPayment:
|
||||||
|
return 'GBP Open Banking Payment';
|
||||||
|
case PaymentType.lowCostAch:
|
||||||
|
return 'Low Cost ACH';
|
||||||
|
case PaymentType.mobileWallet:
|
||||||
|
return 'Mobile Wallet';
|
||||||
|
case PaymentType.pixInstantPayment:
|
||||||
|
return 'PIX Instant Payment';
|
||||||
|
case PaymentType.yellowCardBankTransfer:
|
||||||
|
return 'Yellow Card Bank Transfer';
|
||||||
|
case PaymentType.fiatBalance:
|
||||||
|
return 'Fiat Balance';
|
||||||
|
case PaymentType.bancontact:
|
||||||
|
return 'Bancontact';
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? get lightIconPath {
|
||||||
|
switch (this) {
|
||||||
|
case PaymentType.all:
|
||||||
|
return 'assets/images/usd_round_light.svg';
|
||||||
|
case PaymentType.creditCard:
|
||||||
|
case PaymentType.debitCard:
|
||||||
|
case PaymentType.yellowCardBankTransfer:
|
||||||
|
return 'assets/images/card.svg';
|
||||||
|
case PaymentType.bankTransfer:
|
||||||
|
return 'assets/images/bank_light.svg';
|
||||||
|
case PaymentType.skrill:
|
||||||
|
return 'assets/images/skrill.svg';
|
||||||
|
case PaymentType.applePay:
|
||||||
|
return 'assets/images/apple_pay_round_light.svg';
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? get darkIconPath {
|
||||||
|
switch (this) {
|
||||||
|
case PaymentType.all:
|
||||||
|
return 'assets/images/usd_round_dark.svg';
|
||||||
|
case PaymentType.creditCard:
|
||||||
|
case PaymentType.debitCard:
|
||||||
|
case PaymentType.yellowCardBankTransfer:
|
||||||
|
return 'assets/images/card_dark.svg';
|
||||||
|
case PaymentType.bankTransfer:
|
||||||
|
return 'assets/images/bank_dark.svg';
|
||||||
|
case PaymentType.skrill:
|
||||||
|
return 'assets/images/skrill.svg';
|
||||||
|
case PaymentType.applePay:
|
||||||
|
return 'assets/images/apple_pay_round_dark.svg';
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? get description {
|
||||||
|
switch (this) {
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PaymentMethod extends SelectableOption {
|
||||||
|
PaymentMethod({
|
||||||
|
required this.paymentMethodType,
|
||||||
|
required this.customTitle,
|
||||||
|
required this.customIconPath,
|
||||||
|
this.customDescription,
|
||||||
|
}) : super(title: paymentMethodType.title ?? customTitle);
|
||||||
|
|
||||||
|
final PaymentType paymentMethodType;
|
||||||
|
final String customTitle;
|
||||||
|
final String customIconPath;
|
||||||
|
final String? customDescription;
|
||||||
|
bool isSelected = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get description => paymentMethodType.description ?? customDescription;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lightIconPath => paymentMethodType.lightIconPath ?? customIconPath;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get darkIconPath => paymentMethodType.darkIconPath ?? customIconPath;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isOptionSelected => isSelected;
|
||||||
|
|
||||||
|
factory PaymentMethod.all() {
|
||||||
|
return PaymentMethod(
|
||||||
|
paymentMethodType: PaymentType.all,
|
||||||
|
customTitle: 'All Payment Methods',
|
||||||
|
customIconPath: 'assets/images/dollar_coin.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
factory PaymentMethod.fromOnramperJson(Map<String, dynamic> json) {
|
||||||
|
final type = PaymentMethod.getPaymentTypeId(json['paymentTypeId'] as String?);
|
||||||
|
return PaymentMethod(
|
||||||
|
paymentMethodType: type,
|
||||||
|
customTitle: json['name'] as String? ?? 'Unknown',
|
||||||
|
customIconPath: json['icon'] as String? ?? 'assets/images/card.png',
|
||||||
|
customDescription: json['description'] as String?);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory PaymentMethod.fromDFX(String paymentMethod, PaymentType paymentType) {
|
||||||
|
return PaymentMethod(
|
||||||
|
paymentMethodType: paymentType,
|
||||||
|
customTitle: paymentMethod,
|
||||||
|
customIconPath: 'assets/images/card.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
factory PaymentMethod.fromMoonPayJson(Map<String, dynamic> json, PaymentType paymentType) {
|
||||||
|
return PaymentMethod(
|
||||||
|
paymentMethodType: paymentType,
|
||||||
|
customTitle: json['paymentMethod'] as String,
|
||||||
|
customIconPath: 'assets/images/card.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
factory PaymentMethod.fromMeldJson(Map<String, dynamic> json) {
|
||||||
|
final type = PaymentMethod.getPaymentTypeId(json['paymentMethod'] as String?);
|
||||||
|
final logos = json['logos'] as Map<String, dynamic>;
|
||||||
|
return PaymentMethod(
|
||||||
|
paymentMethodType: type,
|
||||||
|
customTitle: json['name'] as String? ?? 'Unknown',
|
||||||
|
customIconPath: logos['dark'] as String? ?? 'assets/images/card.png',
|
||||||
|
customDescription: json['description'] as String?);
|
||||||
|
}
|
||||||
|
|
||||||
|
static PaymentType getPaymentTypeId(String? type) {
|
||||||
|
switch (type?.toLowerCase()) {
|
||||||
|
case 'banktransfer':
|
||||||
|
case 'bank':
|
||||||
|
case 'yellow_card_bank_transfer':
|
||||||
|
return PaymentType.bankTransfer;
|
||||||
|
case 'creditcard':
|
||||||
|
case 'card':
|
||||||
|
case 'credit_debit_card':
|
||||||
|
return PaymentType.creditCard;
|
||||||
|
case 'debitcard':
|
||||||
|
return PaymentType.debitCard;
|
||||||
|
case 'applepay':
|
||||||
|
case 'apple_pay':
|
||||||
|
return PaymentType.applePay;
|
||||||
|
case 'googlepay':
|
||||||
|
case 'google_pay':
|
||||||
|
return PaymentType.googlePay;
|
||||||
|
case 'revolutpay':
|
||||||
|
return PaymentType.revolutPay;
|
||||||
|
case 'neteller':
|
||||||
|
return PaymentType.neteller;
|
||||||
|
case 'skrill':
|
||||||
|
return PaymentType.skrill;
|
||||||
|
case 'sepabanktransfer':
|
||||||
|
case 'sepa':
|
||||||
|
case 'sepa_bank_transfer':
|
||||||
|
return PaymentType.sepa;
|
||||||
|
case 'sepainstant':
|
||||||
|
case 'sepa_instant':
|
||||||
|
return PaymentType.sepaInstant;
|
||||||
|
case 'ach':
|
||||||
|
case 'ach_bank_transfer':
|
||||||
|
return PaymentType.ach;
|
||||||
|
case 'iach':
|
||||||
|
case 'instant_ach':
|
||||||
|
return PaymentType.achInstant;
|
||||||
|
case 'khipu':
|
||||||
|
return PaymentType.Khipu;
|
||||||
|
case 'palomabanktransfer':
|
||||||
|
return PaymentType.palomaBanktTansfer;
|
||||||
|
case 'ovo':
|
||||||
|
return PaymentType.ovo;
|
||||||
|
case 'zalopay':
|
||||||
|
return PaymentType.zaloPay;
|
||||||
|
case 'zalobanktransfer':
|
||||||
|
case 'za_bank_transfer':
|
||||||
|
return PaymentType.zaloBankTransfer;
|
||||||
|
case 'gcash':
|
||||||
|
return PaymentType.gcash;
|
||||||
|
case 'imps':
|
||||||
|
return PaymentType.imps;
|
||||||
|
case 'dana':
|
||||||
|
return PaymentType.dana;
|
||||||
|
case 'ideal':
|
||||||
|
return PaymentType.ideal;
|
||||||
|
case 'paypal':
|
||||||
|
return PaymentType.paypal;
|
||||||
|
case 'sepa_open_banking_payment':
|
||||||
|
return PaymentType.sepaOpenBankingPayment;
|
||||||
|
case 'bancontact':
|
||||||
|
return PaymentType.bancontact;
|
||||||
|
default:
|
||||||
|
return PaymentType.all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,18 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
||||||
import 'package:cake_wallet/buy/buy_provider.dart';
|
import 'package:cake_wallet/buy/buy_provider.dart';
|
||||||
|
import 'package:cake_wallet/buy/buy_quote.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/generated/i18n.dart';
|
||||||
import 'package:cake_wallet/routes.dart';
|
import 'package:cake_wallet/routes.dart';
|
||||||
import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart';
|
import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart';
|
||||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||||
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
|
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
|
||||||
|
import 'package:cw_core/crypto_currency.dart';
|
||||||
import 'package:cw_core/wallet_base.dart';
|
import 'package:cw_core/wallet_base.dart';
|
||||||
import 'package:cw_core/wallet_type.dart';
|
import 'package:cw_core/wallet_type.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -15,7 +20,8 @@ import 'package:http/http.dart' as http;
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class RobinhoodBuyProvider extends BuyProvider {
|
class RobinhoodBuyProvider extends BuyProvider {
|
||||||
RobinhoodBuyProvider({required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM})
|
RobinhoodBuyProvider(
|
||||||
|
{required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM})
|
||||||
: super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: ledgerVM);
|
: super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: ledgerVM);
|
||||||
|
|
||||||
static const _baseUrl = 'applink.robinhood.com';
|
static const _baseUrl = 'applink.robinhood.com';
|
||||||
|
@ -33,6 +39,9 @@ class RobinhoodBuyProvider extends BuyProvider {
|
||||||
@override
|
@override
|
||||||
String get darkIcon => 'assets/images/robinhood_dark.png';
|
String get darkIcon => 'assets/images/robinhood_dark.png';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isAggregator => false;
|
||||||
|
|
||||||
String get _applicationId => secrets.robinhoodApplicationId;
|
String get _applicationId => secrets.robinhoodApplicationId;
|
||||||
|
|
||||||
String get _apiSecret => secrets.exchangeHelperApiKey;
|
String get _apiSecret => secrets.exchangeHelperApiKey;
|
||||||
|
@ -86,7 +95,13 @@ class RobinhoodBuyProvider extends BuyProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
|
Future<void>? launchProvider(
|
||||||
|
{required BuildContext context,
|
||||||
|
required Quote quote,
|
||||||
|
required double amount,
|
||||||
|
required bool isBuyAction,
|
||||||
|
required String cryptoCurrencyAddress,
|
||||||
|
String? countryCode}) async {
|
||||||
if (wallet.isHardwareWallet) {
|
if (wallet.isHardwareWallet) {
|
||||||
if (!ledgerVM!.isConnected) {
|
if (!ledgerVM!.isConnected) {
|
||||||
await Navigator.of(context).pushNamed(Routes.connectDevices,
|
await Navigator.of(context).pushNamed(Routes.connectDevices,
|
||||||
|
@ -116,4 +131,87 @@ class RobinhoodBuyProvider extends BuyProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Quote>?> fetchQuote(
|
||||||
|
{required CryptoCurrency cryptoCurrency,
|
||||||
|
required FiatCurrency fiatCurrency,
|
||||||
|
required double amount,
|
||||||
|
required bool isBuyAction,
|
||||||
|
required String walletAddress,
|
||||||
|
PaymentType? paymentType,
|
||||||
|
String? countryCode}) async {
|
||||||
|
String? paymentMethod;
|
||||||
|
|
||||||
|
if (paymentType != null && paymentType != PaymentType.all) {
|
||||||
|
paymentMethod = normalizePaymentMethod(paymentType);
|
||||||
|
if (paymentMethod == null) paymentMethod = paymentType.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
final action = isBuyAction ? 'buy' : 'sell';
|
||||||
|
log('Robinhood: Fetching $action quote: ${isBuyAction ? cryptoCurrency.title : fiatCurrency.name.toUpperCase()} -> ${isBuyAction ? fiatCurrency.name.toUpperCase() : cryptoCurrency.title}, amount: $amount paymentMethod: $paymentMethod');
|
||||||
|
|
||||||
|
final queryParams = {
|
||||||
|
'applicationId': _applicationId,
|
||||||
|
'fiatCode': fiatCurrency.name,
|
||||||
|
'assetCode': cryptoCurrency.title,
|
||||||
|
'fiatAmount': amount.toString(),
|
||||||
|
if (paymentMethod != null) 'paymentMethod': paymentMethod,
|
||||||
|
};
|
||||||
|
|
||||||
|
final uri =
|
||||||
|
Uri.https('api.robinhood.com', '/catpay/v1/${cryptoCurrency.title}/quote/', queryParams);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await http.get(uri, headers: {'accept': 'application/json'});
|
||||||
|
final responseData = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final paymentType = _getPaymentTypeByString(responseData['paymentMethod'] as String?);
|
||||||
|
final quote = Quote.fromRobinhoodJson(responseData, isBuyAction, paymentType);
|
||||||
|
quote.setFiatCurrency = fiatCurrency;
|
||||||
|
quote.setCryptoCurrency = cryptoCurrency;
|
||||||
|
return [quote];
|
||||||
|
} else {
|
||||||
|
if (responseData.containsKey('message')) {
|
||||||
|
log('Robinhood Error: ${responseData['message']}');
|
||||||
|
} else {
|
||||||
|
print('Robinhood Failed to fetch $action quote: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log('Robinhood: Failed to fetch $action quote: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ● buying_power
|
||||||
|
// ● crypto_balance
|
||||||
|
// ● debit_card
|
||||||
|
// ● bank_transfer
|
||||||
|
}
|
||||||
|
|
||||||
|
String? normalizePaymentMethod(PaymentType paymentMethod) {
|
||||||
|
switch (paymentMethod) {
|
||||||
|
case PaymentType.creditCard:
|
||||||
|
return 'debit_card';
|
||||||
|
case PaymentType.debitCard:
|
||||||
|
return 'debit_card';
|
||||||
|
case PaymentType.bankTransfer:
|
||||||
|
return 'bank_transfer';
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PaymentType _getPaymentTypeByString(String? paymentMethod) {
|
||||||
|
switch (paymentMethod) {
|
||||||
|
case 'debit_card':
|
||||||
|
return PaymentType.debitCard;
|
||||||
|
case 'bank_transfer':
|
||||||
|
return PaymentType.bankTransfer;
|
||||||
|
default:
|
||||||
|
return PaymentType.all;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
20
lib/buy/sell_buy_states.dart
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
abstract class PaymentMethodLoadingState {}
|
||||||
|
|
||||||
|
class InitialPaymentMethod extends PaymentMethodLoadingState {}
|
||||||
|
|
||||||
|
class PaymentMethodLoading extends PaymentMethodLoadingState {}
|
||||||
|
|
||||||
|
class PaymentMethodLoaded extends PaymentMethodLoadingState {}
|
||||||
|
|
||||||
|
class PaymentMethodFailed extends PaymentMethodLoadingState {}
|
||||||
|
|
||||||
|
|
||||||
|
abstract class BuySellQuotLoadingState {}
|
||||||
|
|
||||||
|
class InitialBuySellQuotState extends BuySellQuotLoadingState {}
|
||||||
|
|
||||||
|
class BuySellQuotLoading extends BuySellQuotLoadingState {}
|
||||||
|
|
||||||
|
class BuySellQuotLoaded extends BuySellQuotLoadingState {}
|
||||||
|
|
||||||
|
class BuySellQuotFailed extends BuySellQuotLoadingState {}
|
|
@ -42,6 +42,9 @@ class WyreBuyProvider extends BuyProvider {
|
||||||
@override
|
@override
|
||||||
String get darkIcon => 'assets/images/robinhood_dark.png';
|
String get darkIcon => 'assets/images/robinhood_dark.png';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isAggregator => false;
|
||||||
|
|
||||||
String get trackUrl => isTestEnvironment ? _trackTestUrl : _trackProductUrl;
|
String get trackUrl => isTestEnvironment ? _trackTestUrl : _trackProductUrl;
|
||||||
|
|
||||||
String baseApiUrl;
|
String baseApiUrl;
|
||||||
|
@ -148,10 +151,4 @@ class WyreBuyProvider extends BuyProvider {
|
||||||
receiveAddress: wallet.walletAddresses.address,
|
receiveAddress: wallet.walletAddresses.address,
|
||||||
walletId: wallet.id);
|
walletId: wallet.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> launchProvider(BuildContext context, bool? isBuyAction) {
|
|
||||||
// TODO: implement launchProvider
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
class CakePayOrder {
|
class CakePayOrder {
|
||||||
final String orderId;
|
final String orderId;
|
||||||
final List<OrderCard> cards;
|
final List<OrderCard> cards;
|
||||||
|
|
|
@ -82,11 +82,13 @@ class CakePayService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Logout
|
/// Logout
|
||||||
Future<void> logout(String email) async {
|
Future<void> logout([String? email]) async {
|
||||||
await secureStorage.delete(key: cakePayUsernameStorageKey);
|
await secureStorage.delete(key: cakePayUsernameStorageKey);
|
||||||
await secureStorage.delete(key: cakePayUserTokenKey);
|
await secureStorage.delete(key: cakePayUserTokenKey);
|
||||||
|
if (email != null) {
|
||||||
await cakePayApi.logoutUser(email: email, apiKey: cakePayApiKey);
|
await cakePayApi.logoutUser(email: email, apiKey: cakePayApiKey);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Purchase Gift Card
|
/// Purchase Gift Card
|
||||||
Future<CakePayOrder> createOrder(
|
Future<CakePayOrder> createOrder(
|
||||||
|
|
|
@ -268,9 +268,7 @@ class BackupService {
|
||||||
final currentFiatCurrency = data[PreferencesKey.currentFiatCurrencyKey] as String?;
|
final currentFiatCurrency = data[PreferencesKey.currentFiatCurrencyKey] as String?;
|
||||||
final shouldSaveRecipientAddress = data[PreferencesKey.shouldSaveRecipientAddressKey] as bool?;
|
final shouldSaveRecipientAddress = data[PreferencesKey.shouldSaveRecipientAddressKey] as bool?;
|
||||||
final isAppSecure = data[PreferencesKey.isAppSecureKey] as bool?;
|
final isAppSecure = data[PreferencesKey.isAppSecureKey] as bool?;
|
||||||
final disableBuy = data[PreferencesKey.disableBuyKey] as bool?;
|
final disableTradeOption = data[PreferencesKey.disableTradeOption] as bool?;
|
||||||
final disableSell = data[PreferencesKey.disableSellKey] as bool?;
|
|
||||||
final defaultBuyProvider = data[PreferencesKey.defaultBuyProvider] as int?;
|
|
||||||
final currentTransactionPriorityKeyLegacy =
|
final currentTransactionPriorityKeyLegacy =
|
||||||
data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?;
|
data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?;
|
||||||
final currentBitcoinElectrumSererId =
|
final currentBitcoinElectrumSererId =
|
||||||
|
@ -323,14 +321,8 @@ class BackupService {
|
||||||
if (isAppSecure != null)
|
if (isAppSecure != null)
|
||||||
await _sharedPreferences.setBool(PreferencesKey.isAppSecureKey, isAppSecure);
|
await _sharedPreferences.setBool(PreferencesKey.isAppSecureKey, isAppSecure);
|
||||||
|
|
||||||
if (disableBuy != null)
|
if (disableTradeOption != null)
|
||||||
await _sharedPreferences.setBool(PreferencesKey.disableBuyKey, disableBuy);
|
await _sharedPreferences.setBool(PreferencesKey.disableTradeOption, disableTradeOption);
|
||||||
|
|
||||||
if (disableSell != null)
|
|
||||||
await _sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell);
|
|
||||||
|
|
||||||
if (defaultBuyProvider != null)
|
|
||||||
await _sharedPreferences.setInt(PreferencesKey.defaultBuyProvider, defaultBuyProvider);
|
|
||||||
|
|
||||||
if (currentTransactionPriorityKeyLegacy != null)
|
if (currentTransactionPriorityKeyLegacy != null)
|
||||||
await _sharedPreferences.setInt(
|
await _sharedPreferences.setInt(
|
||||||
|
@ -516,10 +508,7 @@ class BackupService {
|
||||||
_sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey),
|
_sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey),
|
||||||
PreferencesKey.shouldSaveRecipientAddressKey:
|
PreferencesKey.shouldSaveRecipientAddressKey:
|
||||||
_sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey),
|
_sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey),
|
||||||
PreferencesKey.disableBuyKey: _sharedPreferences.getBool(PreferencesKey.disableBuyKey),
|
PreferencesKey.disableTradeOption: _sharedPreferences.getBool(PreferencesKey.disableTradeOption),
|
||||||
PreferencesKey.disableSellKey: _sharedPreferences.getBool(PreferencesKey.disableSellKey),
|
|
||||||
PreferencesKey.defaultBuyProvider:
|
|
||||||
_sharedPreferences.getInt(PreferencesKey.defaultBuyProvider),
|
|
||||||
PreferencesKey.currentPinLength: _sharedPreferences.getInt(PreferencesKey.currentPinLength),
|
PreferencesKey.currentPinLength: _sharedPreferences.getInt(PreferencesKey.currentPinLength),
|
||||||
PreferencesKey.currentTransactionPriorityKeyLegacy:
|
PreferencesKey.currentTransactionPriorityKeyLegacy:
|
||||||
_sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy),
|
_sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy),
|
||||||
|
|
47
lib/core/selectable_option.dart
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
abstract class SelectableItem {
|
||||||
|
SelectableItem({required this.title});
|
||||||
|
final String title;
|
||||||
|
}
|
||||||
|
|
||||||
|
class OptionTitle extends SelectableItem {
|
||||||
|
OptionTitle({required String title}) : super(title: title);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class SelectableOption extends SelectableItem {
|
||||||
|
SelectableOption({required String title}) : super(title: title);
|
||||||
|
|
||||||
|
String get lightIconPath;
|
||||||
|
|
||||||
|
String get darkIconPath;
|
||||||
|
|
||||||
|
String? get description => null;
|
||||||
|
|
||||||
|
String? get topLeftSubTitle => null;
|
||||||
|
|
||||||
|
String? get topLeftSubTitleIconPath => null;
|
||||||
|
|
||||||
|
String? get topRightSubTitle => null;
|
||||||
|
|
||||||
|
String? get topRightSubTitleLightIconPath => null;
|
||||||
|
|
||||||
|
String? get topRightSubTitleDarkIconPath => null;
|
||||||
|
|
||||||
|
String? get bottomLeftSubTitle => null;
|
||||||
|
|
||||||
|
String? get bottomLeftSubTitleIconPath => null;
|
||||||
|
|
||||||
|
String? get bottomRightSubTitle => null;
|
||||||
|
|
||||||
|
String? get bottomRightSubTitleLightIconPath => null;
|
||||||
|
|
||||||
|
String? get bottomRightSubTitleDarkIconPath => null;
|
||||||
|
|
||||||
|
List<String> get badges => [];
|
||||||
|
|
||||||
|
bool get isOptionSelected => false;
|
||||||
|
|
||||||
|
set isOptionSelected(bool isSelected) => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
39
lib/di.dart
|
@ -19,6 +19,7 @@ import 'package:cake_wallet/core/backup_service.dart';
|
||||||
import 'package:cake_wallet/core/key_service.dart';
|
import 'package:cake_wallet/core/key_service.dart';
|
||||||
import 'package:cake_wallet/core/new_wallet_type_arguments.dart';
|
import 'package:cake_wallet/core/new_wallet_type_arguments.dart';
|
||||||
import 'package:cake_wallet/core/secure_storage.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/totp_request_details.dart';
|
||||||
import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart';
|
import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart';
|
||||||
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
|
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
|
||||||
|
@ -34,6 +35,8 @@ import 'package:cake_wallet/entities/exchange_api_mode.dart';
|
||||||
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
|
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
|
||||||
import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart';
|
import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart';
|
||||||
import 'package:cake_wallet/entities/wallet_manager.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/receive/address_list_page.dart';
|
import 'package:cake_wallet/src/screens/receive/address_list_page.dart';
|
||||||
import 'package:cake_wallet/src/screens/settings/mweb_logs_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_node_page.dart';
|
||||||
|
@ -61,7 +64,6 @@ 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/auth/auth_page.dart';
|
||||||
import 'package:cake_wallet/src/screens/backup/backup_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/backup/edit_backup_password_page.dart';
|
||||||
import 'package:cake_wallet/src/screens/buy/buy_options_page.dart';
|
|
||||||
import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart';
|
import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart';
|
||||||
import 'package:cake_wallet/src/screens/buy/webview_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_list_page.dart';
|
||||||
|
@ -125,6 +127,7 @@ import 'package:cake_wallet/src/screens/subaddress/address_edit_or_create_page.d
|
||||||
import 'package:cake_wallet/src/screens/support/support_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_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/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/wallet_edit_page.dart';
|
||||||
import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.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_arguments.dart';
|
||||||
|
@ -134,6 +137,8 @@ import 'package:cake_wallet/utils/device_info.dart';
|
||||||
import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart';
|
import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart';
|
||||||
import 'package:cake_wallet/utils/payment_request.dart';
|
import 'package:cake_wallet/utils/payment_request.dart';
|
||||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
import 'package:cake_wallet/utils/responsive_layout_util.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/dashboard/desktop_sidebar_view_model.dart';
|
||||||
import 'package:cake_wallet/view_model/anon_invoice_page_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/anonpay_details_view_model.dart';
|
||||||
|
@ -247,6 +252,8 @@ import 'package:get_it/get_it.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:mobx/mobx.dart';
|
import 'package:mobx/mobx.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'buy/meld/meld_buy_provider.dart';
|
||||||
|
import 'src/screens/buy/buy_sell_page.dart';
|
||||||
import 'cake_pay/cake_pay_payment_credantials.dart';
|
import 'cake_pay/cake_pay_payment_credantials.dart';
|
||||||
|
|
||||||
final getIt = GetIt.instance;
|
final getIt = GetIt.instance;
|
||||||
|
@ -905,6 +912,11 @@ Future<void> setup({
|
||||||
|
|
||||||
getIt.registerFactory(() => WalletKeysPage(getIt.get<WalletKeysViewModel>()));
|
getIt.registerFactory(() => WalletKeysPage(getIt.get<WalletKeysViewModel>()));
|
||||||
|
|
||||||
|
getIt.registerFactory(() => AnimatedURModel(getIt.get<AppStore>()));
|
||||||
|
|
||||||
|
getIt.registerFactoryParam<AnimatedURPage, String, void>((String urQr, _) =>
|
||||||
|
AnimatedURPage(getIt.get<AnimatedURModel>(), urQr: urQr));
|
||||||
|
|
||||||
getIt.registerFactoryParam<ContactViewModel, ContactRecord?, void>(
|
getIt.registerFactoryParam<ContactViewModel, ContactRecord?, void>(
|
||||||
(ContactRecord? contact, _) => ContactViewModel(_contactSource, contact: contact));
|
(ContactRecord? contact, _) => ContactViewModel(_contactSource, contact: contact));
|
||||||
|
|
||||||
|
@ -998,6 +1010,10 @@ Future<void> setup({
|
||||||
wallet: getIt.get<AppStore>().wallet!,
|
wallet: getIt.get<AppStore>().wallet!,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
getIt.registerFactory<MeldBuyProvider>(() => MeldBuyProvider(
|
||||||
|
wallet: getIt.get<AppStore>().wallet!,
|
||||||
|
));
|
||||||
|
|
||||||
getIt.registerFactoryParam<WebViewPage, String, Uri>((title, uri) => WebViewPage(title, uri));
|
getIt.registerFactoryParam<WebViewPage, String, Uri>((title, uri) => WebViewPage(title, uri));
|
||||||
|
|
||||||
getIt.registerFactory<PayfuraBuyProvider>(() => PayfuraBuyProvider(
|
getIt.registerFactory<PayfuraBuyProvider>(() => PayfuraBuyProvider(
|
||||||
|
@ -1189,8 +1205,25 @@ Future<void> setup({
|
||||||
|
|
||||||
getIt.registerFactory(() => BuyAmountViewModel());
|
getIt.registerFactory(() => BuyAmountViewModel());
|
||||||
|
|
||||||
getIt.registerFactoryParam<BuySellOptionsPage, bool, void>(
|
getIt.registerFactory(() => BuySellViewModel(getIt.get<AppStore>()));
|
||||||
(isBuyOption, _) => BuySellOptionsPage(getIt.get<DashboardViewModel>(), isBuyOption));
|
|
||||||
|
getIt.registerFactory(() => BuySellPage(getIt.get<BuySellViewModel>()));
|
||||||
|
|
||||||
|
getIt.registerFactoryParam<BuyOptionsPage, List<dynamic>, void>((List<dynamic> args, _) {
|
||||||
|
final items = args.first as List<SelectableItem>;
|
||||||
|
final pickAnOption = args[1] as void Function(SelectableOption option)?;
|
||||||
|
final confirmOption = args[2] as void Function(BuildContext contex)?;
|
||||||
|
return BuyOptionsPage(
|
||||||
|
items: items, pickAnOption: pickAnOption, confirmOption: confirmOption);
|
||||||
|
});
|
||||||
|
|
||||||
|
getIt.registerFactoryParam<PaymentMethodOptionsPage, List<dynamic>, void>((List<dynamic> args, _) {
|
||||||
|
final items = args.first as List<SelectableOption>;
|
||||||
|
final pickAnOption = args[1] as void Function(SelectableOption option)?;
|
||||||
|
|
||||||
|
return PaymentMethodOptionsPage(
|
||||||
|
items: items, pickAnOption: pickAnOption);
|
||||||
|
});
|
||||||
|
|
||||||
getIt.registerFactory(() {
|
getIt.registerFactory(() {
|
||||||
final wallet = getIt.get<AppStore>().wallet;
|
final wallet = getIt.get<AppStore>().wallet;
|
||||||
|
|
|
@ -261,9 +261,13 @@ Future<void> defaultSettingsMigration(
|
||||||
updateBtcElectrumNodeToUseSSL(nodes, sharedPreferences);
|
updateBtcElectrumNodeToUseSSL(nodes, sharedPreferences);
|
||||||
break;
|
break;
|
||||||
case 43:
|
case 43:
|
||||||
|
_updateCakeXmrNode(nodes);
|
||||||
|
break;
|
||||||
|
case 44:
|
||||||
await addZanoNodeList(nodes: nodes);
|
await addZanoNodeList(nodes: nodes);
|
||||||
await changeZanoCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes);
|
await changeZanoCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -278,6 +282,15 @@ Future<void> defaultSettingsMigration(
|
||||||
await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, version);
|
await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, version);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _updateCakeXmrNode(Box<Node> nodes) {
|
||||||
|
final node = nodes.values.firstWhereOrNull((element) => element.uriRaw == newCakeWalletMoneroUri);
|
||||||
|
|
||||||
|
if (node != null && !node.trusted) {
|
||||||
|
node.trusted = true;
|
||||||
|
node.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void updateBtcElectrumNodeToUseSSL(Box<Node> nodes, SharedPreferences sharedPreferences) {
|
void updateBtcElectrumNodeToUseSSL(Box<Node> nodes, SharedPreferences sharedPreferences) {
|
||||||
final btcElectrumNode = nodes.values.firstWhereOrNull((element) => element.uriRaw == newCakeWalletBitcoinUri);
|
final btcElectrumNode = nodes.values.firstWhereOrNull((element) => element.uriRaw == newCakeWalletBitcoinUri);
|
||||||
|
|
||||||
|
@ -854,7 +867,7 @@ Future<void> changeDefaultMoneroNode(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero);
|
final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero, trusted: true);
|
||||||
|
|
||||||
await nodeSource.add(newCakeWalletNode);
|
await nodeSource.add(newCakeWalletNode);
|
||||||
|
|
||||||
|
|
|
@ -23,31 +23,18 @@ class MainActions {
|
||||||
});
|
});
|
||||||
|
|
||||||
static List<MainActions> all = [
|
static List<MainActions> all = [
|
||||||
buyAction,
|
showWalletsAction,
|
||||||
receiveAction,
|
receiveAction,
|
||||||
exchangeAction,
|
exchangeAction,
|
||||||
sendAction,
|
sendAction,
|
||||||
sellAction,
|
tradeAction,
|
||||||
];
|
];
|
||||||
|
|
||||||
static MainActions buyAction = MainActions._(
|
static MainActions showWalletsAction = MainActions._(
|
||||||
name: (context) => S.of(context).buy,
|
name: (context) => S.of(context).wallets,
|
||||||
image: 'assets/images/buy.png',
|
image: 'assets/images/wallet_new.png',
|
||||||
isEnabled: (viewModel) => viewModel.isEnabledBuyAction,
|
|
||||||
canShow: (viewModel) => viewModel.hasBuyAction,
|
|
||||||
onTap: (BuildContext context, DashboardViewModel viewModel) async {
|
onTap: (BuildContext context, DashboardViewModel viewModel) async {
|
||||||
if (!viewModel.isEnabledBuyAction) {
|
Navigator.pushNamed(context, Routes.walletList);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final defaultBuyProvider = viewModel.defaultBuyProvider;
|
|
||||||
try {
|
|
||||||
defaultBuyProvider != null
|
|
||||||
? await defaultBuyProvider.launchProvider(context, true)
|
|
||||||
: await Navigator.of(context).pushNamed(Routes.buySellPage, arguments: true);
|
|
||||||
} catch (e) {
|
|
||||||
await _showErrorDialog(context, defaultBuyProvider.toString(), e.toString());
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -79,39 +66,15 @@ class MainActions {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
static MainActions sellAction = MainActions._(
|
|
||||||
name: (context) => S.of(context).sell,
|
static MainActions tradeAction = MainActions._(
|
||||||
image: 'assets/images/sell.png',
|
name: (context) => '${S.of(context).buy} / ${S.of(context).sell}',
|
||||||
isEnabled: (viewModel) => viewModel.isEnabledSellAction,
|
image: 'assets/images/buy_sell.png',
|
||||||
canShow: (viewModel) => viewModel.hasSellAction,
|
isEnabled: (viewModel) => viewModel.isEnabledTradeAction,
|
||||||
|
canShow: (viewModel) => viewModel.hasTradeAction,
|
||||||
onTap: (BuildContext context, DashboardViewModel viewModel) async {
|
onTap: (BuildContext context, DashboardViewModel viewModel) async {
|
||||||
if (!viewModel.isEnabledSellAction) {
|
if (!viewModel.isEnabledTradeAction) return;
|
||||||
return;
|
await Navigator.of(context).pushNamed(Routes.buySellPage, arguments: false);
|
||||||
}
|
|
||||||
|
|
||||||
final defaultSellProvider = viewModel.defaultSellProvider;
|
|
||||||
try {
|
|
||||||
defaultSellProvider != null
|
|
||||||
? await defaultSellProvider.launchProvider(context, false)
|
|
||||||
: await Navigator.of(context).pushNamed(Routes.buySellPage, arguments: false);
|
|
||||||
} catch (e) {
|
|
||||||
await _showErrorDialog(context, defaultSellProvider.toString(), e.toString());
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
static Future<void> _showErrorDialog(
|
|
||||||
BuildContext context, String title, String errorMessage) async {
|
|
||||||
await showPopUp<void>(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return AlertWithOneAction(
|
|
||||||
alertTitle: title,
|
|
||||||
alertContent: errorMessage,
|
|
||||||
buttonText: S.of(context).ok,
|
|
||||||
buttonAction: () => Navigator.of(context).pop(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -22,10 +22,8 @@ class PreferencesKey {
|
||||||
static const currentBalanceDisplayModeKey = 'current_balance_display_mode';
|
static const currentBalanceDisplayModeKey = 'current_balance_display_mode';
|
||||||
static const shouldSaveRecipientAddressKey = 'save_recipient_address';
|
static const shouldSaveRecipientAddressKey = 'save_recipient_address';
|
||||||
static const isAppSecureKey = 'is_app_secure';
|
static const isAppSecureKey = 'is_app_secure';
|
||||||
static const disableBuyKey = 'disable_buy';
|
static const disableTradeOption = 'disable_buy';
|
||||||
static const disableSellKey = 'disable_sell';
|
|
||||||
static const disableBulletinKey = 'disable_bulletin';
|
static const disableBulletinKey = 'disable_bulletin';
|
||||||
static const defaultBuyProvider = 'default_buy_provider';
|
|
||||||
static const walletListOrder = 'wallet_list_order';
|
static const walletListOrder = 'wallet_list_order';
|
||||||
static const contactListOrder = 'contact_list_order';
|
static const contactListOrder = 'contact_list_order';
|
||||||
static const walletListAscending = 'wallet_list_ascending';
|
static const walletListAscending = 'wallet_list_ascending';
|
||||||
|
|
|
@ -1,24 +1,18 @@
|
||||||
import 'package:cake_wallet/buy/buy_provider.dart';
|
import 'package:cake_wallet/buy/buy_provider.dart';
|
||||||
import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart';
|
import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart';
|
||||||
|
import 'package:cake_wallet/buy/meld/meld_buy_provider.dart';
|
||||||
import 'package:cake_wallet/buy/moonpay/moonpay_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/buy/onramper/onramper_buy_provider.dart';
|
||||||
import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart';
|
import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart';
|
||||||
import 'package:cake_wallet/di.dart';
|
import 'package:cake_wallet/di.dart';
|
||||||
import 'package:cw_core/wallet_type.dart';
|
import 'package:cw_core/wallet_type.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
|
||||||
enum ProviderType {
|
enum ProviderType { robinhood, dfx, onramper, moonpay, meld }
|
||||||
askEachTime,
|
|
||||||
robinhood,
|
|
||||||
dfx,
|
|
||||||
onramper,
|
|
||||||
moonpay,
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ProviderTypeName on ProviderType {
|
extension ProviderTypeName on ProviderType {
|
||||||
String get title {
|
String get title {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case ProviderType.askEachTime:
|
|
||||||
return 'Ask each time';
|
|
||||||
case ProviderType.robinhood:
|
case ProviderType.robinhood:
|
||||||
return 'Robinhood Connect';
|
return 'Robinhood Connect';
|
||||||
case ProviderType.dfx:
|
case ProviderType.dfx:
|
||||||
|
@ -27,13 +21,13 @@ extension ProviderTypeName on ProviderType {
|
||||||
return 'Onramper';
|
return 'Onramper';
|
||||||
case ProviderType.moonpay:
|
case ProviderType.moonpay:
|
||||||
return 'MoonPay';
|
return 'MoonPay';
|
||||||
|
case ProviderType.meld:
|
||||||
|
return 'Meld';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String get id {
|
String get id {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case ProviderType.askEachTime:
|
|
||||||
return 'ask_each_time_provider';
|
|
||||||
case ProviderType.robinhood:
|
case ProviderType.robinhood:
|
||||||
return 'robinhood_connect_provider';
|
return 'robinhood_connect_provider';
|
||||||
case ProviderType.dfx:
|
case ProviderType.dfx:
|
||||||
|
@ -42,6 +36,8 @@ extension ProviderTypeName on ProviderType {
|
||||||
return 'onramper_provider';
|
return 'onramper_provider';
|
||||||
case ProviderType.moonpay:
|
case ProviderType.moonpay:
|
||||||
return 'moonpay_provider';
|
return 'moonpay_provider';
|
||||||
|
case ProviderType.meld:
|
||||||
|
return 'meld_provider';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,14 +48,13 @@ class ProvidersHelper {
|
||||||
case WalletType.nano:
|
case WalletType.nano:
|
||||||
case WalletType.banano:
|
case WalletType.banano:
|
||||||
case WalletType.wownero:
|
case WalletType.wownero:
|
||||||
return [ProviderType.askEachTime, ProviderType.onramper];
|
return [ProviderType.onramper];
|
||||||
case WalletType.monero:
|
case WalletType.monero:
|
||||||
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx];
|
return [ProviderType.onramper, ProviderType.dfx];
|
||||||
case WalletType.bitcoin:
|
case WalletType.bitcoin:
|
||||||
case WalletType.polygon:
|
case WalletType.polygon:
|
||||||
case WalletType.ethereum:
|
case WalletType.ethereum:
|
||||||
return [
|
return [
|
||||||
ProviderType.askEachTime,
|
|
||||||
ProviderType.onramper,
|
ProviderType.onramper,
|
||||||
ProviderType.dfx,
|
ProviderType.dfx,
|
||||||
ProviderType.robinhood,
|
ProviderType.robinhood,
|
||||||
|
@ -68,10 +63,13 @@ class ProvidersHelper {
|
||||||
case WalletType.litecoin:
|
case WalletType.litecoin:
|
||||||
case WalletType.bitcoinCash:
|
case WalletType.bitcoinCash:
|
||||||
case WalletType.solana:
|
case WalletType.solana:
|
||||||
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood, ProviderType.moonpay];
|
return [
|
||||||
|
ProviderType.onramper,
|
||||||
|
ProviderType.robinhood,
|
||||||
|
ProviderType.moonpay
|
||||||
|
];
|
||||||
case WalletType.tron:
|
case WalletType.tron:
|
||||||
return [
|
return [
|
||||||
ProviderType.askEachTime,
|
|
||||||
ProviderType.onramper,
|
ProviderType.onramper,
|
||||||
ProviderType.robinhood,
|
ProviderType.robinhood,
|
||||||
ProviderType.moonpay,
|
ProviderType.moonpay,
|
||||||
|
@ -89,28 +87,24 @@ class ProvidersHelper {
|
||||||
case WalletType.ethereum:
|
case WalletType.ethereum:
|
||||||
case WalletType.polygon:
|
case WalletType.polygon:
|
||||||
return [
|
return [
|
||||||
ProviderType.askEachTime,
|
|
||||||
ProviderType.onramper,
|
ProviderType.onramper,
|
||||||
ProviderType.moonpay,
|
ProviderType.moonpay,
|
||||||
ProviderType.dfx,
|
ProviderType.dfx,
|
||||||
];
|
];
|
||||||
case WalletType.litecoin:
|
case WalletType.litecoin:
|
||||||
case WalletType.bitcoinCash:
|
case WalletType.bitcoinCash:
|
||||||
return [ProviderType.askEachTime, ProviderType.moonpay];
|
return [ProviderType.moonpay];
|
||||||
case WalletType.solana:
|
case WalletType.solana:
|
||||||
return [
|
return [
|
||||||
ProviderType.askEachTime,
|
|
||||||
ProviderType.onramper,
|
ProviderType.onramper,
|
||||||
ProviderType.robinhood,
|
|
||||||
ProviderType.moonpay,
|
ProviderType.moonpay,
|
||||||
];
|
];
|
||||||
case WalletType.tron:
|
case WalletType.tron:
|
||||||
return [
|
return [
|
||||||
ProviderType.askEachTime,
|
|
||||||
ProviderType.robinhood,
|
|
||||||
ProviderType.moonpay,
|
ProviderType.moonpay,
|
||||||
];
|
];
|
||||||
case WalletType.monero:
|
case WalletType.monero:
|
||||||
|
return [ProviderType.dfx];
|
||||||
case WalletType.nano:
|
case WalletType.nano:
|
||||||
case WalletType.banano:
|
case WalletType.banano:
|
||||||
case WalletType.none:
|
case WalletType.none:
|
||||||
|
@ -131,7 +125,9 @@ class ProvidersHelper {
|
||||||
return getIt.get<OnRamperBuyProvider>();
|
return getIt.get<OnRamperBuyProvider>();
|
||||||
case ProviderType.moonpay:
|
case ProviderType.moonpay:
|
||||||
return getIt.get<MoonPayProvider>();
|
return getIt.get<MoonPayProvider>();
|
||||||
case ProviderType.askEachTime:
|
case ProviderType.meld:
|
||||||
|
return getIt.get<MeldBuyProvider>();
|
||||||
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,376 @@
|
||||||
import 'package:barcode_scan2/barcode_scan2.dart';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:cake_wallet/generated/i18n.dart';
|
||||||
|
import 'package:cake_wallet/main.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:fast_scanner/fast_scanner.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
|
||||||
var isQrScannerShown = false;
|
var isQrScannerShown = false;
|
||||||
|
|
||||||
Future<String> presentQRScanner() async {
|
Future<String> presentQRScanner(BuildContext context) async {
|
||||||
isQrScannerShown = true;
|
isQrScannerShown = true;
|
||||||
try {
|
try {
|
||||||
final result = await BarcodeScanner.scan();
|
final result = await Navigator.of(context).push<String>(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder:(context) {
|
||||||
|
return BarcodeScannerSimple();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
isQrScannerShown = false;
|
isQrScannerShown = false;
|
||||||
return result.rawContent.trim();
|
return result??'';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
isQrScannerShown = false;
|
isQrScannerShown = false;
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://github.com/MrCyjaneK/fast_scanner/blob/master/example/lib/barcode_scanner_simple.dart
|
||||||
|
class BarcodeScannerSimple extends StatefulWidget {
|
||||||
|
const BarcodeScannerSimple({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BarcodeScannerSimple> createState() => _BarcodeScannerSimpleState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BarcodeScannerSimpleState extends State<BarcodeScannerSimple> {
|
||||||
|
Barcode? _barcode;
|
||||||
|
bool popped = false;
|
||||||
|
|
||||||
|
List<String> urCodes = [];
|
||||||
|
late var ur = URQRToURQRData(urCodes);
|
||||||
|
|
||||||
|
void _handleBarcode(BarcodeCapture barcodes) {
|
||||||
|
try {
|
||||||
|
_handleBarcodeInternal(barcodes);
|
||||||
|
} catch (e) {
|
||||||
|
showPopUp<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertWithOneAction(
|
||||||
|
alertTitle: S.of(context).error,
|
||||||
|
alertContent: S.of(context).error_dialog_content,
|
||||||
|
buttonText: 'ok',
|
||||||
|
buttonAction: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleBarcodeInternal(BarcodeCapture barcodes) {
|
||||||
|
for (final barcode in barcodes.barcodes) {
|
||||||
|
// don't handle unknown QR codes
|
||||||
|
if (barcode.rawValue?.trim().isEmpty??false == false) continue;
|
||||||
|
if (barcode.rawValue!.startsWith("ur:")) {
|
||||||
|
if (urCodes.contains(barcode.rawValue)) continue;
|
||||||
|
setState(() {
|
||||||
|
urCodes.add(barcode.rawValue!);
|
||||||
|
ur = URQRToURQRData(urCodes);
|
||||||
|
});
|
||||||
|
if (ur.progress == 1) {
|
||||||
|
setState(() {
|
||||||
|
popped = true;
|
||||||
|
});
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
|
Navigator.of(context).pop(ur.inputs.join("\n"));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (urCodes.isNotEmpty) return;
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_barcode = barcodes.barcodes.firstOrNull;
|
||||||
|
});
|
||||||
|
if (_barcode != null && popped != true) {
|
||||||
|
setState(() {
|
||||||
|
popped = true;
|
||||||
|
});
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
|
Navigator.of(context).pop(_barcode?.rawValue ?? "");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final MobileScannerController ctrl = MobileScannerController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Scan'),
|
||||||
|
actions: [
|
||||||
|
SwitchCameraButton(controller: ctrl),
|
||||||
|
ToggleFlashlightButton(controller: ctrl),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
MobileScanner(
|
||||||
|
onDetect: _handleBarcode,
|
||||||
|
controller: ctrl,
|
||||||
|
),
|
||||||
|
if (ur.inputs.length != 0)
|
||||||
|
Center(child:
|
||||||
|
Text(
|
||||||
|
"${ur.inputs.length}/${ur.count}",
|
||||||
|
style: Theme.of(context).textTheme.displayLarge?.copyWith(color: Colors.white)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
child: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 250,
|
||||||
|
height: 250,
|
||||||
|
child: CustomPaint(
|
||||||
|
painter: ProgressPainter(
|
||||||
|
urQrProgress: URQrProgress(
|
||||||
|
expectedPartCount: ur.count - 1,
|
||||||
|
processedPartsCount: ur.inputs.length,
|
||||||
|
receivedPartIndexes: _urParts(),
|
||||||
|
percentage: ur.progress,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> _urParts() {
|
||||||
|
List<int> l = [];
|
||||||
|
for (var inp in ur.inputs) {
|
||||||
|
try {
|
||||||
|
l.add(int.parse(inp.split("/")[1].split("-")[0]));
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ToggleFlashlightButton extends StatelessWidget {
|
||||||
|
const ToggleFlashlightButton({required this.controller, super.key});
|
||||||
|
|
||||||
|
final MobileScannerController controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ValueListenableBuilder(
|
||||||
|
valueListenable: controller,
|
||||||
|
builder: (context, state, child) {
|
||||||
|
if (!state.isInitialized || !state.isRunning) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (state.torchState) {
|
||||||
|
case TorchState.auto:
|
||||||
|
return IconButton(
|
||||||
|
iconSize: 32.0,
|
||||||
|
icon: const Icon(Icons.flash_auto),
|
||||||
|
onPressed: () async {
|
||||||
|
await controller.toggleTorch();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
case TorchState.off:
|
||||||
|
return IconButton(
|
||||||
|
iconSize: 32.0,
|
||||||
|
icon: const Icon(Icons.flash_off),
|
||||||
|
onPressed: () async {
|
||||||
|
await controller.toggleTorch();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
case TorchState.on:
|
||||||
|
return IconButton(
|
||||||
|
iconSize: 32.0,
|
||||||
|
icon: const Icon(Icons.flash_on),
|
||||||
|
onPressed: () async {
|
||||||
|
await controller.toggleTorch();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
case TorchState.unavailable:
|
||||||
|
return const Icon(
|
||||||
|
Icons.no_flash,
|
||||||
|
color: Colors.grey,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SwitchCameraButton extends StatelessWidget {
|
||||||
|
const SwitchCameraButton({required this.controller, super.key});
|
||||||
|
|
||||||
|
final MobileScannerController controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ValueListenableBuilder(
|
||||||
|
valueListenable: controller,
|
||||||
|
builder: (context, state, child) {
|
||||||
|
if (!state.isInitialized || !state.isRunning) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final int? availableCameras = state.availableCameras;
|
||||||
|
|
||||||
|
if (availableCameras != null && availableCameras < 2) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final Widget icon;
|
||||||
|
|
||||||
|
switch (state.cameraDirection) {
|
||||||
|
case CameraFacing.front:
|
||||||
|
icon = const Icon(Icons.camera_front);
|
||||||
|
case CameraFacing.back:
|
||||||
|
icon = const Icon(Icons.camera_rear);
|
||||||
|
}
|
||||||
|
|
||||||
|
return IconButton(
|
||||||
|
iconSize: 32.0,
|
||||||
|
icon: icon,
|
||||||
|
onPressed: () async {
|
||||||
|
await controller.switchCamera();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class URQRData {
|
||||||
|
URQRData(
|
||||||
|
{required this.tag,
|
||||||
|
required this.str,
|
||||||
|
required this.progress,
|
||||||
|
required this.count,
|
||||||
|
required this.error,
|
||||||
|
required this.inputs});
|
||||||
|
final String tag;
|
||||||
|
final String str;
|
||||||
|
final double progress;
|
||||||
|
final int count;
|
||||||
|
final String error;
|
||||||
|
final List<String> inputs;
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
"tag": tag,
|
||||||
|
"str": str,
|
||||||
|
"progress": progress,
|
||||||
|
"count": count,
|
||||||
|
"error": error,
|
||||||
|
"inputs": inputs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
URQRData URQRToURQRData(List<String> urqr_) {
|
||||||
|
final urqr = urqr_.toSet().toList();
|
||||||
|
urqr.sort((s1, s2) {
|
||||||
|
final s1s = s1.split("/");
|
||||||
|
final s1frameStr = s1s[1].split("-");
|
||||||
|
final s1curFrame = int.parse(s1frameStr[0]);
|
||||||
|
final s2s = s2.split("/");
|
||||||
|
final s2frameStr = s2s[1].split("-");
|
||||||
|
final s2curFrame = int.parse(s2frameStr[0]);
|
||||||
|
return s1curFrame - s2curFrame;
|
||||||
|
});
|
||||||
|
|
||||||
|
String tag = '';
|
||||||
|
int count = 0;
|
||||||
|
String bw = '';
|
||||||
|
for (var elm in urqr) {
|
||||||
|
final s = elm.substring(elm.indexOf(":") + 1); // strip down ur: prefix
|
||||||
|
final s2 = s.split("/");
|
||||||
|
tag = s2[0];
|
||||||
|
final frameStr = s2[1].split("-");
|
||||||
|
// final curFrame = int.parse(frameStr[0]);
|
||||||
|
count = int.parse(frameStr[1]);
|
||||||
|
final byteWords = s2[2];
|
||||||
|
bw += byteWords;
|
||||||
|
}
|
||||||
|
String? error;
|
||||||
|
|
||||||
|
return URQRData(
|
||||||
|
tag: tag,
|
||||||
|
str: bw,
|
||||||
|
progress: count == 0 ? 0 : (urqr.length / count),
|
||||||
|
count: count,
|
||||||
|
error: error ?? "",
|
||||||
|
inputs: urqr,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProgressPainter extends CustomPainter {
|
||||||
|
final URQrProgress urQrProgress;
|
||||||
|
|
||||||
|
ProgressPainter({required this.urQrProgress});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final c = Offset(size.width / 2.0, size.height / 2.0);
|
||||||
|
final radius = size.width * 0.9;
|
||||||
|
final rect = Rect.fromCenter(center: c, width: radius, height: radius);
|
||||||
|
const fullAngle = 360.0;
|
||||||
|
var startAngle = 0.0;
|
||||||
|
for (int i = 0; i < urQrProgress.expectedPartCount.toInt(); i++) {
|
||||||
|
var sweepAngle =
|
||||||
|
(1 / urQrProgress.expectedPartCount) * fullAngle * pi / 180.0;
|
||||||
|
drawSector(canvas, urQrProgress.receivedPartIndexes.contains(i), rect,
|
||||||
|
startAngle, sweepAngle);
|
||||||
|
startAngle += sweepAngle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawSector(Canvas canvas, bool isActive, Rect rect, double startAngle,
|
||||||
|
double sweepAngle) {
|
||||||
|
final paint = Paint()
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 8
|
||||||
|
..strokeCap = StrokeCap.round
|
||||||
|
..strokeJoin = StrokeJoin.round
|
||||||
|
..color = isActive ? const Color(0xffff6600) : Colors.white70;
|
||||||
|
canvas.drawArc(rect, startAngle, sweepAngle, false, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant ProgressPainter oldDelegate) {
|
||||||
|
return urQrProgress != oldDelegate.urQrProgress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class URQrProgress {
|
||||||
|
int expectedPartCount;
|
||||||
|
int processedPartsCount;
|
||||||
|
List<int> receivedPartIndexes;
|
||||||
|
double percentage;
|
||||||
|
|
||||||
|
URQrProgress({
|
||||||
|
required this.expectedPartCount,
|
||||||
|
required this.processedPartsCount,
|
||||||
|
required this.receivedPartIndexes,
|
||||||
|
required this.percentage,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool equals(URQrProgress? progress) {
|
||||||
|
if (progress == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return processedPartsCount == progress.processedPartsCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -248,6 +248,7 @@ class CWMonero extends Monero {
|
||||||
final moneroWallet = wallet as MoneroWallet;
|
final moneroWallet = wallet as MoneroWallet;
|
||||||
final keys = moneroWallet.keys;
|
final keys = moneroWallet.keys;
|
||||||
return <String, String>{
|
return <String, String>{
|
||||||
|
'primaryAddress': keys.primaryAddress,
|
||||||
'privateSpendKey': keys.privateSpendKey,
|
'privateSpendKey': keys.privateSpendKey,
|
||||||
'privateViewKey': keys.privateViewKey,
|
'privateViewKey': keys.privateViewKey,
|
||||||
'publicSpendKey': keys.publicSpendKey,
|
'publicSpendKey': keys.publicSpendKey,
|
||||||
|
@ -358,8 +359,31 @@ class CWMonero extends Monero {
|
||||||
return monero_wallet_api.getCurrentHeight();
|
return monero_wallet_api.getCurrentHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool importKeyImagesUR(Object wallet, String ur) {
|
||||||
|
final moneroWallet = wallet as MoneroWallet;
|
||||||
|
return moneroWallet.importKeyImagesUR(ur);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> commitTransactionUR(Object wallet, String ur) {
|
||||||
|
final moneroWallet = wallet as MoneroWallet;
|
||||||
|
return moneroWallet.submitTransactionUR(ur);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String exportOutputsUR(Object wallet, bool all) {
|
||||||
|
final moneroWallet = wallet as MoneroWallet;
|
||||||
|
return moneroWallet.exportOutputsUR(all);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void monerocCheck() {
|
void monerocCheck() {
|
||||||
checkIfMoneroCIsFine();
|
checkIfMoneroCIsFine();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isViewOnly() {
|
||||||
|
return isViewOnlyBySpendKey();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,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/auth/auth_page.dart';
|
||||||
import 'package:cake_wallet/src/screens/backup/backup_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/backup/edit_backup_password_page.dart';
|
||||||
import 'package:cake_wallet/src/screens/buy/buy_options_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/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/buy/webview_page.dart';
|
||||||
import 'package:cake_wallet/src/screens/cake_pay/auth/cake_pay_account_page.dart';
|
import 'package:cake_wallet/src/screens/cake_pay/auth/cake_pay_account_page.dart';
|
||||||
import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart';
|
import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart';
|
||||||
|
@ -96,6 +97,7 @@ import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dar
|
||||||
import 'package:cake_wallet/src/screens/transaction_details/transaction_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_details_page.dart';
|
||||||
import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_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/wallet_edit_page.dart';
|
||||||
import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.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_keys/wallet_keys_page.dart';
|
||||||
|
@ -128,7 +130,8 @@ import 'package:cw_core/wallet_type.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart';
|
||||||
|
import 'src/screens/buy/buy_sell_page.dart';
|
||||||
import 'src/screens/dashboard/pages/nft_import_page.dart';
|
import 'src/screens/dashboard/pages/nft_import_page.dart';
|
||||||
|
|
||||||
late RouteSettings currentRouteSettings;
|
late RouteSettings currentRouteSettings;
|
||||||
|
@ -570,7 +573,15 @@ Route<dynamic> createRoute(RouteSettings settings) {
|
||||||
|
|
||||||
case Routes.buySellPage:
|
case Routes.buySellPage:
|
||||||
final args = settings.arguments as bool;
|
final args = settings.arguments as bool;
|
||||||
return MaterialPageRoute<void>(builder: (_) => getIt.get<BuySellOptionsPage>(param1: args));
|
return MaterialPageRoute<void>(builder: (_) => getIt.get<BuySellPage>(param1: args));
|
||||||
|
|
||||||
|
case Routes.buyOptionsPage:
|
||||||
|
final args = settings.arguments as List;
|
||||||
|
return MaterialPageRoute<void>(builder: (_) => getIt.get<BuyOptionsPage>(param1: args));
|
||||||
|
|
||||||
|
case Routes.paymentMethodOptionsPage:
|
||||||
|
final args = settings.arguments as List;
|
||||||
|
return MaterialPageRoute<void>(builder: (_) => getIt.get<PaymentMethodOptionsPage>(param1: args));
|
||||||
|
|
||||||
case Routes.buyWebView:
|
case Routes.buyWebView:
|
||||||
final args = settings.arguments as List;
|
final args = settings.arguments as List;
|
||||||
|
@ -732,6 +743,9 @@ Route<dynamic> createRoute(RouteSettings settings) {
|
||||||
case Routes.setup2faInfoPage:
|
case Routes.setup2faInfoPage:
|
||||||
return MaterialPageRoute<void>(builder: (_) => getIt.get<Setup2FAInfoPage>());
|
return MaterialPageRoute<void>(builder: (_) => getIt.get<Setup2FAInfoPage>());
|
||||||
|
|
||||||
|
case Routes.urqrAnimatedPage:
|
||||||
|
return MaterialPageRoute<void>(builder: (_) => getIt.get<AnimatedURPage>(param1: settings.arguments));
|
||||||
|
|
||||||
case Routes.homeSettings:
|
case Routes.homeSettings:
|
||||||
return CupertinoPageRoute<void>(
|
return CupertinoPageRoute<void>(
|
||||||
builder: (_) => getIt.get<HomeSettingsPage>(param1: settings.arguments),
|
builder: (_) => getIt.get<HomeSettingsPage>(param1: settings.arguments),
|
||||||
|
|
|
@ -59,6 +59,8 @@ class Routes {
|
||||||
static const supportOtherLinks = '/support/other';
|
static const supportOtherLinks = '/support/other';
|
||||||
static const orderDetails = '/order_details';
|
static const orderDetails = '/order_details';
|
||||||
static const buySellPage = '/buy_sell_page';
|
static const buySellPage = '/buy_sell_page';
|
||||||
|
static const buyOptionsPage = '/buy_sell_options';
|
||||||
|
static const paymentMethodOptionsPage = '/payment_method_options';
|
||||||
static const buyWebView = '/buy_web_view';
|
static const buyWebView = '/buy_web_view';
|
||||||
static const unspentCoinsList = '/unspent_coins_list';
|
static const unspentCoinsList = '/unspent_coins_list';
|
||||||
static const unspentCoinsDetails = '/unspent_coins_details';
|
static const unspentCoinsDetails = '/unspent_coins_details';
|
||||||
|
@ -108,6 +110,7 @@ class Routes {
|
||||||
|
|
||||||
static const signPage = '/sign_page';
|
static const signPage = '/sign_page';
|
||||||
static const connectDevices = '/device/connect';
|
static const connectDevices = '/device/connect';
|
||||||
|
static const urqrAnimatedPage = '/urqr/animated_page';
|
||||||
static const walletGroupsDisplayPage = '/wallet_groups_display_page';
|
static const walletGroupsDisplayPage = '/wallet_groups_display_page';
|
||||||
static const walletGroupDescription = '/wallet_group_description';
|
static const walletGroupDescription = '/wallet_group_description';
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,74 +0,0 @@
|
||||||
import 'package:cake_wallet/generated/i18n.dart';
|
|
||||||
import 'package:cake_wallet/src/screens/base_page.dart';
|
|
||||||
import 'package:cake_wallet/src/widgets/option_tile.dart';
|
|
||||||
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
|
|
||||||
import 'package:cake_wallet/themes/extensions/option_tile_theme.dart';
|
|
||||||
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
|
|
||||||
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class BuySellOptionsPage extends BasePage {
|
|
||||||
BuySellOptionsPage(this.dashboardViewModel, this.isBuyAction);
|
|
||||||
|
|
||||||
final DashboardViewModel dashboardViewModel;
|
|
||||||
final bool isBuyAction;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get title => isBuyAction ? S.current.buy : S.current.sell;
|
|
||||||
|
|
||||||
@override
|
|
||||||
AppBarStyle get appBarStyle => AppBarStyle.regular;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget body(BuildContext context) {
|
|
||||||
final isLightMode = Theme.of(context).extension<OptionTileTheme>()?.useDarkImage ?? false;
|
|
||||||
final availableProviders = isBuyAction
|
|
||||||
? dashboardViewModel.availableBuyProviders
|
|
||||||
: dashboardViewModel.availableSellProviders;
|
|
||||||
|
|
||||||
return ScrollableWithBottomSection(
|
|
||||||
content: Container(
|
|
||||||
child: Center(
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(maxWidth: 330),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
...availableProviders.map((provider) {
|
|
||||||
final icon = Image.asset(
|
|
||||||
isLightMode ? provider.lightIcon : provider.darkIcon,
|
|
||||||
height: 40,
|
|
||||||
width: 40,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.only(top: 24),
|
|
||||||
child: OptionTile(
|
|
||||||
image: icon,
|
|
||||||
title: provider.toString(),
|
|
||||||
description: provider.providerDescription,
|
|
||||||
onPressed: () => provider.launchProvider(context, isBuyAction),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
bottomSection: Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(24, 24, 24, 32),
|
|
||||||
child: Text(
|
|
||||||
isBuyAction
|
|
||||||
? S.of(context).select_buy_provider_notice
|
|
||||||
: S.of(context).select_sell_provider_notice,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.normal,
|
|
||||||
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
48
lib/src/screens/buy/buy_sell_options_page.dart
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import 'package:cake_wallet/core/selectable_option.dart';
|
||||||
|
import 'package:cake_wallet/generated/i18n.dart';
|
||||||
|
import 'package:cake_wallet/src/screens/select_options_page.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
|
class BuyOptionsPage extends SelectOptionsPage {
|
||||||
|
BuyOptionsPage({required this.items, this.pickAnOption, this.confirmOption});
|
||||||
|
|
||||||
|
final List<SelectableItem> items;
|
||||||
|
final Function(SelectableOption option)? pickAnOption;
|
||||||
|
final Function(BuildContext context)? confirmOption;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pageTitle => S.current.choose_a_provider;
|
||||||
|
|
||||||
|
@override
|
||||||
|
EdgeInsets? get contentPadding => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
EdgeInsets? get tilePadding => EdgeInsets.only(top: 8);
|
||||||
|
|
||||||
|
@override
|
||||||
|
EdgeInsets? get innerPadding => EdgeInsets.symmetric(horizontal: 24, vertical: 8);
|
||||||
|
|
||||||
|
@override
|
||||||
|
double? get imageHeight => 40;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double? get imageWidth => 40;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get selectedBackgroundColor => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double? get tileBorderRadius => 30;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get bottomSectionText => '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void Function(SelectableOption option)? get onOptionTap => pickAnOption;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get primaryButtonText => S.current.confirm;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void Function(BuildContext context)? get primaryButtonAction => confirmOption;
|
||||||
|
}
|
469
lib/src/screens/buy/buy_sell_page.dart
Normal file
|
@ -0,0 +1,469 @@
|
||||||
|
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';
|
||||||
|
import 'package:cake_wallet/src/screens/base_page.dart';
|
||||||
|
import 'package:cake_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart';
|
||||||
|
import 'package:cake_wallet/src/screens/exchange/widgets/exchange_card.dart';
|
||||||
|
import 'package:cake_wallet/src/screens/exchange/widgets/mobile_exchange_cards_section.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/provider_optoin_tile.dart';
|
||||||
|
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
|
||||||
|
import 'package:cake_wallet/src/widgets/trail_button.dart';
|
||||||
|
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
|
||||||
|
import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart';
|
||||||
|
import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
|
||||||
|
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
|
||||||
|
import 'package:cake_wallet/themes/theme_base.dart';
|
||||||
|
import 'package:cake_wallet/typography.dart';
|
||||||
|
import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
|
||||||
|
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||||
|
import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart';
|
||||||
|
import 'package:cw_core/crypto_currency.dart';
|
||||||
|
import 'package:cw_core/currency.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';
|
||||||
|
import 'package:mobx/mobx.dart';
|
||||||
|
|
||||||
|
class BuySellPage extends BasePage {
|
||||||
|
BuySellPage(this.buySellViewModel);
|
||||||
|
|
||||||
|
final BuySellViewModel buySellViewModel;
|
||||||
|
final cryptoCurrencyKey = GlobalKey<ExchangeCardState>();
|
||||||
|
final fiatCurrencyKey = GlobalKey<ExchangeCardState>();
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _fiatAmountFocus = FocusNode();
|
||||||
|
final _cryptoAmountFocus = FocusNode();
|
||||||
|
final _cryptoAddressFocus = FocusNode();
|
||||||
|
var _isReactionsSet = false;
|
||||||
|
|
||||||
|
final arrowBottomPurple = Image.asset(
|
||||||
|
'assets/images/arrow_bottom_purple_icon.png',
|
||||||
|
color: Colors.white,
|
||||||
|
height: 8,
|
||||||
|
);
|
||||||
|
final arrowBottomCakeGreen = Image.asset(
|
||||||
|
'assets/images/arrow_bottom_cake_green.png',
|
||||||
|
color: Colors.white,
|
||||||
|
height: 8,
|
||||||
|
);
|
||||||
|
|
||||||
|
late final String? depositWalletName;
|
||||||
|
late final String? receiveWalletName;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get title => S.current.buy + '/' + S.current.sell;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get gradientBackground => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get gradientAll => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get resizeToAvoidBottomInset => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get extendBodyBehindAppBar => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
AppBarStyle get appBarStyle => AppBarStyle.transparent;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Function(BuildContext)? get pushToNextWidget => (context) {
|
||||||
|
FocusScopeNode currentFocus = FocusScope.of(context);
|
||||||
|
if (!currentFocus.hasPrimaryFocus) {
|
||||||
|
currentFocus.focusedChild?.unfocus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget trailing(BuildContext context) => TrailButton(
|
||||||
|
caption: S.of(context).clear,
|
||||||
|
onPressed: () {
|
||||||
|
_formKey.currentState?.reset();
|
||||||
|
buySellViewModel.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? leading(BuildContext context) {
|
||||||
|
final _backButton = Icon(
|
||||||
|
Icons.arrow_back_ios,
|
||||||
|
color: titleColor(context),
|
||||||
|
size: 16,
|
||||||
|
);
|
||||||
|
final _closeButton =
|
||||||
|
currentTheme.type == ThemeType.dark ? closeButtonImageDarkTheme : closeButtonImage;
|
||||||
|
|
||||||
|
bool isMobileView = responsiveLayoutUtil.shouldRenderMobileUI;
|
||||||
|
|
||||||
|
return MergeSemantics(
|
||||||
|
child: SizedBox(
|
||||||
|
height: isMobileView ? 37 : 45,
|
||||||
|
width: isMobileView ? 37 : 45,
|
||||||
|
child: ButtonTheme(
|
||||||
|
minWidth: double.minPositive,
|
||||||
|
child: Semantics(
|
||||||
|
label: !isMobileView ? S.of(context).close : S.of(context).seed_alert_back,
|
||||||
|
child: TextButton(
|
||||||
|
style: ButtonStyle(
|
||||||
|
overlayColor: MaterialStateColor.resolveWith((states) => Colors.transparent),
|
||||||
|
),
|
||||||
|
onPressed: () => onClose(context),
|
||||||
|
child: !isMobileView ? _closeButton : _backButton,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget body(BuildContext context) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _setReactions(context, buySellViewModel));
|
||||||
|
|
||||||
|
return KeyboardActions(
|
||||||
|
disableScroll: true,
|
||||||
|
config: KeyboardActionsConfig(
|
||||||
|
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
|
||||||
|
keyboardBarColor: Theme.of(context).extension<KeyboardTheme>()!.keyboardBarColor,
|
||||||
|
nextFocus: false,
|
||||||
|
actions: [
|
||||||
|
KeyboardActionsItem(
|
||||||
|
focusNode: _fiatAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()]),
|
||||||
|
KeyboardActionsItem(
|
||||||
|
focusNode: _cryptoAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()])
|
||||||
|
]),
|
||||||
|
child: Container(
|
||||||
|
color: Theme.of(context).colorScheme.background,
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: ScrollableWithBottomSection(
|
||||||
|
contentPadding: EdgeInsets.only(bottom: 24),
|
||||||
|
content: Observer(
|
||||||
|
builder: (_) => Column(children: [
|
||||||
|
_exchangeCardsSection(context),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(height: 12),
|
||||||
|
_buildPaymentMethodTile(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
])),
|
||||||
|
bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
|
||||||
|
bottomSection: Column(children: [
|
||||||
|
Observer(
|
||||||
|
builder: (_) => LoadingPrimaryButton(
|
||||||
|
text: S.current.choose_a_provider,
|
||||||
|
onPressed: () async {
|
||||||
|
if(!_formKey.currentState!.validate()) return;
|
||||||
|
buySellViewModel.onTapChoseProvider(context);
|
||||||
|
},
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
textColor: Colors.white,
|
||||||
|
isDisabled: false,
|
||||||
|
isLoading: !buySellViewModel.isReadyToTrade)),
|
||||||
|
]),
|
||||||
|
)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPaymentMethodTile(BuildContext context) {
|
||||||
|
if (buySellViewModel.paymentMethodState is PaymentMethodLoading ||
|
||||||
|
buySellViewModel.paymentMethodState is InitialPaymentMethod) {
|
||||||
|
return OptionTilePlaceholder(
|
||||||
|
withBadge: false,
|
||||||
|
withSubtitle: false,
|
||||||
|
borderRadius: 30,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
||||||
|
leadingIcon: Icons.arrow_forward_ios,
|
||||||
|
isDarkTheme: buySellViewModel.isDarkTheme);
|
||||||
|
}
|
||||||
|
if (buySellViewModel.paymentMethodState is PaymentMethodFailed) {
|
||||||
|
return OptionTilePlaceholder(errorText: 'No payment methods available', borderRadius: 30);
|
||||||
|
}
|
||||||
|
if (buySellViewModel.paymentMethodState is PaymentMethodLoaded &&
|
||||||
|
buySellViewModel.selectedPaymentMethod != null) {
|
||||||
|
return Observer(builder: (_) {
|
||||||
|
final selectedPaymentMethod = buySellViewModel.selectedPaymentMethod!;
|
||||||
|
return ProviderOptionTile(
|
||||||
|
lightImagePath: selectedPaymentMethod.lightIconPath,
|
||||||
|
darkImagePath: selectedPaymentMethod.darkIconPath,
|
||||||
|
title: selectedPaymentMethod.title,
|
||||||
|
onPressed: () => _pickPaymentMethod(context),
|
||||||
|
leadingIcon: Icons.arrow_forward_ios,
|
||||||
|
isLightMode: !buySellViewModel.isDarkTheme,
|
||||||
|
borderRadius: 30,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
||||||
|
titleTextStyle:
|
||||||
|
textLargeBold(color: Theme.of(context).extension<CakeTextTheme>()!.titleColor),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return OptionTilePlaceholder(errorText: 'No payment methods available', borderRadius: 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _pickPaymentMethod(BuildContext context) async {
|
||||||
|
final currentOption = buySellViewModel.selectedPaymentMethod;
|
||||||
|
await Navigator.of(context).pushNamed(
|
||||||
|
Routes.paymentMethodOptionsPage,
|
||||||
|
arguments: [
|
||||||
|
buySellViewModel.paymentMethods,
|
||||||
|
buySellViewModel.changeOption,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
buySellViewModel.selectedPaymentMethod;
|
||||||
|
if (currentOption != null &&
|
||||||
|
currentOption.paymentMethodType !=
|
||||||
|
buySellViewModel.selectedPaymentMethod?.paymentMethodType) {
|
||||||
|
await buySellViewModel.calculateBestRate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setReactions(BuildContext context, BuySellViewModel buySellViewModel) {
|
||||||
|
if (_isReactionsSet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final fiatAmountController = fiatCurrencyKey.currentState!.amountController;
|
||||||
|
final cryptoAmountController = cryptoCurrencyKey.currentState!.amountController;
|
||||||
|
final cryptoAddressController = cryptoCurrencyKey.currentState!.addressController;
|
||||||
|
|
||||||
|
_onCurrencyChange(buySellViewModel.cryptoCurrency, buySellViewModel, cryptoCurrencyKey);
|
||||||
|
_onCurrencyChange(buySellViewModel.fiatCurrency, buySellViewModel, fiatCurrencyKey);
|
||||||
|
|
||||||
|
reaction(
|
||||||
|
(_) => buySellViewModel.wallet.name,
|
||||||
|
(String _) =>
|
||||||
|
_onWalletNameChange(buySellViewModel, buySellViewModel.cryptoCurrency, cryptoCurrencyKey));
|
||||||
|
|
||||||
|
reaction(
|
||||||
|
(_) => buySellViewModel.cryptoCurrency,
|
||||||
|
(CryptoCurrency currency) =>
|
||||||
|
_onCurrencyChange(currency, buySellViewModel, cryptoCurrencyKey));
|
||||||
|
|
||||||
|
reaction(
|
||||||
|
(_) => buySellViewModel.fiatCurrency,
|
||||||
|
(FiatCurrency currency) =>
|
||||||
|
_onCurrencyChange(currency, buySellViewModel, fiatCurrencyKey));
|
||||||
|
|
||||||
|
reaction((_) => buySellViewModel.fiatAmount, (String amount) {
|
||||||
|
if (fiatCurrencyKey.currentState!.amountController.text != amount) {
|
||||||
|
fiatCurrencyKey.currentState!.amountController.text = amount;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
reaction((_) => buySellViewModel.isCryptoCurrencyAddressEnabled, (bool isEnabled) {
|
||||||
|
cryptoCurrencyKey.currentState!.isAddressEditable(isEditable: isEnabled);
|
||||||
|
});
|
||||||
|
|
||||||
|
reaction((_) => buySellViewModel.cryptoAmount, (String amount) {
|
||||||
|
if (cryptoCurrencyKey.currentState!.amountController.text != amount) {
|
||||||
|
cryptoCurrencyKey.currentState!.amountController.text = amount;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
reaction((_) => buySellViewModel.cryptoCurrencyAddress, (String address) {
|
||||||
|
if (cryptoAddressController != address) {
|
||||||
|
cryptoCurrencyKey.currentState!.addressController.text = address;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fiatAmountController.addListener(() {
|
||||||
|
if (fiatAmountController.text != buySellViewModel.fiatAmount) {
|
||||||
|
buySellViewModel.changeFiatAmount(amount: fiatAmountController.text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cryptoAmountController.addListener(() {
|
||||||
|
if (cryptoAmountController.text != buySellViewModel.cryptoAmount) {
|
||||||
|
buySellViewModel.changeCryptoAmount(amount: cryptoAmountController.text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cryptoAddressController.addListener(() {
|
||||||
|
buySellViewModel.changeCryptoCurrencyAddress(cryptoAddressController.text);
|
||||||
|
});
|
||||||
|
|
||||||
|
_cryptoAddressFocus.addListener(() async {
|
||||||
|
if (!_cryptoAddressFocus.hasFocus && cryptoAddressController.text.isNotEmpty) {
|
||||||
|
final domain = cryptoAddressController.text;
|
||||||
|
buySellViewModel.cryptoCurrencyAddress =
|
||||||
|
await fetchParsedAddress(context, domain, buySellViewModel.cryptoCurrency);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
reaction((_) => buySellViewModel.wallet.walletAddresses.addressForExchange, (String address) {
|
||||||
|
if (buySellViewModel.cryptoCurrency == CryptoCurrency.xmr) {
|
||||||
|
cryptoCurrencyKey.currentState!.changeAddress(address: address);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
reaction((_) => buySellViewModel.isReadyToTrade, (bool isReady) {
|
||||||
|
if (isReady) {
|
||||||
|
if (cryptoAmountController.text.isNotEmpty &&
|
||||||
|
cryptoAmountController.text != S.current.fetching) {
|
||||||
|
buySellViewModel.changeCryptoAmount(amount: cryptoAmountController.text);
|
||||||
|
} else if (fiatAmountController.text.isNotEmpty &&
|
||||||
|
fiatAmountController.text != S.current.fetching) {
|
||||||
|
buySellViewModel.changeFiatAmount(amount: fiatAmountController.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_isReactionsSet = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onCurrencyChange(Currency currency, BuySellViewModel buySellViewModel,
|
||||||
|
GlobalKey<ExchangeCardState> key) {
|
||||||
|
final isCurrentTypeWallet = currency == buySellViewModel.wallet.currency;
|
||||||
|
|
||||||
|
key.currentState!.changeSelectedCurrency(currency);
|
||||||
|
key.currentState!.changeWalletName(isCurrentTypeWallet ? buySellViewModel.wallet.name : '');
|
||||||
|
|
||||||
|
key.currentState!.changeAddress(
|
||||||
|
address: isCurrentTypeWallet ? buySellViewModel.wallet.walletAddresses.addressForExchange : '');
|
||||||
|
|
||||||
|
key.currentState!.changeAmount(amount: '');
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onWalletNameChange(BuySellViewModel buySellViewModel, CryptoCurrency currency,
|
||||||
|
GlobalKey<ExchangeCardState> key) {
|
||||||
|
final isCurrentTypeWallet = currency == buySellViewModel.wallet.currency;
|
||||||
|
|
||||||
|
if (isCurrentTypeWallet) {
|
||||||
|
key.currentState!.changeWalletName(buySellViewModel.wallet.name);
|
||||||
|
key.currentState!.addressController.text = buySellViewModel.wallet.walletAddresses.addressForExchange;
|
||||||
|
} else if (key.currentState!.addressController.text ==
|
||||||
|
buySellViewModel.wallet.walletAddresses.addressForExchange) {
|
||||||
|
key.currentState!.changeWalletName('');
|
||||||
|
key.currentState!.addressController.text = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void disposeBestRateSync() => {};
|
||||||
|
|
||||||
|
Widget _exchangeCardsSection(BuildContext context) {
|
||||||
|
final fiatExchangeCard = Observer(
|
||||||
|
builder: (_) => ExchangeCard(
|
||||||
|
cardInstanceName: 'fiat_currency_trade_card',
|
||||||
|
onDispose: disposeBestRateSync,
|
||||||
|
amountFocusNode: _fiatAmountFocus,
|
||||||
|
key: fiatCurrencyKey,
|
||||||
|
title: 'FIAT ${S.of(context).amount}',
|
||||||
|
initialCurrency: buySellViewModel.fiatCurrency,
|
||||||
|
initialWalletName: '',
|
||||||
|
initialAddress: '',
|
||||||
|
initialIsAmountEditable: true,
|
||||||
|
isAmountEstimated: false,
|
||||||
|
currencyRowPadding: EdgeInsets.zero,
|
||||||
|
addressRowPadding: EdgeInsets.zero,
|
||||||
|
isMoneroWallet: buySellViewModel.wallet == WalletType.monero,
|
||||||
|
showAddressField: false,
|
||||||
|
showLimitsField: false,
|
||||||
|
currencies: buySellViewModel.fiatCurrencies,
|
||||||
|
onCurrencySelected: (currency) =>
|
||||||
|
buySellViewModel.changeFiatCurrency(currency: currency),
|
||||||
|
imageArrow: arrowBottomPurple,
|
||||||
|
currencyButtonColor: Colors.transparent,
|
||||||
|
addressButtonsColor:
|
||||||
|
Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
|
||||||
|
borderColor:
|
||||||
|
Theme.of(context).extension<ExchangePageTheme>()!.textFieldBorderTopPanelColor,
|
||||||
|
onPushPasteButton: (context) async {},
|
||||||
|
onPushAddressBookButton: (context) async {},
|
||||||
|
));
|
||||||
|
|
||||||
|
final cryptoExchangeCard = Observer(
|
||||||
|
builder: (_) => ExchangeCard(
|
||||||
|
cardInstanceName: 'crypto_currency_trade_card',
|
||||||
|
onDispose: disposeBestRateSync,
|
||||||
|
amountFocusNode: _cryptoAmountFocus,
|
||||||
|
addressFocusNode: _cryptoAddressFocus,
|
||||||
|
key: cryptoCurrencyKey,
|
||||||
|
title: 'Crypto ${S.of(context).amount}',
|
||||||
|
initialCurrency: buySellViewModel.cryptoCurrency,
|
||||||
|
initialWalletName: '',
|
||||||
|
initialAddress: buySellViewModel.cryptoCurrency == buySellViewModel.wallet.currency
|
||||||
|
? buySellViewModel.wallet.walletAddresses.addressForExchange
|
||||||
|
: buySellViewModel.cryptoCurrencyAddress,
|
||||||
|
initialIsAmountEditable: true,
|
||||||
|
isAmountEstimated: true,
|
||||||
|
showLimitsField: false,
|
||||||
|
currencyRowPadding: EdgeInsets.zero,
|
||||||
|
addressRowPadding: EdgeInsets.zero,
|
||||||
|
isMoneroWallet: buySellViewModel.wallet == WalletType.monero,
|
||||||
|
currencies: buySellViewModel.cryptoCurrencies,
|
||||||
|
onCurrencySelected: (currency) =>
|
||||||
|
buySellViewModel.changeCryptoCurrency(currency: currency),
|
||||||
|
imageArrow: arrowBottomCakeGreen,
|
||||||
|
currencyButtonColor: Colors.transparent,
|
||||||
|
addressButtonsColor:
|
||||||
|
Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
|
||||||
|
borderColor:
|
||||||
|
Theme.of(context).extension<ExchangePageTheme>()!.textFieldBorderBottomPanelColor,
|
||||||
|
addressTextFieldValidator: AddressValidator(type: buySellViewModel.cryptoCurrency),
|
||||||
|
onPushPasteButton: (context) async {},
|
||||||
|
onPushAddressBookButton: (context) async {},
|
||||||
|
));
|
||||||
|
|
||||||
|
if (responsiveLayoutUtil.shouldRenderMobileUI) {
|
||||||
|
return Observer(
|
||||||
|
builder: (_) {
|
||||||
|
if (buySellViewModel.isBuyAction) {
|
||||||
|
return MobileExchangeCardsSection(
|
||||||
|
firstExchangeCard: fiatExchangeCard,
|
||||||
|
secondExchangeCard: cryptoExchangeCard,
|
||||||
|
onBuyTap: () => null,
|
||||||
|
onSellTap: () =>
|
||||||
|
buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null,
|
||||||
|
isBuySellOption: true,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return MobileExchangeCardsSection(
|
||||||
|
firstExchangeCard: cryptoExchangeCard,
|
||||||
|
secondExchangeCard: fiatExchangeCard,
|
||||||
|
onBuyTap: () =>
|
||||||
|
!buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null,
|
||||||
|
onSellTap: () => null,
|
||||||
|
isBuySellOption: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Observer(
|
||||||
|
builder: (_) {
|
||||||
|
if (buySellViewModel.isBuyAction) {
|
||||||
|
return DesktopExchangeCardsSection(
|
||||||
|
firstExchangeCard: fiatExchangeCard,
|
||||||
|
secondExchangeCard: cryptoExchangeCard,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return DesktopExchangeCardsSection(
|
||||||
|
firstExchangeCard: cryptoExchangeCard,
|
||||||
|
secondExchangeCard: fiatExchangeCard,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> fetchParsedAddress(
|
||||||
|
BuildContext context, String domain, CryptoCurrency currency) async {
|
||||||
|
final parsedAddress = await getIt.get<AddressResolver>().resolve(context, domain, currency);
|
||||||
|
final address = await extractAddressFromParsed(context, parsedAddress);
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
}
|
47
lib/src/screens/buy/payment_method_options_page.dart
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import 'package:cake_wallet/core/selectable_option.dart';
|
||||||
|
import 'package:cake_wallet/generated/i18n.dart';
|
||||||
|
import 'package:cake_wallet/src/screens/select_options_page.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
|
class PaymentMethodOptionsPage extends SelectOptionsPage {
|
||||||
|
PaymentMethodOptionsPage({required this.items, this.pickAnOption});
|
||||||
|
|
||||||
|
final List<SelectableItem> items;
|
||||||
|
final Function(SelectableOption option)? pickAnOption;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pageTitle => S.current.choose_a_payment_method;
|
||||||
|
|
||||||
|
@override
|
||||||
|
EdgeInsets? get contentPadding => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
EdgeInsets? get tilePadding => EdgeInsets.only(top: 12);
|
||||||
|
|
||||||
|
@override
|
||||||
|
EdgeInsets? get innerPadding => EdgeInsets.symmetric(horizontal: 24, vertical: 12);
|
||||||
|
|
||||||
|
@override
|
||||||
|
double? get imageHeight => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double? get imageWidth => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get selectedBackgroundColor => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double? get tileBorderRadius => 30;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get bottomSectionText => '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void Function(SelectableOption option)? get onOptionTap => pickAnOption;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get primaryButtonText => S.current.confirm;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void Function(BuildContext context)? get primaryButtonAction => null;
|
||||||
|
}
|
|
@ -258,7 +258,11 @@ class CakePayBuyCardDetailPage extends BasePage {
|
||||||
if (!isLogged) {
|
if (!isLogged) {
|
||||||
Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
|
Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
|
||||||
} else {
|
} else {
|
||||||
|
try {
|
||||||
await cakePayPurchaseViewModel.createOrder();
|
await cakePayPurchaseViewModel.createOrder();
|
||||||
|
} catch (_) {
|
||||||
|
await cakePayPurchaseViewModel.cakePayService.logout();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -343,8 +347,8 @@ class CakePayBuyCardDetailPage extends BasePage {
|
||||||
rightButtonText: S.of(popupContext).send,
|
rightButtonText: S.of(popupContext).send,
|
||||||
leftButtonText: S.of(popupContext).cancel,
|
leftButtonText: S.of(popupContext).cancel,
|
||||||
actionRightButton: () async {
|
actionRightButton: () async {
|
||||||
Navigator.of(popupContext).pop();
|
Navigator.of(context).pop();
|
||||||
await cakePayPurchaseViewModel.sendViewModel.commitTransaction();
|
await cakePayPurchaseViewModel.sendViewModel.commitTransaction(context);
|
||||||
},
|
},
|
||||||
actionLeftButton: () => Navigator.of(popupContext).pop()));
|
actionLeftButton: () => Navigator.of(popupContext).pop()));
|
||||||
},
|
},
|
||||||
|
|
|
@ -21,6 +21,14 @@ class DesktopDashboardActions extends StatelessWidget {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
DesktopActionButton(
|
||||||
|
title: MainActions.showWalletsAction.name(context),
|
||||||
|
image: MainActions.showWalletsAction.image,
|
||||||
|
canShow: MainActions.showWalletsAction.canShow?.call(dashboardViewModel),
|
||||||
|
isEnabled: MainActions.showWalletsAction.isEnabled?.call(dashboardViewModel),
|
||||||
|
onTap: () async =>
|
||||||
|
await MainActions.showWalletsAction.onTap(context, dashboardViewModel),
|
||||||
|
),
|
||||||
DesktopActionButton(
|
DesktopActionButton(
|
||||||
title: MainActions.exchangeAction.name(context),
|
title: MainActions.exchangeAction.name(context),
|
||||||
image: MainActions.exchangeAction.image,
|
image: MainActions.exchangeAction.image,
|
||||||
|
@ -55,20 +63,11 @@ class DesktopDashboardActions extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: DesktopActionButton(
|
child: DesktopActionButton(
|
||||||
title: MainActions.buyAction.name(context),
|
title: MainActions.tradeAction.name(context),
|
||||||
image: MainActions.buyAction.image,
|
image: MainActions.tradeAction.image,
|
||||||
canShow: MainActions.buyAction.canShow?.call(dashboardViewModel),
|
canShow: MainActions.tradeAction.canShow?.call(dashboardViewModel),
|
||||||
isEnabled: MainActions.buyAction.isEnabled?.call(dashboardViewModel),
|
isEnabled: MainActions.tradeAction.isEnabled?.call(dashboardViewModel),
|
||||||
onTap: () async => await MainActions.buyAction.onTap(context, dashboardViewModel),
|
onTap: () async => await MainActions.tradeAction.onTap(context, dashboardViewModel),
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: DesktopActionButton(
|
|
||||||
title: MainActions.sellAction.name(context),
|
|
||||||
image: MainActions.sellAction.image,
|
|
||||||
canShow: MainActions.sellAction.canShow?.call(dashboardViewModel),
|
|
||||||
isEnabled: MainActions.sellAction.isEnabled?.call(dashboardViewModel),
|
|
||||||
onTap: () async => await MainActions.sellAction.onTap(context, dashboardViewModel),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -103,6 +103,9 @@ class MenuWidgetState extends State<MenuWidget> {
|
||||||
if (!widget.dashboardViewModel.hasSilentPayments) {
|
if (!widget.dashboardViewModel.hasSilentPayments) {
|
||||||
items.removeWhere((element) => element.name(context) == S.of(context).silent_payments_settings);
|
items.removeWhere((element) => element.name(context) == S.of(context).silent_payments_settings);
|
||||||
}
|
}
|
||||||
|
if (!widget.dashboardViewModel.isMoneroViewOnly) {
|
||||||
|
items.removeWhere((element) => element.name(context) == S.of(context).export_outputs);
|
||||||
|
}
|
||||||
if (!widget.dashboardViewModel.hasMweb) {
|
if (!widget.dashboardViewModel.hasMweb) {
|
||||||
items.removeWhere((element) => element.name(context) == S.of(context).litecoin_mweb_settings);
|
items.removeWhere((element) => element.name(context) == S.of(context).litecoin_mweb_settings);
|
||||||
}
|
}
|
||||||
|
@ -191,7 +194,6 @@ class MenuWidgetState extends State<MenuWidget> {
|
||||||
index--;
|
index--;
|
||||||
|
|
||||||
final item = items[index];
|
final item = items[index];
|
||||||
|
|
||||||
final isLastTile = index == itemCount - 1;
|
final isLastTile = index == itemCount - 1;
|
||||||
|
|
||||||
return SettingActionButton(
|
return SettingActionButton(
|
||||||
|
|
|
@ -18,7 +18,7 @@ import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
|
||||||
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
|
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
|
||||||
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
|
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
|
||||||
|
|
||||||
class ExchangeCard extends StatefulWidget {
|
class ExchangeCard<T extends Currency> extends StatefulWidget {
|
||||||
ExchangeCard({
|
ExchangeCard({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.initialCurrency,
|
required this.initialCurrency,
|
||||||
|
@ -40,19 +40,23 @@ class ExchangeCard extends StatefulWidget {
|
||||||
this.borderColor = Colors.transparent,
|
this.borderColor = Colors.transparent,
|
||||||
this.hasAllAmount = false,
|
this.hasAllAmount = false,
|
||||||
this.isAllAmountEnabled = false,
|
this.isAllAmountEnabled = false,
|
||||||
|
this.showAddressField = true,
|
||||||
|
this.showLimitsField = true,
|
||||||
this.amountFocusNode,
|
this.amountFocusNode,
|
||||||
this.addressFocusNode,
|
this.addressFocusNode,
|
||||||
this.allAmount,
|
this.allAmount,
|
||||||
|
this.currencyRowPadding,
|
||||||
|
this.addressRowPadding,
|
||||||
this.onPushPasteButton,
|
this.onPushPasteButton,
|
||||||
this.onPushAddressBookButton,
|
this.onPushAddressBookButton,
|
||||||
this.onDispose,
|
this.onDispose,
|
||||||
required this.cardInstanceName,
|
required this.cardInstanceName,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final List<CryptoCurrency> currencies;
|
final List<T> currencies;
|
||||||
final Function(CryptoCurrency) onCurrencySelected;
|
final Function(T) onCurrencySelected;
|
||||||
final String title;
|
final String title;
|
||||||
final CryptoCurrency initialCurrency;
|
final T initialCurrency;
|
||||||
final String initialWalletName;
|
final String initialWalletName;
|
||||||
final String initialAddress;
|
final String initialAddress;
|
||||||
final bool initialIsAmountEditable;
|
final bool initialIsAmountEditable;
|
||||||
|
@ -70,18 +74,22 @@ class ExchangeCard extends StatefulWidget {
|
||||||
final FocusNode? amountFocusNode;
|
final FocusNode? amountFocusNode;
|
||||||
final FocusNode? addressFocusNode;
|
final FocusNode? addressFocusNode;
|
||||||
final bool hasAllAmount;
|
final bool hasAllAmount;
|
||||||
|
final bool showAddressField;
|
||||||
|
final bool showLimitsField;
|
||||||
final bool isAllAmountEnabled;
|
final bool isAllAmountEnabled;
|
||||||
final VoidCallback? allAmount;
|
final VoidCallback? allAmount;
|
||||||
|
final EdgeInsets? currencyRowPadding;
|
||||||
|
final EdgeInsets? addressRowPadding;
|
||||||
final void Function(BuildContext context)? onPushPasteButton;
|
final void Function(BuildContext context)? onPushPasteButton;
|
||||||
final void Function(BuildContext context)? onPushAddressBookButton;
|
final void Function(BuildContext context)? onPushAddressBookButton;
|
||||||
final Function()? onDispose;
|
final Function()? onDispose;
|
||||||
final String cardInstanceName;
|
final String cardInstanceName;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ExchangeCardState createState() => ExchangeCardState();
|
ExchangeCardState<T> createState() => ExchangeCardState<T>();
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExchangeCardState extends State<ExchangeCard> {
|
class ExchangeCardState<T extends Currency> extends State<ExchangeCard<T>> {
|
||||||
ExchangeCardState()
|
ExchangeCardState()
|
||||||
: _title = '',
|
: _title = '',
|
||||||
_min = '',
|
_min = '',
|
||||||
|
@ -89,7 +97,6 @@ class ExchangeCardState extends State<ExchangeCard> {
|
||||||
_isAmountEditable = false,
|
_isAmountEditable = false,
|
||||||
_isAddressEditable = false,
|
_isAddressEditable = false,
|
||||||
_walletName = '',
|
_walletName = '',
|
||||||
_selectedCurrency = CryptoCurrency.btc,
|
|
||||||
_isAmountEstimated = false,
|
_isAmountEstimated = false,
|
||||||
_isMoneroWallet = false,
|
_isMoneroWallet = false,
|
||||||
_cardInstanceName = '';
|
_cardInstanceName = '';
|
||||||
|
@ -101,7 +108,7 @@ class ExchangeCardState extends State<ExchangeCard> {
|
||||||
String _title;
|
String _title;
|
||||||
String? _min;
|
String? _min;
|
||||||
String? _max;
|
String? _max;
|
||||||
CryptoCurrency _selectedCurrency;
|
late T _selectedCurrency;
|
||||||
String _walletName;
|
String _walletName;
|
||||||
bool _isAmountEditable;
|
bool _isAmountEditable;
|
||||||
bool _isAddressEditable;
|
bool _isAddressEditable;
|
||||||
|
@ -118,7 +125,8 @@ class ExchangeCardState extends State<ExchangeCard> {
|
||||||
_selectedCurrency = widget.initialCurrency;
|
_selectedCurrency = widget.initialCurrency;
|
||||||
_isAmountEstimated = widget.isAmountEstimated;
|
_isAmountEstimated = widget.isAmountEstimated;
|
||||||
_isMoneroWallet = widget.isMoneroWallet;
|
_isMoneroWallet = widget.isMoneroWallet;
|
||||||
addressController.text = widget.initialAddress;
|
addressController.text = _normalizeAddressFormat(widget.initialAddress);
|
||||||
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,7 +144,7 @@ class ExchangeCardState extends State<ExchangeCard> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void changeSelectedCurrency(CryptoCurrency currency) {
|
void changeSelectedCurrency(T currency) {
|
||||||
setState(() => _selectedCurrency = currency);
|
setState(() => _selectedCurrency = currency);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,7 +165,7 @@ class ExchangeCardState extends State<ExchangeCard> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void changeAddress({required String address}) {
|
void changeAddress({required String address}) {
|
||||||
setState(() => addressController.text = address);
|
setState(() => addressController.text = _normalizeAddressFormat(address));
|
||||||
}
|
}
|
||||||
|
|
||||||
void changeAmount({required String amount}) {
|
void changeAmount({required String amount}) {
|
||||||
|
@ -222,7 +230,7 @@ class ExchangeCardState extends State<ExchangeCard> {
|
||||||
Divider(height: 1, color: Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor),
|
Divider(height: 1, color: Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(top: 5),
|
padding: EdgeInsets.only(top: 5),
|
||||||
child: Container(
|
child: widget.showLimitsField ? Container(
|
||||||
height: 15,
|
height: 15,
|
||||||
child: Row(mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[
|
child: Row(mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[
|
||||||
_min != null
|
_min != null
|
||||||
|
@ -247,7 +255,7 @@ class ExchangeCardState extends State<ExchangeCard> {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Offstage(),
|
: Offstage(),
|
||||||
])),
|
])) : Offstage(),
|
||||||
),
|
),
|
||||||
!_isAddressEditable && widget.hasRefundAddress
|
!_isAddressEditable && widget.hasRefundAddress
|
||||||
? Padding(
|
? Padding(
|
||||||
|
@ -261,10 +269,11 @@ class ExchangeCardState extends State<ExchangeCard> {
|
||||||
))
|
))
|
||||||
: Offstage(),
|
: Offstage(),
|
||||||
_isAddressEditable
|
_isAddressEditable
|
||||||
|
? widget.showAddressField
|
||||||
? FocusTraversalOrder(
|
? FocusTraversalOrder(
|
||||||
order: NumericFocusOrder(2),
|
order: NumericFocusOrder(2),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.only(top: 20),
|
padding: widget.addressRowPadding ?? EdgeInsets.only(top: 20),
|
||||||
child: AddressTextField(
|
child: AddressTextField(
|
||||||
addressKey: ValueKey('${_cardInstanceName}_editable_address_textfield_key'),
|
addressKey: ValueKey('${_cardInstanceName}_editable_address_textfield_key'),
|
||||||
focusNode: widget.addressFocusNode,
|
focusNode: widget.addressFocusNode,
|
||||||
|
@ -280,19 +289,21 @@ class ExchangeCardState extends State<ExchangeCard> {
|
||||||
widget.amountFocusNode?.requestFocus();
|
widget.amountFocusNode?.requestFocus();
|
||||||
amountController.text = paymentRequest.amount;
|
amountController.text = paymentRequest.amount;
|
||||||
},
|
},
|
||||||
placeholder: widget.hasRefundAddress ? S.of(context).refund_address : null,
|
placeholder:
|
||||||
|
widget.hasRefundAddress ? S.of(context).refund_address : null,
|
||||||
options: [
|
options: [
|
||||||
AddressTextFieldOption.paste,
|
AddressTextFieldOption.paste,
|
||||||
AddressTextFieldOption.qrCode,
|
AddressTextFieldOption.qrCode,
|
||||||
AddressTextFieldOption.addressBook,
|
AddressTextFieldOption.addressBook,
|
||||||
],
|
],
|
||||||
isBorderExist: false,
|
isBorderExist: false,
|
||||||
textStyle:
|
textStyle: TextStyle(
|
||||||
TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
|
fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
|
||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Theme.of(context).extension<ExchangePageTheme>()!.hintTextColor),
|
color:
|
||||||
|
Theme.of(context).extension<ExchangePageTheme>()!.hintTextColor),
|
||||||
buttonColor: widget.addressButtonsColor,
|
buttonColor: widget.addressButtonsColor,
|
||||||
validator: widget.addressTextFieldValidator,
|
validator: widget.addressTextFieldValidator,
|
||||||
onPushPasteButton: widget.onPushPasteButton,
|
onPushPasteButton: widget.onPushPasteButton,
|
||||||
|
@ -300,6 +311,7 @@ class ExchangeCardState extends State<ExchangeCard> {
|
||||||
selectedCurrency: _selectedCurrency),
|
selectedCurrency: _selectedCurrency),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
: Offstage()
|
||||||
: Padding(
|
: Padding(
|
||||||
padding: EdgeInsets.only(top: 10),
|
padding: EdgeInsets.only(top: 10),
|
||||||
child: Builder(
|
child: Builder(
|
||||||
|
@ -402,7 +414,7 @@ class ExchangeCardState extends State<ExchangeCard> {
|
||||||
hintText: S.of(context).search_currency,
|
hintText: S.of(context).search_currency,
|
||||||
isMoneroWallet: _isMoneroWallet,
|
isMoneroWallet: _isMoneroWallet,
|
||||||
isConvertFrom: widget.hasRefundAddress,
|
isConvertFrom: widget.hasRefundAddress,
|
||||||
onItemSelected: (Currency item) => widget.onCurrencySelected(item as CryptoCurrency),
|
onItemSelected: (Currency item) => widget.onCurrencySelected(item as T),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -424,4 +436,10 @@ class ExchangeCardState extends State<ExchangeCard> {
|
||||||
actionLeftButton: () => Navigator.of(dialogContext).pop());
|
actionLeftButton: () => Navigator.of(dialogContext).pop());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _normalizeAddressFormat(String address) {
|
||||||
|
if (address.startsWith('bitcoincash:')) address = address.substring(12);
|
||||||
|
return address;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,29 @@
|
||||||
|
import 'package:cake_wallet/generated/i18n.dart';
|
||||||
|
import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart';
|
||||||
import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart';
|
import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart';
|
||||||
|
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class MobileExchangeCardsSection extends StatelessWidget {
|
class MobileExchangeCardsSection extends StatelessWidget {
|
||||||
final Widget firstExchangeCard;
|
final Widget firstExchangeCard;
|
||||||
final Widget secondExchangeCard;
|
final Widget secondExchangeCard;
|
||||||
|
final bool isBuySellOption;
|
||||||
|
final VoidCallback? onBuyTap;
|
||||||
|
final VoidCallback? onSellTap;
|
||||||
|
|
||||||
const MobileExchangeCardsSection({
|
const MobileExchangeCardsSection({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.firstExchangeCard,
|
required this.firstExchangeCard,
|
||||||
required this.secondExchangeCard,
|
required this.secondExchangeCard,
|
||||||
|
this.isBuySellOption = false,
|
||||||
|
this.onBuyTap,
|
||||||
|
this.onSellTap,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: EdgeInsets.only(bottom: 32),
|
padding: EdgeInsets.only(bottom: isBuySellOption ? 8 : 32),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
bottomLeft: Radius.circular(24),
|
bottomLeft: Radius.circular(24),
|
||||||
|
@ -45,8 +54,18 @@ class MobileExchangeCardsSection extends StatelessWidget {
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
padding: EdgeInsets.fromLTRB(24, 100, 24, 32),
|
padding: EdgeInsets.fromLTRB(24, 90, 24, isBuySellOption ? 8 : 32),
|
||||||
child: firstExchangeCard,
|
child: Column(
|
||||||
|
children: [
|
||||||
|
if (isBuySellOption) Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
BuySellOptionButtons(onBuyTap: onBuyTap, onSellTap: onSellTap),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
firstExchangeCard,
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(top: 29, left: 24, right: 24),
|
padding: EdgeInsets.only(top: 29, left: 24, right: 24),
|
||||||
|
@ -57,3 +76,69 @@ class MobileExchangeCardsSection extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BuySellOptionButtons extends StatefulWidget {
|
||||||
|
final VoidCallback? onBuyTap;
|
||||||
|
final VoidCallback? onSellTap;
|
||||||
|
|
||||||
|
const BuySellOptionButtons({this.onBuyTap, this.onSellTap});
|
||||||
|
|
||||||
|
@override
|
||||||
|
_BuySellOptionButtonsState createState() => _BuySellOptionButtonsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BuySellOptionButtonsState extends State<BuySellOptionButtons> {
|
||||||
|
bool isBuySelected = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 24),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(flex: 2, child: SizedBox()),
|
||||||
|
Expanded(
|
||||||
|
flex: 5,
|
||||||
|
child: SelectButton(
|
||||||
|
height: 44,
|
||||||
|
text: S.of(context).buy,
|
||||||
|
isSelected: isBuySelected,
|
||||||
|
showTrailingIcon: false,
|
||||||
|
textColor: Colors.white,
|
||||||
|
image: Image.asset('assets/images/buy.png', height: 25, width: 25),
|
||||||
|
padding: EdgeInsets.only(left: 10, right: 30),
|
||||||
|
color: isBuySelected
|
||||||
|
? null
|
||||||
|
: Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
|
||||||
|
onTap: () {
|
||||||
|
setState(() => isBuySelected = true);
|
||||||
|
if (widget.onBuyTap != null) widget.onBuyTap!();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(child: const SizedBox()),
|
||||||
|
Expanded(
|
||||||
|
flex: 5,
|
||||||
|
child: SelectButton(
|
||||||
|
height: 44,
|
||||||
|
text: S.of(context).sell,
|
||||||
|
isSelected: !isBuySelected,
|
||||||
|
showTrailingIcon: false,
|
||||||
|
textColor: Colors.white,
|
||||||
|
image: Image.asset('assets/images/sell.png', height: 25, width: 25),
|
||||||
|
padding: EdgeInsets.only(left: 10, right: 30),
|
||||||
|
color: !isBuySelected
|
||||||
|
? null
|
||||||
|
: Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
|
||||||
|
onTap: () {
|
||||||
|
setState(() => isBuySelected = false);
|
||||||
|
if (widget.onSellTap != null) widget.onSellTap!();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(flex: 2, child: SizedBox()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -277,7 +277,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
|
||||||
actionRightButton: () async {
|
actionRightButton: () async {
|
||||||
Navigator.of(popupContext).pop();
|
Navigator.of(popupContext).pop();
|
||||||
await widget.exchangeTradeViewModel.sendViewModel
|
await widget.exchangeTradeViewModel.sendViewModel
|
||||||
.commitTransaction();
|
.commitTransaction(context);
|
||||||
transactionStatePopup();
|
transactionStatePopup();
|
||||||
},
|
},
|
||||||
actionLeftButton: () => Navigator.of(popupContext).pop(),
|
actionLeftButton: () => Navigator.of(popupContext).pop(),
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:cake_wallet/generated/i18n.dart';
|
import 'package:cake_wallet/generated/i18n.dart';
|
||||||
import 'package:cake_wallet/routes.dart';
|
import 'package:cake_wallet/routes.dart';
|
||||||
import 'package:cake_wallet/src/screens/InfoPage.dart';
|
import 'package:cake_wallet/src/screens/Info_page.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
class PreSeedPage extends InfoPage {
|
class PreSeedPage extends InfoPage {
|
||||||
|
|
199
lib/src/screens/select_options_page.dart
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
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/themes/extensions/option_tile_theme.dart';
|
||||||
|
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
abstract class SelectOptionsPage extends BasePage {
|
||||||
|
SelectOptionsPage();
|
||||||
|
|
||||||
|
String get pageTitle;
|
||||||
|
|
||||||
|
EdgeInsets? get contentPadding;
|
||||||
|
|
||||||
|
EdgeInsets? get tilePadding;
|
||||||
|
|
||||||
|
EdgeInsets? get innerPadding;
|
||||||
|
|
||||||
|
double? get imageHeight;
|
||||||
|
|
||||||
|
double? get imageWidth;
|
||||||
|
|
||||||
|
Color? get selectedBackgroundColor;
|
||||||
|
|
||||||
|
double? get tileBorderRadius;
|
||||||
|
|
||||||
|
String get bottomSectionText;
|
||||||
|
|
||||||
|
bool get primaryButtonEnabled => true;
|
||||||
|
|
||||||
|
String get primaryButtonText => '';
|
||||||
|
|
||||||
|
List<SelectableItem> get items;
|
||||||
|
|
||||||
|
void Function(SelectableOption option)? get onOptionTap;
|
||||||
|
|
||||||
|
void Function(BuildContext context)? get primaryButtonAction;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get title => pageTitle;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget body(BuildContext context) {
|
||||||
|
return ScrollableWithBottomSection(
|
||||||
|
content: BodySelectOptionsPage(
|
||||||
|
items: items,
|
||||||
|
onOptionTap: onOptionTap,
|
||||||
|
tilePadding: tilePadding,
|
||||||
|
tileBorderRadius: tileBorderRadius,
|
||||||
|
imageHeight: imageHeight,
|
||||||
|
imageWidth: imageWidth,
|
||||||
|
innerPadding: innerPadding),
|
||||||
|
bottomSection: Padding(
|
||||||
|
padding: contentPadding ?? EdgeInsets.zero,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
bottomSectionText,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (primaryButtonEnabled)
|
||||||
|
LoadingPrimaryButton(
|
||||||
|
text: primaryButtonText,
|
||||||
|
onPressed: () {
|
||||||
|
primaryButtonAction != null
|
||||||
|
? primaryButtonAction!(context)
|
||||||
|
: Navigator.pop(context);
|
||||||
|
},
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
textColor: Colors.white,
|
||||||
|
isDisabled: false,
|
||||||
|
isLoading: false)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BodySelectOptionsPage extends StatefulWidget {
|
||||||
|
const BodySelectOptionsPage({
|
||||||
|
required this.items,
|
||||||
|
this.onOptionTap,
|
||||||
|
this.tilePadding,
|
||||||
|
this.tileBorderRadius,
|
||||||
|
this.imageHeight,
|
||||||
|
this.imageWidth,
|
||||||
|
this.innerPadding,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<SelectableItem> items;
|
||||||
|
final void Function(SelectableOption option)? onOptionTap;
|
||||||
|
final EdgeInsets? tilePadding;
|
||||||
|
final double? tileBorderRadius;
|
||||||
|
final double? imageHeight;
|
||||||
|
final double? imageWidth;
|
||||||
|
final EdgeInsets? innerPadding;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_BodySelectOptionsPageState createState() => _BodySelectOptionsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BodySelectOptionsPageState extends State<BodySelectOptionsPage> {
|
||||||
|
late List<SelectableItem> _items;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_items = widget.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleOptionTap(SelectableOption option) {
|
||||||
|
setState(() {
|
||||||
|
for (var item in _items) {
|
||||||
|
if (item is SelectableOption) {
|
||||||
|
item.isOptionSelected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
option.isOptionSelected = true;
|
||||||
|
});
|
||||||
|
widget.onOptionTap?.call(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isLightMode = Theme.of(context).extension<OptionTileTheme>()?.useDarkImage ?? false;
|
||||||
|
|
||||||
|
Color titleColor =
|
||||||
|
isLightMode ? Theme.of(context).appBarTheme.titleTextStyle!.color! : Colors.white;
|
||||||
|
|
||||||
|
return Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 350),
|
||||||
|
child: Column(
|
||||||
|
children: _items.map((item) {
|
||||||
|
if (item is OptionTitle) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 18, bottom: 8),
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: titleColor,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
|
child: Text(
|
||||||
|
item.title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: titleColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (item is SelectableOption) {
|
||||||
|
return Padding(
|
||||||
|
padding: widget.tilePadding ?? const EdgeInsets.only(top: 24),
|
||||||
|
child: ProviderOptionTile(
|
||||||
|
title: item.title,
|
||||||
|
lightImagePath: item.lightIconPath,
|
||||||
|
darkImagePath: item.darkIconPath,
|
||||||
|
imageHeight: widget.imageHeight,
|
||||||
|
imageWidth: widget.imageWidth,
|
||||||
|
padding: widget.innerPadding,
|
||||||
|
description: item.description,
|
||||||
|
topLeftSubTitle: item.topLeftSubTitle,
|
||||||
|
topRightSubTitle: item.topRightSubTitle,
|
||||||
|
rightSubTitleLightIconPath: item.topRightSubTitleLightIconPath,
|
||||||
|
rightSubTitleDarkIconPath: item.topRightSubTitleDarkIconPath,
|
||||||
|
bottomLeftSubTitle: item.bottomLeftSubTitle,
|
||||||
|
badges: item.badges,
|
||||||
|
isSelected: item.isOptionSelected,
|
||||||
|
borderRadius: widget.tileBorderRadius,
|
||||||
|
isLightMode: isLightMode,
|
||||||
|
onPressed: () => _handleOptionTap(item),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -498,7 +498,7 @@ class SendPage extends BasePage {
|
||||||
ValueKey('send_page_confirm_sending_dialog_cancel_button_key'),
|
ValueKey('send_page_confirm_sending_dialog_cancel_button_key'),
|
||||||
actionRightButton: () async {
|
actionRightButton: () async {
|
||||||
Navigator.of(_dialogContext).pop();
|
Navigator.of(_dialogContext).pop();
|
||||||
sendViewModel.commitTransaction();
|
sendViewModel.commitTransaction(context);
|
||||||
await showPopUp<void>(
|
await showPopUp<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext _dialogContext) {
|
builder: (BuildContext _dialogContext) {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:cake_wallet/src/widgets/setting_action_button.dart';
|
||||||
import 'package:cake_wallet/src/widgets/setting_actions.dart';
|
import 'package:cake_wallet/src/widgets/setting_actions.dart';
|
||||||
import 'package:cake_wallet/typography.dart';
|
import 'package:cake_wallet/typography.dart';
|
||||||
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
|
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||||
|
import 'package:cw_core/wallet_type.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:cake_wallet/router.dart' as Router;
|
import 'package:cake_wallet/router.dart' as Router;
|
||||||
import 'package:cake_wallet/themes/extensions/menu_theme.dart';
|
import 'package:cake_wallet/themes/extensions/menu_theme.dart';
|
||||||
|
@ -60,8 +61,10 @@ class _DesktopSettingsPageState extends State<DesktopSettingsPage> {
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!widget.dashboardViewModel.hasMweb &&
|
if ((!widget.dashboardViewModel.isMoneroViewOnly &&
|
||||||
item.name(context) == S.of(context).litecoin_mweb_settings) {
|
item.name(context) == S.of(context).export_outputs) ||
|
||||||
|
(!widget.dashboardViewModel.hasMweb &&
|
||||||
|
item.name(context) == S.of(context).litecoin_mweb_settings)) {
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,22 +57,6 @@ class OtherSettingsPage extends BasePage {
|
||||||
handler: (BuildContext context) =>
|
handler: (BuildContext context) =>
|
||||||
Navigator.of(context).pushNamed(Routes.changeRep),
|
Navigator.of(context).pushNamed(Routes.changeRep),
|
||||||
),
|
),
|
||||||
if(_otherSettingsViewModel.isEnabledBuyAction)
|
|
||||||
SettingsPickerCell(
|
|
||||||
title: S.current.default_buy_provider,
|
|
||||||
items: _otherSettingsViewModel.availableBuyProvidersTypes,
|
|
||||||
displayItem: _otherSettingsViewModel.getBuyProviderType,
|
|
||||||
selectedItem: _otherSettingsViewModel.buyProviderType,
|
|
||||||
onItemSelected: _otherSettingsViewModel.onBuyProviderTypeSelected
|
|
||||||
),
|
|
||||||
if(_otherSettingsViewModel.isEnabledSellAction)
|
|
||||||
SettingsPickerCell(
|
|
||||||
title: S.current.default_sell_provider,
|
|
||||||
items: _otherSettingsViewModel.availableSellProvidersTypes,
|
|
||||||
displayItem: _otherSettingsViewModel.getSellProviderType,
|
|
||||||
selectedItem: _otherSettingsViewModel.sellProviderType,
|
|
||||||
onItemSelected: _otherSettingsViewModel.onSellProviderTypeSelected,
|
|
||||||
),
|
|
||||||
SettingsCellWithArrow(
|
SettingsCellWithArrow(
|
||||||
title: S.current.settings_terms_and_conditions,
|
title: S.current.settings_terms_and_conditions,
|
||||||
handler: (BuildContext context) =>
|
handler: (BuildContext context) =>
|
||||||
|
|
|
@ -73,16 +73,10 @@ class PrivacyPage extends BasePage {
|
||||||
_privacySettingsViewModel.setIsAppSecure(value);
|
_privacySettingsViewModel.setIsAppSecure(value);
|
||||||
}),
|
}),
|
||||||
SettingsSwitcherCell(
|
SettingsSwitcherCell(
|
||||||
title: S.current.disable_buy,
|
title: S.current.disable_trade_option,
|
||||||
value: _privacySettingsViewModel.disableBuy,
|
value: _privacySettingsViewModel.disableTradeOption,
|
||||||
onValueChange: (BuildContext _, bool value) {
|
onValueChange: (BuildContext _, bool value) {
|
||||||
_privacySettingsViewModel.setDisableBuy(value);
|
_privacySettingsViewModel.setDisableTradeOption(value);
|
||||||
}),
|
|
||||||
SettingsSwitcherCell(
|
|
||||||
title: S.current.disable_sell,
|
|
||||||
value: _privacySettingsViewModel.disableSell,
|
|
||||||
onValueChange: (BuildContext _, bool value) {
|
|
||||||
_privacySettingsViewModel.setDisableSell(value);
|
|
||||||
}),
|
}),
|
||||||
SettingsSwitcherCell(
|
SettingsSwitcherCell(
|
||||||
title: S.current.disable_bulletin,
|
title: S.current.disable_bulletin,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:cake_wallet/generated/i18n.dart';
|
import 'package:cake_wallet/generated/i18n.dart';
|
||||||
import 'package:cake_wallet/routes.dart';
|
import 'package:cake_wallet/routes.dart';
|
||||||
import 'package:cake_wallet/src/screens/InfoPage.dart';
|
import 'package:cake_wallet/src/screens/Info_page.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
class Setup2FAInfoPage extends InfoPage {
|
class Setup2FAInfoPage extends InfoPage {
|
||||||
|
|
|
@ -168,7 +168,7 @@ class RBFDetailsPage extends BasePage {
|
||||||
leftButtonText: S.of(popupContext).cancel,
|
leftButtonText: S.of(popupContext).cancel,
|
||||||
actionRightButton: () async {
|
actionRightButton: () async {
|
||||||
Navigator.of(popupContext).pop();
|
Navigator.of(popupContext).pop();
|
||||||
await transactionDetailsViewModel.sendViewModel.commitTransaction();
|
await transactionDetailsViewModel.sendViewModel.commitTransaction(context);
|
||||||
try {
|
try {
|
||||||
Navigator.of(popupContext).pop();
|
Navigator.of(popupContext).pop();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
184
lib/src/screens/ur/animated_ur_page.dart
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
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/monero/monero.dart';
|
||||||
|
import 'package:cake_wallet/src/screens/base_page.dart';
|
||||||
|
import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
|
||||||
|
import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.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/utils/show_pop_up.dart';
|
||||||
|
import 'package:cake_wallet/view_model/animated_ur_model.dart';
|
||||||
|
import 'package:cake_wallet/view_model/dashboard/wallet_balance.dart';
|
||||||
|
import 'package:cw_core/balance.dart';
|
||||||
|
import 'package:cw_core/transaction_history.dart';
|
||||||
|
import 'package:cw_core/transaction_info.dart';
|
||||||
|
import 'package:cw_core/wallet_base.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
// ur:xmr-txunsigned - unsigned transaction
|
||||||
|
// should show a scanner afterwards.
|
||||||
|
|
||||||
|
class AnimatedURPage extends BasePage {
|
||||||
|
final bool isAll;
|
||||||
|
AnimatedURPage(this.animatedURmodel, {required String urQr, this.isAll = false}) {
|
||||||
|
if (urQr == "export-outputs") {
|
||||||
|
this.urQr = monero!.exportOutputsUR(animatedURmodel.wallet, false);
|
||||||
|
} else if (urQr == "export-outputs-all") {
|
||||||
|
this.urQr = monero!.exportOutputsUR(animatedURmodel.wallet, true);
|
||||||
|
} else {
|
||||||
|
this.urQr = urQr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
late String urQr;
|
||||||
|
|
||||||
|
final AnimatedURModel animatedURmodel;
|
||||||
|
|
||||||
|
String get urQrType {
|
||||||
|
final first = urQr.trim().split("\n")[0];
|
||||||
|
return first.split('/')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget body(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 64.0),
|
||||||
|
child: URQR(
|
||||||
|
frames: urQr.trim().split("\n"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 32),
|
||||||
|
if (urQrType == "ur:xmr-txunsigned" || urQrType == "ur:xmr-output")
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.maxFinite,
|
||||||
|
child: PrimaryButton(
|
||||||
|
onPressed: () => _continue(context),
|
||||||
|
text: "Continue",
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
textColor: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 32),
|
||||||
|
if (urQrType == "ur:xmr-output" && !isAll) Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.maxFinite,
|
||||||
|
child: PrimaryButton(
|
||||||
|
onPressed: () => _exportAll(context),
|
||||||
|
text: "Export all",
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
textColor: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _exportAll(BuildContext context) {
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) {
|
||||||
|
return AnimatedURPage(animatedURmodel, urQr: "export-outputs-all", isAll: true);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _continue(BuildContext context) async {
|
||||||
|
try {
|
||||||
|
switch (urQrType) {
|
||||||
|
case "ur:xmr-txunsigned": // ur:xmr-txsigned
|
||||||
|
final ur = await presentQRScanner(context);
|
||||||
|
final result = await monero!.commitTransactionUR(animatedURmodel.wallet, ur);
|
||||||
|
if (result) {
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "ur:xmr-output": // xmr-keyimage
|
||||||
|
final ur = await presentQRScanner(context);
|
||||||
|
final result = await monero!.importKeyImagesUR(animatedURmodel.wallet, ur);
|
||||||
|
if (result) {
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw UnimplementedError("unable to handle UR: ${urQrType}");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
await showPopUp<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertWithOneAction(
|
||||||
|
alertTitle: S.of(context).error,
|
||||||
|
alertContent: e.toString(),
|
||||||
|
buttonText: S.of(context).ok,
|
||||||
|
buttonAction: () => Navigator.pop(context, true));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class URQR extends StatefulWidget {
|
||||||
|
URQR({super.key, required this.frames});
|
||||||
|
|
||||||
|
List<String> frames;
|
||||||
|
|
||||||
|
@override
|
||||||
|
// ignore: library_private_types_in_public_api
|
||||||
|
_URQRState createState() => _URQRState();
|
||||||
|
}
|
||||||
|
|
||||||
|
const urFrameTime = 1000 ~/ 5;
|
||||||
|
|
||||||
|
class _URQRState extends State<URQR> {
|
||||||
|
Timer? t;
|
||||||
|
int frame = 0;
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
setState(() {
|
||||||
|
t = Timer.periodic(const Duration(milliseconds: urFrameTime), (timer) {
|
||||||
|
_nextFrame();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _nextFrame() {
|
||||||
|
setState(() {
|
||||||
|
frame++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
t?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: QrImage(
|
||||||
|
data: widget.frames[frame % widget.frames.length], version: -1,
|
||||||
|
size: 400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -65,7 +65,7 @@ class WCPairingsWidget extends BasePage {
|
||||||
bool isCameraPermissionGranted =
|
bool isCameraPermissionGranted =
|
||||||
await PermissionHandler.checkPermission(Permission.camera, context);
|
await PermissionHandler.checkPermission(Permission.camera, context);
|
||||||
if (!isCameraPermissionGranted) return;
|
if (!isCameraPermissionGranted) return;
|
||||||
uri = await presentQRScanner();
|
uri = await presentQRScanner(context);
|
||||||
} else {
|
} else {
|
||||||
uri = await _showEnterWalletConnectURIPopUp(context);
|
uri = await _showEnterWalletConnectURIPopUp(context);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
import 'package:cake_wallet/utils/device_info.dart';
|
import 'package:cake_wallet/utils/device_info.dart';
|
||||||
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
|
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
|
||||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||||
|
import 'package:cw_core/currency.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:cake_wallet/routes.dart';
|
import 'package:cake_wallet/routes.dart';
|
||||||
import 'package:cake_wallet/generated/i18n.dart';
|
import 'package:cake_wallet/generated/i18n.dart';
|
||||||
import 'package:cake_wallet/entities/qr_scanner.dart';
|
import 'package:cake_wallet/entities/qr_scanner.dart';
|
||||||
import 'package:cake_wallet/entities/contact_base.dart';
|
import 'package:cake_wallet/entities/contact_base.dart';
|
||||||
import 'package:cw_core/crypto_currency.dart';
|
|
||||||
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
|
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
|
||||||
import 'package:cake_wallet/utils/permission_handler.dart';
|
import 'package:cake_wallet/utils/permission_handler.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
enum AddressTextFieldOption { paste, qrCode, addressBook, walletAddresses }
|
enum AddressTextFieldOption { paste, qrCode, addressBook, walletAddresses }
|
||||||
|
|
||||||
class AddressTextField extends StatelessWidget {
|
|
||||||
|
class AddressTextField<T extends Currency> extends StatelessWidget{
|
||||||
AddressTextField({
|
AddressTextField({
|
||||||
required this.controller,
|
required this.controller,
|
||||||
this.isActive = true,
|
this.isActive = true,
|
||||||
|
@ -58,7 +59,7 @@ class AddressTextField extends StatelessWidget {
|
||||||
final Function(BuildContext context)? onPushAddressBookButton;
|
final Function(BuildContext context)? onPushAddressBookButton;
|
||||||
final Function(BuildContext context)? onPushAddressPickerButton;
|
final Function(BuildContext context)? onPushAddressPickerButton;
|
||||||
final Function(ContactBase contact)? onSelectedContact;
|
final Function(ContactBase contact)? onSelectedContact;
|
||||||
final CryptoCurrency? selectedCurrency;
|
final T? selectedCurrency;
|
||||||
final Key? addressKey;
|
final Key? addressKey;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -231,7 +232,7 @@ class AddressTextField extends StatelessWidget {
|
||||||
bool isCameraPermissionGranted =
|
bool isCameraPermissionGranted =
|
||||||
await PermissionHandler.checkPermission(Permission.camera, context);
|
await PermissionHandler.checkPermission(Permission.camera, context);
|
||||||
if (!isCameraPermissionGranted) return;
|
if (!isCameraPermissionGranted) return;
|
||||||
final code = await presentQRScanner();
|
final code = await presentQRScanner(context);
|
||||||
if (code.isEmpty) {
|
if (code.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
527
lib/src/widgets/provider_optoin_tile.dart
Normal file
|
@ -0,0 +1,527 @@
|
||||||
|
import 'package:cake_wallet/themes/extensions/option_tile_theme.dart';
|
||||||
|
import 'package:cake_wallet/themes/extensions/receive_page_theme.dart';
|
||||||
|
import 'package:cake_wallet/typography.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
|
||||||
|
class ProviderOptionTile extends StatelessWidget {
|
||||||
|
const ProviderOptionTile({
|
||||||
|
required this.onPressed,
|
||||||
|
required this.lightImagePath,
|
||||||
|
required this.darkImagePath,
|
||||||
|
required this.title,
|
||||||
|
this.topLeftSubTitle,
|
||||||
|
this.topRightSubTitle,
|
||||||
|
this.bottomLeftSubTitle,
|
||||||
|
this.bottomRightSubTitle,
|
||||||
|
this.leftSubTitleIconPath,
|
||||||
|
this.rightSubTitleLightIconPath,
|
||||||
|
this.rightSubTitleDarkIconPath,
|
||||||
|
this.description,
|
||||||
|
this.badges,
|
||||||
|
this.borderRadius,
|
||||||
|
this.imageHeight,
|
||||||
|
this.imageWidth,
|
||||||
|
this.padding,
|
||||||
|
this.titleTextStyle,
|
||||||
|
this.firstSubTitleTextStyle,
|
||||||
|
this.secondSubTitleTextStyle,
|
||||||
|
this.leadingIcon,
|
||||||
|
this.selectedBackgroundColor,
|
||||||
|
this.isSelected = false,
|
||||||
|
required this.isLightMode,
|
||||||
|
});
|
||||||
|
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
final String lightImagePath;
|
||||||
|
final String darkImagePath;
|
||||||
|
final String title;
|
||||||
|
final String? topLeftSubTitle;
|
||||||
|
final String? topRightSubTitle;
|
||||||
|
final String? bottomLeftSubTitle;
|
||||||
|
final String? bottomRightSubTitle;
|
||||||
|
final String? leftSubTitleIconPath;
|
||||||
|
final String? rightSubTitleLightIconPath;
|
||||||
|
final String? rightSubTitleDarkIconPath;
|
||||||
|
final String? description;
|
||||||
|
final List<String>? badges;
|
||||||
|
final double? borderRadius;
|
||||||
|
final double? imageHeight;
|
||||||
|
final double? imageWidth;
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
final TextStyle? titleTextStyle;
|
||||||
|
final TextStyle? firstSubTitleTextStyle;
|
||||||
|
final TextStyle? secondSubTitleTextStyle;
|
||||||
|
final IconData? leadingIcon;
|
||||||
|
final Color? selectedBackgroundColor;
|
||||||
|
final bool isSelected;
|
||||||
|
final bool isLightMode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final backgroundColor = isSelected
|
||||||
|
? isLightMode
|
||||||
|
? Theme.of(context).extension<ReceivePageTheme>()!.currentTileBackgroundColor
|
||||||
|
: Theme.of(context).extension<OptionTileTheme>()!.titleColor
|
||||||
|
: Theme.of(context).cardColor;
|
||||||
|
|
||||||
|
final textColor = isSelected
|
||||||
|
? isLightMode
|
||||||
|
? Colors.white
|
||||||
|
: Theme.of(context).cardColor
|
||||||
|
: Theme.of(context).extension<OptionTileTheme>()!.titleColor;
|
||||||
|
|
||||||
|
final badgeColor = isSelected
|
||||||
|
? Theme.of(context).cardColor
|
||||||
|
: Theme.of(context).extension<OptionTileTheme>()!.titleColor;
|
||||||
|
|
||||||
|
final badgeTextColor = isSelected
|
||||||
|
? Theme.of(context).extension<OptionTileTheme>()!.titleColor
|
||||||
|
: Theme.of(context).cardColor;
|
||||||
|
|
||||||
|
final imagePath = isSelected
|
||||||
|
? isLightMode
|
||||||
|
? darkImagePath
|
||||||
|
: lightImagePath
|
||||||
|
: isLightMode
|
||||||
|
? lightImagePath
|
||||||
|
: darkImagePath;
|
||||||
|
|
||||||
|
final rightSubTitleIconPath = isSelected
|
||||||
|
? isLightMode
|
||||||
|
? rightSubTitleDarkIconPath
|
||||||
|
: rightSubTitleLightIconPath
|
||||||
|
: isLightMode
|
||||||
|
? rightSubTitleLightIconPath
|
||||||
|
: rightSubTitleDarkIconPath;
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onPressed,
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(borderRadius ?? 12)),
|
||||||
|
border: isSelected && !isLightMode ? Border.all(color: textColor) : null,
|
||||||
|
color: backgroundColor,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: padding ?? const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
getImage(imagePath, height: imageHeight, width: imageWidth),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(title,
|
||||||
|
style: titleTextStyle ?? textLargeBold(color: textColor))),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (leadingIcon != null)
|
||||||
|
Icon(leadingIcon, size: 16, color: textColor),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (topLeftSubTitle != null || topRightSubTitle != null)
|
||||||
|
subTitleWidget(
|
||||||
|
leftSubTitle: topLeftSubTitle,
|
||||||
|
subTitleIconPath: leftSubTitleIconPath,
|
||||||
|
textColor: textColor,
|
||||||
|
rightSubTitle: topRightSubTitle,
|
||||||
|
rightSubTitleIconPath: rightSubTitleIconPath),
|
||||||
|
if (bottomLeftSubTitle != null || bottomRightSubTitle != null)
|
||||||
|
subTitleWidget(
|
||||||
|
leftSubTitle: bottomLeftSubTitle,
|
||||||
|
textColor: textColor,
|
||||||
|
subTitleFontSize: 12),
|
||||||
|
if (badges != null && badges!.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 12),
|
||||||
|
child: Row(children: [
|
||||||
|
...badges!
|
||||||
|
.map((badge) => Badge(
|
||||||
|
title: badge, textColor: badgeTextColor, backgroundColor: badgeColor))
|
||||||
|
.toList()
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class subTitleWidget extends StatelessWidget {
|
||||||
|
const subTitleWidget({
|
||||||
|
super.key,
|
||||||
|
this.leftSubTitle,
|
||||||
|
this.subTitleIconPath,
|
||||||
|
required this.textColor,
|
||||||
|
this.rightSubTitle,
|
||||||
|
this.rightSubTitleIconPath,
|
||||||
|
this.subTitleFontSize = 16,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? leftSubTitle;
|
||||||
|
final String? subTitleIconPath;
|
||||||
|
final Color textColor;
|
||||||
|
final String? rightSubTitle;
|
||||||
|
final String? rightSubTitleIconPath;
|
||||||
|
final double subTitleFontSize;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
leftSubTitle != null || subTitleIconPath != null
|
||||||
|
? Row(
|
||||||
|
children: [
|
||||||
|
if (subTitleIconPath != null && subTitleIconPath!.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 6),
|
||||||
|
child: getImage(subTitleIconPath!),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
leftSubTitle ?? '',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: subTitleFontSize,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: textColor),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Offstage(),
|
||||||
|
rightSubTitle != null || rightSubTitleIconPath != null
|
||||||
|
? Row(
|
||||||
|
children: [
|
||||||
|
if (rightSubTitleIconPath != null && rightSubTitleIconPath!.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 4),
|
||||||
|
child: getImage(rightSubTitleIconPath!, imageColor: textColor),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
rightSubTitle ?? '',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: subTitleFontSize, fontWeight: FontWeight.w700, color: textColor),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Offstage(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Badge extends StatelessWidget {
|
||||||
|
Badge({required this.textColor, required this.backgroundColor, required this.title});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final Color textColor;
|
||||||
|
final Color backgroundColor;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8),
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.fitHeight,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(24)), color: backgroundColor),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
color: textColor,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget getImage(String imagePath, {double? height, double? width, Color? imageColor}) {
|
||||||
|
final bool isNetworkImage = imagePath.startsWith('http') || imagePath.startsWith('https');
|
||||||
|
final bool isSvg = imagePath.endsWith('.svg');
|
||||||
|
final double imageHeight = height ?? 35;
|
||||||
|
final double imageWidth = width ?? 35;
|
||||||
|
|
||||||
|
if (isNetworkImage) {
|
||||||
|
return isSvg
|
||||||
|
? SvgPicture.network(
|
||||||
|
imagePath,
|
||||||
|
height: imageHeight,
|
||||||
|
width: imageWidth,
|
||||||
|
colorFilter: imageColor != null ? ColorFilter.mode(imageColor, BlendMode.srcIn) : null,
|
||||||
|
placeholderBuilder: (BuildContext context) => Container(
|
||||||
|
height: imageHeight,
|
||||||
|
width: imageWidth,
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Image.network(
|
||||||
|
imagePath,
|
||||||
|
height: imageHeight,
|
||||||
|
width: imageWidth,
|
||||||
|
loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
|
||||||
|
if (loadingProgress == null) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
return Container(
|
||||||
|
height: imageHeight,
|
||||||
|
width: imageWidth,
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: loadingProgress.expectedTotalBytes != null
|
||||||
|
? loadingProgress.cumulativeBytesLoaded /
|
||||||
|
loadingProgress.expectedTotalBytes!
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) {
|
||||||
|
return Container(
|
||||||
|
height: imageHeight,
|
||||||
|
width: imageWidth,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return isSvg
|
||||||
|
? SvgPicture.asset(
|
||||||
|
imagePath,
|
||||||
|
height: imageHeight,
|
||||||
|
width: imageWidth,
|
||||||
|
colorFilter: imageColor != null ? ColorFilter.mode(imageColor, BlendMode.srcIn) : null,
|
||||||
|
)
|
||||||
|
: Image.asset(imagePath, height: imageHeight, width: imageWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OptionTilePlaceholder extends StatefulWidget {
|
||||||
|
OptionTilePlaceholder({
|
||||||
|
this.borderRadius,
|
||||||
|
this.imageHeight,
|
||||||
|
this.imageWidth,
|
||||||
|
this.padding,
|
||||||
|
this.leadingIcon,
|
||||||
|
this.withBadge = true,
|
||||||
|
this.withSubtitle = true,
|
||||||
|
this.isDarkTheme = false,
|
||||||
|
this.errorText,
|
||||||
|
});
|
||||||
|
|
||||||
|
final double? borderRadius;
|
||||||
|
final double? imageHeight;
|
||||||
|
final double? imageWidth;
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
final IconData? leadingIcon;
|
||||||
|
final bool withBadge;
|
||||||
|
final bool withSubtitle;
|
||||||
|
final bool isDarkTheme;
|
||||||
|
final String? errorText;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_OptionTilePlaceholderState createState() => _OptionTilePlaceholderState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OptionTilePlaceholderState extends State<OptionTilePlaceholder>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<double> _animation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_controller = AnimationController(
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
vsync: this,
|
||||||
|
)..repeat();
|
||||||
|
|
||||||
|
_animation = CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: Curves.linear,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final backgroundColor = Theme.of(context).cardColor;
|
||||||
|
final titleColor = Theme.of(context).extension<OptionTileTheme>()!.titleColor.withOpacity(0.4);
|
||||||
|
|
||||||
|
return widget.errorText != null
|
||||||
|
? Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: widget.padding ?? EdgeInsets.all(16),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius ?? 12)),
|
||||||
|
color: backgroundColor,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.errorText!,
|
||||||
|
style: TextStyle(
|
||||||
|
color: titleColor,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.withSubtitle) SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
color: titleColor,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: AnimatedBuilder(
|
||||||
|
animation: _animation,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: widget.padding ?? EdgeInsets.all(16),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius ?? 12)),
|
||||||
|
color: backgroundColor,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: widget.imageHeight ?? 35,
|
||||||
|
width: widget.imageWidth ?? 35,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: titleColor,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 20,
|
||||||
|
width: 70,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: titleColor,
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.leadingIcon != null)
|
||||||
|
Icon(widget.leadingIcon, size: 16, color: titleColor),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (widget.withSubtitle)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 20,
|
||||||
|
width: 170,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: titleColor,
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.withBadge)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 30,
|
||||||
|
width: 70,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: titleColor,
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
|
height: 30,
|
||||||
|
width: 70,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: titleColor,
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned.fill(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius ?? 12)),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment(-2, -4),
|
||||||
|
end: Alignment(2, 4),
|
||||||
|
stops: [
|
||||||
|
_animation.value - 0.2,
|
||||||
|
_animation.value,
|
||||||
|
_animation.value + 0.2,
|
||||||
|
],
|
||||||
|
colors: [
|
||||||
|
backgroundColor.withOpacity(widget.isDarkTheme ? 0.4 : 0.7),
|
||||||
|
backgroundColor.withOpacity(widget.isDarkTheme ? 0.7 : 0.4),
|
||||||
|
backgroundColor.withOpacity(widget.isDarkTheme ? 0.4 : 0.7),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ class SettingActions {
|
||||||
addressBookSettingAction,
|
addressBookSettingAction,
|
||||||
silentPaymentsSettingAction,
|
silentPaymentsSettingAction,
|
||||||
litecoinMwebSettingAction,
|
litecoinMwebSettingAction,
|
||||||
|
exportOutputsAction,
|
||||||
securityBackupSettingAction,
|
securityBackupSettingAction,
|
||||||
privacySettingAction,
|
privacySettingAction,
|
||||||
displaySettingAction,
|
displaySettingAction,
|
||||||
|
@ -50,6 +51,16 @@ class SettingActions {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static SettingActions exportOutputsAction = SettingActions._(
|
||||||
|
key: ValueKey('dashboard_page_menu_widget_export_outputs_settings_button_key'),
|
||||||
|
name: (context) => S.of(context).export_outputs,
|
||||||
|
image: 'assets/images/monero_menu.png',
|
||||||
|
onTap: (BuildContext context) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, arguments: 'export-outputs');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
static SettingActions litecoinMwebSettingAction = SettingActions._(
|
static SettingActions litecoinMwebSettingAction = SettingActions._(
|
||||||
key: ValueKey('dashboard_page_menu_widget_litecoin_mweb_settings_button_key'),
|
key: ValueKey('dashboard_page_menu_widget_litecoin_mweb_settings_button_key'),
|
||||||
name: (context) => S.of(context).litecoin_mweb_settings,
|
name: (context) => S.of(context).litecoin_mweb_settings,
|
||||||
|
|
|
@ -65,8 +65,7 @@ abstract class SettingsStoreBase with Store {
|
||||||
required BitcoinSeedType initialBitcoinSeedType,
|
required BitcoinSeedType initialBitcoinSeedType,
|
||||||
required NanoSeedType initialNanoSeedType,
|
required NanoSeedType initialNanoSeedType,
|
||||||
required bool initialAppSecure,
|
required bool initialAppSecure,
|
||||||
required bool initialDisableBuy,
|
required bool initialDisableTrade,
|
||||||
required bool initialDisableSell,
|
|
||||||
required FilterListOrderType initialWalletListOrder,
|
required FilterListOrderType initialWalletListOrder,
|
||||||
required FilterListOrderType initialContactListOrder,
|
required FilterListOrderType initialContactListOrder,
|
||||||
required bool initialDisableBulletin,
|
required bool initialDisableBulletin,
|
||||||
|
@ -155,8 +154,7 @@ abstract class SettingsStoreBase with Store {
|
||||||
useTOTP2FA = initialUseTOTP2FA,
|
useTOTP2FA = initialUseTOTP2FA,
|
||||||
numberOfFailedTokenTrials = initialFailedTokenTrial,
|
numberOfFailedTokenTrials = initialFailedTokenTrial,
|
||||||
isAppSecure = initialAppSecure,
|
isAppSecure = initialAppSecure,
|
||||||
disableBuy = initialDisableBuy,
|
disableTradeOption = initialDisableTrade,
|
||||||
disableSell = initialDisableSell,
|
|
||||||
disableBulletin = initialDisableBulletin,
|
disableBulletin = initialDisableBulletin,
|
||||||
walletListOrder = initialWalletListOrder,
|
walletListOrder = initialWalletListOrder,
|
||||||
contactListOrder = initialContactListOrder,
|
contactListOrder = initialContactListOrder,
|
||||||
|
@ -183,9 +181,7 @@ abstract class SettingsStoreBase with Store {
|
||||||
initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings,
|
initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings,
|
||||||
currentSyncMode = initialSyncMode,
|
currentSyncMode = initialSyncMode,
|
||||||
currentSyncAll = initialSyncAll,
|
currentSyncAll = initialSyncAll,
|
||||||
priority = ObservableMap<WalletType, TransactionPriority>(),
|
priority = ObservableMap<WalletType, TransactionPriority>() {
|
||||||
defaultBuyProviders = ObservableMap<WalletType, ProviderType>(),
|
|
||||||
defaultSellProviders = ObservableMap<WalletType, ProviderType>() {
|
|
||||||
//this.nodes = ObservableMap<WalletType, Node>.of(nodes);
|
//this.nodes = ObservableMap<WalletType, Node>.of(nodes);
|
||||||
|
|
||||||
if (initialMoneroTransactionPriority != null) {
|
if (initialMoneroTransactionPriority != null) {
|
||||||
|
@ -229,30 +225,6 @@ abstract class SettingsStoreBase with Store {
|
||||||
|
|
||||||
initializeTrocadorProviderStates();
|
initializeTrocadorProviderStates();
|
||||||
|
|
||||||
WalletType.values.forEach((walletType) {
|
|
||||||
final key = 'buyProvider_${walletType.toString()}';
|
|
||||||
final providerId = sharedPreferences.getString(key);
|
|
||||||
if (providerId != null) {
|
|
||||||
defaultBuyProviders[walletType] = ProviderType.values.firstWhere(
|
|
||||||
(provider) => provider.id == providerId,
|
|
||||||
orElse: () => ProviderType.askEachTime);
|
|
||||||
} else {
|
|
||||||
defaultBuyProviders[walletType] = ProviderType.askEachTime;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
WalletType.values.forEach((walletType) {
|
|
||||||
final key = 'sellProvider_${walletType.toString()}';
|
|
||||||
final providerId = sharedPreferences.getString(key);
|
|
||||||
if (providerId != null) {
|
|
||||||
defaultSellProviders[walletType] = ProviderType.values.firstWhere(
|
|
||||||
(provider) => provider.id == providerId,
|
|
||||||
orElse: () => ProviderType.askEachTime);
|
|
||||||
} else {
|
|
||||||
defaultSellProviders[walletType] = ProviderType.askEachTime;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
reaction(
|
reaction(
|
||||||
(_) => fiatCurrency,
|
(_) => fiatCurrency,
|
||||||
(FiatCurrency fiatCurrency) => sharedPreferences.setString(
|
(FiatCurrency fiatCurrency) => sharedPreferences.setString(
|
||||||
|
@ -275,20 +247,6 @@ abstract class SettingsStoreBase with Store {
|
||||||
reaction((_) => shouldShowRepWarning,
|
reaction((_) => shouldShowRepWarning,
|
||||||
(bool val) => sharedPreferences.setBool(PreferencesKey.shouldShowRepWarning, val));
|
(bool val) => sharedPreferences.setBool(PreferencesKey.shouldShowRepWarning, val));
|
||||||
|
|
||||||
defaultBuyProviders.observe((change) {
|
|
||||||
final String key = 'buyProvider_${change.key.toString()}';
|
|
||||||
if (change.newValue != null) {
|
|
||||||
sharedPreferences.setString(key, change.newValue!.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
defaultSellProviders.observe((change) {
|
|
||||||
final String key = 'sellProvider_${change.key.toString()}';
|
|
||||||
if (change.newValue != null) {
|
|
||||||
sharedPreferences.setString(key, change.newValue!.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
priority.observe((change) {
|
priority.observe((change) {
|
||||||
final String? key;
|
final String? key;
|
||||||
switch (change.key) {
|
switch (change.key) {
|
||||||
|
@ -340,13 +298,8 @@ abstract class SettingsStoreBase with Store {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
reaction((_) => disableBuy,
|
reaction((_) => disableTradeOption,
|
||||||
(bool disableBuy) => sharedPreferences.setBool(PreferencesKey.disableBuyKey, disableBuy));
|
(bool disableTradeOption) => sharedPreferences.setBool(PreferencesKey.disableTradeOption, disableTradeOption));
|
||||||
|
|
||||||
reaction(
|
|
||||||
(_) => disableSell,
|
|
||||||
(bool disableSell) =>
|
|
||||||
sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell));
|
|
||||||
|
|
||||||
reaction(
|
reaction(
|
||||||
(_) => disableBulletin,
|
(_) => disableBulletin,
|
||||||
|
@ -691,10 +644,7 @@ abstract class SettingsStoreBase with Store {
|
||||||
bool isAppSecure;
|
bool isAppSecure;
|
||||||
|
|
||||||
@observable
|
@observable
|
||||||
bool disableBuy;
|
bool disableTradeOption;
|
||||||
|
|
||||||
@observable
|
|
||||||
bool disableSell;
|
|
||||||
|
|
||||||
@observable
|
@observable
|
||||||
FilterListOrderType contactListOrder;
|
FilterListOrderType contactListOrder;
|
||||||
|
@ -780,12 +730,6 @@ abstract class SettingsStoreBase with Store {
|
||||||
@observable
|
@observable
|
||||||
ObservableMap<String, bool> trocadorProviderStates = ObservableMap<String, bool>();
|
ObservableMap<String, bool> trocadorProviderStates = ObservableMap<String, bool>();
|
||||||
|
|
||||||
@observable
|
|
||||||
ObservableMap<WalletType, ProviderType> defaultBuyProviders;
|
|
||||||
|
|
||||||
@observable
|
|
||||||
ObservableMap<WalletType, ProviderType> defaultSellProviders;
|
|
||||||
|
|
||||||
@observable
|
@observable
|
||||||
SortBalanceBy sortBalanceBy;
|
SortBalanceBy sortBalanceBy;
|
||||||
|
|
||||||
|
@ -973,8 +917,7 @@ abstract class SettingsStoreBase with Store {
|
||||||
final shouldSaveRecipientAddress =
|
final shouldSaveRecipientAddress =
|
||||||
sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ?? false;
|
sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ?? false;
|
||||||
final isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? false;
|
final isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? false;
|
||||||
final disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? false;
|
final disableTradeOption = sharedPreferences.getBool(PreferencesKey.disableTradeOption) ?? false;
|
||||||
final disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? false;
|
|
||||||
final disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? false;
|
final disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? false;
|
||||||
final walletListOrder =
|
final walletListOrder =
|
||||||
FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0];
|
FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0];
|
||||||
|
@ -1279,8 +1222,7 @@ abstract class SettingsStoreBase with Store {
|
||||||
initialBitcoinSeedType: bitcoinSeedType,
|
initialBitcoinSeedType: bitcoinSeedType,
|
||||||
initialNanoSeedType: nanoSeedType,
|
initialNanoSeedType: nanoSeedType,
|
||||||
initialAppSecure: isAppSecure,
|
initialAppSecure: isAppSecure,
|
||||||
initialDisableBuy: disableBuy,
|
initialDisableTrade: disableTradeOption,
|
||||||
initialDisableSell: disableSell,
|
|
||||||
initialDisableBulletin: disableBulletin,
|
initialDisableBulletin: disableBulletin,
|
||||||
initialWalletListOrder: walletListOrder,
|
initialWalletListOrder: walletListOrder,
|
||||||
initialWalletListAscending: walletListAscending,
|
initialWalletListAscending: walletListAscending,
|
||||||
|
@ -1435,8 +1377,7 @@ abstract class SettingsStoreBase with Store {
|
||||||
numberOfFailedTokenTrials =
|
numberOfFailedTokenTrials =
|
||||||
sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? numberOfFailedTokenTrials;
|
sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? numberOfFailedTokenTrials;
|
||||||
isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? isAppSecure;
|
isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? isAppSecure;
|
||||||
disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? disableBuy;
|
disableTradeOption = sharedPreferences.getBool(PreferencesKey.disableTradeOption) ?? disableTradeOption;
|
||||||
disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? disableSell;
|
|
||||||
disableBulletin =
|
disableBulletin =
|
||||||
sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? disableBulletin;
|
sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? disableBulletin;
|
||||||
walletListOrder =
|
walletListOrder =
|
||||||
|
|
|
@ -22,6 +22,8 @@ TextStyle textMediumSemiBold({Color? color}) => _cakeSemiBold(22, color);
|
||||||
|
|
||||||
TextStyle textLarge({Color? color}) => _cakeRegular(18, color);
|
TextStyle textLarge({Color? color}) => _cakeRegular(18, color);
|
||||||
|
|
||||||
|
TextStyle textLargeBold({Color? color}) => _cakeBold(18, color);
|
||||||
|
|
||||||
TextStyle textLargeSemiBold({Color? color}) => _cakeSemiBold(24, color);
|
TextStyle textLargeSemiBold({Color? color}) => _cakeSemiBold(24, color);
|
||||||
|
|
||||||
TextStyle textXLarge({Color? color}) => _cakeRegular(32, color);
|
TextStyle textXLarge({Color? color}) => _cakeRegular(32, color);
|
||||||
|
|
10
lib/view_model/animated_ur_model.dart
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import 'package:cake_wallet/store/app_store.dart';
|
||||||
|
import 'package:cw_core/wallet_base.dart';
|
||||||
|
import 'package:mobx/mobx.dart';
|
||||||
|
|
||||||
|
class AnimatedURModel with Store {
|
||||||
|
AnimatedURModel(this.appStore)
|
||||||
|
: wallet = appStore.wallet!;
|
||||||
|
final AppStore appStore;
|
||||||
|
final WalletBase wallet;
|
||||||
|
}
|
446
lib/view_model/buy/buy_sell_view_model.dart
Normal file
|
@ -0,0 +1,446 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:cake_wallet/buy/buy_provider.dart';
|
||||||
|
import 'package:cake_wallet/buy/buy_quote.dart';
|
||||||
|
import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart';
|
||||||
|
import 'package:cake_wallet/buy/payment_method.dart';
|
||||||
|
import 'package:cake_wallet/buy/sell_buy_states.dart';
|
||||||
|
import 'package:cake_wallet/core/selectable_option.dart';
|
||||||
|
import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
|
||||||
|
import 'package:cake_wallet/entities/fiat_currency.dart';
|
||||||
|
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:cake_wallet/store/settings_store.dart';
|
||||||
|
import 'package:cake_wallet/themes/theme_base.dart';
|
||||||
|
import 'package:cw_core/crypto_currency.dart';
|
||||||
|
import 'package:cw_core/currency_for_wallet_type.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:mobx/mobx.dart';
|
||||||
|
|
||||||
|
part 'buy_sell_view_model.g.dart';
|
||||||
|
|
||||||
|
class BuySellViewModel = BuySellViewModelBase with _$BuySellViewModel;
|
||||||
|
|
||||||
|
abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with Store {
|
||||||
|
BuySellViewModelBase(
|
||||||
|
AppStore appStore,
|
||||||
|
) : _cryptoNumberFormat = NumberFormat(),
|
||||||
|
cryptoAmount = '',
|
||||||
|
fiatAmount = '',
|
||||||
|
cryptoCurrencyAddress = '',
|
||||||
|
isCryptoCurrencyAddressEnabled = false,
|
||||||
|
cryptoCurrencies = <CryptoCurrency>[],
|
||||||
|
fiatCurrencies = <FiatCurrency>[],
|
||||||
|
paymentMethodState = InitialPaymentMethod(),
|
||||||
|
buySellQuotState = InitialBuySellQuotState(),
|
||||||
|
cryptoCurrency = appStore.wallet!.currency,
|
||||||
|
fiatCurrency = appStore.settingsStore.fiatCurrency,
|
||||||
|
providerList = [],
|
||||||
|
sortedRecommendedQuotes = ObservableList<Quote>(),
|
||||||
|
sortedQuotes = ObservableList<Quote>(),
|
||||||
|
paymentMethods = ObservableList<PaymentMethod>(),
|
||||||
|
settingsStore = appStore.settingsStore,
|
||||||
|
super(appStore: appStore) {
|
||||||
|
const excludeFiatCurrencies = [];
|
||||||
|
const excludeCryptoCurrencies = [];
|
||||||
|
|
||||||
|
fiatCurrencies =
|
||||||
|
FiatCurrency.all.where((currency) => !excludeFiatCurrencies.contains(currency)).toList();
|
||||||
|
cryptoCurrencies = CryptoCurrency.all
|
||||||
|
.where((currency) => !excludeCryptoCurrencies.contains(currency))
|
||||||
|
.toList();
|
||||||
|
_initialize();
|
||||||
|
|
||||||
|
isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
final NumberFormat _cryptoNumberFormat;
|
||||||
|
late Timer bestRateSync;
|
||||||
|
|
||||||
|
List<BuyProvider> get availableBuyProviders {
|
||||||
|
final providerTypes = ProvidersHelper.getAvailableBuyProviderTypes(
|
||||||
|
walletTypeForCurrency(cryptoCurrency) ?? wallet.type);
|
||||||
|
return providerTypes
|
||||||
|
.map((type) => ProvidersHelper.getProviderByType(type))
|
||||||
|
.where((provider) => provider != null)
|
||||||
|
.cast<BuyProvider>()
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<BuyProvider> get availableSellProviders {
|
||||||
|
final providerTypes = ProvidersHelper.getAvailableSellProviderTypes(
|
||||||
|
walletTypeForCurrency(cryptoCurrency) ?? wallet.type);
|
||||||
|
return providerTypes
|
||||||
|
.map((type) => ProvidersHelper.getProviderByType(type))
|
||||||
|
.where((provider) => provider != null)
|
||||||
|
.cast<BuyProvider>()
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWalletChange(wallet) {
|
||||||
|
cryptoCurrency = wallet.currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isDarkTheme => settingsStore.currentTheme.type == ThemeType.dark;
|
||||||
|
|
||||||
|
double get amount {
|
||||||
|
final formattedFiatAmount = double.tryParse(fiatAmount) ?? 200.0;
|
||||||
|
final formattedCryptoAmount =
|
||||||
|
double.tryParse(cryptoAmount) ?? (cryptoCurrency == CryptoCurrency.btc ? 0.001 : 1);
|
||||||
|
|
||||||
|
return isBuyAction ? formattedFiatAmount : formattedCryptoAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsStore settingsStore;
|
||||||
|
|
||||||
|
Quote? bestRateQuote;
|
||||||
|
|
||||||
|
Quote? selectedQuote;
|
||||||
|
|
||||||
|
@observable
|
||||||
|
List<CryptoCurrency> cryptoCurrencies;
|
||||||
|
|
||||||
|
@observable
|
||||||
|
List<FiatCurrency> fiatCurrencies;
|
||||||
|
|
||||||
|
@observable
|
||||||
|
bool isBuyAction = true;
|
||||||
|
|
||||||
|
@observable
|
||||||
|
List<BuyProvider> providerList;
|
||||||
|
|
||||||
|
@observable
|
||||||
|
ObservableList<Quote> sortedRecommendedQuotes;
|
||||||
|
|
||||||
|
@observable
|
||||||
|
ObservableList<Quote> sortedQuotes;
|
||||||
|
|
||||||
|
@observable
|
||||||
|
ObservableList<PaymentMethod> paymentMethods;
|
||||||
|
|
||||||
|
@observable
|
||||||
|
FiatCurrency fiatCurrency;
|
||||||
|
|
||||||
|
@observable
|
||||||
|
CryptoCurrency cryptoCurrency;
|
||||||
|
|
||||||
|
@observable
|
||||||
|
String cryptoAmount;
|
||||||
|
|
||||||
|
@observable
|
||||||
|
String fiatAmount;
|
||||||
|
|
||||||
|
@observable
|
||||||
|
String cryptoCurrencyAddress;
|
||||||
|
|
||||||
|
@observable
|
||||||
|
bool isCryptoCurrencyAddressEnabled;
|
||||||
|
|
||||||
|
@observable
|
||||||
|
PaymentMethod? selectedPaymentMethod;
|
||||||
|
|
||||||
|
@observable
|
||||||
|
PaymentMethodLoadingState paymentMethodState;
|
||||||
|
|
||||||
|
@observable
|
||||||
|
BuySellQuotLoadingState buySellQuotState;
|
||||||
|
|
||||||
|
@computed
|
||||||
|
bool get isReadyToTrade {
|
||||||
|
final hasSelectedQuote = selectedQuote != null;
|
||||||
|
final hasSelectedPaymentMethod = selectedPaymentMethod != null;
|
||||||
|
final isPaymentMethodLoaded = paymentMethodState is PaymentMethodLoaded;
|
||||||
|
final isBuySellQuotLoaded = buySellQuotState is BuySellQuotLoaded;
|
||||||
|
|
||||||
|
return hasSelectedQuote &&
|
||||||
|
hasSelectedPaymentMethod &&
|
||||||
|
isPaymentMethodLoaded &&
|
||||||
|
isBuySellQuotLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
void reset() {
|
||||||
|
cryptoCurrency = wallet.currency;
|
||||||
|
fiatCurrency = settingsStore.fiatCurrency;
|
||||||
|
isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency);
|
||||||
|
_initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
void changeBuySellAction() {
|
||||||
|
isBuyAction = !isBuyAction;
|
||||||
|
_initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
void changeFiatCurrency({required FiatCurrency currency}) {
|
||||||
|
fiatCurrency = currency;
|
||||||
|
_onPairChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
void changeCryptoCurrency({required CryptoCurrency currency}) {
|
||||||
|
cryptoCurrency = currency;
|
||||||
|
_onPairChange();
|
||||||
|
isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
void changeCryptoCurrencyAddress(String address) => cryptoCurrencyAddress = address;
|
||||||
|
|
||||||
|
@action
|
||||||
|
Future<void> changeFiatAmount({required String amount}) async {
|
||||||
|
fiatAmount = amount;
|
||||||
|
|
||||||
|
if (amount.isEmpty) {
|
||||||
|
fiatAmount = '';
|
||||||
|
cryptoAmount = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0;
|
||||||
|
|
||||||
|
if (!isReadyToTrade) {
|
||||||
|
cryptoAmount = S.current.fetching;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestRateQuote != null) {
|
||||||
|
_cryptoNumberFormat.maximumFractionDigits = cryptoCurrency.decimals;
|
||||||
|
cryptoAmount = _cryptoNumberFormat
|
||||||
|
.format(enteredAmount / bestRateQuote!.rate)
|
||||||
|
.toString()
|
||||||
|
.replaceAll(RegExp('\\,'), '');
|
||||||
|
} else {
|
||||||
|
await calculateBestRate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
Future<void> changeCryptoAmount({required String amount}) async {
|
||||||
|
cryptoAmount = amount;
|
||||||
|
|
||||||
|
if (amount.isEmpty) {
|
||||||
|
fiatAmount = '';
|
||||||
|
cryptoAmount = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0;
|
||||||
|
|
||||||
|
if (!isReadyToTrade) {
|
||||||
|
fiatAmount = S.current.fetching;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestRateQuote != null) {
|
||||||
|
fiatAmount = _cryptoNumberFormat
|
||||||
|
.format(enteredAmount * bestRateQuote!.rate)
|
||||||
|
.toString()
|
||||||
|
.replaceAll(RegExp('\\,'), '');
|
||||||
|
} else {
|
||||||
|
await calculateBestRate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
void changeOption(SelectableOption option) {
|
||||||
|
if (option is Quote) {
|
||||||
|
sortedRecommendedQuotes.forEach((element) => element.setIsSelected = false);
|
||||||
|
sortedQuotes.forEach((element) => element.setIsSelected = false);
|
||||||
|
option.setIsSelected = true;
|
||||||
|
selectedQuote = option;
|
||||||
|
} else if (option is PaymentMethod) {
|
||||||
|
paymentMethods.forEach((element) => element.isSelected = false);
|
||||||
|
option.isSelected = true;
|
||||||
|
selectedPaymentMethod = option;
|
||||||
|
} else {
|
||||||
|
throw ArgumentError('Unknown option type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onTapChoseProvider(BuildContext context) async {
|
||||||
|
final initialQuotes = List<Quote>.from(sortedRecommendedQuotes + sortedQuotes);
|
||||||
|
await calculateBestRate();
|
||||||
|
final newQuotes = (sortedRecommendedQuotes + sortedQuotes);
|
||||||
|
|
||||||
|
for (var quote in newQuotes) quote.limits = null;
|
||||||
|
|
||||||
|
final newQuoteProviders = newQuotes
|
||||||
|
.map((quote) => quote.provider.isAggregator ? quote.rampName : quote.provider.title)
|
||||||
|
.toSet();
|
||||||
|
|
||||||
|
final outOfLimitQuotes = initialQuotes.where((initialQuote) {
|
||||||
|
return !newQuoteProviders.contains(
|
||||||
|
initialQuote.provider.isAggregator ? initialQuote.rampName : initialQuote.provider.title);
|
||||||
|
}).map((missingQuote) {
|
||||||
|
final quote = Quote(
|
||||||
|
rate: missingQuote.rate,
|
||||||
|
feeAmount: missingQuote.feeAmount,
|
||||||
|
networkFee: missingQuote.networkFee,
|
||||||
|
transactionFee: missingQuote.transactionFee,
|
||||||
|
payout: missingQuote.payout,
|
||||||
|
rampId: missingQuote.rampId,
|
||||||
|
rampName: missingQuote.rampName,
|
||||||
|
rampIconPath: missingQuote.rampIconPath,
|
||||||
|
paymentType: missingQuote.paymentType,
|
||||||
|
quoteId: missingQuote.quoteId,
|
||||||
|
recommendations: missingQuote.recommendations,
|
||||||
|
provider: missingQuote.provider,
|
||||||
|
isBuyAction: missingQuote.isBuyAction,
|
||||||
|
limits: missingQuote.limits,
|
||||||
|
);
|
||||||
|
quote.setFiatCurrency = missingQuote.fiatCurrency;
|
||||||
|
quote.setCryptoCurrency = missingQuote.cryptoCurrency;
|
||||||
|
return quote;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
final updatedQuoteOptions = List<SelectableItem>.from([
|
||||||
|
OptionTitle(title: 'Recommended'),
|
||||||
|
...sortedRecommendedQuotes,
|
||||||
|
if (sortedQuotes.isNotEmpty) OptionTitle(title: 'All Providers'),
|
||||||
|
...sortedQuotes,
|
||||||
|
if (outOfLimitQuotes.isNotEmpty) OptionTitle(title: 'Out of Limits'),
|
||||||
|
...outOfLimitQuotes,
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Navigator.of(context).pushNamed(
|
||||||
|
Routes.buyOptionsPage,
|
||||||
|
arguments: [
|
||||||
|
updatedQuoteOptions,
|
||||||
|
changeOption,
|
||||||
|
launchTrade,
|
||||||
|
],
|
||||||
|
).then((value) => calculateBestRate());
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onPairChange() {
|
||||||
|
_initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setProviders() =>
|
||||||
|
providerList = isBuyAction ? availableBuyProviders : availableSellProviders;
|
||||||
|
|
||||||
|
Future<void> _initialize() async {
|
||||||
|
_setProviders();
|
||||||
|
cryptoAmount = '';
|
||||||
|
fiatAmount = '';
|
||||||
|
cryptoCurrencyAddress = _getInitialCryptoCurrencyAddress();
|
||||||
|
paymentMethodState = InitialPaymentMethod();
|
||||||
|
buySellQuotState = InitialBuySellQuotState();
|
||||||
|
await _getAvailablePaymentTypes();
|
||||||
|
await calculateBestRate();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getInitialCryptoCurrencyAddress() {
|
||||||
|
return cryptoCurrency == wallet.currency ? wallet.walletAddresses.address : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
Future<void> _getAvailablePaymentTypes() async {
|
||||||
|
paymentMethodState = PaymentMethodLoading();
|
||||||
|
selectedPaymentMethod = null;
|
||||||
|
final result = await Future.wait(providerList.map((element) => element
|
||||||
|
.getAvailablePaymentTypes(fiatCurrency.title, cryptoCurrency.title, isBuyAction)
|
||||||
|
.timeout(
|
||||||
|
Duration(seconds: 10),
|
||||||
|
onTimeout: () => [],
|
||||||
|
)));
|
||||||
|
|
||||||
|
final Map<PaymentType, PaymentMethod> uniquePaymentMethods = {};
|
||||||
|
for (var methods in result) {
|
||||||
|
for (var method in methods) {
|
||||||
|
uniquePaymentMethods[method.paymentMethodType] = method;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
paymentMethods = ObservableList<PaymentMethod>.of(uniquePaymentMethods.values);
|
||||||
|
if (paymentMethods.isNotEmpty) {
|
||||||
|
paymentMethods.insert(0, PaymentMethod.all());
|
||||||
|
selectedPaymentMethod = paymentMethods.first;
|
||||||
|
selectedPaymentMethod!.isSelected = true;
|
||||||
|
paymentMethodState = PaymentMethodLoaded();
|
||||||
|
} else {
|
||||||
|
paymentMethodState = PaymentMethodFailed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
Future<void> calculateBestRate() async {
|
||||||
|
buySellQuotState = BuySellQuotLoading();
|
||||||
|
|
||||||
|
final result = await Future.wait<List<Quote>?>(providerList.map((element) => element
|
||||||
|
.fetchQuote(
|
||||||
|
cryptoCurrency: cryptoCurrency,
|
||||||
|
fiatCurrency: fiatCurrency,
|
||||||
|
amount: amount,
|
||||||
|
paymentType: selectedPaymentMethod?.paymentMethodType,
|
||||||
|
isBuyAction: isBuyAction,
|
||||||
|
walletAddress: wallet.walletAddresses.address,
|
||||||
|
)
|
||||||
|
.timeout(
|
||||||
|
Duration(seconds: 10),
|
||||||
|
onTimeout: () => null,
|
||||||
|
)));
|
||||||
|
|
||||||
|
sortedRecommendedQuotes.clear();
|
||||||
|
sortedQuotes.clear();
|
||||||
|
|
||||||
|
final validQuotes = result
|
||||||
|
.where((element) => element != null && element.isNotEmpty)
|
||||||
|
.expand((element) => element!)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (validQuotes.isEmpty) {
|
||||||
|
buySellQuotState = BuySellQuotFailed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
validQuotes.sort((a, b) => a.rate.compareTo(b.rate));
|
||||||
|
|
||||||
|
final Set<String> addedProviders = {};
|
||||||
|
final List<Quote> uniqueProviderQuotes = validQuotes.where((element) {
|
||||||
|
if (addedProviders.contains(element.provider.title)) return false;
|
||||||
|
addedProviders.add(element.provider.title);
|
||||||
|
return true;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
sortedRecommendedQuotes.addAll(uniqueProviderQuotes);
|
||||||
|
|
||||||
|
sortedQuotes = ObservableList.of(
|
||||||
|
validQuotes.where((element) => !uniqueProviderQuotes.contains(element)).toList());
|
||||||
|
|
||||||
|
if (sortedRecommendedQuotes.isNotEmpty) {
|
||||||
|
sortedRecommendedQuotes.first
|
||||||
|
..setIsBestRate = true
|
||||||
|
..recommendations.insert(0, ProviderRecommendation.bestRate);
|
||||||
|
bestRateQuote = sortedRecommendedQuotes.first;
|
||||||
|
|
||||||
|
sortedRecommendedQuotes.sort((a, b) {
|
||||||
|
if (a.provider is OnRamperBuyProvider) return -1;
|
||||||
|
if (b.provider is OnRamperBuyProvider) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
selectedQuote = sortedRecommendedQuotes.first;
|
||||||
|
sortedRecommendedQuotes.first.setIsSelected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
buySellQuotState = BuySellQuotLoaded();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
Future<void> launchTrade(BuildContext context) async {
|
||||||
|
final provider = selectedQuote!.provider;
|
||||||
|
await provider.launchProvider(
|
||||||
|
context: context,
|
||||||
|
quote: selectedQuote!,
|
||||||
|
amount: amount,
|
||||||
|
isBuyAction: isBuyAction,
|
||||||
|
cryptoCurrencyAddress: cryptoCurrencyAddress,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -71,8 +71,7 @@ abstract class DashboardViewModelBase with Store {
|
||||||
required this.anonpayTransactionsStore,
|
required this.anonpayTransactionsStore,
|
||||||
required this.sharedPreferences,
|
required this.sharedPreferences,
|
||||||
required this.keyService})
|
required this.keyService})
|
||||||
: hasSellAction = false,
|
: hasTradeAction = false,
|
||||||
hasBuyAction = false,
|
|
||||||
hasExchangeAction = false,
|
hasExchangeAction = false,
|
||||||
isShowFirstYatIntroduction = false,
|
isShowFirstYatIntroduction = false,
|
||||||
isShowSecondYatIntroduction = false,
|
isShowSecondYatIntroduction = false,
|
||||||
|
@ -393,6 +392,12 @@ abstract class DashboardViewModelBase with Store {
|
||||||
wallet.type == WalletType.wownero ||
|
wallet.type == WalletType.wownero ||
|
||||||
wallet.type == WalletType.haven;
|
wallet.type == WalletType.haven;
|
||||||
|
|
||||||
|
@computed
|
||||||
|
bool get isMoneroViewOnly {
|
||||||
|
if (wallet.type != WalletType.monero) return false;
|
||||||
|
return monero!.isViewOnly();
|
||||||
|
}
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
String? get getMoneroError {
|
String? get getMoneroError {
|
||||||
if (wallet.type != WalletType.monero) return null;
|
if (wallet.type != WalletType.monero) return null;
|
||||||
|
@ -515,37 +520,8 @@ abstract class DashboardViewModelBase with Store {
|
||||||
|
|
||||||
Map<String, List<FilterItem>> filterItems;
|
Map<String, List<FilterItem>> filterItems;
|
||||||
|
|
||||||
BuyProvider? get defaultBuyProvider => ProvidersHelper.getProviderByType(
|
|
||||||
settingsStore.defaultBuyProviders[wallet.type] ?? ProviderType.askEachTime);
|
|
||||||
|
|
||||||
BuyProvider? get defaultSellProvider => ProvidersHelper.getProviderByType(
|
|
||||||
settingsStore.defaultSellProviders[wallet.type] ?? ProviderType.askEachTime);
|
|
||||||
|
|
||||||
bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled;
|
bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled;
|
||||||
|
|
||||||
List<BuyProvider> get availableBuyProviders {
|
|
||||||
final providerTypes = ProvidersHelper.getAvailableBuyProviderTypes(wallet.type);
|
|
||||||
return providerTypes
|
|
||||||
.map((type) => ProvidersHelper.getProviderByType(type))
|
|
||||||
.where((provider) => provider != null)
|
|
||||||
.cast<BuyProvider>()
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get hasBuyProviders => ProvidersHelper.getAvailableBuyProviderTypes(wallet.type).isNotEmpty;
|
|
||||||
|
|
||||||
List<BuyProvider> get availableSellProviders {
|
|
||||||
final providerTypes = ProvidersHelper.getAvailableSellProviderTypes(wallet.type);
|
|
||||||
return providerTypes
|
|
||||||
.map((type) => ProvidersHelper.getProviderByType(type))
|
|
||||||
.where((provider) => provider != null)
|
|
||||||
.cast<BuyProvider>()
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get hasSellProviders =>
|
|
||||||
ProvidersHelper.getAvailableSellProviderTypes(wallet.type).isNotEmpty;
|
|
||||||
|
|
||||||
bool get shouldShowYatPopup => settingsStore.shouldShowYatPopup;
|
bool get shouldShowYatPopup => settingsStore.shouldShowYatPopup;
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -558,16 +534,10 @@ abstract class DashboardViewModelBase with Store {
|
||||||
bool hasExchangeAction;
|
bool hasExchangeAction;
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
bool get isEnabledBuyAction => !settingsStore.disableBuy && hasBuyProviders;
|
bool get isEnabledTradeAction => !settingsStore.disableTradeOption;
|
||||||
|
|
||||||
@observable
|
@observable
|
||||||
bool hasBuyAction;
|
bool hasTradeAction;
|
||||||
|
|
||||||
@computed
|
|
||||||
bool get isEnabledSellAction => !settingsStore.disableSell && hasSellProviders;
|
|
||||||
|
|
||||||
@observable
|
|
||||||
bool hasSellAction;
|
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
bool get isEnabledBulletinAction => !settingsStore.disableBulletin;
|
bool get isEnabledBulletinAction => !settingsStore.disableBulletin;
|
||||||
|
@ -771,8 +741,7 @@ abstract class DashboardViewModelBase with Store {
|
||||||
|
|
||||||
void updateActions() {
|
void updateActions() {
|
||||||
hasExchangeAction = !isHaven;
|
hasExchangeAction = !isHaven;
|
||||||
hasBuyAction = !isHaven;
|
hasTradeAction = !isHaven;
|
||||||
hasSellAction = !isHaven;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
|
|
|
@ -214,7 +214,7 @@ abstract class NodeCreateOrEditViewModelBase with Store {
|
||||||
bool isCameraPermissionGranted =
|
bool isCameraPermissionGranted =
|
||||||
await PermissionHandler.checkPermission(Permission.camera, context);
|
await PermissionHandler.checkPermission(Permission.camera, context);
|
||||||
if (!isCameraPermissionGranted) return;
|
if (!isCameraPermissionGranted) return;
|
||||||
String code = await presentQRScanner();
|
String code = await presentQRScanner(context);
|
||||||
|
|
||||||
if (code.isEmpty) {
|
if (code.isEmpty) {
|
||||||
throw Exception('Unexpected scan QR code value: value is empty');
|
throw Exception('Unexpected scan QR code value: value is empty');
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:cake_wallet/view_model/restore/restore_mode.dart';
|
import 'package:cake_wallet/view_model/restore/restore_mode.dart';
|
||||||
import 'package:cw_core/wallet_type.dart';
|
import 'package:cw_core/wallet_type.dart';
|
||||||
|
|
||||||
|
@ -32,6 +34,16 @@ class RestoredWallet {
|
||||||
final String? privateKey;
|
final String? privateKey;
|
||||||
|
|
||||||
factory RestoredWallet.fromKey(Map<String, dynamic> json) {
|
factory RestoredWallet.fromKey(Map<String, dynamic> json) {
|
||||||
|
try {
|
||||||
|
final codeParsed = jsonDecode(json['raw_qr'].toString());
|
||||||
|
if (codeParsed["version"] == 0) {
|
||||||
|
json['address'] = codeParsed["primaryAddress"];
|
||||||
|
json['view_key'] = codeParsed["privateViewKey"];
|
||||||
|
json['height'] = codeParsed["restoreHeight"].toString();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// fine, we don't care, it is only for monero anyway
|
||||||
|
}
|
||||||
final height = json['height'] as String?;
|
final height = json['height'] as String?;
|
||||||
return RestoredWallet(
|
return RestoredWallet(
|
||||||
restoreMode: json['mode'] as WalletRestoreMode,
|
restoreMode: json['mode'] as WalletRestoreMode,
|
||||||
|
@ -39,7 +51,7 @@ class RestoredWallet {
|
||||||
address: json['address'] as String?,
|
address: json['address'] as String?,
|
||||||
spendKey: json['spend_key'] as String?,
|
spendKey: json['spend_key'] as String?,
|
||||||
viewKey: json['view_key'] as String?,
|
viewKey: json['view_key'] as String?,
|
||||||
height: height != null ? int.parse(height) : 0,
|
height: height != null ? int.tryParse(height)??0 : 0,
|
||||||
privateKey: json['private_key'] as String?,
|
privateKey: json['private_key'] as String?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:cake_wallet/core/seed_validator.dart';
|
import 'package:cake_wallet/core/seed_validator.dart';
|
||||||
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
|
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
|
||||||
import 'package:cake_wallet/entities/qr_scanner.dart';
|
import 'package:cake_wallet/entities/qr_scanner.dart';
|
||||||
|
@ -51,6 +53,17 @@ class WalletRestoreFromQRCode {
|
||||||
|
|
||||||
final extracted = sortedKeys.firstWhereOrNull((key) => code.toLowerCase().contains(key));
|
final extracted = sortedKeys.firstWhereOrNull((key) => code.toLowerCase().contains(key));
|
||||||
|
|
||||||
|
if (extracted == null) {
|
||||||
|
// Special case for view-only monero wallet
|
||||||
|
final codeParsed = json.decode(code);
|
||||||
|
if (codeParsed["version"] == 0 &&
|
||||||
|
codeParsed["primaryAddress"] != null &&
|
||||||
|
codeParsed["privateViewKey"] != null &&
|
||||||
|
codeParsed["restoreHeight"] != null) {
|
||||||
|
return WalletType.monero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return _walletTypeMap[extracted];
|
return _walletTypeMap[extracted];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +91,7 @@ class WalletRestoreFromQRCode {
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<RestoredWallet> scanQRCodeForRestoring(BuildContext context) async {
|
static Future<RestoredWallet> scanQRCodeForRestoring(BuildContext context) async {
|
||||||
String code = await presentQRScanner();
|
String code = await presentQRScanner(context);
|
||||||
if (code.isEmpty) throw Exception('Unexpected scan QR code value: value is empty');
|
if (code.isEmpty) throw Exception('Unexpected scan QR code value: value is empty');
|
||||||
|
|
||||||
WalletType? walletType;
|
WalletType? walletType;
|
||||||
|
@ -112,7 +125,7 @@ class WalletRestoreFromQRCode {
|
||||||
queryParameters['address'] = _extractAddressFromUrl(code, walletType!);
|
queryParameters['address'] = _extractAddressFromUrl(code, walletType!);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> credentials = {'type': walletType, ...queryParameters};
|
Map<String, dynamic> credentials = {'type': walletType, ...queryParameters, 'raw_qr': code};
|
||||||
|
|
||||||
credentials['mode'] = _determineWalletRestoreMode(credentials);
|
credentials['mode'] = _determineWalletRestoreMode(credentials);
|
||||||
|
|
||||||
|
@ -208,6 +221,17 @@ class WalletRestoreFromQRCode {
|
||||||
return WalletRestoreMode.keys;
|
return WalletRestoreMode.keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type == WalletType.monero) {
|
||||||
|
final codeParsed = json.decode(credentials['raw_qr'].toString());
|
||||||
|
if (codeParsed["version"] != 0) throw UnimplementedError("Found view-only restore with unsupported version");
|
||||||
|
if (codeParsed["primaryAddress"] == null ||
|
||||||
|
codeParsed["privateViewKey"] == null ||
|
||||||
|
codeParsed["restoreHeight"] == null) {
|
||||||
|
throw UnimplementedError("Missing one or more attributes in the JSON");
|
||||||
|
}
|
||||||
|
return WalletRestoreMode.keys;
|
||||||
|
}
|
||||||
|
|
||||||
throw Exception('Unexpected restore mode: restore params are invalid');
|
throw Exception('Unexpected restore mode: restore params are invalid');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|