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/scripts/monero_c/release
|
||||
key: ${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh' ,'**/cache_dependencies.yml') }}
|
||||
|
||||
- if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }}
|
||||
name: Generate Externals
|
||||
run: |
|
||||
cd /opt/android/cake_wallet/scripts/android/
|
||||
source ./app_env.sh cakewallet
|
||||
./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/
|
||||
./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
|
||||
run: |
|
||||
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 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 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 letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart
|
||||
echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart
|
||||
|
@ -202,6 +212,36 @@ jobs:
|
|||
run: |
|
||||
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
|
||||
run: |
|
||||
cd /opt/android/cake_wallet
|
||||
|
@ -232,6 +272,13 @@ jobs:
|
|||
with:
|
||||
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
|
||||
continue-on-error: true
|
||||
uses: adrey/slack-file-upload-action@1.0.5
|
||||
|
@ -242,3 +289,4 @@ jobs:
|
|||
title: "${{ env.BRANCH_NAME }}.apk"
|
||||
filename: ${{ env.BRANCH_NAME }}.apk
|
||||
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 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 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 letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> 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.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.useAndroidX=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
|
||||
is_default: true
|
||||
trusted: true
|
||||
-
|
||||
uri: cakexmrl7bonq7ovjka5kuwuyd3f7qnkz6z6s6dmsy3uckwra7bvggyd.onion:18081
|
||||
is_default: false
|
||||
|
|
|
@ -11,7 +11,6 @@ import 'package:blockchain_utils/blockchain_utils.dart';
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:cw_bitcoin/address_from_output.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_priority.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_unspent.dart';
|
||||
|
@ -597,8 +596,8 @@ abstract class ElectrumWalletBase
|
|||
|
||||
UtxoDetails _createUTXOS({
|
||||
required bool sendAll,
|
||||
required int credentialsAmount,
|
||||
required bool paysToSilentPayment,
|
||||
int credentialsAmount = 0,
|
||||
int? inputsCount,
|
||||
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
|
||||
}) {
|
||||
|
@ -732,13 +731,11 @@ abstract class ElectrumWalletBase
|
|||
List<BitcoinOutput> outputs,
|
||||
int feeRate, {
|
||||
String? memo,
|
||||
int credentialsAmount = 0,
|
||||
bool hasSilentPayment = false,
|
||||
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
|
||||
}) async {
|
||||
final utxoDetails = _createUTXOS(
|
||||
sendAll: true,
|
||||
credentialsAmount: credentialsAmount,
|
||||
paysToSilentPayment: hasSilentPayment,
|
||||
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
||||
);
|
||||
|
@ -764,23 +761,11 @@ abstract class ElectrumWalletBase
|
|||
throw BitcoinTransactionWrongBalanceException(amount: utxoDetails.allInputsAmount + fee);
|
||||
}
|
||||
|
||||
if (amount <= 0) {
|
||||
throw BitcoinTransactionWrongBalanceException();
|
||||
}
|
||||
|
||||
// Attempting to send less than the dust limit
|
||||
if (_isBelowDust(amount)) {
|
||||
throw BitcoinTransactionNoDustException();
|
||||
}
|
||||
|
||||
if (credentialsAmount > 0) {
|
||||
final amountLeftForFee = amount - credentialsAmount;
|
||||
if (amountLeftForFee > 0 && _isBelowDust(amountLeftForFee)) {
|
||||
amount -= amountLeftForFee;
|
||||
fee += amountLeftForFee;
|
||||
}
|
||||
}
|
||||
|
||||
if (outputs.length == 1) {
|
||||
outputs[0] = BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount));
|
||||
}
|
||||
|
@ -810,6 +795,11 @@ abstract class ElectrumWalletBase
|
|||
bool hasSilentPayment = false,
|
||||
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
|
||||
}) async {
|
||||
// Attempting to send less than the dust limit
|
||||
if (_isBelowDust(credentialsAmount)) {
|
||||
throw BitcoinTransactionNoDustException();
|
||||
}
|
||||
|
||||
final utxoDetails = _createUTXOS(
|
||||
sendAll: false,
|
||||
credentialsAmount: credentialsAmount,
|
||||
|
@ -894,7 +884,43 @@ abstract class ElectrumWalletBase
|
|||
final lastOutput = updatedOutputs.last;
|
||||
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.
|
||||
updatedOutputs[updatedOutputs.length - 1] = BitcoinOutput(
|
||||
address: lastOutput.address,
|
||||
|
@ -908,88 +934,20 @@ abstract class ElectrumWalletBase
|
|||
isSilentPayment: lastOutput.isSilentPayment,
|
||||
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,
|
||||
return EstimatedTxResult(
|
||||
utxos: utxoDetails.utxos,
|
||||
inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
|
||||
publicKeys: utxoDetails.publicKeys,
|
||||
fee: fee,
|
||||
amount: amount,
|
||||
hasChange: true,
|
||||
isSendAll: spendingAllCoins,
|
||||
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),
|
||||
spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
|
||||
spendsSilentPayment: utxoDetails.spendsSilentPayment,
|
||||
);
|
||||
}
|
||||
|
||||
// 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(
|
||||
utxos: utxoDetails.utxos,
|
||||
inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
|
||||
publicKeys: utxoDetails.publicKeys,
|
||||
fee: fee,
|
||||
amount: amount,
|
||||
hasChange: true,
|
||||
isSendAll: false,
|
||||
memo: memo,
|
||||
spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
|
||||
spendsSilentPayment: utxoDetails.spendsSilentPayment,
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> calcFee({
|
||||
|
@ -1080,15 +1038,20 @@ abstract class ElectrumWalletBase
|
|||
: feeRate(transactionCredentials.priority!);
|
||||
|
||||
EstimatedTxResult estimatedTx;
|
||||
final updatedOutputs =
|
||||
outputs.map((e) => BitcoinOutput(address: e.address, value: e.value)).toList();
|
||||
final updatedOutputs = outputs
|
||||
.map((e) => BitcoinOutput(
|
||||
address: e.address,
|
||||
value: e.value,
|
||||
isSilentPayment: e.isSilentPayment,
|
||||
isChange: e.isChange,
|
||||
))
|
||||
.toList();
|
||||
|
||||
if (sendAll) {
|
||||
estimatedTx = await estimateSendAllTx(
|
||||
updatedOutputs,
|
||||
feeRateInt,
|
||||
memo: memo,
|
||||
credentialsAmount: credentialsAmount,
|
||||
hasSilentPayment: hasSilentPayment,
|
||||
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
||||
);
|
||||
|
|
|
@ -153,4 +153,9 @@ class PendingBitcoinTransaction with PendingTransaction {
|
|||
inputAddresses: _tx.inputs.map((input) => input.txId).toList(),
|
||||
outputAddresses: outputAddresses,
|
||||
fee: fee);
|
||||
|
||||
@override
|
||||
Future<String?> commitUR() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,4 +85,8 @@ class PendingBitcoinCashTransaction with PendingTransaction {
|
|||
fee: fee,
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
const MoneroWalletKeys(
|
||||
{required this.privateSpendKey,
|
||||
{required this.primaryAddress,
|
||||
required this.privateSpendKey,
|
||||
required this.privateViewKey,
|
||||
required this.publicSpendKey,
|
||||
required this.publicViewKey});
|
||||
|
||||
final String primaryAddress;
|
||||
final String publicViewKey;
|
||||
final String privateViewKey;
|
||||
final String publicSpendKey;
|
||||
|
|
|
@ -14,5 +14,8 @@ mixin PendingTransaction {
|
|||
int? get outputCount => null;
|
||||
PendingChange? change;
|
||||
|
||||
bool shouldCommitUR() => false;
|
||||
|
||||
Future<void> commit();
|
||||
Future<String?> commitUR();
|
||||
}
|
||||
|
|
|
@ -47,4 +47,9 @@ class PendingEVMChainTransaction with PendingTransaction {
|
|||
|
||||
return '0x${Hex.HEX.encode(txid)}';
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> commitUR() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,9 @@ class WalletRestoreFromKeysException implements Exception {
|
|||
WalletRestoreFromKeysException({required this.message});
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return message;
|
||||
}
|
||||
}
|
|
@ -73,6 +73,7 @@ abstract class HavenWalletBase
|
|||
|
||||
@override
|
||||
MoneroWalletKeys get keys => MoneroWalletKeys(
|
||||
primaryAddress: haven_wallet.getAddress(accountIndex: 0, addressIndex: 0),
|
||||
privateSpendKey: haven_wallet.getSecretSpendKey(),
|
||||
privateViewKey: haven_wallet.getSecretViewKey(),
|
||||
publicSpendKey: haven_wallet.getPublicSpendKey(),
|
||||
|
|
|
@ -48,4 +48,9 @@ class PendingHavenTransaction with PendingTransaction {
|
|||
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;
|
||||
|
||||
monero.wallet? wptr = null;
|
||||
bool get isViewOnly => int.tryParse(monero.Wallet_secretSpendKey(wptr!)) == 0;
|
||||
|
||||
int _wlptrForW = 0;
|
||||
monero.WalletListener? _wlptr = null;
|
||||
|
|
|
@ -13,7 +13,13 @@ import 'package:mutex/mutex.dart';
|
|||
|
||||
|
||||
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();
|
||||
monero.TransactionHistory? txhistory;
|
||||
|
@ -178,12 +184,13 @@ PendingTransactionDescription createTransactionMultDestSync(
|
|||
);
|
||||
}
|
||||
|
||||
void commitTransactionFromPointerAddress({required int address}) =>
|
||||
commitTransaction(transactionPointer: monero.PendingTransaction.fromAddress(address));
|
||||
String? commitTransactionFromPointerAddress({required int address, required bool useUR}) =>
|
||||
commitTransaction(transactionPointer: monero.PendingTransaction.fromAddress(address), useUR: useUR);
|
||||
|
||||
void commitTransaction({required monero.PendingTransaction transactionPointer}) {
|
||||
|
||||
final txCommit = monero.PendingTransaction_commit(transactionPointer, filename: '', overwrite: false);
|
||||
String? commitTransaction({required monero.PendingTransaction transactionPointer, required bool useUR}) {
|
||||
final txCommit = useUR
|
||||
? monero.PendingTransaction_commitUR(transactionPointer, 120)
|
||||
: monero.PendingTransaction_commit(transactionPointer, filename: '', overwrite: false);
|
||||
|
||||
final String? error = (() {
|
||||
final status = monero.PendingTransaction_status(transactionPointer.cast());
|
||||
|
@ -196,6 +203,11 @@ void commitTransaction({required monero.PendingTransaction transactionPointer})
|
|||
if (error != null) {
|
||||
throw CreationTransactionException(message: error);
|
||||
}
|
||||
if (useUR) {
|
||||
return txCommit as String?;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<PendingTransactionDescription> _createTransactionSync(Map args) async {
|
||||
|
|
|
@ -425,3 +425,5 @@ Future<void> restoreFromSpendKey(
|
|||
});
|
||||
|
||||
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/wallet_base.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/monero_output.dart';
|
||||
import 'package:cw_monero/api/structs/pending_transaction.dart';
|
||||
|
@ -121,6 +122,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
|
|||
|
||||
@override
|
||||
MoneroWalletKeys get keys => MoneroWalletKeys(
|
||||
primaryAddress: monero_wallet.getAddress(accountIndex: 0, addressIndex: 0),
|
||||
privateSpendKey: monero_wallet.getSecretSpendKey(),
|
||||
privateViewKey: monero_wallet.getSecretViewKey(),
|
||||
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
|
||||
Future<PendingTransaction> createTransaction(Object credentials) async {
|
||||
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/transaction_history.dart'
|
||||
as monero_transaction_history;
|
||||
|
@ -35,11 +36,32 @@ class PendingMoneroTransaction with PendingTransaction {
|
|||
String get feeFormatted => AmountConverter.amountIntToString(
|
||||
CryptoCurrency.xmr, pendingTransactionDescription.fee);
|
||||
|
||||
bool shouldCommitUR() => isViewOnly;
|
||||
|
||||
@override
|
||||
Future<void> commit() async {
|
||||
try {
|
||||
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) {
|
||||
final message = e.toString();
|
||||
|
||||
|
|
|
@ -37,4 +37,9 @@ class PendingNanoTransaction with PendingTransaction {
|
|||
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
|
||||
String get id => '';
|
||||
|
||||
@override
|
||||
Future<String?> commitUR() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,4 +30,9 @@ class PendingTronTransaction with PendingTransaction {
|
|||
|
||||
@override
|
||||
String get id => '';
|
||||
|
||||
@override
|
||||
Future<String?> commitUR() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,5 +3,6 @@ class WalletRestoreFromKeysException implements Exception {
|
|||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
|
@ -50,4 +50,9 @@ class PendingWowneroTransaction with PendingTransaction {
|
|||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> commitUR() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -120,6 +120,7 @@ abstract class WowneroWalletBase
|
|||
|
||||
@override
|
||||
MoneroWalletKeys get keys => MoneroWalletKeys(
|
||||
primaryAddress: wownero_wallet.getAddress(accountIndex: 0, addressIndex: 0),
|
||||
privateSpendKey: wownero_wallet.getSecretSpendKey(),
|
||||
privateViewKey: wownero_wallet.getSecretViewKey(),
|
||||
publicSpendKey: wownero_wallet.getPublicSpendKey(),
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
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/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:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
@ -23,14 +27,38 @@ abstract class BuyProvider {
|
|||
|
||||
String get darkIcon;
|
||||
|
||||
bool get isAggregator;
|
||||
|
||||
@override
|
||||
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<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:developer';
|
||||
|
||||
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/routes.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/utils/device_info.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.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_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -15,10 +19,12 @@ import 'package:http/http.dart' as http;
|
|||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
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);
|
||||
|
||||
static const _baseUrl = 'api.dfx.swiss';
|
||||
|
||||
// static const _signMessagePath = '/v1/auth/signMessage';
|
||||
static const _authPath = '/v1/auth';
|
||||
static const walletName = 'CakeWallet';
|
||||
|
@ -35,24 +41,8 @@ class DFXBuyProvider extends BuyProvider {
|
|||
@override
|
||||
String get darkIcon => 'assets/images/dfx_dark.png';
|
||||
|
||||
String get assetOut {
|
||||
switch (wallet.type) {
|
||||
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}");
|
||||
}
|
||||
}
|
||||
@override
|
||||
bool get isAggregator => false;
|
||||
|
||||
String get blockchain {
|
||||
switch (wallet.type) {
|
||||
|
@ -60,21 +50,13 @@ class DFXBuyProvider extends BuyProvider {
|
|||
case WalletType.bitcoinCash:
|
||||
case WalletType.litecoin:
|
||||
return 'Bitcoin';
|
||||
case WalletType.monero:
|
||||
return 'Monero';
|
||||
case WalletType.ethereum:
|
||||
return 'Ethereum';
|
||||
case WalletType.polygon:
|
||||
return 'Polygon';
|
||||
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";
|
||||
|
||||
// // 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 {
|
||||
final signMessage = await getSignature(await getSignMessage());
|
||||
Future<String> auth(String walletAddress) async {
|
||||
final signMessage = await getSignature(
|
||||
await getSignMessage(walletAddress), walletAddress);
|
||||
|
||||
final requestBody = jsonEncode({
|
||||
'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) {
|
||||
case WalletType.ethereum:
|
||||
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
|
||||
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 (!ledgerVM!.isConnected) {
|
||||
await Navigator.of(context).pushNamed(Routes.connectDevices,
|
||||
|
@ -152,26 +305,21 @@ class DFXBuyProvider extends BuyProvider {
|
|||
}
|
||||
|
||||
try {
|
||||
final assetOut = this.assetOut;
|
||||
final blockchain = this.blockchain;
|
||||
final actionType = isBuyAction == true ? '/buy' : '/sell';
|
||||
final actionType = isBuyAction ? '/buy' : '/sell';
|
||||
|
||||
final accessToken = await auth();
|
||||
final accessToken = await auth(cryptoCurrencyAddress);
|
||||
|
||||
final uri = Uri.https('services.dfx.swiss', actionType, {
|
||||
'session': accessToken,
|
||||
'lang': 'en',
|
||||
'asset-out': assetOut,
|
||||
'asset-out': isBuyAction ? quote.cryptoCurrency.toString() : quote.fiatCurrency.toString(),
|
||||
'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 (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 {
|
||||
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:developer';
|
||||
|
||||
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_provider.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/payment_method.dart';
|
||||
import 'package:cake_wallet/entities/fiat_currency.dart';
|
||||
import 'package:cake_wallet/exchange/trade_state.dart';
|
||||
import 'package:cake_wallet/generated/i18n.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/store/settings_store.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/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
|
@ -39,6 +40,15 @@ class MoonPayProvider extends BuyProvider {
|
|||
static const _baseBuyProductUrl = 'buy.moonpay.com';
|
||||
static const _cIdBaseUrl = 'exchange-helper.cakewallet.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
|
||||
String get providerDescription =>
|
||||
|
@ -53,6 +63,17 @@ class MoonPayProvider extends BuyProvider {
|
|||
@override
|
||||
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) {
|
||||
switch (theme.type) {
|
||||
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 {
|
||||
final uri = Uri.https(_cIdBaseUrl, "/api/moonpay");
|
||||
|
||||
final response = await post(
|
||||
uri,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': _exchangeHelperApiKey,
|
||||
},
|
||||
body: json.encode({'query': query}),
|
||||
);
|
||||
final response = await post(uri,
|
||||
headers: {'Content-Type': 'application/json', 'x-api-key': _exchangeHelperApiKey},
|
||||
body: json.encode({'query': query}));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return (jsonDecode(response.body) as Map<String, dynamic>)['signature'] as String;
|
||||
|
@ -94,85 +99,195 @@ class MoonPayProvider extends BuyProvider {
|
|||
}
|
||||
}
|
||||
|
||||
Future<Uri> requestSellMoonPayUrl({
|
||||
required CryptoCurrency currency,
|
||||
required String refundWalletAddress,
|
||||
required SettingsStore settingsStore,
|
||||
}) 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.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}',
|
||||
'defaultCurrencyCode': _normalizeCurrency(currency),
|
||||
'refundWalletAddress': refundWalletAddress,
|
||||
};
|
||||
Future<Map<String, dynamic>> fetchFiatCredentials(
|
||||
String fiatCurrency, String cryptocurrency, String? paymentMethod) async {
|
||||
final params = {'baseCurrencyCode': fiatCurrency.toLowerCase(), 'apiKey': _apiKey};
|
||||
|
||||
if (_apiKey.isNotEmpty) {
|
||||
params['apiKey'] = _apiKey;
|
||||
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 {};
|
||||
}
|
||||
|
||||
final originalUri = Uri.https(
|
||||
baseSellUrl,
|
||||
'',
|
||||
params,
|
||||
);
|
||||
|
||||
if (isTestEnvironment) {
|
||||
return originalUri;
|
||||
}
|
||||
|
||||
final signature = await getMoonpaySignature('?${originalUri.query}');
|
||||
|
||||
final query = Map<String, dynamic>.from(originalUri.queryParameters);
|
||||
query['signature'] = signature;
|
||||
final signedUri = originalUri.replace(queryParameters: query);
|
||||
return signedUri;
|
||||
}
|
||||
|
||||
// BUY:
|
||||
static const _currenciesSuffix = '/v3/currencies';
|
||||
static const _quoteSuffix = '/buy_quote';
|
||||
static const _transactionsSuffix = '/v1/transactions';
|
||||
static const _ipAddressSuffix = '/v4/ip_address';
|
||||
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();
|
||||
|
||||
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
|
||||
'baseCurrencyCode': baseCurrencyCode,
|
||||
'baseCurrencyAmount': amount.toString(),
|
||||
'amount': amount.toString(),
|
||||
'paymentMethod': paymentMethod,
|
||||
'areFeesIncluded': 'false',
|
||||
'apiKey': _apiKey
|
||||
};
|
||||
|
||||
log('MoonPay: Fetching $action quote: ${isBuyAction ? formattedCryptoCurrency : fiatCurrency.name.toLowerCase()} -> ${isBuyAction ? baseCurrencyCode : formattedCryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod');
|
||||
|
||||
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 paymentMethods = data['paymentMethod'] as String?;
|
||||
final quote =
|
||||
Quote.fromMoonPayJson(data, isBuyAction, _getPaymentTypeByString(paymentMethods));
|
||||
|
||||
quote.setFiatCurrency = fiatCurrency;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void>? launchProvider(
|
||||
{required BuildContext context,
|
||||
required Quote quote,
|
||||
required double amount,
|
||||
required bool isBuyAction,
|
||||
required String cryptoCurrencyAddress,
|
||||
String? countryCode}) async {
|
||||
|
||||
final Map<String, String> params = {
|
||||
'theme': themeToMoonPayTheme(_settingsStore.currentTheme),
|
||||
'language': _settingsStore.languageCode,
|
||||
'colorCode': _settingsStore.currentTheme.type == ThemeType.dark
|
||||
? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}'
|
||||
: '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}',
|
||||
'baseCurrencyCode': settingsStore.fiatCurrency.title,
|
||||
'baseCurrencyAmount': amount ?? '0',
|
||||
'currencyCode': _normalizeCurrency(currency),
|
||||
'walletAddress': walletAddress,
|
||||
'baseCurrencyCode': isBuyAction ? quote.fiatCurrency.name : quote.cryptoCurrency.name,
|
||||
'baseCurrencyAmount': amount.toString(),
|
||||
'walletAddress': cryptoCurrencyAddress,
|
||||
'lockAmount': 'false',
|
||||
'showAllCurrencies': 'false',
|
||||
'showWalletAddressForm': 'false',
|
||||
'enabledPaymentMethods':
|
||||
'credit_debit_card,apple_pay,google_pay,samsung_pay,sepa_bank_transfer,gbp_bank_transfer,gbp_open_banking_payment',
|
||||
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',
|
||||
if (!isBuyAction) 'refundWalletAddress': cryptoCurrencyAddress
|
||||
};
|
||||
|
||||
if (_apiKey.isNotEmpty) {
|
||||
params['apiKey'] = _apiKey;
|
||||
}
|
||||
if (isBuyAction) params['currencyCode'] = quote.cryptoCurrency.name;
|
||||
if (!isBuyAction) params['quoteCurrencyCode'] = quote.cryptoCurrency.name;
|
||||
|
||||
final originalUri = Uri.https(
|
||||
baseBuyUrl,
|
||||
'',
|
||||
params,
|
||||
);
|
||||
try {
|
||||
{
|
||||
final uri = await requestMoonPayUrl(
|
||||
walletAddress: cryptoCurrencyAddress,
|
||||
settingsStore: _settingsStore,
|
||||
isBuyAction: isBuyAction,
|
||||
amount: amount.toString(),
|
||||
params: params);
|
||||
|
||||
if (isTestEnvironment) {
|
||||
return originalUri;
|
||||
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(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 query = Map<String, dynamic>.from(originalUri.queryParameters);
|
||||
|
@ -181,33 +296,6 @@ class MoonPayProvider extends BuyProvider {
|
|||
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 {
|
||||
final url = _apiUrl + _transactionsSuffix + '/$id' + '?apiKey=' + _apiKey;
|
||||
final uri = Uri.parse(url);
|
||||
|
@ -235,74 +323,83 @@ class MoonPayProvider extends BuyProvider {
|
|||
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) {
|
||||
if (currency == CryptoCurrency.maticpoly) {
|
||||
return "POL_POLYGON";
|
||||
} else if (currency == CryptoCurrency.matic) {
|
||||
return "POL";
|
||||
if (currency.tag == 'POLY') {
|
||||
return '${currency.title.toLowerCase()}_polygon';
|
||||
}
|
||||
|
||||
if (currency.tag == 'TRX') {
|
||||
return '${currency.title.toLowerCase()}_trx';
|
||||
}
|
||||
|
||||
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/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/routes.dart';
|
||||
import 'package:cake_wallet/store/settings_store.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/currency.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class OnRamperBuyProvider extends BuyProvider {
|
||||
|
@ -16,9 +22,15 @@ class OnRamperBuyProvider extends BuyProvider {
|
|||
: super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: null);
|
||||
|
||||
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;
|
||||
|
||||
String get _apiKey => secrets.onramperApiKey;
|
||||
|
||||
@override
|
||||
String get title => 'Onramper';
|
||||
|
||||
|
@ -31,74 +43,327 @@ class OnRamperBuyProvider extends BuyProvider {
|
|||
@override
|
||||
String get darkIcon => 'assets/images/onramper_dark.png';
|
||||
|
||||
String get _apiKey => secrets.onramperApiKey;
|
||||
@override
|
||||
bool get isAggregator => true;
|
||||
|
||||
String get _normalizeCryptoCurrency {
|
||||
switch (wallet.currency) {
|
||||
case CryptoCurrency.ltc:
|
||||
return "LTC_LITECOIN";
|
||||
case CryptoCurrency.xmr:
|
||||
return "XMR_MONERO";
|
||||
case CryptoCurrency.bch:
|
||||
return "BCH_BITCOINCASH";
|
||||
case CryptoCurrency.nano:
|
||||
return "XNO_NANO";
|
||||
default:
|
||||
return wallet.currency.title;
|
||||
Future<List<PaymentMethod>> getAvailablePaymentTypes(
|
||||
String fiatCurrency, String cryptoCurrency, bool isBuyAction) async {
|
||||
final params = {
|
||||
'fiatCurrency': fiatCurrency,
|
||||
'type': isBuyAction ? 'buy' : 'sell',
|
||||
'isRecurringPayment': 'false'
|
||||
};
|
||||
|
||||
final url = Uri.https(_baseApiUrl, '$supported$paymentTypes/$fiatCurrency', params);
|
||||
|
||||
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> 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) {
|
||||
return color.value.toRadixString(16).replaceAll(RegExp(r'^ff'), "");
|
||||
Future<Map<String, dynamic>> getOnrampMetadata() async {
|
||||
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) {
|
||||
String primaryColor,
|
||||
secondaryColor,
|
||||
primaryTextColor,
|
||||
secondaryTextColor,
|
||||
containerColor,
|
||||
cardColor;
|
||||
@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;
|
||||
|
||||
primaryColor = getColorStr(Theme.of(context).primaryColor);
|
||||
secondaryColor = getColorStr(Theme.of(context).colorScheme.background);
|
||||
primaryTextColor =
|
||||
getColorStr(Theme.of(context).extension<CakeTextTheme>()!.titleColor);
|
||||
secondaryTextColor = getColorStr(
|
||||
Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor);
|
||||
containerColor = getColorStr(Theme.of(context).colorScheme.background);
|
||||
cardColor = getColorStr(Theme.of(context).cardColor);
|
||||
if (paymentType != null && paymentType != PaymentType.all) {
|
||||
paymentMethod = normalizePaymentMethod(paymentType);
|
||||
if (paymentMethod == null) paymentMethod = paymentType.name;
|
||||
}
|
||||
|
||||
final actionType = isBuyAction ? 'buy' : 'sell';
|
||||
|
||||
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) {
|
||||
cardColor = getColorStr(Colors.white);
|
||||
}
|
||||
|
||||
final networkName =
|
||||
wallet.currency.fullName?.toUpperCase().replaceAll(" ", "");
|
||||
final defaultCrypto = _getNormalizeCryptoCurrency(quote.cryptoCurrency);
|
||||
|
||||
return Uri.https(_baseUrl, '', <String, dynamic>{
|
||||
final paymentMethod = normalizePaymentMethod(quote.paymentType);
|
||||
|
||||
final uri = Uri.https(_baseUrl, '', {
|
||||
'apiKey': _apiKey,
|
||||
'defaultCrypto': _normalizeCryptoCurrency,
|
||||
'sell_defaultCrypto': _normalizeCryptoCurrency,
|
||||
'networkWallets': '${networkName}:${wallet.walletAddresses.address}',
|
||||
'mode': actionType,
|
||||
'${prefix}defaultFiat': quote.fiatCurrency.name,
|
||||
'${prefix}defaultCrypto': defaultCrypto,
|
||||
'${prefix}defaultAmount': amount.toString(),
|
||||
if (paymentMethod != null) '${prefix}defaultPaymentMethod': paymentMethod,
|
||||
'onlyOnramps': quote.rampId,
|
||||
'networkWallets': '$defaultCrypto:$cryptoCurrencyAddress',
|
||||
'walletAddress': cryptoCurrencyAddress,
|
||||
'supportSwap': "false",
|
||||
'primaryColor': primaryColor,
|
||||
'secondaryColor': secondaryColor,
|
||||
'containerColor': containerColor,
|
||||
'primaryTextColor': primaryTextColor,
|
||||
'secondaryTextColor': secondaryTextColor,
|
||||
'containerColor': containerColor,
|
||||
'cardColor': cardColor,
|
||||
'mode': isBuyAction == true ? 'buy' : 'sell',
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
|
||||
final uri = requestOnramperUrl(context, isBuyAction);
|
||||
if (DeviceInfo.instance.isMobile) {
|
||||
Navigator.of(context)
|
||||
.pushNamed(Routes.webViewPage, arguments: [title, uri]);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
} else {
|
||||
await launchUrl(uri);
|
||||
throw Exception('Could not launch URL');
|
||||
}
|
||||
}
|
||||
|
||||
List<CryptoCurrency> mainCurrency = [
|
||||
CryptoCurrency.btc,
|
||||
CryptoCurrency.eth,
|
||||
CryptoCurrency.sol,
|
||||
];
|
||||
|
||||
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:developer';
|
||||
|
||||
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/generated/i18n.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.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_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -15,7 +20,8 @@ import 'package:http/http.dart' as http;
|
|||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
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);
|
||||
|
||||
static const _baseUrl = 'applink.robinhood.com';
|
||||
|
@ -33,6 +39,9 @@ class RobinhoodBuyProvider extends BuyProvider {
|
|||
@override
|
||||
String get darkIcon => 'assets/images/robinhood_dark.png';
|
||||
|
||||
@override
|
||||
bool get isAggregator => false;
|
||||
|
||||
String get _applicationId => secrets.robinhoodApplicationId;
|
||||
|
||||
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 (!ledgerVM!.isConnected) {
|
||||
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
|
||||
String get darkIcon => 'assets/images/robinhood_dark.png';
|
||||
|
||||
@override
|
||||
bool get isAggregator => false;
|
||||
|
||||
String get trackUrl => isTestEnvironment ? _trackTestUrl : _trackProductUrl;
|
||||
|
||||
String baseApiUrl;
|
||||
|
@ -148,10 +151,4 @@ class WyreBuyProvider extends BuyProvider {
|
|||
receiveAddress: wallet.walletAddresses.address,
|
||||
walletId: wallet.id);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> launchProvider(BuildContext context, bool? isBuyAction) {
|
||||
// TODO: implement launchProvider
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
class CakePayOrder {
|
||||
final String orderId;
|
||||
final List<OrderCard> cards;
|
||||
|
|
|
@ -82,10 +82,12 @@ class CakePayService {
|
|||
}
|
||||
|
||||
/// Logout
|
||||
Future<void> logout(String email) async {
|
||||
Future<void> logout([String? email]) async {
|
||||
await secureStorage.delete(key: cakePayUsernameStorageKey);
|
||||
await secureStorage.delete(key: cakePayUserTokenKey);
|
||||
await cakePayApi.logoutUser(email: email, apiKey: cakePayApiKey);
|
||||
if (email != null) {
|
||||
await cakePayApi.logoutUser(email: email, apiKey: cakePayApiKey);
|
||||
}
|
||||
}
|
||||
|
||||
/// Purchase Gift Card
|
||||
|
|
|
@ -268,9 +268,7 @@ class BackupService {
|
|||
final currentFiatCurrency = data[PreferencesKey.currentFiatCurrencyKey] as String?;
|
||||
final shouldSaveRecipientAddress = data[PreferencesKey.shouldSaveRecipientAddressKey] as bool?;
|
||||
final isAppSecure = data[PreferencesKey.isAppSecureKey] as bool?;
|
||||
final disableBuy = data[PreferencesKey.disableBuyKey] as bool?;
|
||||
final disableSell = data[PreferencesKey.disableSellKey] as bool?;
|
||||
final defaultBuyProvider = data[PreferencesKey.defaultBuyProvider] as int?;
|
||||
final disableTradeOption = data[PreferencesKey.disableTradeOption] as bool?;
|
||||
final currentTransactionPriorityKeyLegacy =
|
||||
data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?;
|
||||
final currentBitcoinElectrumSererId =
|
||||
|
@ -323,14 +321,8 @@ class BackupService {
|
|||
if (isAppSecure != null)
|
||||
await _sharedPreferences.setBool(PreferencesKey.isAppSecureKey, isAppSecure);
|
||||
|
||||
if (disableBuy != null)
|
||||
await _sharedPreferences.setBool(PreferencesKey.disableBuyKey, disableBuy);
|
||||
|
||||
if (disableSell != null)
|
||||
await _sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell);
|
||||
|
||||
if (defaultBuyProvider != null)
|
||||
await _sharedPreferences.setInt(PreferencesKey.defaultBuyProvider, defaultBuyProvider);
|
||||
if (disableTradeOption != null)
|
||||
await _sharedPreferences.setBool(PreferencesKey.disableTradeOption, disableTradeOption);
|
||||
|
||||
if (currentTransactionPriorityKeyLegacy != null)
|
||||
await _sharedPreferences.setInt(
|
||||
|
@ -516,10 +508,7 @@ class BackupService {
|
|||
_sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey),
|
||||
PreferencesKey.shouldSaveRecipientAddressKey:
|
||||
_sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey),
|
||||
PreferencesKey.disableBuyKey: _sharedPreferences.getBool(PreferencesKey.disableBuyKey),
|
||||
PreferencesKey.disableSellKey: _sharedPreferences.getBool(PreferencesKey.disableSellKey),
|
||||
PreferencesKey.defaultBuyProvider:
|
||||
_sharedPreferences.getInt(PreferencesKey.defaultBuyProvider),
|
||||
PreferencesKey.disableTradeOption: _sharedPreferences.getBool(PreferencesKey.disableTradeOption),
|
||||
PreferencesKey.currentPinLength: _sharedPreferences.getInt(PreferencesKey.currentPinLength),
|
||||
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/new_wallet_type_arguments.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/wallet_connect/wallet_connect_key_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/wallet_edit_page_arguments.dart';
|
||||
import 'package:cake_wallet/entities/wallet_manager.dart';
|
||||
import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart';
|
||||
import 'package:cake_wallet/src/screens/buy/payment_method_options_page.dart';
|
||||
import 'package:cake_wallet/src/screens/receive/address_list_page.dart';
|
||||
import 'package:cake_wallet/src/screens/settings/mweb_logs_page.dart';
|
||||
import 'package:cake_wallet/src/screens/settings/mweb_node_page.dart';
|
||||
|
@ -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/backup/backup_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/webview_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_chat/support_chat_page.dart';
|
||||
import 'package:cake_wallet/src/screens/support_other_links/support_other_links_page.dart';
|
||||
import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart';
|
||||
|
@ -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/utils/payment_request.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/anon_invoice_page_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:mobx/mobx.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';
|
||||
|
||||
final getIt = GetIt.instance;
|
||||
|
@ -904,6 +911,11 @@ Future<void> setup({
|
|||
getIt.registerFactory(() => WalletKeysViewModel(getIt.get<AppStore>()));
|
||||
|
||||
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>(
|
||||
(ContactRecord? contact, _) => ContactViewModel(_contactSource, contact: contact));
|
||||
|
@ -998,6 +1010,10 @@ Future<void> setup({
|
|||
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.registerFactory<PayfuraBuyProvider>(() => PayfuraBuyProvider(
|
||||
|
@ -1189,8 +1205,25 @@ Future<void> setup({
|
|||
|
||||
getIt.registerFactory(() => BuyAmountViewModel());
|
||||
|
||||
getIt.registerFactoryParam<BuySellOptionsPage, bool, void>(
|
||||
(isBuyOption, _) => BuySellOptionsPage(getIt.get<DashboardViewModel>(), isBuyOption));
|
||||
getIt.registerFactory(() => BuySellViewModel(getIt.get<AppStore>()));
|
||||
|
||||
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(() {
|
||||
final wallet = getIt.get<AppStore>().wallet;
|
||||
|
|
|
@ -261,9 +261,13 @@ Future<void> defaultSettingsMigration(
|
|||
updateBtcElectrumNodeToUseSSL(nodes, sharedPreferences);
|
||||
break;
|
||||
case 43:
|
||||
_updateCakeXmrNode(nodes);
|
||||
break;
|
||||
case 44:
|
||||
await addZanoNodeList(nodes: nodes);
|
||||
await changeZanoCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -278,6 +282,15 @@ Future<void> defaultSettingsMigration(
|
|||
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) {
|
||||
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);
|
||||
|
||||
|
|
|
@ -23,31 +23,18 @@ class MainActions {
|
|||
});
|
||||
|
||||
static List<MainActions> all = [
|
||||
buyAction,
|
||||
showWalletsAction,
|
||||
receiveAction,
|
||||
exchangeAction,
|
||||
sendAction,
|
||||
sellAction,
|
||||
tradeAction,
|
||||
];
|
||||
|
||||
static MainActions buyAction = MainActions._(
|
||||
name: (context) => S.of(context).buy,
|
||||
image: 'assets/images/buy.png',
|
||||
isEnabled: (viewModel) => viewModel.isEnabledBuyAction,
|
||||
canShow: (viewModel) => viewModel.hasBuyAction,
|
||||
static MainActions showWalletsAction = MainActions._(
|
||||
name: (context) => S.of(context).wallets,
|
||||
image: 'assets/images/wallet_new.png',
|
||||
onTap: (BuildContext context, DashboardViewModel viewModel) async {
|
||||
if (!viewModel.isEnabledBuyAction) {
|
||||
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());
|
||||
}
|
||||
Navigator.pushNamed(context, Routes.walletList);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -79,39 +66,15 @@ class MainActions {
|
|||
},
|
||||
);
|
||||
|
||||
static MainActions sellAction = MainActions._(
|
||||
name: (context) => S.of(context).sell,
|
||||
image: 'assets/images/sell.png',
|
||||
isEnabled: (viewModel) => viewModel.isEnabledSellAction,
|
||||
canShow: (viewModel) => viewModel.hasSellAction,
|
||||
onTap: (BuildContext context, DashboardViewModel viewModel) async {
|
||||
if (!viewModel.isEnabledSellAction) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 MainActions tradeAction = MainActions._(
|
||||
name: (context) => '${S.of(context).buy} / ${S.of(context).sell}',
|
||||
image: 'assets/images/buy_sell.png',
|
||||
isEnabled: (viewModel) => viewModel.isEnabledTradeAction,
|
||||
canShow: (viewModel) => viewModel.hasTradeAction,
|
||||
onTap: (BuildContext context, DashboardViewModel viewModel) async {
|
||||
if (!viewModel.isEnabledTradeAction) return;
|
||||
await Navigator.of(context).pushNamed(Routes.buySellPage, arguments: false);
|
||||
},
|
||||
);
|
||||
|
||||
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 shouldSaveRecipientAddressKey = 'save_recipient_address';
|
||||
static const isAppSecureKey = 'is_app_secure';
|
||||
static const disableBuyKey = 'disable_buy';
|
||||
static const disableSellKey = 'disable_sell';
|
||||
static const disableTradeOption = 'disable_buy';
|
||||
static const disableBulletinKey = 'disable_bulletin';
|
||||
static const defaultBuyProvider = 'default_buy_provider';
|
||||
static const walletListOrder = 'wallet_list_order';
|
||||
static const contactListOrder = 'contact_list_order';
|
||||
static const walletListAscending = 'wallet_list_ascending';
|
||||
|
|
|
@ -1,24 +1,18 @@
|
|||
import 'package:cake_wallet/buy/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/onramper/onramper_buy_provider.dart';
|
||||
import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart';
|
||||
import 'package:cake_wallet/di.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
enum ProviderType {
|
||||
askEachTime,
|
||||
robinhood,
|
||||
dfx,
|
||||
onramper,
|
||||
moonpay,
|
||||
}
|
||||
enum ProviderType { robinhood, dfx, onramper, moonpay, meld }
|
||||
|
||||
extension ProviderTypeName on ProviderType {
|
||||
String get title {
|
||||
switch (this) {
|
||||
case ProviderType.askEachTime:
|
||||
return 'Ask each time';
|
||||
case ProviderType.robinhood:
|
||||
return 'Robinhood Connect';
|
||||
case ProviderType.dfx:
|
||||
|
@ -27,13 +21,13 @@ extension ProviderTypeName on ProviderType {
|
|||
return 'Onramper';
|
||||
case ProviderType.moonpay:
|
||||
return 'MoonPay';
|
||||
case ProviderType.meld:
|
||||
return 'Meld';
|
||||
}
|
||||
}
|
||||
|
||||
String get id {
|
||||
switch (this) {
|
||||
case ProviderType.askEachTime:
|
||||
return 'ask_each_time_provider';
|
||||
case ProviderType.robinhood:
|
||||
return 'robinhood_connect_provider';
|
||||
case ProviderType.dfx:
|
||||
|
@ -42,6 +36,8 @@ extension ProviderTypeName on ProviderType {
|
|||
return 'onramper_provider';
|
||||
case ProviderType.moonpay:
|
||||
return 'moonpay_provider';
|
||||
case ProviderType.meld:
|
||||
return 'meld_provider';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -52,14 +48,13 @@ class ProvidersHelper {
|
|||
case WalletType.nano:
|
||||
case WalletType.banano:
|
||||
case WalletType.wownero:
|
||||
return [ProviderType.askEachTime, ProviderType.onramper];
|
||||
return [ProviderType.onramper];
|
||||
case WalletType.monero:
|
||||
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx];
|
||||
return [ProviderType.onramper, ProviderType.dfx];
|
||||
case WalletType.bitcoin:
|
||||
case WalletType.polygon:
|
||||
case WalletType.ethereum:
|
||||
return [
|
||||
ProviderType.askEachTime,
|
||||
ProviderType.onramper,
|
||||
ProviderType.dfx,
|
||||
ProviderType.robinhood,
|
||||
|
@ -68,10 +63,13 @@ class ProvidersHelper {
|
|||
case WalletType.litecoin:
|
||||
case WalletType.bitcoinCash:
|
||||
case WalletType.solana:
|
||||
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood, ProviderType.moonpay];
|
||||
return [
|
||||
ProviderType.onramper,
|
||||
ProviderType.robinhood,
|
||||
ProviderType.moonpay
|
||||
];
|
||||
case WalletType.tron:
|
||||
return [
|
||||
ProviderType.askEachTime,
|
||||
ProviderType.onramper,
|
||||
ProviderType.robinhood,
|
||||
ProviderType.moonpay,
|
||||
|
@ -89,28 +87,24 @@ class ProvidersHelper {
|
|||
case WalletType.ethereum:
|
||||
case WalletType.polygon:
|
||||
return [
|
||||
ProviderType.askEachTime,
|
||||
ProviderType.onramper,
|
||||
ProviderType.moonpay,
|
||||
ProviderType.dfx,
|
||||
];
|
||||
case WalletType.litecoin:
|
||||
case WalletType.bitcoinCash:
|
||||
return [ProviderType.askEachTime, ProviderType.moonpay];
|
||||
return [ProviderType.moonpay];
|
||||
case WalletType.solana:
|
||||
return [
|
||||
ProviderType.askEachTime,
|
||||
ProviderType.onramper,
|
||||
ProviderType.robinhood,
|
||||
ProviderType.moonpay,
|
||||
];
|
||||
case WalletType.tron:
|
||||
return [
|
||||
ProviderType.askEachTime,
|
||||
ProviderType.robinhood,
|
||||
ProviderType.moonpay,
|
||||
];
|
||||
case WalletType.monero:
|
||||
return [ProviderType.dfx];
|
||||
case WalletType.nano:
|
||||
case WalletType.banano:
|
||||
case WalletType.none:
|
||||
|
@ -131,7 +125,9 @@ class ProvidersHelper {
|
|||
return getIt.get<OnRamperBuyProvider>();
|
||||
case ProviderType.moonpay:
|
||||
return getIt.get<MoonPayProvider>();
|
||||
case ProviderType.askEachTime:
|
||||
case ProviderType.meld:
|
||||
return getIt.get<MeldBuyProvider>();
|
||||
default:
|
||||
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;
|
||||
|
||||
Future<String> presentQRScanner() async {
|
||||
Future<String> presentQRScanner(BuildContext context) async {
|
||||
isQrScannerShown = true;
|
||||
try {
|
||||
final result = await BarcodeScanner.scan();
|
||||
final result = await Navigator.of(context).push<String>(
|
||||
MaterialPageRoute(
|
||||
builder:(context) {
|
||||
return BarcodeScannerSimple();
|
||||
},
|
||||
),
|
||||
);
|
||||
isQrScannerShown = false;
|
||||
return result.rawContent.trim();
|
||||
return result??'';
|
||||
} catch (e) {
|
||||
isQrScannerShown = false;
|
||||
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 keys = moneroWallet.keys;
|
||||
return <String, String>{
|
||||
'primaryAddress': keys.primaryAddress,
|
||||
'privateSpendKey': keys.privateSpendKey,
|
||||
'privateViewKey': keys.privateViewKey,
|
||||
'publicSpendKey': keys.publicSpendKey,
|
||||
|
@ -357,9 +358,32 @@ class CWMonero extends Monero {
|
|||
Future<int> getCurrentHeight() async {
|
||||
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
|
||||
void monerocCheck() {
|
||||
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/backup/backup_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/payment_method_options_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/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/unspent_coins/unspent_coins_details_page.dart';
|
||||
import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart';
|
||||
import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.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/material.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';
|
||||
|
||||
late RouteSettings currentRouteSettings;
|
||||
|
@ -570,7 +573,15 @@ Route<dynamic> createRoute(RouteSettings settings) {
|
|||
|
||||
case Routes.buySellPage:
|
||||
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:
|
||||
final args = settings.arguments as List;
|
||||
|
@ -732,6 +743,9 @@ Route<dynamic> createRoute(RouteSettings settings) {
|
|||
case Routes.setup2faInfoPage:
|
||||
return MaterialPageRoute<void>(builder: (_) => getIt.get<Setup2FAInfoPage>());
|
||||
|
||||
case Routes.urqrAnimatedPage:
|
||||
return MaterialPageRoute<void>(builder: (_) => getIt.get<AnimatedURPage>(param1: settings.arguments));
|
||||
|
||||
case Routes.homeSettings:
|
||||
return CupertinoPageRoute<void>(
|
||||
builder: (_) => getIt.get<HomeSettingsPage>(param1: settings.arguments),
|
||||
|
|
|
@ -59,6 +59,8 @@ class Routes {
|
|||
static const supportOtherLinks = '/support/other';
|
||||
static const orderDetails = '/order_details';
|
||||
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 unspentCoinsList = '/unspent_coins_list';
|
||||
static const unspentCoinsDetails = '/unspent_coins_details';
|
||||
|
@ -108,6 +110,7 @@ class Routes {
|
|||
|
||||
static const signPage = '/sign_page';
|
||||
static const connectDevices = '/device/connect';
|
||||
static const urqrAnimatedPage = '/urqr/animated_page';
|
||||
static const walletGroupsDisplayPage = '/wallet_groups_display_page';
|
||||
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) {
|
||||
Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
|
||||
} else {
|
||||
await cakePayPurchaseViewModel.createOrder();
|
||||
try {
|
||||
await cakePayPurchaseViewModel.createOrder();
|
||||
} catch (_) {
|
||||
await cakePayPurchaseViewModel.cakePayService.logout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -343,8 +347,8 @@ class CakePayBuyCardDetailPage extends BasePage {
|
|||
rightButtonText: S.of(popupContext).send,
|
||||
leftButtonText: S.of(popupContext).cancel,
|
||||
actionRightButton: () async {
|
||||
Navigator.of(popupContext).pop();
|
||||
await cakePayPurchaseViewModel.sendViewModel.commitTransaction();
|
||||
Navigator.of(context).pop();
|
||||
await cakePayPurchaseViewModel.sendViewModel.commitTransaction(context);
|
||||
},
|
||||
actionLeftButton: () => Navigator.of(popupContext).pop()));
|
||||
},
|
||||
|
|
|
@ -21,6 +21,14 @@ class DesktopDashboardActions extends StatelessWidget {
|
|||
return Column(
|
||||
children: [
|
||||
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(
|
||||
title: MainActions.exchangeAction.name(context),
|
||||
image: MainActions.exchangeAction.image,
|
||||
|
@ -55,20 +63,11 @@ class DesktopDashboardActions extends StatelessWidget {
|
|||
children: [
|
||||
Expanded(
|
||||
child: DesktopActionButton(
|
||||
title: MainActions.buyAction.name(context),
|
||||
image: MainActions.buyAction.image,
|
||||
canShow: MainActions.buyAction.canShow?.call(dashboardViewModel),
|
||||
isEnabled: MainActions.buyAction.isEnabled?.call(dashboardViewModel),
|
||||
onTap: () async => await MainActions.buyAction.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),
|
||||
title: MainActions.tradeAction.name(context),
|
||||
image: MainActions.tradeAction.image,
|
||||
canShow: MainActions.tradeAction.canShow?.call(dashboardViewModel),
|
||||
isEnabled: MainActions.tradeAction.isEnabled?.call(dashboardViewModel),
|
||||
onTap: () async => await MainActions.tradeAction.onTap(context, dashboardViewModel),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -103,6 +103,9 @@ class MenuWidgetState extends State<MenuWidget> {
|
|||
if (!widget.dashboardViewModel.hasSilentPayments) {
|
||||
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) {
|
||||
items.removeWhere((element) => element.name(context) == S.of(context).litecoin_mweb_settings);
|
||||
}
|
||||
|
@ -191,7 +194,6 @@ class MenuWidgetState extends State<MenuWidget> {
|
|||
index--;
|
||||
|
||||
final item = items[index];
|
||||
|
||||
final isLastTile = index == itemCount - 1;
|
||||
|
||||
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/themes/extensions/send_page_theme.dart';
|
||||
|
||||
class ExchangeCard extends StatefulWidget {
|
||||
class ExchangeCard<T extends Currency> extends StatefulWidget {
|
||||
ExchangeCard({
|
||||
Key? key,
|
||||
required this.initialCurrency,
|
||||
|
@ -40,19 +40,23 @@ class ExchangeCard extends StatefulWidget {
|
|||
this.borderColor = Colors.transparent,
|
||||
this.hasAllAmount = false,
|
||||
this.isAllAmountEnabled = false,
|
||||
this.showAddressField = true,
|
||||
this.showLimitsField = true,
|
||||
this.amountFocusNode,
|
||||
this.addressFocusNode,
|
||||
this.allAmount,
|
||||
this.currencyRowPadding,
|
||||
this.addressRowPadding,
|
||||
this.onPushPasteButton,
|
||||
this.onPushAddressBookButton,
|
||||
this.onDispose,
|
||||
required this.cardInstanceName,
|
||||
}) : super(key: key);
|
||||
|
||||
final List<CryptoCurrency> currencies;
|
||||
final Function(CryptoCurrency) onCurrencySelected;
|
||||
final List<T> currencies;
|
||||
final Function(T) onCurrencySelected;
|
||||
final String title;
|
||||
final CryptoCurrency initialCurrency;
|
||||
final T initialCurrency;
|
||||
final String initialWalletName;
|
||||
final String initialAddress;
|
||||
final bool initialIsAmountEditable;
|
||||
|
@ -70,18 +74,22 @@ class ExchangeCard extends StatefulWidget {
|
|||
final FocusNode? amountFocusNode;
|
||||
final FocusNode? addressFocusNode;
|
||||
final bool hasAllAmount;
|
||||
final bool showAddressField;
|
||||
final bool showLimitsField;
|
||||
final bool isAllAmountEnabled;
|
||||
final VoidCallback? allAmount;
|
||||
final EdgeInsets? currencyRowPadding;
|
||||
final EdgeInsets? addressRowPadding;
|
||||
final void Function(BuildContext context)? onPushPasteButton;
|
||||
final void Function(BuildContext context)? onPushAddressBookButton;
|
||||
final Function()? onDispose;
|
||||
final String cardInstanceName;
|
||||
|
||||
@override
|
||||
ExchangeCardState createState() => ExchangeCardState();
|
||||
ExchangeCardState<T> createState() => ExchangeCardState<T>();
|
||||
}
|
||||
|
||||
class ExchangeCardState extends State<ExchangeCard> {
|
||||
class ExchangeCardState<T extends Currency> extends State<ExchangeCard<T>> {
|
||||
ExchangeCardState()
|
||||
: _title = '',
|
||||
_min = '',
|
||||
|
@ -89,7 +97,6 @@ class ExchangeCardState extends State<ExchangeCard> {
|
|||
_isAmountEditable = false,
|
||||
_isAddressEditable = false,
|
||||
_walletName = '',
|
||||
_selectedCurrency = CryptoCurrency.btc,
|
||||
_isAmountEstimated = false,
|
||||
_isMoneroWallet = false,
|
||||
_cardInstanceName = '';
|
||||
|
@ -101,7 +108,7 @@ class ExchangeCardState extends State<ExchangeCard> {
|
|||
String _title;
|
||||
String? _min;
|
||||
String? _max;
|
||||
CryptoCurrency _selectedCurrency;
|
||||
late T _selectedCurrency;
|
||||
String _walletName;
|
||||
bool _isAmountEditable;
|
||||
bool _isAddressEditable;
|
||||
|
@ -118,7 +125,8 @@ class ExchangeCardState extends State<ExchangeCard> {
|
|||
_selectedCurrency = widget.initialCurrency;
|
||||
_isAmountEstimated = widget.isAmountEstimated;
|
||||
_isMoneroWallet = widget.isMoneroWallet;
|
||||
addressController.text = widget.initialAddress;
|
||||
addressController.text = _normalizeAddressFormat(widget.initialAddress);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -136,7 +144,7 @@ class ExchangeCardState extends State<ExchangeCard> {
|
|||
});
|
||||
}
|
||||
|
||||
void changeSelectedCurrency(CryptoCurrency currency) {
|
||||
void changeSelectedCurrency(T currency) {
|
||||
setState(() => _selectedCurrency = currency);
|
||||
}
|
||||
|
||||
|
@ -157,7 +165,7 @@ class ExchangeCardState extends State<ExchangeCard> {
|
|||
}
|
||||
|
||||
void changeAddress({required String address}) {
|
||||
setState(() => addressController.text = address);
|
||||
setState(() => addressController.text = _normalizeAddressFormat(address));
|
||||
}
|
||||
|
||||
void changeAmount({required String amount}) {
|
||||
|
@ -222,7 +230,7 @@ class ExchangeCardState extends State<ExchangeCard> {
|
|||
Divider(height: 1, color: Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 5),
|
||||
child: Container(
|
||||
child: widget.showLimitsField ? Container(
|
||||
height: 15,
|
||||
child: Row(mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[
|
||||
_min != null
|
||||
|
@ -247,7 +255,7 @@ class ExchangeCardState extends State<ExchangeCard> {
|
|||
),
|
||||
)
|
||||
: Offstage(),
|
||||
])),
|
||||
])) : Offstage(),
|
||||
),
|
||||
!_isAddressEditable && widget.hasRefundAddress
|
||||
? Padding(
|
||||
|
@ -261,10 +269,11 @@ class ExchangeCardState extends State<ExchangeCard> {
|
|||
))
|
||||
: Offstage(),
|
||||
_isAddressEditable
|
||||
? widget.showAddressField
|
||||
? FocusTraversalOrder(
|
||||
order: NumericFocusOrder(2),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 20),
|
||||
padding: widget.addressRowPadding ?? EdgeInsets.only(top: 20),
|
||||
child: AddressTextField(
|
||||
addressKey: ValueKey('${_cardInstanceName}_editable_address_textfield_key'),
|
||||
focusNode: widget.addressFocusNode,
|
||||
|
@ -280,26 +289,29 @@ class ExchangeCardState extends State<ExchangeCard> {
|
|||
widget.amountFocusNode?.requestFocus();
|
||||
amountController.text = paymentRequest.amount;
|
||||
},
|
||||
placeholder: widget.hasRefundAddress ? S.of(context).refund_address : null,
|
||||
placeholder:
|
||||
widget.hasRefundAddress ? S.of(context).refund_address : null,
|
||||
options: [
|
||||
AddressTextFieldOption.paste,
|
||||
AddressTextFieldOption.qrCode,
|
||||
AddressTextFieldOption.addressBook,
|
||||
],
|
||||
isBorderExist: false,
|
||||
textStyle:
|
||||
TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
|
||||
textStyle: TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).extension<ExchangePageTheme>()!.hintTextColor),
|
||||
color:
|
||||
Theme.of(context).extension<ExchangePageTheme>()!.hintTextColor),
|
||||
buttonColor: widget.addressButtonsColor,
|
||||
validator: widget.addressTextFieldValidator,
|
||||
onPushPasteButton: widget.onPushPasteButton,
|
||||
onPushAddressBookButton: widget.onPushAddressBookButton,
|
||||
selectedCurrency: _selectedCurrency),
|
||||
),
|
||||
)
|
||||
)
|
||||
: Offstage()
|
||||
: Padding(
|
||||
padding: EdgeInsets.only(top: 10),
|
||||
child: Builder(
|
||||
|
@ -402,7 +414,7 @@ class ExchangeCardState extends State<ExchangeCard> {
|
|||
hintText: S.of(context).search_currency,
|
||||
isMoneroWallet: _isMoneroWallet,
|
||||
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());
|
||||
});
|
||||
}
|
||||
|
||||
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/send_page_theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MobileExchangeCardsSection extends StatelessWidget {
|
||||
final Widget firstExchangeCard;
|
||||
final Widget secondExchangeCard;
|
||||
final bool isBuySellOption;
|
||||
final VoidCallback? onBuyTap;
|
||||
final VoidCallback? onSellTap;
|
||||
|
||||
const MobileExchangeCardsSection({
|
||||
Key? key,
|
||||
required this.firstExchangeCard,
|
||||
required this.secondExchangeCard,
|
||||
this.isBuySellOption = false,
|
||||
this.onBuyTap,
|
||||
this.onSellTap,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.only(bottom: 32),
|
||||
padding: EdgeInsets.only(bottom: isBuySellOption ? 8 : 32),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(24),
|
||||
|
@ -45,8 +54,18 @@ class MobileExchangeCardsSection extends StatelessWidget {
|
|||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.fromLTRB(24, 100, 24, 32),
|
||||
child: firstExchangeCard,
|
||||
padding: EdgeInsets.fromLTRB(24, 90, 24, isBuySellOption ? 8 : 32),
|
||||
child: Column(
|
||||
children: [
|
||||
if (isBuySellOption) Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
BuySellOptionButtons(onBuyTap: onBuyTap, onSellTap: onSellTap),
|
||||
],
|
||||
),
|
||||
firstExchangeCard,
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
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 {
|
||||
Navigator.of(popupContext).pop();
|
||||
await widget.exchangeTradeViewModel.sendViewModel
|
||||
.commitTransaction();
|
||||
.commitTransaction(context);
|
||||
transactionStatePopup();
|
||||
},
|
||||
actionLeftButton: () => Navigator.of(popupContext).pop(),
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:cake_wallet/generated/i18n.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';
|
||||
|
||||
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'),
|
||||
actionRightButton: () async {
|
||||
Navigator.of(_dialogContext).pop();
|
||||
sendViewModel.commitTransaction();
|
||||
sendViewModel.commitTransaction(context);
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
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/typography.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:cake_wallet/router.dart' as Router;
|
||||
import 'package:cake_wallet/themes/extensions/menu_theme.dart';
|
||||
|
@ -60,8 +61,10 @@ class _DesktopSettingsPageState extends State<DesktopSettingsPage> {
|
|||
return Container();
|
||||
}
|
||||
|
||||
if (!widget.dashboardViewModel.hasMweb &&
|
||||
item.name(context) == S.of(context).litecoin_mweb_settings) {
|
||||
if ((!widget.dashboardViewModel.isMoneroViewOnly &&
|
||||
item.name(context) == S.of(context).export_outputs) ||
|
||||
(!widget.dashboardViewModel.hasMweb &&
|
||||
item.name(context) == S.of(context).litecoin_mweb_settings)) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
|
|
|
@ -57,22 +57,6 @@ class OtherSettingsPage extends BasePage {
|
|||
handler: (BuildContext context) =>
|
||||
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(
|
||||
title: S.current.settings_terms_and_conditions,
|
||||
handler: (BuildContext context) =>
|
||||
|
|
|
@ -73,16 +73,10 @@ class PrivacyPage extends BasePage {
|
|||
_privacySettingsViewModel.setIsAppSecure(value);
|
||||
}),
|
||||
SettingsSwitcherCell(
|
||||
title: S.current.disable_buy,
|
||||
value: _privacySettingsViewModel.disableBuy,
|
||||
title: S.current.disable_trade_option,
|
||||
value: _privacySettingsViewModel.disableTradeOption,
|
||||
onValueChange: (BuildContext _, bool value) {
|
||||
_privacySettingsViewModel.setDisableBuy(value);
|
||||
}),
|
||||
SettingsSwitcherCell(
|
||||
title: S.current.disable_sell,
|
||||
value: _privacySettingsViewModel.disableSell,
|
||||
onValueChange: (BuildContext _, bool value) {
|
||||
_privacySettingsViewModel.setDisableSell(value);
|
||||
_privacySettingsViewModel.setDisableTradeOption(value);
|
||||
}),
|
||||
SettingsSwitcherCell(
|
||||
title: S.current.disable_bulletin,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:cake_wallet/generated/i18n.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';
|
||||
|
||||
class Setup2FAInfoPage extends InfoPage {
|
||||
|
|
|
@ -168,7 +168,7 @@ class RBFDetailsPage extends BasePage {
|
|||
leftButtonText: S.of(popupContext).cancel,
|
||||
actionRightButton: () async {
|
||||
Navigator.of(popupContext).pop();
|
||||
await transactionDetailsViewModel.sendViewModel.commitTransaction();
|
||||
await transactionDetailsViewModel.sendViewModel.commitTransaction(context);
|
||||
try {
|
||||
Navigator.of(popupContext).pop();
|
||||
} 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 =
|
||||
await PermissionHandler.checkPermission(Permission.camera, context);
|
||||
if (!isCameraPermissionGranted) return;
|
||||
uri = await presentQRScanner();
|
||||
uri = await presentQRScanner(context);
|
||||
} else {
|
||||
uri = await _showEnterWalletConnectURIPopUp(context);
|
||||
}
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
import 'package:cake_wallet/utils/device_info.dart';
|
||||
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
|
||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:cw_core/currency.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/entities/qr_scanner.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/utils/permission_handler.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
enum AddressTextFieldOption { paste, qrCode, addressBook, walletAddresses }
|
||||
|
||||
class AddressTextField extends StatelessWidget {
|
||||
|
||||
class AddressTextField<T extends Currency> extends StatelessWidget{
|
||||
AddressTextField({
|
||||
required this.controller,
|
||||
this.isActive = true,
|
||||
|
@ -58,7 +59,7 @@ class AddressTextField extends StatelessWidget {
|
|||
final Function(BuildContext context)? onPushAddressBookButton;
|
||||
final Function(BuildContext context)? onPushAddressPickerButton;
|
||||
final Function(ContactBase contact)? onSelectedContact;
|
||||
final CryptoCurrency? selectedCurrency;
|
||||
final T? selectedCurrency;
|
||||
final Key? addressKey;
|
||||
|
||||
@override
|
||||
|
@ -231,7 +232,7 @@ class AddressTextField extends StatelessWidget {
|
|||
bool isCameraPermissionGranted =
|
||||
await PermissionHandler.checkPermission(Permission.camera, context);
|
||||
if (!isCameraPermissionGranted) return;
|
||||
final code = await presentQRScanner();
|
||||
final code = await presentQRScanner(context);
|
||||
if (code.isEmpty) {
|
||||
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,
|
||||
silentPaymentsSettingAction,
|
||||
litecoinMwebSettingAction,
|
||||
exportOutputsAction,
|
||||
securityBackupSettingAction,
|
||||
privacySettingAction,
|
||||
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._(
|
||||
key: ValueKey('dashboard_page_menu_widget_litecoin_mweb_settings_button_key'),
|
||||
name: (context) => S.of(context).litecoin_mweb_settings,
|
||||
|
|
|
@ -65,8 +65,7 @@ abstract class SettingsStoreBase with Store {
|
|||
required BitcoinSeedType initialBitcoinSeedType,
|
||||
required NanoSeedType initialNanoSeedType,
|
||||
required bool initialAppSecure,
|
||||
required bool initialDisableBuy,
|
||||
required bool initialDisableSell,
|
||||
required bool initialDisableTrade,
|
||||
required FilterListOrderType initialWalletListOrder,
|
||||
required FilterListOrderType initialContactListOrder,
|
||||
required bool initialDisableBulletin,
|
||||
|
@ -155,8 +154,7 @@ abstract class SettingsStoreBase with Store {
|
|||
useTOTP2FA = initialUseTOTP2FA,
|
||||
numberOfFailedTokenTrials = initialFailedTokenTrial,
|
||||
isAppSecure = initialAppSecure,
|
||||
disableBuy = initialDisableBuy,
|
||||
disableSell = initialDisableSell,
|
||||
disableTradeOption = initialDisableTrade,
|
||||
disableBulletin = initialDisableBulletin,
|
||||
walletListOrder = initialWalletListOrder,
|
||||
contactListOrder = initialContactListOrder,
|
||||
|
@ -183,9 +181,7 @@ abstract class SettingsStoreBase with Store {
|
|||
initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings,
|
||||
currentSyncMode = initialSyncMode,
|
||||
currentSyncAll = initialSyncAll,
|
||||
priority = ObservableMap<WalletType, TransactionPriority>(),
|
||||
defaultBuyProviders = ObservableMap<WalletType, ProviderType>(),
|
||||
defaultSellProviders = ObservableMap<WalletType, ProviderType>() {
|
||||
priority = ObservableMap<WalletType, TransactionPriority>() {
|
||||
//this.nodes = ObservableMap<WalletType, Node>.of(nodes);
|
||||
|
||||
if (initialMoneroTransactionPriority != null) {
|
||||
|
@ -229,30 +225,6 @@ abstract class SettingsStoreBase with Store {
|
|||
|
||||
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(
|
||||
(_) => fiatCurrency,
|
||||
(FiatCurrency fiatCurrency) => sharedPreferences.setString(
|
||||
|
@ -275,20 +247,6 @@ abstract class SettingsStoreBase with Store {
|
|||
reaction((_) => shouldShowRepWarning,
|
||||
(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) {
|
||||
final String? key;
|
||||
switch (change.key) {
|
||||
|
@ -340,14 +298,9 @@ abstract class SettingsStoreBase with Store {
|
|||
});
|
||||
}
|
||||
|
||||
reaction((_) => disableBuy,
|
||||
(bool disableBuy) => sharedPreferences.setBool(PreferencesKey.disableBuyKey, disableBuy));
|
||||
|
||||
reaction(
|
||||
(_) => disableSell,
|
||||
(bool disableSell) =>
|
||||
sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell));
|
||||
|
||||
reaction((_) => disableTradeOption,
|
||||
(bool disableTradeOption) => sharedPreferences.setBool(PreferencesKey.disableTradeOption, disableTradeOption));
|
||||
|
||||
reaction(
|
||||
(_) => disableBulletin,
|
||||
(bool disableBulletin) =>
|
||||
|
@ -691,10 +644,7 @@ abstract class SettingsStoreBase with Store {
|
|||
bool isAppSecure;
|
||||
|
||||
@observable
|
||||
bool disableBuy;
|
||||
|
||||
@observable
|
||||
bool disableSell;
|
||||
bool disableTradeOption;
|
||||
|
||||
@observable
|
||||
FilterListOrderType contactListOrder;
|
||||
|
@ -780,12 +730,6 @@ abstract class SettingsStoreBase with Store {
|
|||
@observable
|
||||
ObservableMap<String, bool> trocadorProviderStates = ObservableMap<String, bool>();
|
||||
|
||||
@observable
|
||||
ObservableMap<WalletType, ProviderType> defaultBuyProviders;
|
||||
|
||||
@observable
|
||||
ObservableMap<WalletType, ProviderType> defaultSellProviders;
|
||||
|
||||
@observable
|
||||
SortBalanceBy sortBalanceBy;
|
||||
|
||||
|
@ -973,8 +917,7 @@ abstract class SettingsStoreBase with Store {
|
|||
final shouldSaveRecipientAddress =
|
||||
sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ?? false;
|
||||
final isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? false;
|
||||
final disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? false;
|
||||
final disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? false;
|
||||
final disableTradeOption = sharedPreferences.getBool(PreferencesKey.disableTradeOption) ?? false;
|
||||
final disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? false;
|
||||
final walletListOrder =
|
||||
FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0];
|
||||
|
@ -1279,8 +1222,7 @@ abstract class SettingsStoreBase with Store {
|
|||
initialBitcoinSeedType: bitcoinSeedType,
|
||||
initialNanoSeedType: nanoSeedType,
|
||||
initialAppSecure: isAppSecure,
|
||||
initialDisableBuy: disableBuy,
|
||||
initialDisableSell: disableSell,
|
||||
initialDisableTrade: disableTradeOption,
|
||||
initialDisableBulletin: disableBulletin,
|
||||
initialWalletListOrder: walletListOrder,
|
||||
initialWalletListAscending: walletListAscending,
|
||||
|
@ -1435,8 +1377,7 @@ abstract class SettingsStoreBase with Store {
|
|||
numberOfFailedTokenTrials =
|
||||
sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? numberOfFailedTokenTrials;
|
||||
isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? isAppSecure;
|
||||
disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? disableBuy;
|
||||
disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? disableSell;
|
||||
disableTradeOption = sharedPreferences.getBool(PreferencesKey.disableTradeOption) ?? disableTradeOption;
|
||||
disableBulletin =
|
||||
sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? disableBulletin;
|
||||
walletListOrder =
|
||||
|
|
|
@ -22,6 +22,8 @@ TextStyle textMediumSemiBold({Color? color}) => _cakeSemiBold(22, color);
|
|||
|
||||
TextStyle textLarge({Color? color}) => _cakeRegular(18, color);
|
||||
|
||||
TextStyle textLargeBold({Color? color}) => _cakeBold(18, color);
|
||||
|
||||
TextStyle textLargeSemiBold({Color? color}) => _cakeSemiBold(24, 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.sharedPreferences,
|
||||
required this.keyService})
|
||||
: hasSellAction = false,
|
||||
hasBuyAction = false,
|
||||
: hasTradeAction = false,
|
||||
hasExchangeAction = false,
|
||||
isShowFirstYatIntroduction = false,
|
||||
isShowSecondYatIntroduction = false,
|
||||
|
@ -393,6 +392,12 @@ abstract class DashboardViewModelBase with Store {
|
|||
wallet.type == WalletType.wownero ||
|
||||
wallet.type == WalletType.haven;
|
||||
|
||||
@computed
|
||||
bool get isMoneroViewOnly {
|
||||
if (wallet.type != WalletType.monero) return false;
|
||||
return monero!.isViewOnly();
|
||||
}
|
||||
|
||||
@computed
|
||||
String? get getMoneroError {
|
||||
if (wallet.type != WalletType.monero) return null;
|
||||
|
@ -515,37 +520,8 @@ abstract class DashboardViewModelBase with Store {
|
|||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
@action
|
||||
|
@ -558,16 +534,10 @@ abstract class DashboardViewModelBase with Store {
|
|||
bool hasExchangeAction;
|
||||
|
||||
@computed
|
||||
bool get isEnabledBuyAction => !settingsStore.disableBuy && hasBuyProviders;
|
||||
bool get isEnabledTradeAction => !settingsStore.disableTradeOption;
|
||||
|
||||
@observable
|
||||
bool hasBuyAction;
|
||||
|
||||
@computed
|
||||
bool get isEnabledSellAction => !settingsStore.disableSell && hasSellProviders;
|
||||
|
||||
@observable
|
||||
bool hasSellAction;
|
||||
bool hasTradeAction;
|
||||
|
||||
@computed
|
||||
bool get isEnabledBulletinAction => !settingsStore.disableBulletin;
|
||||
|
@ -771,8 +741,7 @@ abstract class DashboardViewModelBase with Store {
|
|||
|
||||
void updateActions() {
|
||||
hasExchangeAction = !isHaven;
|
||||
hasBuyAction = !isHaven;
|
||||
hasSellAction = !isHaven;
|
||||
hasTradeAction = !isHaven;
|
||||
}
|
||||
|
||||
@computed
|
||||
|
|
|
@ -214,7 +214,7 @@ abstract class NodeCreateOrEditViewModelBase with Store {
|
|||
bool isCameraPermissionGranted =
|
||||
await PermissionHandler.checkPermission(Permission.camera, context);
|
||||
if (!isCameraPermissionGranted) return;
|
||||
String code = await presentQRScanner();
|
||||
String code = await presentQRScanner(context);
|
||||
|
||||
if (code.isEmpty) {
|
||||
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:cw_core/wallet_type.dart';
|
||||
|
||||
|
@ -32,6 +34,16 @@ class RestoredWallet {
|
|||
final String? privateKey;
|
||||
|
||||
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?;
|
||||
return RestoredWallet(
|
||||
restoreMode: json['mode'] as WalletRestoreMode,
|
||||
|
@ -39,7 +51,7 @@ class RestoredWallet {
|
|||
address: json['address'] as String?,
|
||||
spendKey: json['spend_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?,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:cake_wallet/core/seed_validator.dart';
|
||||
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
|
||||
import 'package:cake_wallet/entities/qr_scanner.dart';
|
||||
|
@ -51,6 +53,17 @@ class WalletRestoreFromQRCode {
|
|||
|
||||
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];
|
||||
}
|
||||
|
||||
|
@ -78,7 +91,7 @@ class WalletRestoreFromQRCode {
|
|||
}
|
||||
|
||||
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');
|
||||
|
||||
WalletType? walletType;
|
||||
|
@ -112,7 +125,7 @@ class WalletRestoreFromQRCode {
|
|||
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);
|
||||
|
||||
|
@ -208,6 +221,17 @@ class WalletRestoreFromQRCode {
|
|||
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');
|
||||
}
|
||||
}
|
||||
|
|