Cw 657 new buy sell flow (#1553)

* init commit * onramper * moonPay * dfx provider * meld * dfx payment methods * fiat buy credentials * moonpay payment method * payment loading state * dfx sell quote * onramper launch trade * meld launch trade * country picker * update option tile * buy/sell action * meld refactor * update pr_test_build.yml * ui fixes * revert country picker commit * update the ui * recommended providers * payment method [skip ci] * provider option tile * remove buy action * minor fixes * update the best rate when the amount is changed * fixes for currency title * fix icons * code refactoring * null issue * code review fixes * Update pr_test_build_linux.yml * Update meld_buy_provider.dart * Update meld_buy_provider.dart * add show wallets action * remove default sell / buy provider setting * localisation * providerTypes * icons * remove duplicate file [skip ci] * minor changes [skip ci] * fixes from review * disable dfx for non eur/chf currencies fix providers to be fetched with the selected currency * fix breaking from loop if one element failed * fix minor naming issue from merging conflicts * add fiat check for moonpay * fix address validation * merge conflict * fix destination and source currency * minor fix * minor fix * update the flow * fix bch address format * fix wallet addresses * fix initial fetching amount. * Update address_validator.dart * review comments * revert switch case to return null * minor fix --------- Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>

Serhii committed Nov 9, 2024 at 21:00 UTC 02f53055b1a72b5e7d33cc5bda2565412857b187
88 files changed +4074 -678
.github/workflows/pr_test_build_android.yml
+2
@@ -194,6 +194,8 @@ jobs:
194 echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
195 echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
196 echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
197 + echo "const meldTestApiKey = '${{ secrets.MELD_TEST_API_KEY }}';" >> lib/.secrets.g.dart
198 + echo "const meldTestPublicKey = '${{ secrets.MELD_TEST_PUBLIC_KEY}}';" >> lib/.secrets.g.dar
199 echo "const letsExchangeBearerToken = '${{ secrets.LETS_EXCHANGE_TOKEN }}';" >> lib/.secrets.g.dart
200 echo "const letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart
201 echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart
.github/workflows/pr_test_build_linux.yml
+2
@@ -175,6 +175,8 @@ jobs:
175 echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
176 echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
177 echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
178 + echo "const meldTestApiKey = '${{ secrets.MELD_TEST_API_KEY }}';" >> lib/.secrets.g.dart
179 + echo "const meldTestPublicKey = '${{ secrets.MELD_TEST_PUBLIC_KEY}}';" >> lib/.secrets.g.dar
180 echo "const letsExchangeBearerToken = '${{ secrets.LETS_EXCHANGE_TOKEN }}';" >> lib/.secrets.g.dart
181 echo "const letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart
182 echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart
assets/images/apple_pay_logo.png
Binary files /dev/null and b/assets/images/apple_pay_logo.png differ
assets/images/apple_pay_round_dark.svg new
+9
@@ -0,0 +1,9 @@
1 +<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">
2 +
3 +<defs>
4 +</defs>
5 + <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)" >
6 + <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" />
7 + <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" />
8 +</g>
9 +</svg>
\ No newline at end of file
assets/images/apple_pay_round_light.svg new
+9
@@ -0,0 +1,9 @@
1 +<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">
2 +
3 +<defs>
4 +</defs>
5 +<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)" >
6 + <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" />
7 + <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" />
8 +</g>
9 +</svg>
\ No newline at end of file
assets/images/bank.png
Binary files /dev/null and b/assets/images/bank.png differ
assets/images/bank_dark.svg new
+8
@@ -0,0 +1,8 @@
1 +<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">
2 +
3 +<defs>
4 +</defs>
5 + <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)" >
6 + <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" />
7 +</g>
8 +</svg>
\ No newline at end of file
assets/images/bank_light.svg new
+8
@@ -0,0 +1,8 @@
1 +<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">
2 +
3 +<defs>
4 +</defs>
5 + <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)" >
6 + <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" />
7 +</g>
8 +</svg>
\ No newline at end of file
assets/images/buy_sell.png
Binary files /dev/null and b/assets/images/buy_sell.png differ
assets/images/card.svg new
+1
@@ -0,0 +1 @@
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>
\ No newline at end of file
assets/images/card_dark.svg new
+7
@@ -0,0 +1,7 @@
1 +<?xml version="1.0" encoding="iso-8859-1"?>
2 +<!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3 +<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">
4 + <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"/>
5 + <rect x="3" y="16" width="44" height="5" style="fill:white;stroke:#ffffff;stroke-width:2;"/>
6 + <line style="fill:none;stroke:#ffffff;stroke-width:2;stroke-miterlimit:10;" x1="9" y1="25" x2="25" y2="25"/>
7 +</svg>
\ No newline at end of file
assets/images/dollar_coin.svg new
+12
@@ -0,0 +1,12 @@
1 +<?xml version="1.0" ?>
2 + <!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
3 +<svg fill="#000000" width="800px" height="800px" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
4 + <g id="Outline">
5 + <g data-name="Outline" id="Outline-2">
6 + <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"/>
7 + <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"/>
8 + <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"/>
9 + <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"/>
10 + </g>
11 + </g>
12 + </svg>
\ No newline at end of file
assets/images/google_pay_icon.png
Binary files /dev/null and b/assets/images/google_pay_icon.png differ
assets/images/meld_logo.svg new
+30
@@ -0,0 +1,30 @@
1 +<svg width="40" height="42" viewBox="0 0 40 42" fill="none" xmlns="http://www.w3.org/2000/svg">
2 +<path opacity="0.7" d="M19.9526 41.9076L7.3877 34.655V26.1226L19.9526 33.3751V41.9076Z" fill="url(#paint0_linear_2113_32117)"/>
3 +<path opacity="0.7" d="M19.9521 41.9076L32.5171 34.655V26.1226L19.9521 33.3751V41.9076Z" fill="url(#paint1_linear_2113_32117)"/>
4 +<path opacity="0.7" d="M39.9095 7.34521V21.8562L32.5166 26.1225V11.6114L39.9095 7.34521Z" fill="url(#paint2_linear_2113_32117)"/>
5 +<path d="M39.9099 7.34536L27.345 0.0927734L19.9521 4.359L32.5171 11.6116L39.9099 7.34536Z" fill="url(#paint3_linear_2113_32117)"/>
6 +<path d="M0 7.34536L12.5649 0.0927734L19.9519 4.359L7.387 11.6116L0 7.34536Z" fill="#F969D3"/>
7 +<path opacity="0.7" d="M0 7.34521V21.8562L7.387 26.1225V11.6114L0 7.34521Z" fill="url(#paint4_linear_2113_32117)"/>
8 +<defs>
9 +<linearGradient id="paint0_linear_2113_32117" x1="18.6099" y1="41.8335" x2="7.73529" y2="8.31842" gradientUnits="userSpaceOnUse">
10 +<stop stop-color="#E98ADA"/>
11 +<stop offset="1" stop-color="#7E4DBD"/>
12 +</linearGradient>
13 +<linearGradient id="paint1_linear_2113_32117" x1="26.2346" y1="26.1226" x2="26.2346" y2="41.9076" gradientUnits="userSpaceOnUse">
14 +<stop stop-color="#719DED"/>
15 +<stop offset="1" stop-color="#2545BE"/>
16 +</linearGradient>
17 +<linearGradient id="paint2_linear_2113_32117" x1="36.213" y1="7.34521" x2="36.213" y2="26.1225" gradientUnits="userSpaceOnUse">
18 +<stop stop-color="#93EBFF"/>
19 +<stop offset="1" stop-color="#197DDB"/>
20 +</linearGradient>
21 +<linearGradient id="paint3_linear_2113_32117" x1="29.931" y1="0.0927734" x2="38.2156" y2="14.8448" gradientUnits="userSpaceOnUse">
22 +<stop stop-color="#F969D3"/>
23 +<stop offset="1" stop-color="#4F51C0"/>
24 +</linearGradient>
25 +<linearGradient id="paint4_linear_2113_32117" x1="18.1251" y1="44.2539" x2="-7.06792" y2="15.2763" gradientUnits="userSpaceOnUse">
26 +<stop stop-color="#E98ADA"/>
27 +<stop offset="1" stop-color="#7E4DBD"/>
28 +</linearGradient>
29 +</defs>
30 +</svg>
assets/images/revolut.png
Binary files /dev/null and b/assets/images/revolut.png differ
assets/images/skrill.svg new
+15
@@ -0,0 +1,15 @@
1 +<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">
2 +
3 +<defs>
4 +</defs>
5 +<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)" >
6 + <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) "/>
7 + <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) "/>
8 + <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) "/>
9 + <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) "/>
10 + <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" />
11 + <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" />
12 + <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" />
13 + <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" />
14 +</g>
15 +</svg>
\ No newline at end of file
assets/images/usd_round_dark.svg new
+4
@@ -0,0 +1,4 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 24 24" width="512" height="512">
3 + <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"/>
4 +</svg>
\ No newline at end of file
assets/images/usd_round_light.svg new
+2
@@ -0,0 +1,2 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<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>
assets/images/wallet_new.png
Binary files /dev/null and b/assets/images/wallet_new.png differ
cw_core/lib/currency_for_wallet_type.dart
+31
@@ -35,3 +35,34 @@ CryptoCurrency currencyForWalletType(WalletType type, {bool? isTestnet}) {
35 'Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType');
36 }
37 }
38 +
39 +WalletType? walletTypeForCurrency(CryptoCurrency currency) {
40 + switch (currency) {
41 + case CryptoCurrency.btc:
42 + return WalletType.bitcoin;
43 + case CryptoCurrency.xmr:
44 + return WalletType.monero;
45 + case CryptoCurrency.ltc:
46 + return WalletType.litecoin;
47 + case CryptoCurrency.xhv:
48 + return WalletType.haven;
49 + case CryptoCurrency.eth:
50 + return WalletType.ethereum;
51 + case CryptoCurrency.bch:
52 + return WalletType.bitcoinCash;
53 + case CryptoCurrency.nano:
54 + return WalletType.nano;
55 + case CryptoCurrency.banano:
56 + return WalletType.banano;
57 + case CryptoCurrency.maticpoly:
58 + return WalletType.polygon;
59 + case CryptoCurrency.sol:
60 + return WalletType.solana;
61 + case CryptoCurrency.trx:
62 + return WalletType.tron;
63 + case CryptoCurrency.wow:
64 + return WalletType.wownero;
65 + default:
66 + return null;
67 + }
68 +}
lib/buy/buy_provider.dart
+30 -2
@@ -1,6 +1,10 @@
1 import 'package:cake_wallet/buy/buy_amount.dart';
2 +import 'package:cake_wallet/buy/buy_quote.dart';
3 import 'package:cake_wallet/buy/order.dart';
4 +import 'package:cake_wallet/buy/payment_method.dart';
5 +import 'package:cake_wallet/entities/fiat_currency.dart';
6 import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
7 +import 'package:cw_core/crypto_currency.dart';
8 import 'package:cw_core/wallet_base.dart';
9 import 'package:flutter/material.dart';
10
@@ -23,14 +27,38 @@ abstract class BuyProvider {
27
28 String get darkIcon;
29
30 + bool get isAggregator;
31 +
32 @override
33 String toString() => title;
34
29 - Future<void> launchProvider(BuildContext context, bool? isBuyAction);
35 + Future<void>? launchProvider(
36 + {required BuildContext context,
37 + required Quote quote,
38 + required double amount,
39 + required bool isBuyAction,
40 + required String cryptoCurrencyAddress,
41 + String? countryCode}) =>
42 + null;
43
44 Future<String> requestUrl(String amount, String sourceCurrency) => throw UnimplementedError();
45
46 Future<Order> findOrderById(String id) => throw UnimplementedError();
47
35 - Future<BuyAmount> calculateAmount(String amount, String sourceCurrency) => throw UnimplementedError();
48 + Future<BuyAmount> calculateAmount(String amount, String sourceCurrency) =>
49 + throw UnimplementedError();
50 +
51 + Future<List<PaymentMethod>> getAvailablePaymentTypes(
52 + String fiatCurrency, String cryptoCurrency, bool isBuyAction) async =>
53 + [];
54 +
55 + Future<List<Quote>?> fetchQuote(
56 + {required CryptoCurrency cryptoCurrency,
57 + required FiatCurrency fiatCurrency,
58 + required double amount,
59 + required bool isBuyAction,
60 + required String walletAddress,
61 + PaymentType? paymentType,
62 + String? countryCode}) async =>
63 + null;
64 }
lib/buy/buy_quote.dart new
+302
@@ -0,0 +1,302 @@
1 +import 'package:cake_wallet/buy/buy_provider.dart';
2 +import 'package:cake_wallet/buy/payment_method.dart';
3 +import 'package:cake_wallet/core/selectable_option.dart';
4 +import 'package:cake_wallet/entities/fiat_currency.dart';
5 +import 'package:cake_wallet/entities/provider_types.dart';
6 +import 'package:cake_wallet/exchange/limits.dart';
7 +import 'package:cw_core/crypto_currency.dart';
8 +
9 +enum ProviderRecommendation { bestRate, lowKyc, successRate }
10 +
11 +extension RecommendationTitle on ProviderRecommendation {
12 + String get title {
13 + switch (this) {
14 + case ProviderRecommendation.bestRate:
15 + return 'BEST RATE';
16 + case ProviderRecommendation.lowKyc:
17 + return 'LOW KYC';
18 + case ProviderRecommendation.successRate:
19 + return 'SUCCESS RATE';
20 + }
21 + }
22 +}
23 +
24 +ProviderRecommendation? getRecommendationFromString(String title) {
25 + switch (title) {
26 + case 'BEST RATE':
27 + return ProviderRecommendation.bestRate;
28 + case 'LowKyc':
29 + return ProviderRecommendation.lowKyc;
30 + case 'SuccessRate':
31 + return ProviderRecommendation.successRate;
32 + default:
33 + return null;
34 + }
35 +}
36 +
37 +class Quote extends SelectableOption {
38 + Quote({
39 + required this.rate,
40 + required this.feeAmount,
41 + required this.networkFee,
42 + required this.transactionFee,
43 + required this.payout,
44 + required this.provider,
45 + required this.paymentType,
46 + required this.recommendations,
47 + this.isBuyAction = true,
48 + this.quoteId,
49 + this.rampId,
50 + this.rampName,
51 + this.rampIconPath,
52 + this.limits,
53 + }) : super(title: provider.isAggregator ? rampName ?? '' : provider.title);
54 +
55 + final double rate;
56 + final double feeAmount;
57 + final double networkFee;
58 + final double transactionFee;
59 + final double payout;
60 + final PaymentType paymentType;
61 + final BuyProvider provider;
62 + final String? quoteId;
63 + final List<ProviderRecommendation> recommendations;
64 + String? rampId;
65 + String? rampName;
66 + String? rampIconPath;
67 + bool _isSelected = false;
68 + bool _isBestRate = false;
69 + bool isBuyAction;
70 + Limits? limits;
71 +
72 + late FiatCurrency _fiatCurrency;
73 + late CryptoCurrency _cryptoCurrency;
74 +
75 +
76 + bool get isSelected => _isSelected;
77 + bool get isBestRate => _isBestRate;
78 + FiatCurrency get fiatCurrency => _fiatCurrency;
79 + CryptoCurrency get cryptoCurrency => _cryptoCurrency;
80 +
81 + @override
82 + bool get isOptionSelected => this._isSelected;
83 +
84 + @override
85 + String get lightIconPath =>
86 + provider.isAggregator ? rampIconPath ?? provider.lightIcon : provider.lightIcon;
87 +
88 + @override
89 + String get darkIconPath =>
90 + provider.isAggregator ? rampIconPath ?? provider.darkIcon : provider.darkIcon;
91 +
92 + @override
93 + List<String> get badges => recommendations.map((e) => e.title).toList();
94 +
95 + @override
96 + String get topLeftSubTitle =>
97 + this.rate > 0 ? '1 $cryptoName = ${rate.toStringAsFixed(2)} $fiatName' : '';
98 +
99 + @override
100 + String get bottomLeftSubTitle {
101 + if (limits != null) {
102 + final min = limits!.min;
103 + final max = limits!.max;
104 + return 'min: ${min} ${fiatCurrency.toString()} | max: ${max == double.infinity ? '' : '${max} ${fiatCurrency.toString()}'}';
105 + }
106 + return '';
107 + }
108 +
109 + String get fiatName => isBuyAction ? fiatCurrency.toString() : cryptoCurrency.toString();
110 +
111 + String get cryptoName => isBuyAction ? cryptoCurrency.toString() : fiatCurrency.toString();
112 +
113 + @override
114 + String? get topRightSubTitle => '';
115 +
116 + @override
117 + String get topRightSubTitleLightIconPath => provider.isAggregator ? provider.lightIcon : '';
118 +
119 + @override
120 + String get topRightSubTitleDarkIconPath => provider.isAggregator ? provider.darkIcon : '';
121 +
122 + String get quoteTitle => '${provider.title} - ${paymentType.name}';
123 +
124 + String get formatedFee => '$feeAmount ${isBuyAction ? fiatCurrency : cryptoCurrency}';
125 +
126 + set setIsSelected(bool isSelected) => _isSelected = isSelected;
127 + set setIsBestRate(bool isBestRate) => _isBestRate = isBestRate;
128 + set setFiatCurrency(FiatCurrency fiatCurrency) => _fiatCurrency = fiatCurrency;
129 + set setCryptoCurrency(CryptoCurrency cryptoCurrency) => _cryptoCurrency = cryptoCurrency;
130 + set setLimits(Limits limits) => this.limits = limits;
131 +
132 + factory Quote.fromOnramperJson(Map<String, dynamic> json, bool isBuyAction,
133 + Map<String, dynamic> metaData, PaymentType paymentType) {
134 + final rate = _toDouble(json['rate']) ?? 0.0;
135 + final networkFee = _toDouble(json['networkFee']) ?? 0.0;
136 + final transactionFee = _toDouble(json['transactionFee']) ?? 0.0;
137 + final feeAmount = double.parse((networkFee + transactionFee).toStringAsFixed(2));
138 +
139 + final rampId = json['ramp'] as String? ?? '';
140 + final rampData = metaData[rampId] ?? {};
141 + final rampName = rampData['displayName'] as String? ?? '';
142 + final rampIconPath = rampData['svg'] as String? ?? '';
143 +
144 + final recommendations = json['recommendations'] != null
145 + ? List<String>.from(json['recommendations'] as List<dynamic>)
146 + : <String>[];
147 +
148 + final enumRecommendations = recommendations
149 + .map((e) => getRecommendationFromString(e))
150 + .whereType<ProviderRecommendation>()
151 + .toList();
152 +
153 + final availablePaymentMethods = json['availablePaymentMethods'] as List<dynamic>? ?? [];
154 + double minLimit = 0.0;
155 + double maxLimit = double.infinity;
156 +
157 + for (var paymentMethod in availablePaymentMethods) {
158 + if (paymentMethod is Map<String, dynamic>) {
159 + final details = paymentMethod['details'] as Map<String, dynamic>?;
160 +
161 + if (details != null) {
162 + final limits = details['limits'] as Map<String, dynamic>?;
163 +
164 + if (limits != null && limits.isNotEmpty) {
165 + final firstLimitEntry = limits.values.first as Map<String, dynamic>?;
166 + if (firstLimitEntry != null) {
167 + minLimit = _toDouble(firstLimitEntry['min'])?.roundToDouble() ?? 0.0;
168 + maxLimit = _toDouble(firstLimitEntry['max'])?.roundToDouble() ?? double.infinity;
169 + break;
170 + }
171 + }
172 + }
173 + }
174 + }
175 +
176 + return Quote(
177 + rate: rate,
178 + feeAmount: feeAmount,
179 + networkFee: networkFee,
180 + transactionFee: transactionFee,
181 + payout: json['payout'] as double? ?? 0.0,
182 + rampId: rampId,
183 + rampName: rampName,
184 + rampIconPath: rampIconPath,
185 + paymentType: paymentType,
186 + quoteId: json['quoteId'] as String? ?? '',
187 + recommendations: enumRecommendations,
188 + provider: ProvidersHelper.getProviderByType(ProviderType.onramper)!,
189 + isBuyAction: isBuyAction,
190 + limits: Limits(min: minLimit, max: maxLimit),
191 + );
192 + }
193 +
194 + factory Quote.fromMoonPayJson(
195 + Map<String, dynamic> json, bool isBuyAction, PaymentType paymentType) {
196 + final rate = isBuyAction
197 + ? json['quoteCurrencyPrice'] as double? ?? 0.0
198 + : json['baseCurrencyPrice'] as double? ?? 0.0;
199 + final fee = _toDouble(json['feeAmount']) ?? 0.0;
200 + final networkFee = _toDouble(json['networkFeeAmount']) ?? 0.0;
201 + final transactionFee = _toDouble(json['extraFeeAmount']) ?? 0.0;
202 + final feeAmount = double.parse((fee + networkFee + transactionFee).toStringAsFixed(2));
203 +
204 + final baseCurrency = json['baseCurrency'] as Map<String, dynamic>?;
205 +
206 + double minLimit = 0.0;
207 + double maxLimit = double.infinity;
208 +
209 + if (baseCurrency != null) {
210 + minLimit = _toDouble(baseCurrency['minAmount']) ?? minLimit;
211 + maxLimit = _toDouble(baseCurrency['maxAmount']) ?? maxLimit;
212 + }
213 +
214 + return Quote(
215 + rate: rate,
216 + feeAmount: feeAmount,
217 + networkFee: networkFee,
218 + transactionFee: transactionFee,
219 + payout: _toDouble(json['quoteCurrencyAmount']) ?? 0.0,
220 + paymentType: paymentType,
221 + recommendations: [],
222 + quoteId: json['signature'] as String? ?? '',
223 + provider: ProvidersHelper.getProviderByType(ProviderType.moonpay)!,
224 + isBuyAction: isBuyAction,
225 + limits: Limits(min: minLimit, max: maxLimit),
226 + );
227 + }
228 +
229 + factory Quote.fromDFXJson(
230 + Map<String, dynamic> json,
231 + bool isBuyAction,
232 + PaymentType paymentType,
233 + ) {
234 + final rate = _toDouble(json['exchangeRate']) ?? 0.0;
235 + final fees = json['fees'] as Map<String, dynamic>;
236 +
237 + final minVolume = _toDouble(json['minVolume']) ?? 0.0;
238 + final maxVolume = _toDouble(json['maxVolume']) ?? double.infinity;
239 +
240 + return Quote(
241 + rate: isBuyAction ? rate : 1 / rate,
242 + feeAmount: _toDouble(json['feeAmount']) ?? 0.0,
243 + networkFee: _toDouble(fees['network']) ?? 0.0,
244 + transactionFee: _toDouble(fees['rate']) ?? 0.0,
245 + payout: _toDouble(json['payout']) ?? 0.0,
246 + paymentType: paymentType,
247 + recommendations: [ProviderRecommendation.lowKyc],
248 + provider: ProvidersHelper.getProviderByType(ProviderType.dfx)!,
249 + isBuyAction: isBuyAction,
250 + limits: Limits(min: minVolume, max: maxVolume),
251 + );
252 + }
253 +
254 + factory Quote.fromRobinhoodJson(
255 + Map<String, dynamic> json, bool isBuyAction, PaymentType paymentType) {
256 + final networkFee = json['networkFee'] as Map<String, dynamic>;
257 + final processingFee = json['processingFee'] as Map<String, dynamic>;
258 + final networkFeeAmount = _toDouble(networkFee['fiatAmount']) ?? 0.0;
259 + final transactionFeeAmount = _toDouble(processingFee['fiatAmount']) ?? 0.0;
260 + final feeAmount = double.parse((networkFeeAmount + transactionFeeAmount).toStringAsFixed(2));
261 +
262 + return Quote(
263 + rate: _toDouble(json['price']) ?? 0.0,
264 + feeAmount: feeAmount,
265 + networkFee: _toDouble(networkFee['fiatAmount']) ?? 0.0,
266 + transactionFee: _toDouble(processingFee['fiatAmount']) ?? 0.0,
267 + payout: _toDouble(json['cryptoAmount']) ?? 0.0,
268 + paymentType: paymentType,
269 + recommendations: [],
270 + provider: ProvidersHelper.getProviderByType(ProviderType.robinhood)!,
271 + isBuyAction: isBuyAction,
272 + limits: Limits(min: 0.0, max: double.infinity),
273 + );
274 + }
275 +
276 + factory Quote.fromMeldJson(Map<String, dynamic> json, bool isBuyAction, PaymentType paymentType) {
277 + final quotes = json['quotes'][0] as Map<String, dynamic>;
278 + return Quote(
279 + rate: quotes['exchangeRate'] as double? ?? 0.0,
280 + feeAmount: quotes['totalFee'] as double? ?? 0.0,
281 + networkFee: quotes['networkFee'] as double? ?? 0.0,
282 + transactionFee: quotes['transactionFee'] as double? ?? 0.0,
283 + payout: quotes['payout'] as double? ?? 0.0,
284 + paymentType: paymentType,
285 + recommendations: [],
286 + provider: ProvidersHelper.getProviderByType(ProviderType.meld)!,
287 + isBuyAction: isBuyAction,
288 + limits: Limits(min: 0.0, max: double.infinity),
289 + );
290 + }
291 +
292 + static double? _toDouble(dynamic value) {
293 + if (value is int) {
294 + return value.toDouble();
295 + } else if (value is double) {
296 + return value;
297 + } else if (value is String) {
298 + return double.tryParse(value);
299 + }
300 + return null;
301 + }
302 +}
lib/buy/dfx/dfx_buy_provider.dart
+228 -45
@@ -1,13 +1,17 @@
1 import 'dart:convert';
2 +import 'dart:developer';
3
4 import 'package:cake_wallet/buy/buy_provider.dart';
5 +import 'package:cake_wallet/buy/buy_quote.dart';
6 +import 'package:cake_wallet/buy/payment_method.dart';
7 +import 'package:cake_wallet/entities/fiat_currency.dart';
8 import 'package:cake_wallet/generated/i18n.dart';
9 import 'package:cake_wallet/routes.dart';
10 import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart';
11 import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
8 -import 'package:cake_wallet/utils/device_info.dart';
12 import 'package:cake_wallet/utils/show_pop_up.dart';
13 import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
14 +import 'package:cw_core/crypto_currency.dart';
15 import 'package:cw_core/wallet_base.dart';
16 import 'package:cw_core/wallet_type.dart';
17 import 'package:flutter/material.dart';
@@ -15,10 +19,12 @@ import 'package:http/http.dart' as http;
19 import 'package:url_launcher/url_launcher.dart';
20
21 class DFXBuyProvider extends BuyProvider {
18 - DFXBuyProvider({required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM})
22 + DFXBuyProvider(
23 + {required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM})
24 : super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: ledgerVM);
25
26 static const _baseUrl = 'api.dfx.swiss';
27 +
28 // static const _signMessagePath = '/v1/auth/signMessage';
29 static const _authPath = '/v1/auth';
30 static const walletName = 'CakeWallet';
@@ -35,24 +41,8 @@ class DFXBuyProvider extends BuyProvider {
41 @override
42 String get darkIcon => 'assets/images/dfx_dark.png';
43
38 - String get assetOut {
39 - switch (wallet.type) {
40 - case WalletType.bitcoin:
41 - return 'BTC';
42 - case WalletType.bitcoinCash:
43 - return 'BCH';
44 - case WalletType.litecoin:
45 - return 'LTC';
46 - case WalletType.monero:
47 - return 'XMR';
48 - case WalletType.ethereum:
49 - return 'ETH';
50 - case WalletType.polygon:
51 - return 'MATIC';
52 - default:
53 - throw Exception("WalletType is not available for DFX ${wallet.type}");
54 - }
55 - }
44 + @override
45 + bool get isAggregator => false;
46
47 String get blockchain {
48 switch (wallet.type) {
@@ -60,21 +50,13 @@ class DFXBuyProvider extends BuyProvider {
50 case WalletType.bitcoinCash:
51 case WalletType.litecoin:
52 return 'Bitcoin';
63 - case WalletType.monero:
64 - return 'Monero';
65 - case WalletType.ethereum:
66 - return 'Ethereum';
67 - case WalletType.polygon:
68 - return 'Polygon';
53 default:
70 - throw Exception("WalletType is not available for DFX ${wallet.type}");
54 + return walletTypeToString(wallet.type);
55 }
56 }
57
74 - String get walletAddress =>
75 - wallet.walletAddresses.primaryAddress ?? wallet.walletAddresses.address;
58
77 - Future<String> getSignMessage() async =>
59 + Future<String> getSignMessage(String walletAddress) async =>
60 "By_signing_this_message,_you_confirm_that_you_are_the_sole_owner_of_the_provided_Blockchain_address._Your_ID:_$walletAddress";
61
62 // // Lets keep this just in case, but we can avoid this API Call
@@ -92,8 +74,9 @@ class DFXBuyProvider extends BuyProvider {
74 // }
75 // }
76
95 - Future<String> auth() async {
96 - final signMessage = await getSignature(await getSignMessage());
77 + Future<String> auth(String walletAddress) async {
78 + final signMessage = await getSignature(
79 + await getSignMessage(walletAddress), walletAddress);
80
81 final requestBody = jsonEncode({
82 'wallet': walletName,
@@ -120,7 +103,7 @@ class DFXBuyProvider extends BuyProvider {
103 }
104 }
105
123 - Future<String> getSignature(String message) async {
106 + Future<String> getSignature(String message, String walletAddress) async {
107 switch (wallet.type) {
108 case WalletType.ethereum:
109 case WalletType.polygon:
@@ -135,8 +118,178 @@ class DFXBuyProvider extends BuyProvider {
118 }
119 }
120
121 + Future<Map<String, dynamic>> fetchFiatCredentials(String fiatCurrency) async {
122 + final url = Uri.https(_baseUrl, '/v1/fiat');
123 +
124 + try {
125 + final response = await http.get(url, headers: {'accept': 'application/json'});
126 +
127 + if (response.statusCode == 200) {
128 + final data = jsonDecode(response.body) as List<dynamic>;
129 + for (final item in data) {
130 + if (item['name'] == fiatCurrency) return item as Map<String, dynamic>;
131 + }
132 + log('DFX does not support fiat: $fiatCurrency');
133 + return {};
134 + } else {
135 + log('DFX Failed to fetch fiat currencies: ${response.statusCode}');
136 + return {};
137 + }
138 + } catch (e) {
139 + print('DFX Error fetching fiat currencies: $e');
140 + return {};
141 + }
142 + }
143 +
144 + Future<Map<String, dynamic>> fetchAssetCredential(String assetsName) async {
145 + final url = Uri.https(_baseUrl, '/v1/asset', {'blockchains': blockchain});
146 +
147 + try {
148 + final response = await http.get(url, headers: {'accept': 'application/json'});
149 +
150 + if (response.statusCode == 200) {
151 + final responseData = jsonDecode(response.body);
152 +
153 + if (responseData is List && responseData.isNotEmpty) {
154 + return responseData.first as Map<String, dynamic>;
155 + } else if (responseData is Map<String, dynamic>) {
156 + return responseData;
157 + } else {
158 + log('DFX: Does not support this asset name : ${blockchain}');
159 + }
160 + } else {
161 + log('DFX: Failed to fetch assets: ${response.statusCode}');
162 + }
163 + } catch (e) {
164 + log('DFX: Error fetching assets: $e');
165 + }
166 + return {};
167 + }
168 +
169 + Future<List<PaymentMethod>> getAvailablePaymentTypes(
170 + String fiatCurrency, String cryptoCurrency, bool isBuyAction) async {
171 + final List<PaymentMethod> paymentMethods = [];
172 +
173 + if (isBuyAction) {
174 + final fiatBuyCredentials = await fetchFiatCredentials(fiatCurrency);
175 + if (fiatBuyCredentials.isNotEmpty) {
176 + fiatBuyCredentials.forEach((key, value) {
177 + if (key == 'limits') {
178 + final limits = value as Map<String, dynamic>;
179 + limits.forEach((paymentMethodKey, paymentMethodValue) {
180 + final min = _toDouble(paymentMethodValue['minVolume']);
181 + final max = _toDouble(paymentMethodValue['maxVolume']);
182 + if (min != null && max != null && min > 0 && max > 0) {
183 + final paymentMethod = PaymentMethod.fromDFX(
184 + paymentMethodKey, _getPaymentTypeByString(paymentMethodKey));
185 + paymentMethods.add(paymentMethod);
186 + }
187 + });
188 + }
189 + });
190 + }
191 + } else {
192 + final assetCredentials = await fetchAssetCredential(cryptoCurrency);
193 + if (assetCredentials.isNotEmpty) {
194 + if (assetCredentials['sellable'] == true) {
195 + final availablePaymentTypes = [
196 + PaymentType.bankTransfer,
197 + PaymentType.creditCard,
198 + PaymentType.sepa
199 + ];
200 + availablePaymentTypes.forEach((element) {
201 + final paymentMethod = PaymentMethod.fromDFX(normalizePaymentMethod(element)!, element);
202 + paymentMethods.add(paymentMethod);
203 + });
204 + }
205 + }
206 + }
207 +
208 + return paymentMethods;
209 + }
210 +
211 @override
139 - Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
212 + Future<List<Quote>?> fetchQuote(
213 + {required CryptoCurrency cryptoCurrency,
214 + required FiatCurrency fiatCurrency,
215 + required double amount,
216 + required bool isBuyAction,
217 + required String walletAddress,
218 + PaymentType? paymentType,
219 + String? countryCode}) async {
220 + /// if buying with any currency other than eur or chf then DFX is not supported
221 +
222 + if (isBuyAction && (fiatCurrency != FiatCurrency.eur && fiatCurrency != FiatCurrency.chf)) {
223 + return null;
224 + }
225 +
226 + String? paymentMethod;
227 + if (paymentType != null && paymentType != PaymentType.all) {
228 + paymentMethod = normalizePaymentMethod(paymentType);
229 + if (paymentMethod == null) paymentMethod = paymentType.name;
230 + } else {
231 + paymentMethod = 'Bank';
232 + }
233 +
234 + final action = isBuyAction ? 'buy' : 'sell';
235 +
236 + if (isBuyAction && cryptoCurrency != wallet.currency) return null;
237 +
238 + final fiatCredentials = await fetchFiatCredentials(fiatCurrency.name.toString());
239 + if (fiatCredentials['id'] == null) return null;
240 +
241 + final assetCredentials = await fetchAssetCredential(cryptoCurrency.title.toString());
242 + if (assetCredentials['id'] == null) return null;
243 +
244 + log('DFX: Fetching $action quote: ${isBuyAction ? cryptoCurrency : fiatCurrency} -> ${isBuyAction ? fiatCurrency : cryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod');
245 +
246 + final url = Uri.https(_baseUrl, '/v1/$action/quote');
247 + final headers = {'accept': 'application/json', 'Content-Type': 'application/json'};
248 + final body = jsonEncode({
249 + 'currency': {'id': fiatCredentials['id'] as int},
250 + 'asset': {'id': assetCredentials['id']},
251 + 'amount': amount,
252 + 'targetAmount': 0,
253 + 'paymentMethod': paymentMethod,
254 + 'discountCode': ''
255 + });
256 +
257 + try {
258 + final response = await http.put(url, headers: headers, body: body);
259 + final responseData = jsonDecode(response.body);
260 +
261 + if (response.statusCode == 200) {
262 + if (responseData is Map<String, dynamic>) {
263 + final paymentType = _getPaymentTypeByString(responseData['paymentMethod'] as String?);
264 + final quote = Quote.fromDFXJson(responseData, isBuyAction, paymentType);
265 + quote.setFiatCurrency = fiatCurrency;
266 + quote.setCryptoCurrency = cryptoCurrency;
267 + return [quote];
268 + } else {
269 + print('DFX: Unexpected data type: ${responseData.runtimeType}');
270 + return null;
271 + }
272 + } else {
273 + if (responseData is Map<String, dynamic> && responseData.containsKey('message')) {
274 + print('DFX Error: ${responseData['message']}');
275 + } else {
276 + print('DFX Failed to fetch buy quote: ${response.statusCode}');
277 + }
278 + return null;
279 + }
280 + } catch (e) {
281 + print('DFX Error fetching buy quote: $e');
282 + return null;
283 + }
284 + }
285 +
286 + Future<void>? launchProvider(
287 + {required BuildContext context,
288 + required Quote quote,
289 + required double amount,
290 + required bool isBuyAction,
291 + required String cryptoCurrencyAddress,
292 + String? countryCode}) async {
293 if (wallet.isHardwareWallet) {
294 if (!ledgerVM!.isConnected) {
295 await Navigator.of(context).pushNamed(Routes.connectDevices,
@@ -152,26 +305,21 @@ class DFXBuyProvider extends BuyProvider {
305 }
306
307 try {
155 - final assetOut = this.assetOut;
156 - final blockchain = this.blockchain;
157 - final actionType = isBuyAction == true ? '/buy' : '/sell';
308 + final actionType = isBuyAction ? '/buy' : '/sell';
309
159 - final accessToken = await auth();
310 + final accessToken = await auth(cryptoCurrencyAddress);
311
312 final uri = Uri.https('services.dfx.swiss', actionType, {
313 'session': accessToken,
314 'lang': 'en',
164 - 'asset-out': assetOut,
315 + 'asset-out': isBuyAction ? quote.cryptoCurrency.toString() : quote.fiatCurrency.toString(),
316 'blockchain': blockchain,
166 - 'asset-in': 'EUR',
317 + 'asset-in': isBuyAction ? quote.fiatCurrency.toString() : quote.cryptoCurrency.toString(),
318 + 'amount': amount.toString() //TODO: Amount does not work
319 });
320
321 if (await canLaunchUrl(uri)) {
170 - if (DeviceInfo.instance.isMobile) {
171 - Navigator.of(context).pushNamed(Routes.webViewPage, arguments: [title, uri]);
172 - } else {
173 - await launchUrl(uri, mode: LaunchMode.externalApplication);
174 - }
322 + await launchUrl(uri, mode: LaunchMode.externalApplication);
323 } else {
324 throw Exception('Could not launch URL');
325 }
@@ -187,4 +335,39 @@ class DFXBuyProvider extends BuyProvider {
335 });
336 }
337 }
338 +
339 + String? normalizePaymentMethod(PaymentType paymentMethod) {
340 + switch (paymentMethod) {
341 + case PaymentType.bankTransfer:
342 + return 'Bank';
343 + case PaymentType.creditCard:
344 + return 'Card';
345 + case PaymentType.sepa:
346 + return 'Instant';
347 + default:
348 + return null;
349 + }
350 + }
351 +
352 + PaymentType _getPaymentTypeByString(String? paymentMethod) {
353 + switch (paymentMethod) {
354 + case 'Bank':
355 + return PaymentType.bankTransfer;
356 + case 'Card':
357 + return PaymentType.creditCard;
358 + case 'Instant':
359 + return PaymentType.sepa;
360 + default:
361 + return PaymentType.all;
362 + }
363 + }
364 +
365 + double? _toDouble(dynamic value) {
366 + if (value is int) {
367 + return value.toDouble();
368 + } else if (value is double) {
369 + return value;
370 + }
371 + return null;
372 + }
373 }
lib/buy/meld/meld_buy_provider.dart new
+266
@@ -0,0 +1,266 @@
1 +import 'dart:convert';
2 +
3 +import 'package:cake_wallet/.secrets.g.dart' as secrets;
4 +import 'package:cake_wallet/buy/buy_provider.dart';
5 +import 'package:cake_wallet/buy/buy_quote.dart';
6 +import 'package:cake_wallet/buy/payment_method.dart';
7 +import 'package:cake_wallet/entities/fiat_currency.dart';
8 +import 'package:cake_wallet/entities/provider_types.dart';
9 +import 'package:cake_wallet/generated/i18n.dart';
10 +import 'package:cake_wallet/routes.dart';
11 +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
12 +import 'package:cake_wallet/utils/device_info.dart';
13 +import 'package:cake_wallet/utils/show_pop_up.dart';
14 +import 'package:cw_core/crypto_currency.dart';
15 +import 'package:cw_core/currency.dart';
16 +import 'package:cw_core/wallet_base.dart';
17 +import 'package:flutter/material.dart';
18 +import 'dart:developer';
19 +import 'package:http/http.dart' as http;
20 +import 'package:url_launcher/url_launcher.dart';
21 +
22 +class MeldBuyProvider extends BuyProvider {
23 + MeldBuyProvider({required WalletBase wallet, bool isTestEnvironment = false})
24 + : super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: null);
25 +
26 + static const _isProduction = false;
27 +
28 + static const _baseUrl = _isProduction ? 'api.meld.io' : 'api-sb.meld.io';
29 + static const _providersProperties = '/service-providers/properties';
30 + static const _paymentMethodsPath = '/payment-methods';
31 + static const _quotePath = '/payments/crypto/quote';
32 +
33 + static const String sandboxUrl = 'sb.fluidmoney.xyz';
34 + static const String productionUrl = 'fluidmoney.xyz';
35 +
36 + static const String _baseWidgetUrl = _isProduction ? productionUrl : sandboxUrl;
37 +
38 + static String get _testApiKey => secrets.meldTestApiKey;
39 +
40 + static String get _testPublicKey => '' ; //secrets.meldTestPublicKey;
41 +
42 + @override
43 + String get title => 'Meld';
44 +
45 + @override
46 + String get providerDescription => 'Meld Buy Provider';
47 +
48 + @override
49 + String get lightIcon => 'assets/images/meld_logo.svg';
50 +
51 + @override
52 + String get darkIcon => 'assets/images/meld_logo.svg';
53 +
54 + @override
55 + bool get isAggregator => true;
56 +
57 + @override
58 + Future<List<PaymentMethod>> getAvailablePaymentTypes(
59 + String fiatCurrency, String cryptoCurrency, bool isBuyAction) async {
60 + final params = {'fiatCurrencies': fiatCurrency, 'statuses': 'LIVE,RECENTLY_ADDED,BUILDING'};
61 +
62 + final path = '$_providersProperties$_paymentMethodsPath';
63 + final url = Uri.https(_baseUrl, path, params);
64 +
65 + try {
66 + final response = await http.get(
67 + url,
68 + headers: {
69 + 'Authorization': _isProduction ? '' : _testApiKey,
70 + 'Meld-Version': '2023-12-19',
71 + 'accept': 'application/json',
72 + 'content-type': 'application/json',
73 + },
74 + );
75 +
76 + if (response.statusCode == 200) {
77 + final data = jsonDecode(response.body) as List<dynamic>;
78 + final paymentMethods =
79 + data.map((e) => PaymentMethod.fromMeldJson(e as Map<String, dynamic>)).toList();
80 + return paymentMethods;
81 + } else {
82 + print('Meld: Failed to fetch payment types');
83 + return List<PaymentMethod>.empty();
84 + }
85 + } catch (e) {
86 + print('Meld: Failed to fetch payment types: $e');
87 + return List<PaymentMethod>.empty();
88 + }
89 + }
90 +
91 + @override
92 + Future<List<Quote>?> fetchQuote(
93 + {required CryptoCurrency cryptoCurrency,
94 + required FiatCurrency fiatCurrency,
95 + required double amount,
96 + required bool isBuyAction,
97 + required String walletAddress,
98 + PaymentType? paymentType,
99 + String? countryCode}) async {
100 + String? paymentMethod;
101 + if (paymentType != null && paymentType != PaymentType.all) {
102 + paymentMethod = normalizePaymentMethod(paymentType);
103 + if (paymentMethod == null) paymentMethod = paymentType.name;
104 + }
105 +
106 + log('Meld: Fetching buy quote: ${isBuyAction ? cryptoCurrency : fiatCurrency} -> ${isBuyAction ? fiatCurrency : cryptoCurrency}, amount: $amount');
107 +
108 + final url = Uri.https(_baseUrl, _quotePath);
109 + final headers = {
110 + 'Authorization': _testApiKey,
111 + 'Meld-Version': '2023-12-19',
112 + 'accept': 'application/json',
113 + 'content-type': 'application/json',
114 + };
115 + final body = jsonEncode({
116 + 'countryCode': countryCode,
117 + 'destinationCurrencyCode': isBuyAction ? fiatCurrency.name : cryptoCurrency.title,
118 + 'sourceAmount': amount,
119 + 'sourceCurrencyCode': isBuyAction ? cryptoCurrency.title : fiatCurrency.name,
120 + if (paymentMethod != null) 'paymentMethod': paymentMethod,
121 + });
122 +
123 + try {
124 + final response = await http.post(url, headers: headers, body: body);
125 +
126 + if (response.statusCode == 200) {
127 + final data = jsonDecode(response.body) as Map<String, dynamic>;
128 + final paymentType = _getPaymentTypeByString(data['paymentMethodType'] as String?);
129 + final quote = Quote.fromMeldJson(data, isBuyAction, paymentType);
130 +
131 + quote.setFiatCurrency = fiatCurrency;
132 + quote.setCryptoCurrency = cryptoCurrency;
133 +
134 + return [quote];
135 + } else {
136 + return null;
137 + }
138 + } catch (e) {
139 + print('Error fetching buy quote: $e');
140 + return null;
141 + }
142 + }
143 +
144 + Future<void>? launchProvider(
145 + {required BuildContext context,
146 + required Quote quote,
147 + required double amount,
148 + required bool isBuyAction,
149 + required String cryptoCurrencyAddress,
150 + String? countryCode}) async {
151 + final actionType = isBuyAction ? 'BUY' : 'SELL';
152 +
153 + final params = {
154 + 'publicKey': _isProduction ? '' : _testPublicKey,
155 + 'countryCode': countryCode,
156 + //'paymentMethodType': normalizePaymentMethod(paymentMethod.paymentMethodType),
157 + 'sourceAmount': amount.toString(),
158 + 'sourceCurrencyCode': quote.fiatCurrency,
159 + 'destinationCurrencyCode': quote.cryptoCurrency,
160 + 'walletAddress': cryptoCurrencyAddress,
161 + 'transactionType': actionType
162 + };
163 +
164 + final uri = Uri.https(_baseWidgetUrl, '', params);
165 +
166 + try {
167 + if (await canLaunchUrl(uri)) {
168 + await launchUrl(uri, mode: LaunchMode.externalApplication);
169 + } else {
170 + throw Exception('Could not launch URL');
171 + }
172 + } catch (e) {
173 + await showPopUp<void>(
174 + context: context,
175 + builder: (BuildContext context) {
176 + return AlertWithOneAction(
177 + alertTitle: "Meld",
178 + alertContent: S.of(context).buy_provider_unavailable + ': $e',
179 + buttonText: S.of(context).ok,
180 + buttonAction: () => Navigator.of(context).pop());
181 + });
182 + }
183 + }
184 +
185 + String? normalizePaymentMethod(PaymentType paymentType) {
186 + switch (paymentType) {
187 + case PaymentType.creditCard:
188 + return 'CREDIT_DEBIT_CARD';
189 + case PaymentType.applePay:
190 + return 'APPLE_PAY';
191 + case PaymentType.googlePay:
192 + return 'GOOGLE_PAY';
193 + case PaymentType.neteller:
194 + return 'NETELLER';
195 + case PaymentType.skrill:
196 + return 'SKRILL';
197 + case PaymentType.sepa:
198 + return 'SEPA';
199 + case PaymentType.sepaInstant:
200 + return 'SEPA_INSTANT';
201 + case PaymentType.ach:
202 + return 'ACH';
203 + case PaymentType.achInstant:
204 + return 'INSTANT_ACH';
205 + case PaymentType.Khipu:
206 + return 'KHIPU';
207 + case PaymentType.ovo:
208 + return 'OVO';
209 + case PaymentType.zaloPay:
210 + return 'ZALOPAY';
211 + case PaymentType.zaloBankTransfer:
212 + return 'ZA_BANK_TRANSFER';
213 + case PaymentType.gcash:
214 + return 'GCASH';
215 + case PaymentType.imps:
216 + return 'IMPS';
217 + case PaymentType.dana:
218 + return 'DANA';
219 + case PaymentType.ideal:
220 + return 'IDEAL';
221 + default:
222 + return null;
223 + }
224 + }
225 +
226 + PaymentType _getPaymentTypeByString(String? paymentMethod) {
227 + switch (paymentMethod?.toUpperCase()) {
228 + case 'CREDIT_DEBIT_CARD':
229 + return PaymentType.creditCard;
230 + case 'APPLE_PAY':
231 + return PaymentType.applePay;
232 + case 'GOOGLE_PAY':
233 + return PaymentType.googlePay;
234 + case 'NETELLER':
235 + return PaymentType.neteller;
236 + case 'SKRILL':
237 + return PaymentType.skrill;
238 + case 'SEPA':
239 + return PaymentType.sepa;
240 + case 'SEPA_INSTANT':
241 + return PaymentType.sepaInstant;
242 + case 'ACH':
243 + return PaymentType.ach;
244 + case 'INSTANT_ACH':
245 + return PaymentType.achInstant;
246 + case 'KHIPU':
247 + return PaymentType.Khipu;
248 + case 'OVO':
249 + return PaymentType.ovo;
250 + case 'ZALOPAY':
251 + return PaymentType.zaloPay;
252 + case 'ZA_BANK_TRANSFER':
253 + return PaymentType.zaloBankTransfer;
254 + case 'GCASH':
255 + return PaymentType.gcash;
256 + case 'IMPS':
257 + return PaymentType.imps;
258 + case 'DANA':
259 + return PaymentType.dana;
260 + case 'IDEAL':
261 + return PaymentType.ideal;
262 + default:
263 + return PaymentType.all;
264 + }
265 + }
266 +}
lib/buy/moonpay/moonpay_provider.dart
+265 -168
@@ -1,19 +1,20 @@
1 import 'dart:convert';
2 +import 'dart:developer';
3
4 import 'package:cake_wallet/.secrets.g.dart' as secrets;
4 -import 'package:cake_wallet/buy/buy_amount.dart';
5 import 'package:cake_wallet/buy/buy_exception.dart';
6 import 'package:cake_wallet/buy/buy_provider.dart';
7 import 'package:cake_wallet/buy/buy_provider_description.dart';
8 +import 'package:cake_wallet/buy/buy_quote.dart';
9 import 'package:cake_wallet/buy/order.dart';
10 +import 'package:cake_wallet/buy/payment_method.dart';
11 +import 'package:cake_wallet/entities/fiat_currency.dart';
12 import 'package:cake_wallet/exchange/trade_state.dart';
13 import 'package:cake_wallet/generated/i18n.dart';
14 import 'package:cake_wallet/palette.dart';
12 -import 'package:cake_wallet/routes.dart';
15 import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
16 import 'package:cake_wallet/store/settings_store.dart';
17 import 'package:cake_wallet/themes/theme_base.dart';
16 -import 'package:cake_wallet/utils/device_info.dart';
18 import 'package:cw_core/crypto_currency.dart';
19 import 'package:cw_core/wallet_base.dart';
20 import 'package:cw_core/wallet_type.dart';
@@ -39,6 +40,15 @@ class MoonPayProvider extends BuyProvider {
40 static const _baseBuyProductUrl = 'buy.moonpay.com';
41 static const _cIdBaseUrl = 'exchange-helper.cakewallet.com';
42 static const _apiUrl = 'https://api.moonpay.com';
43 + static const _baseUrl = 'api.moonpay.com';
44 + static const _currenciesPath = '/v3/currencies';
45 + static const _buyQuote = '/buy_quote';
46 + static const _sellQuote = '/sell_quote';
47 +
48 + static const _transactionsSuffix = '/v1/transactions';
49 +
50 + final String baseBuyUrl;
51 + final String baseSellUrl;
52
53 @override
54 String get providerDescription =>
@@ -53,6 +63,17 @@ class MoonPayProvider extends BuyProvider {
63 @override
64 String get darkIcon => 'assets/images/moonpay_dark.png';
65
66 + @override
67 + bool get isAggregator => false;
68 +
69 + static String get _apiKey => secrets.moonPayApiKey;
70 +
71 + String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase();
72 +
73 + String get trackUrl => baseBuyUrl + '/transaction_receipt?transactionId=';
74 +
75 + static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey;
76 +
77 static String themeToMoonPayTheme(ThemeBase theme) {
78 switch (theme.type) {
79 case ThemeType.bright:
@@ -63,28 +84,12 @@ class MoonPayProvider extends BuyProvider {
84 }
85 }
86
66 - static String get _apiKey => secrets.moonPayApiKey;
67 -
68 - final String baseBuyUrl;
69 - final String baseSellUrl;
70 -
71 - String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase();
72 -
73 - String get trackUrl => baseBuyUrl + '/transaction_receipt?transactionId=';
74 -
75 - static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey;
76 -
87 Future<String> getMoonpaySignature(String query) async {
88 final uri = Uri.https(_cIdBaseUrl, "/api/moonpay");
89
80 - final response = await post(
81 - uri,
82 - headers: {
83 - 'Content-Type': 'application/json',
84 - 'x-api-key': _exchangeHelperApiKey,
85 - },
86 - body: json.encode({'query': query}),
87 - );
90 + final response = await post(uri,
91 + headers: {'Content-Type': 'application/json', 'x-api-key': _exchangeHelperApiKey},
92 + body: json.encode({'query': query}));
93
94 if (response.statusCode == 200) {
95 return (jsonDecode(response.body) as Map<String, dynamic>)['signature'] as String;
@@ -94,85 +99,195 @@ class MoonPayProvider extends BuyProvider {
99 }
100 }
101
97 - Future<Uri> requestSellMoonPayUrl({
98 - required CryptoCurrency currency,
99 - required String refundWalletAddress,
100 - required SettingsStore settingsStore,
101 - }) async {
102 - final params = {
103 - 'theme': themeToMoonPayTheme(settingsStore.currentTheme),
104 - 'language': settingsStore.languageCode,
105 - 'colorCode': settingsStore.currentTheme.type == ThemeType.dark
106 - ? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}'
107 - : '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}',
108 - 'defaultCurrencyCode': _normalizeCurrency(currency),
109 - 'refundWalletAddress': refundWalletAddress,
110 - };
102 + Future<Map<String, dynamic>> fetchFiatCredentials(
103 + String fiatCurrency, String cryptocurrency, String? paymentMethod) async {
104 + final params = {'baseCurrencyCode': fiatCurrency.toLowerCase(), 'apiKey': _apiKey};
105
112 - if (_apiKey.isNotEmpty) {
113 - params['apiKey'] = _apiKey;
114 - }
106 + if (paymentMethod != null) params['paymentMethod'] = paymentMethod;
107
116 - final originalUri = Uri.https(
117 - baseSellUrl,
118 - '',
119 - params,
120 - );
108 + final path = '$_currenciesPath/${cryptocurrency.toLowerCase()}/limits';
109 + final url = Uri.https(_baseUrl, path, params);
110
122 - if (isTestEnvironment) {
123 - return originalUri;
111 + try {
112 + final response = await get(url, headers: {'accept': 'application/json'});
113 + if (response.statusCode == 200) {
114 + return jsonDecode(response.body) as Map<String, dynamic>;
115 + } else {
116 + print('MoonPay does not support fiat: $fiatCurrency');
117 + return {};
118 + }
119 + } catch (e) {
120 + print('MoonPay Error fetching fiat currencies: $e');
121 + return {};
122 }
123 + }
124
126 - final signature = await getMoonpaySignature('?${originalUri.query}');
125 + Future<List<PaymentMethod>> getAvailablePaymentTypes(
126 + String fiatCurrency, String cryptoCurrency, bool isBuyAction) async {
127 + final List<PaymentMethod> paymentMethods = [];
128 +
129 + if (isBuyAction) {
130 + final fiatBuyCredentials = await fetchFiatCredentials(fiatCurrency, cryptoCurrency, null);
131 + if (fiatBuyCredentials.isNotEmpty) {
132 + final paymentMethod = fiatBuyCredentials['paymentMethod'] as String?;
133 + paymentMethods.add(PaymentMethod.fromMoonPayJson(
134 + fiatBuyCredentials, _getPaymentTypeByString(paymentMethod)));
135 + return paymentMethods;
136 + }
137 + }
138
128 - final query = Map<String, dynamic>.from(originalUri.queryParameters);
129 - query['signature'] = signature;
130 - final signedUri = originalUri.replace(queryParameters: query);
131 - return signedUri;
139 + return paymentMethods;
140 }
141
134 - // BUY:
135 - static const _currenciesSuffix = '/v3/currencies';
136 - static const _quoteSuffix = '/buy_quote';
137 - static const _transactionsSuffix = '/v1/transactions';
138 - static const _ipAddressSuffix = '/v4/ip_address';
142 + @override
143 + Future<List<Quote>?> fetchQuote(
144 + {required CryptoCurrency cryptoCurrency,
145 + required FiatCurrency fiatCurrency,
146 + required double amount,
147 + required bool isBuyAction,
148 + required String walletAddress,
149 + PaymentType? paymentType,
150 + String? countryCode}) async {
151 + String? paymentMethod;
152 +
153 + if (paymentType != null && paymentType != PaymentType.all) {
154 + paymentMethod = normalizePaymentMethod(paymentType);
155 + if (paymentMethod == null) paymentMethod = paymentType.name;
156 + } else {
157 + paymentMethod = 'credit_debit_card';
158 + }
159 +
160 + final action = isBuyAction ? 'buy' : 'sell';
161 +
162 + final formattedCryptoCurrency = _normalizeCurrency(cryptoCurrency);
163 + final baseCurrencyCode =
164 + isBuyAction ? fiatCurrency.name.toLowerCase() : cryptoCurrency.title.toLowerCase();
165
140 - Future<Uri> requestBuyMoonPayUrl({
141 - required CryptoCurrency currency,
142 - required SettingsStore settingsStore,
143 - required String walletAddress,
144 - String? amount,
145 - }) async {
166 final params = {
147 - 'theme': themeToMoonPayTheme(settingsStore.currentTheme),
148 - 'language': settingsStore.languageCode,
149 - 'colorCode': settingsStore.currentTheme.type == ThemeType.dark
167 + 'baseCurrencyCode': baseCurrencyCode,
168 + 'baseCurrencyAmount': amount.toString(),
169 + 'amount': amount.toString(),
170 + 'paymentMethod': paymentMethod,
171 + 'areFeesIncluded': 'false',
172 + 'apiKey': _apiKey
173 + };
174 +
175 + log('MoonPay: Fetching $action quote: ${isBuyAction ? formattedCryptoCurrency : fiatCurrency.name.toLowerCase()} -> ${isBuyAction ? baseCurrencyCode : formattedCryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod');
176 +
177 + final quotePath = isBuyAction ? _buyQuote : _sellQuote;
178 +
179 + final path = '$_currenciesPath/$formattedCryptoCurrency$quotePath';
180 + final url = Uri.https(_baseUrl, path, params);
181 + try {
182 + final response = await get(url);
183 +
184 + if (response.statusCode == 200) {
185 + final data = jsonDecode(response.body) as Map<String, dynamic>;
186 +
187 + // Check if the response is for the correct fiat currency
188 + if (isBuyAction) {
189 + final fiatCurrencyCode = data['baseCurrencyCode'] as String?;
190 + if (fiatCurrencyCode == null || fiatCurrencyCode != fiatCurrency.name.toLowerCase())
191 + return null;
192 + } else {
193 + final quoteCurrency = data['quoteCurrency'] as Map<String, dynamic>?;
194 + if (quoteCurrency == null || quoteCurrency['code'] != fiatCurrency.name.toLowerCase())
195 + return null;
196 + }
197 +
198 + final paymentMethods = data['paymentMethod'] as String?;
199 + final quote =
200 + Quote.fromMoonPayJson(data, isBuyAction, _getPaymentTypeByString(paymentMethods));
201 +
202 + quote.setFiatCurrency = fiatCurrency;
203 + quote.setCryptoCurrency = cryptoCurrency;
204 +
205 + return [quote];
206 + } else {
207 + print('Moon Pay: Error fetching buy quote: ');
208 + return null;
209 + }
210 + } catch (e) {
211 + print('Moon Pay: Error fetching buy quote: $e');
212 + return null;
213 + }
214 + }
215 +
216 + @override
217 + Future<void>? launchProvider(
218 + {required BuildContext context,
219 + required Quote quote,
220 + required double amount,
221 + required bool isBuyAction,
222 + required String cryptoCurrencyAddress,
223 + String? countryCode}) async {
224 +
225 + final Map<String, String> params = {
226 + 'theme': themeToMoonPayTheme(_settingsStore.currentTheme),
227 + 'language': _settingsStore.languageCode,
228 + 'colorCode': _settingsStore.currentTheme.type == ThemeType.dark
229 ? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}'
230 : '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}',
152 - 'baseCurrencyCode': settingsStore.fiatCurrency.title,
153 - 'baseCurrencyAmount': amount ?? '0',
154 - 'currencyCode': _normalizeCurrency(currency),
155 - 'walletAddress': walletAddress,
231 + 'baseCurrencyCode': isBuyAction ? quote.fiatCurrency.name : quote.cryptoCurrency.name,
232 + 'baseCurrencyAmount': amount.toString(),
233 + 'walletAddress': cryptoCurrencyAddress,
234 'lockAmount': 'false',
235 'showAllCurrencies': 'false',
236 'showWalletAddressForm': 'false',
159 - 'enabledPaymentMethods':
160 - 'credit_debit_card,apple_pay,google_pay,samsung_pay,sepa_bank_transfer,gbp_bank_transfer,gbp_open_banking_payment',
237 + if (isBuyAction)
238 + 'enabledPaymentMethods': normalizePaymentMethod(quote.paymentType) ??
239 + 'credit_debit_card,apple_pay,google_pay,samsung_pay,sepa_bank_transfer,gbp_bank_transfer,gbp_open_banking_payment',
240 + if (!isBuyAction) 'refundWalletAddress': cryptoCurrencyAddress
241 };
242
163 - if (_apiKey.isNotEmpty) {
164 - params['apiKey'] = _apiKey;
243 + if (isBuyAction) params['currencyCode'] = quote.cryptoCurrency.name;
244 + if (!isBuyAction) params['quoteCurrencyCode'] = quote.cryptoCurrency.name;
245 +
246 + try {
247 + {
248 + final uri = await requestMoonPayUrl(
249 + walletAddress: cryptoCurrencyAddress,
250 + settingsStore: _settingsStore,
251 + isBuyAction: isBuyAction,
252 + amount: amount.toString(),
253 + params: params);
254 +
255 + if (await canLaunchUrl(uri)) {
256 + await launchUrl(uri, mode: LaunchMode.externalApplication);
257 + } else {
258 + throw Exception('Could not launch URL');
259 + }
260 + }
261 + } catch (e) {
262 + if (context.mounted) {
263 + await showDialog<void>(
264 + context: context,
265 + builder: (BuildContext context) {
266 + return AlertWithOneAction(
267 + alertTitle: 'MoonPay',
268 + alertContent: 'The MoonPay service is currently unavailable: $e',
269 + buttonText: S.of(context).ok,
270 + buttonAction: () => Navigator.of(context).pop(),
271 + );
272 + },
273 + );
274 + }
275 }
276 + }
277
167 - final originalUri = Uri.https(
168 - baseBuyUrl,
169 - '',
170 - params,
171 - );
278 + Future<Uri> requestMoonPayUrl({
279 + required String walletAddress,
280 + required SettingsStore settingsStore,
281 + required bool isBuyAction,
282 + required Map<String, String> params,
283 + String? amount,
284 + }) async {
285 + if (_apiKey.isNotEmpty) params['apiKey'] = _apiKey;
286
173 - if (isTestEnvironment) {
174 - return originalUri;
175 - }
287 + final baseUrl = isBuyAction ? baseBuyUrl : baseSellUrl;
288 + final originalUri = Uri.https(baseUrl, '', params);
289 +
290 + if (isTestEnvironment) return originalUri;
291
292 final signature = await getMoonpaySignature('?${originalUri.query}');
293 final query = Map<String, dynamic>.from(originalUri.queryParameters);
@@ -181,33 +296,6 @@ class MoonPayProvider extends BuyProvider {
296 return signedUri;
297 }
298
184 - Future<BuyAmount> calculateAmount(String amount, String sourceCurrency) async {
185 - final url = _apiUrl +
186 - _currenciesSuffix +
187 - '/$currencyCode' +
188 - _quoteSuffix +
189 - '/?apiKey=' +
190 - _apiKey +
191 - '&baseCurrencyAmount=' +
192 - amount +
193 - '&baseCurrencyCode=' +
194 - sourceCurrency.toLowerCase();
195 - final uri = Uri.parse(url);
196 - final response = await get(uri);
197 -
198 - if (response.statusCode != 200) {
199 - throw BuyException(title: providerDescription, content: 'Quote is not found!');
200 - }
201 -
202 - final responseJSON = json.decode(response.body) as Map<String, dynamic>;
203 - final sourceAmount = responseJSON['totalAmount'] as double;
204 - final destAmount = responseJSON['quoteCurrencyAmount'] as double;
205 - final minSourceAmount = responseJSON['baseCurrency']['minAmount'] as int;
206 -
207 - return BuyAmount(
208 - sourceAmount: sourceAmount, destAmount: destAmount, minAmount: minSourceAmount);
209 - }
210 -
299 Future<Order> findOrderById(String id) async {
300 final url = _apiUrl + _transactionsSuffix + '/$id' + '?apiKey=' + _apiKey;
301 final uri = Uri.parse(url);
@@ -235,74 +323,83 @@ class MoonPayProvider extends BuyProvider {
323 walletId: wallet.id);
324 }
325
238 - static Future<bool> onEnabled() async {
239 - final url = _apiUrl + _ipAddressSuffix + '?apiKey=' + _apiKey;
240 - var isBuyEnable = false;
241 - final uri = Uri.parse(url);
242 - final response = await get(uri);
326 + String _normalizeCurrency(CryptoCurrency currency) {
327 + if (currency.tag == 'POLY') {
328 + return '${currency.title.toLowerCase()}_polygon';
329 + }
330
244 - try {
245 - final responseJSON = json.decode(response.body) as Map<String, dynamic>;
246 - isBuyEnable = responseJSON['isBuyAllowed'] as bool;
247 - } catch (e) {
248 - isBuyEnable = false;
249 - print(e.toString());
331 + if (currency.tag == 'TRX') {
332 + return '${currency.title.toLowerCase()}_trx';
333 }
334
252 - return isBuyEnable;
335 + return currency.toString().toLowerCase();
336 }
337
255 - @override
256 - Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
257 - try {
258 - late final Uri uri;
259 - if (isBuyAction ?? true) {
260 - uri = await requestBuyMoonPayUrl(
261 - currency: wallet.currency,
262 - walletAddress: wallet.walletAddresses.address,
263 - settingsStore: _settingsStore,
264 - );
265 - } else {
266 - uri = await requestSellMoonPayUrl(
267 - currency: wallet.currency,
268 - refundWalletAddress: wallet.walletAddresses.address,
269 - settingsStore: _settingsStore,
270 - );
271 - }
272 -
273 - if (await canLaunchUrl(uri)) {
274 - if (DeviceInfo.instance.isMobile) {
275 - Navigator.of(context).pushNamed(Routes.webViewPage, arguments: ['MoonPay', uri]);
276 - } else {
277 - await launchUrl(uri, mode: LaunchMode.externalApplication);
278 - }
279 - } else {
280 - throw Exception('Could not launch URL');
281 - }
282 - } catch (e) {
283 - if (context.mounted) {
284 - await showDialog<void>(
285 - context: context,
286 - builder: (BuildContext context) {
287 - return AlertWithOneAction(
288 - alertTitle: 'MoonPay',
289 - alertContent: 'The MoonPay service is currently unavailable: $e',
290 - buttonText: S.of(context).ok,
291 - buttonAction: () => Navigator.of(context).pop(),
292 - );
293 - },
294 - );
295 - }
338 + String? normalizePaymentMethod(PaymentType paymentMethod) {
339 + switch (paymentMethod) {
340 + case PaymentType.creditCard:
341 + return 'credit_debit_card';
342 + case PaymentType.debitCard:
343 + return 'credit_debit_card';
344 + case PaymentType.ach:
345 + return 'ach_bank_transfer';
346 + case PaymentType.applePay:
347 + return 'apple_pay';
348 + case PaymentType.googlePay:
349 + return 'google_pay';
350 + case PaymentType.sepa:
351 + return 'sepa_bank_transfer';
352 + case PaymentType.paypal:
353 + return 'paypal';
354 + case PaymentType.sepaOpenBankingPayment:
355 + return 'sepa_open_banking_payment';
356 + case PaymentType.gbpOpenBankingPayment:
357 + return 'gbp_open_banking_payment';
358 + case PaymentType.lowCostAch:
359 + return 'low_cost_ach';
360 + case PaymentType.mobileWallet:
361 + return 'mobile_wallet';
362 + case PaymentType.pixInstantPayment:
363 + return 'pix_instant_payment';
364 + case PaymentType.yellowCardBankTransfer:
365 + return 'yellow_card_bank_transfer';
366 + case PaymentType.fiatBalance:
367 + return 'fiat_balance';
368 + default:
369 + return null;
370 }
371 }
372
299 - String _normalizeCurrency(CryptoCurrency currency) {
300 - if (currency == CryptoCurrency.maticpoly) {
301 - return "POL_POLYGON";
302 - } else if (currency == CryptoCurrency.matic) {
303 - return "POL";
373 + PaymentType _getPaymentTypeByString(String? paymentMethod) {
374 + switch (paymentMethod) {
375 + case 'ach_bank_transfer':
376 + return PaymentType.ach;
377 + case 'apple_pay':
378 + return PaymentType.applePay;
379 + case 'credit_debit_card':
380 + return PaymentType.creditCard;
381 + case 'fiat_balance':
382 + return PaymentType.fiatBalance;
383 + case 'gbp_open_banking_payment':
384 + return PaymentType.gbpOpenBankingPayment;
385 + case 'google_pay':
386 + return PaymentType.googlePay;
387 + case 'low_cost_ach':
388 + return PaymentType.lowCostAch;
389 + case 'mobile_wallet':
390 + return PaymentType.mobileWallet;
391 + case 'paypal':
392 + return PaymentType.paypal;
393 + case 'pix_instant_payment':
394 + return PaymentType.pixInstantPayment;
395 + case 'sepa_bank_transfer':
396 + return PaymentType.sepa;
397 + case 'sepa_open_banking_payment':
398 + return PaymentType.sepaOpenBankingPayment;
399 + case 'yellow_card_bank_transfer':
400 + return PaymentType.yellowCardBankTransfer;
401 + default:
402 + return PaymentType.all;
403 }
305 -
306 - return currency.toString().toLowerCase();
404 }
405 }
lib/buy/onramper/onramper_buy_provider.dart
+313 -48
@@ -1,13 +1,19 @@
1 +import 'dart:convert';
2 +import 'dart:developer';
3 +
4 import 'package:cake_wallet/.secrets.g.dart' as secrets;
5 import 'package:cake_wallet/buy/buy_provider.dart';
6 +import 'package:cake_wallet/buy/buy_quote.dart';
7 +import 'package:cake_wallet/buy/payment_method.dart';
8 +import 'package:cake_wallet/entities/fiat_currency.dart';
9 import 'package:cake_wallet/generated/i18n.dart';
4 -import 'package:cake_wallet/routes.dart';
10 import 'package:cake_wallet/store/settings_store.dart';
11 import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
7 -import 'package:cake_wallet/utils/device_info.dart';
12 import 'package:cw_core/crypto_currency.dart';
13 +import 'package:cw_core/currency.dart';
14 import 'package:cw_core/wallet_base.dart';
15 import 'package:flutter/material.dart';
16 +import 'package:http/http.dart' as http;
17 import 'package:url_launcher/url_launcher.dart';
18
19 class OnRamperBuyProvider extends BuyProvider {
@@ -16,9 +22,15 @@ class OnRamperBuyProvider extends BuyProvider {
22 : super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: null);
23
24 static const _baseUrl = 'buy.onramper.com';
25 + static const _baseApiUrl = 'api.onramper.com';
26 + static const quotes = '/quotes';
27 + static const paymentTypes = '/payment-types';
28 + static const supported = '/supported';
29
30 final SettingsStore _settingsStore;
31
32 + String get _apiKey => secrets.onramperApiKey;
33 +
34 @override
35 String get title => 'Onramper';
36
@@ -31,74 +43,327 @@ class OnRamperBuyProvider extends BuyProvider {
43 @override
44 String get darkIcon => 'assets/images/onramper_dark.png';
45
34 - String get _apiKey => secrets.onramperApiKey;
46 + @override
47 + bool get isAggregator => true;
48
36 - String get _normalizeCryptoCurrency {
37 - switch (wallet.currency) {
38 - case CryptoCurrency.ltc:
39 - return "LTC_LITECOIN";
40 - case CryptoCurrency.xmr:
41 - return "XMR_MONERO";
42 - case CryptoCurrency.bch:
43 - return "BCH_BITCOINCASH";
44 - case CryptoCurrency.nano:
45 - return "XNO_NANO";
46 - default:
47 - return wallet.currency.title;
49 + Future<List<PaymentMethod>> getAvailablePaymentTypes(
50 + String fiatCurrency, String cryptoCurrency, bool isBuyAction) async {
51 + final params = {
52 + 'fiatCurrency': fiatCurrency,
53 + 'type': isBuyAction ? 'buy' : 'sell',
54 + 'isRecurringPayment': 'false'
55 + };
56 +
57 + final url = Uri.https(_baseApiUrl, '$supported$paymentTypes/$fiatCurrency', params);
58 +
59 + try {
60 + final response =
61 + await http.get(url, headers: {'Authorization': _apiKey, 'accept': 'application/json'});
62 +
63 + if (response.statusCode == 200) {
64 + final Map<String, dynamic> data = jsonDecode(response.body) as Map<String, dynamic>;
65 + final List<dynamic> message = data['message'] as List<dynamic>;
66 + return message
67 + .map((item) => PaymentMethod.fromOnramperJson(item as Map<String, dynamic>))
68 + .toList();
69 + } else {
70 + print('Failed to fetch available payment types');
71 + return [];
72 + }
73 + } catch (e) {
74 + print('Failed to fetch available payment types: $e');
75 + return [];
76 }
77 }
78
51 - String getColorStr(Color color) {
52 - return color.value.toRadixString(16).replaceAll(RegExp(r'^ff'), "");
79 + Future<Map<String, dynamic>> getOnrampMetadata() async {
80 + final url = Uri.https(_baseApiUrl, '$supported/onramps/all');
81 +
82 + try {
83 + final response =
84 + await http.get(url, headers: {'Authorization': _apiKey, 'accept': 'application/json'});
85 +
86 + if (response.statusCode == 200) {
87 + final Map<String, dynamic> data = jsonDecode(response.body) as Map<String, dynamic>;
88 +
89 + final List<dynamic> onramps = data['message'] as List<dynamic>;
90 +
91 + final Map<String, dynamic> result = {
92 + for (var onramp in onramps)
93 + (onramp['id'] as String): {
94 + 'displayName': onramp['displayName'] as String,
95 + 'svg': onramp['icons']['svg'] as String
96 + }
97 + };
98 +
99 + return result;
100 + } else {
101 + print('Failed to fetch onramp metadata');
102 + return {};
103 + }
104 + } catch (e) {
105 + print('Error occurred: $e');
106 + return {};
107 + }
108 }
109
55 - Uri requestOnramperUrl(BuildContext context, bool? isBuyAction) {
56 - String primaryColor,
57 - secondaryColor,
58 - primaryTextColor,
59 - secondaryTextColor,
60 - containerColor,
61 - cardColor;
62 -
63 - primaryColor = getColorStr(Theme.of(context).primaryColor);
64 - secondaryColor = getColorStr(Theme.of(context).colorScheme.background);
65 - primaryTextColor =
66 - getColorStr(Theme.of(context).extension<CakeTextTheme>()!.titleColor);
67 - secondaryTextColor = getColorStr(
68 - Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor);
69 - containerColor = getColorStr(Theme.of(context).colorScheme.background);
70 - cardColor = getColorStr(Theme.of(context).cardColor);
110 + @override
111 + Future<List<Quote>?> fetchQuote(
112 + {required CryptoCurrency cryptoCurrency,
113 + required FiatCurrency fiatCurrency,
114 + required double amount,
115 + required bool isBuyAction,
116 + required String walletAddress,
117 + PaymentType? paymentType,
118 + String? countryCode}) async {
119 + String? paymentMethod;
120 +
121 + if (paymentType != null && paymentType != PaymentType.all) {
122 + paymentMethod = normalizePaymentMethod(paymentType);
123 + if (paymentMethod == null) paymentMethod = paymentType.name;
124 + }
125 +
126 + final actionType = isBuyAction ? 'buy' : 'sell';
127 +
128 + final normalizedCryptoCurrency = _getNormalizeCryptoCurrency(cryptoCurrency);
129 +
130 + final params = {
131 + 'amount': amount.toString(),
132 + if (paymentMethod != null) 'paymentMethod': paymentMethod,
133 + 'clientName': 'CakeWallet',
134 + 'type': actionType,
135 + 'walletAddress': walletAddress,
136 + 'isRecurringPayment': 'false',
137 + 'input': 'source',
138 + };
139 +
140 + log('Onramper: Fetching $actionType quote: ${isBuyAction ? normalizedCryptoCurrency : fiatCurrency.name} -> ${isBuyAction ? fiatCurrency.name : normalizedCryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod');
141 +
142 + final sourceCurrency = isBuyAction ? fiatCurrency.name : normalizedCryptoCurrency;
143 + final destinationCurrency = isBuyAction ? normalizedCryptoCurrency : fiatCurrency.name;
144 +
145 + final url = Uri.https(_baseApiUrl, '$quotes/${sourceCurrency}/${destinationCurrency}', params);
146 + final headers = {'Authorization': _apiKey, 'accept': 'application/json'};
147 +
148 + try {
149 + final response = await http.get(url, headers: headers);
150 +
151 + if (response.statusCode == 200) {
152 + final data = jsonDecode(response.body) as List<dynamic>;
153 + if (data.isEmpty) return null;
154 +
155 + List<Quote> validQuotes = [];
156 +
157 + final onrampMetadata = await getOnrampMetadata();
158 +
159 + for (var item in data) {
160 +
161 + if (item['errors'] != null) continue;
162 +
163 + final paymentMethod = (item as Map<String, dynamic>)['paymentMethod'] as String;
164 +
165 + final rampId = item['ramp'] as String?;
166 + final rampMetaData = onrampMetadata[rampId] as Map<String, dynamic>?;
167 +
168 + if (rampMetaData == null) continue;
169 +
170 + final quote = Quote.fromOnramperJson(
171 + item, isBuyAction, onrampMetadata, _getPaymentTypeByString(paymentMethod));
172 + quote.setFiatCurrency = fiatCurrency;
173 + quote.setCryptoCurrency = cryptoCurrency;
174 + validQuotes.add(quote);
175 + }
176 +
177 + if (validQuotes.isEmpty) return null;
178 +
179 + return validQuotes;
180 + } else {
181 + print('Onramper: Failed to fetch rate');
182 + return null;
183 + }
184 + } catch (e) {
185 + print('Onramper: Failed to fetch rate $e');
186 + return null;
187 + }
188 + }
189 +
190 + Future<void>? launchProvider(
191 + {required BuildContext context,
192 + required Quote quote,
193 + required double amount,
194 + required bool isBuyAction,
195 + required String cryptoCurrencyAddress,
196 + String? countryCode}) async {
197 + final actionType = isBuyAction ? 'buy' : 'sell';
198 + final prefix = actionType == 'sell' ? actionType + '_' : '';
199 +
200 + final primaryColor = getColorStr(Theme.of(context).primaryColor);
201 + final secondaryColor = getColorStr(Theme.of(context).colorScheme.background);
202 + final primaryTextColor = getColorStr(Theme.of(context).extension<CakeTextTheme>()!.titleColor);
203 + final secondaryTextColor =
204 + getColorStr(Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor);
205 + final containerColor = getColorStr(Theme.of(context).colorScheme.background);
206 + var cardColor = getColorStr(Theme.of(context).cardColor);
207
208 if (_settingsStore.currentTheme.title == S.current.high_contrast_theme) {
209 cardColor = getColorStr(Colors.white);
210 }
211
76 - final networkName =
77 - wallet.currency.fullName?.toUpperCase().replaceAll(" ", "");
212 + final defaultCrypto = _getNormalizeCryptoCurrency(quote.cryptoCurrency);
213 +
214 + final paymentMethod = normalizePaymentMethod(quote.paymentType);
215
79 - return Uri.https(_baseUrl, '', <String, dynamic>{
216 + final uri = Uri.https(_baseUrl, '', {
217 'apiKey': _apiKey,
81 - 'defaultCrypto': _normalizeCryptoCurrency,
82 - 'sell_defaultCrypto': _normalizeCryptoCurrency,
83 - 'networkWallets': '${networkName}:${wallet.walletAddresses.address}',
218 + 'mode': actionType,
219 + '${prefix}defaultFiat': quote.fiatCurrency.name,
220 + '${prefix}defaultCrypto': quote.cryptoCurrency.name,
221 + '${prefix}defaultAmount': amount.toString(),
222 + if (paymentMethod != null) '${prefix}defaultPaymentMethod': paymentMethod,
223 + 'onlyOnramps': quote.rampId,
224 + 'networkWallets': '$defaultCrypto:$cryptoCurrencyAddress',
225 + 'walletAddress': cryptoCurrencyAddress,
226 'supportSwap': "false",
227 'primaryColor': primaryColor,
228 'secondaryColor': secondaryColor,
229 + 'containerColor': containerColor,
230 'primaryTextColor': primaryTextColor,
231 'secondaryTextColor': secondaryTextColor,
89 - 'containerColor': containerColor,
232 'cardColor': cardColor,
91 - 'mode': isBuyAction == true ? 'buy' : 'sell',
233 });
93 - }
234
95 - Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
96 - final uri = requestOnramperUrl(context, isBuyAction);
97 - if (DeviceInfo.instance.isMobile) {
98 - Navigator.of(context)
99 - .pushNamed(Routes.webViewPage, arguments: [title, uri]);
235 + if (await canLaunchUrl(uri)) {
236 + await launchUrl(uri, mode: LaunchMode.externalApplication);
237 } else {
101 - await launchUrl(uri);
238 + throw Exception('Could not launch URL');
239 + }
240 + }
241 +
242 + List<CryptoCurrency> mainCurrency = [
243 + CryptoCurrency.btc,
244 + CryptoCurrency.eth,
245 + CryptoCurrency.sol,
246 + ];
247 +
248 + String _tagToNetwork(String tag) {
249 + switch (tag) {
250 + case 'OMNI':
251 + return tag;
252 + case 'POL':
253 + return 'POLYGON';
254 + default:
255 + return CryptoCurrency.fromString(tag).fullName ?? tag;
256 + }
257 + }
258 +
259 + String _getNormalizeCryptoCurrency(Currency currency) {
260 + if (currency is CryptoCurrency) {
261 + if (!mainCurrency.contains(currency)) {
262 + final network = currency.tag == null ? currency.fullName : _tagToNetwork(currency.tag!);
263 + return '${currency.title}_${network?.replaceAll(' ', '')}'.toUpperCase();
264 + }
265 + return currency.title.toUpperCase();
266 }
267 + return currency.name.toUpperCase();
268 }
269 +
270 + String? normalizePaymentMethod(PaymentType paymentType) {
271 + switch (paymentType) {
272 + case PaymentType.bankTransfer:
273 + return 'banktransfer';
274 + case PaymentType.creditCard:
275 + return 'creditcard';
276 + case PaymentType.debitCard:
277 + return 'debitcard';
278 + case PaymentType.applePay:
279 + return 'applepay';
280 + case PaymentType.googlePay:
281 + return 'googlepay';
282 + case PaymentType.revolutPay:
283 + return 'revolutpay';
284 + case PaymentType.neteller:
285 + return 'neteller';
286 + case PaymentType.skrill:
287 + return 'skrill';
288 + case PaymentType.sepa:
289 + return 'sepabanktransfer';
290 + case PaymentType.sepaInstant:
291 + return 'sepainstant';
292 + case PaymentType.ach:
293 + return 'ach';
294 + case PaymentType.achInstant:
295 + return 'iach';
296 + case PaymentType.Khipu:
297 + return 'khipu';
298 + case PaymentType.palomaBanktTansfer:
299 + return 'palomabanktransfer';
300 + case PaymentType.ovo:
301 + return 'ovo';
302 + case PaymentType.zaloPay:
303 + return 'zalopay';
304 + case PaymentType.zaloBankTransfer:
305 + return 'zalobanktransfer';
306 + case PaymentType.gcash:
307 + return 'gcash';
308 + case PaymentType.imps:
309 + return 'imps';
310 + case PaymentType.dana:
311 + return 'dana';
312 + case PaymentType.ideal:
313 + return 'ideal';
314 + default:
315 + return null;
316 + }
317 + }
318 +
319 + PaymentType _getPaymentTypeByString(String paymentMethod) {
320 + switch (paymentMethod.toLowerCase()) {
321 + case 'banktransfer':
322 + return PaymentType.bankTransfer;
323 + case 'creditcard':
324 + return PaymentType.creditCard;
325 + case 'debitcard':
326 + return PaymentType.debitCard;
327 + case 'applepay':
328 + return PaymentType.applePay;
329 + case 'googlepay':
330 + return PaymentType.googlePay;
331 + case 'revolutpay':
332 + return PaymentType.revolutPay;
333 + case 'neteller':
334 + return PaymentType.neteller;
335 + case 'skrill':
336 + return PaymentType.skrill;
337 + case 'sepabanktransfer':
338 + return PaymentType.sepa;
339 + case 'sepainstant':
340 + return PaymentType.sepaInstant;
341 + case 'ach':
342 + return PaymentType.ach;
343 + case 'iach':
344 + return PaymentType.achInstant;
345 + case 'khipu':
346 + return PaymentType.Khipu;
347 + case 'palomabanktransfer':
348 + return PaymentType.palomaBanktTansfer;
349 + case 'ovo':
350 + return PaymentType.ovo;
351 + case 'zalopay':
352 + return PaymentType.zaloPay;
353 + case 'zalobanktransfer':
354 + return PaymentType.zaloBankTransfer;
355 + case 'gcash':
356 + return PaymentType.gcash;
357 + case 'imps':
358 + return PaymentType.imps;
359 + case 'dana':
360 + return PaymentType.dana;
361 + case 'ideal':
362 + return PaymentType.ideal;
363 + default:
364 + return PaymentType.all;
365 + }
366 + }
367 +
368 + String getColorStr(Color color) => color.value.toRadixString(16).replaceAll(RegExp(r'^ff'), "");
369 }
lib/buy/payment_method.dart new
+287
@@ -0,0 +1,287 @@
1 +import 'dart:ui';
2 +
3 +import 'package:cake_wallet/core/selectable_option.dart';
4 +
5 +enum PaymentType {
6 + all,
7 + bankTransfer,
8 + creditCard,
9 + debitCard,
10 + applePay,
11 + googlePay,
12 + revolutPay,
13 + neteller,
14 + skrill,
15 + sepa,
16 + sepaInstant,
17 + ach,
18 + achInstant,
19 + Khipu,
20 + palomaBanktTansfer,
21 + ovo,
22 + zaloPay,
23 + zaloBankTransfer,
24 + gcash,
25 + imps,
26 + dana,
27 + ideal,
28 + paypal,
29 + sepaOpenBankingPayment,
30 + gbpOpenBankingPayment,
31 + lowCostAch,
32 + mobileWallet,
33 + pixInstantPayment,
34 + yellowCardBankTransfer,
35 + fiatBalance,
36 + bancontact,
37 +}
38 +
39 +extension PaymentTypeTitle on PaymentType {
40 + String? get title {
41 + switch (this) {
42 + case PaymentType.all:
43 + return 'All Payment Methods';
44 + case PaymentType.bankTransfer:
45 + return 'Bank Transfer';
46 + case PaymentType.creditCard:
47 + return 'Credit Card';
48 + case PaymentType.debitCard:
49 + return 'Debit Card';
50 + case PaymentType.applePay:
51 + return 'Apple Pay';
52 + case PaymentType.googlePay:
53 + return 'Google Pay';
54 + case PaymentType.revolutPay:
55 + return 'Revolut Pay';
56 + case PaymentType.neteller:
57 + return 'Neteller';
58 + case PaymentType.skrill:
59 + return 'Skrill';
60 + case PaymentType.sepa:
61 + return 'SEPA';
62 + case PaymentType.sepaInstant:
63 + return 'SEPA Instant';
64 + case PaymentType.ach:
65 + return 'ACH';
66 + case PaymentType.achInstant:
67 + return 'ACH Instant';
68 + case PaymentType.Khipu:
69 + return 'Khipu';
70 + case PaymentType.palomaBanktTansfer:
71 + return 'Paloma Bank Transfer';
72 + case PaymentType.ovo:
73 + return 'OVO';
74 + case PaymentType.zaloPay:
75 + return 'Zalo Pay';
76 + case PaymentType.zaloBankTransfer:
77 + return 'Zalo Bank Transfer';
78 + case PaymentType.gcash:
79 + return 'GCash';
80 + case PaymentType.imps:
81 + return 'IMPS';
82 + case PaymentType.dana:
83 + return 'DANA';
84 + case PaymentType.ideal:
85 + return 'iDEAL';
86 + case PaymentType.paypal:
87 + return 'PayPal';
88 + case PaymentType.sepaOpenBankingPayment:
89 + return 'SEPA Open Banking Payment';
90 + case PaymentType.gbpOpenBankingPayment:
91 + return 'GBP Open Banking Payment';
92 + case PaymentType.lowCostAch:
93 + return 'Low Cost ACH';
94 + case PaymentType.mobileWallet:
95 + return 'Mobile Wallet';
96 + case PaymentType.pixInstantPayment:
97 + return 'PIX Instant Payment';
98 + case PaymentType.yellowCardBankTransfer:
99 + return 'Yellow Card Bank Transfer';
100 + case PaymentType.fiatBalance:
101 + return 'Fiat Balance';
102 + case PaymentType.bancontact:
103 + return 'Bancontact';
104 + default:
105 + return null;
106 + }
107 + }
108 +
109 + String? get lightIconPath {
110 + switch (this) {
111 + case PaymentType.all:
112 + return 'assets/images/usd_round_light.svg';
113 + case PaymentType.creditCard:
114 + case PaymentType.debitCard:
115 + case PaymentType.yellowCardBankTransfer:
116 + return 'assets/images/card.svg';
117 + case PaymentType.bankTransfer:
118 + return 'assets/images/bank_light.svg';
119 + case PaymentType.skrill:
120 + return 'assets/images/skrill.svg';
121 + case PaymentType.applePay:
122 + return 'assets/images/apple_pay_round_light.svg';
123 + default:
124 + return null;
125 + }
126 + }
127 +
128 + String? get darkIconPath {
129 + switch (this) {
130 + case PaymentType.all:
131 + return 'assets/images/usd_round_dark.svg';
132 + case PaymentType.creditCard:
133 + case PaymentType.debitCard:
134 + case PaymentType.yellowCardBankTransfer:
135 + return 'assets/images/card_dark.svg';
136 + case PaymentType.bankTransfer:
137 + return 'assets/images/bank_dark.svg';
138 + case PaymentType.skrill:
139 + return 'assets/images/skrill.svg';
140 + case PaymentType.applePay:
141 + return 'assets/images/apple_pay_round_dark.svg';
142 + default:
143 + return null;
144 + }
145 + }
146 +
147 + String? get description {
148 + switch (this) {
149 + default:
150 + return null;
151 + }
152 + }
153 +}
154 +
155 +class PaymentMethod extends SelectableOption {
156 + PaymentMethod({
157 + required this.paymentMethodType,
158 + required this.customTitle,
159 + required this.customIconPath,
160 + this.customDescription,
161 + }) : super(title: paymentMethodType.title ?? customTitle);
162 +
163 + final PaymentType paymentMethodType;
164 + final String customTitle;
165 + final String customIconPath;
166 + final String? customDescription;
167 + bool isSelected = false;
168 +
169 + @override
170 + String? get description => paymentMethodType.description ?? customDescription;
171 +
172 + @override
173 + String get lightIconPath => paymentMethodType.lightIconPath ?? customIconPath;
174 +
175 + @override
176 + String get darkIconPath => paymentMethodType.darkIconPath ?? customIconPath;
177 +
178 + @override
179 + bool get isOptionSelected => isSelected;
180 +
181 + factory PaymentMethod.all() {
182 + return PaymentMethod(
183 + paymentMethodType: PaymentType.all,
184 + customTitle: 'All Payment Methods',
185 + customIconPath: 'assets/images/dollar_coin.svg');
186 + }
187 +
188 + factory PaymentMethod.fromOnramperJson(Map<String, dynamic> json) {
189 + final type = PaymentMethod.getPaymentTypeId(json['paymentTypeId'] as String?);
190 + return PaymentMethod(
191 + paymentMethodType: type,
192 + customTitle: json['name'] as String? ?? 'Unknown',
193 + customIconPath: json['icon'] as String? ?? 'assets/images/card.png',
194 + customDescription: json['description'] as String?);
195 + }
196 +
197 + factory PaymentMethod.fromDFX(String paymentMethod, PaymentType paymentType) {
198 + return PaymentMethod(
199 + paymentMethodType: paymentType,
200 + customTitle: paymentMethod,
201 + customIconPath: 'assets/images/card.png');
202 + }
203 +
204 + factory PaymentMethod.fromMoonPayJson(Map<String, dynamic> json, PaymentType paymentType) {
205 + return PaymentMethod(
206 + paymentMethodType: paymentType,
207 + customTitle: json['paymentMethod'] as String,
208 + customIconPath: 'assets/images/card.png');
209 + }
210 +
211 + factory PaymentMethod.fromMeldJson(Map<String, dynamic> json) {
212 + final type = PaymentMethod.getPaymentTypeId(json['paymentMethod'] as String?);
213 + final logos = json['logos'] as Map<String, dynamic>;
214 + return PaymentMethod(
215 + paymentMethodType: type,
216 + customTitle: json['name'] as String? ?? 'Unknown',
217 + customIconPath: logos['dark'] as String? ?? 'assets/images/card.png',
218 + customDescription: json['description'] as String?);
219 + }
220 +
221 + static PaymentType getPaymentTypeId(String? type) {
222 + switch (type?.toLowerCase()) {
223 + case 'banktransfer':
224 + case 'bank':
225 + case 'yellow_card_bank_transfer':
226 + return PaymentType.bankTransfer;
227 + case 'creditcard':
228 + case 'card':
229 + case 'credit_debit_card':
230 + return PaymentType.creditCard;
231 + case 'debitcard':
232 + return PaymentType.debitCard;
233 + case 'applepay':
234 + case 'apple_pay':
235 + return PaymentType.applePay;
236 + case 'googlepay':
237 + case 'google_pay':
238 + return PaymentType.googlePay;
239 + case 'revolutpay':
240 + return PaymentType.revolutPay;
241 + case 'neteller':
242 + return PaymentType.neteller;
243 + case 'skrill':
244 + return PaymentType.skrill;
245 + case 'sepabanktransfer':
246 + case 'sepa':
247 + case 'sepa_bank_transfer':
248 + return PaymentType.sepa;
249 + case 'sepainstant':
250 + case 'sepa_instant':
251 + return PaymentType.sepaInstant;
252 + case 'ach':
253 + case 'ach_bank_transfer':
254 + return PaymentType.ach;
255 + case 'iach':
256 + case 'instant_ach':
257 + return PaymentType.achInstant;
258 + case 'khipu':
259 + return PaymentType.Khipu;
260 + case 'palomabanktransfer':
261 + return PaymentType.palomaBanktTansfer;
262 + case 'ovo':
263 + return PaymentType.ovo;
264 + case 'zalopay':
265 + return PaymentType.zaloPay;
266 + case 'zalobanktransfer':
267 + case 'za_bank_transfer':
268 + return PaymentType.zaloBankTransfer;
269 + case 'gcash':
270 + return PaymentType.gcash;
271 + case 'imps':
272 + return PaymentType.imps;
273 + case 'dana':
274 + return PaymentType.dana;
275 + case 'ideal':
276 + return PaymentType.ideal;
277 + case 'paypal':
278 + return PaymentType.paypal;
279 + case 'sepa_open_banking_payment':
280 + return PaymentType.sepaOpenBankingPayment;
281 + case 'bancontact':
282 + return PaymentType.bancontact;
283 + default:
284 + return PaymentType.all;
285 + }
286 + }
287 +}
lib/buy/robinhood/robinhood_buy_provider.dart
+100 -2
@@ -1,13 +1,18 @@
1 import 'dart:convert';
2 +import 'dart:developer';
3
4 import 'package:cake_wallet/.secrets.g.dart' as secrets;
5 import 'package:cake_wallet/buy/buy_provider.dart';
6 +import 'package:cake_wallet/buy/buy_quote.dart';
7 +import 'package:cake_wallet/buy/payment_method.dart';
8 +import 'package:cake_wallet/entities/fiat_currency.dart';
9 import 'package:cake_wallet/generated/i18n.dart';
10 import 'package:cake_wallet/routes.dart';
11 import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart';
12 import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
13 import 'package:cake_wallet/utils/show_pop_up.dart';
14 import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
15 +import 'package:cw_core/crypto_currency.dart';
16 import 'package:cw_core/wallet_base.dart';
17 import 'package:cw_core/wallet_type.dart';
18 import 'package:flutter/material.dart';
@@ -15,7 +20,8 @@ import 'package:http/http.dart' as http;
20 import 'package:url_launcher/url_launcher.dart';
21
22 class RobinhoodBuyProvider extends BuyProvider {
18 - RobinhoodBuyProvider({required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM})
23 + RobinhoodBuyProvider(
24 + {required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM})
25 : super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: ledgerVM);
26
27 static const _baseUrl = 'applink.robinhood.com';
@@ -33,6 +39,9 @@ class RobinhoodBuyProvider extends BuyProvider {
39 @override
40 String get darkIcon => 'assets/images/robinhood_dark.png';
41
42 + @override
43 + bool get isAggregator => false;
44 +
45 String get _applicationId => secrets.robinhoodApplicationId;
46
47 String get _apiSecret => secrets.exchangeHelperApiKey;
@@ -86,7 +95,13 @@ class RobinhoodBuyProvider extends BuyProvider {
95 });
96 }
97
89 - Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
98 + Future<void>? launchProvider(
99 + {required BuildContext context,
100 + required Quote quote,
101 + required double amount,
102 + required bool isBuyAction,
103 + required String cryptoCurrencyAddress,
104 + String? countryCode}) async {
105 if (wallet.isHardwareWallet) {
106 if (!ledgerVM!.isConnected) {
107 await Navigator.of(context).pushNamed(Routes.connectDevices,
@@ -116,4 +131,87 @@ class RobinhoodBuyProvider extends BuyProvider {
131 });
132 }
133 }
134 +
135 + @override
136 + Future<List<Quote>?> fetchQuote(
137 + {required CryptoCurrency cryptoCurrency,
138 + required FiatCurrency fiatCurrency,
139 + required double amount,
140 + required bool isBuyAction,
141 + required String walletAddress,
142 + PaymentType? paymentType,
143 + String? countryCode}) async {
144 + String? paymentMethod;
145 +
146 + if (paymentType != null && paymentType != PaymentType.all) {
147 + paymentMethod = normalizePaymentMethod(paymentType);
148 + if (paymentMethod == null) paymentMethod = paymentType.name;
149 + }
150 +
151 + final action = isBuyAction ? 'buy' : 'sell';
152 + log('Robinhood: Fetching $action quote: ${isBuyAction ? cryptoCurrency.title : fiatCurrency.name.toUpperCase()} -> ${isBuyAction ? fiatCurrency.name.toUpperCase() : cryptoCurrency.title}, amount: $amount paymentMethod: $paymentMethod');
153 +
154 + final queryParams = {
155 + 'applicationId': _applicationId,
156 + 'fiatCode': fiatCurrency.name,
157 + 'assetCode': cryptoCurrency.title,
158 + 'fiatAmount': amount.toString(),
159 + if (paymentMethod != null) 'paymentMethod': paymentMethod,
160 + };
161 +
162 + final uri =
163 + Uri.https('api.robinhood.com', '/catpay/v1/${cryptoCurrency.title}/quote/', queryParams);
164 +
165 + try {
166 + final response = await http.get(uri, headers: {'accept': 'application/json'});
167 + final responseData = jsonDecode(response.body) as Map<String, dynamic>;
168 +
169 + if (response.statusCode == 200) {
170 + final paymentType = _getPaymentTypeByString(responseData['paymentMethod'] as String?);
171 + final quote = Quote.fromRobinhoodJson(responseData, isBuyAction, paymentType);
172 + quote.setFiatCurrency = fiatCurrency;
173 + quote.setCryptoCurrency = cryptoCurrency;
174 + return [quote];
175 + } else {
176 + if (responseData.containsKey('message')) {
177 + log('Robinhood Error: ${responseData['message']}');
178 + } else {
179 + print('Robinhood Failed to fetch $action quote: ${response.statusCode}');
180 + }
181 + return null;
182 + }
183 + } catch (e) {
184 + log('Robinhood: Failed to fetch $action quote: $e');
185 + return null;
186 + }
187 +
188 + // ● buying_power
189 + // ● crypto_balance
190 + // ● debit_card
191 + // ● bank_transfer
192 + }
193 +
194 + String? normalizePaymentMethod(PaymentType paymentMethod) {
195 + switch (paymentMethod) {
196 + case PaymentType.creditCard:
197 + return 'debit_card';
198 + case PaymentType.debitCard:
199 + return 'debit_card';
200 + case PaymentType.bankTransfer:
201 + return 'bank_transfer';
202 + default:
203 + return null;
204 + }
205 + }
206 +
207 + PaymentType _getPaymentTypeByString(String? paymentMethod) {
208 + switch (paymentMethod) {
209 + case 'debit_card':
210 + return PaymentType.debitCard;
211 + case 'bank_transfer':
212 + return PaymentType.bankTransfer;
213 + default:
214 + return PaymentType.all;
215 + }
216 + }
217 }
lib/buy/sell_buy_states.dart new
+20
@@ -0,0 +1,20 @@
1 +abstract class PaymentMethodLoadingState {}
2 +
3 +class InitialPaymentMethod extends PaymentMethodLoadingState {}
4 +
5 +class PaymentMethodLoading extends PaymentMethodLoadingState {}
6 +
7 +class PaymentMethodLoaded extends PaymentMethodLoadingState {}
8 +
9 +class PaymentMethodFailed extends PaymentMethodLoadingState {}
10 +
11 +
12 +abstract class BuySellQuotLoadingState {}
13 +
14 +class InitialBuySellQuotState extends BuySellQuotLoadingState {}
15 +
16 +class BuySellQuotLoading extends BuySellQuotLoadingState {}
17 +
18 +class BuySellQuotLoaded extends BuySellQuotLoadingState {}
19 +
20 +class BuySellQuotFailed extends BuySellQuotLoadingState {}
\ No newline at end of file
lib/buy/wyre/wyre_buy_provider.dart
+3 -6
@@ -42,6 +42,9 @@ class WyreBuyProvider extends BuyProvider {
42 @override
43 String get darkIcon => 'assets/images/robinhood_dark.png';
44
45 + @override
46 + bool get isAggregator => false;
47 +
48 String get trackUrl => isTestEnvironment ? _trackTestUrl : _trackProductUrl;
49
50 String baseApiUrl;
@@ -148,10 +151,4 @@ class WyreBuyProvider extends BuyProvider {
151 receiveAddress: wallet.walletAddresses.address,
152 walletId: wallet.id);
153 }
151 -
152 - @override
153 - Future<void> launchProvider(BuildContext context, bool? isBuyAction) {
154 - // TODO: implement launchProvider
155 - throw UnimplementedError();
156 - }
154 }
lib/core/backup_service.dart
+4 -15
@@ -268,9 +268,7 @@ class BackupService {
268 final currentFiatCurrency = data[PreferencesKey.currentFiatCurrencyKey] as String?;
269 final shouldSaveRecipientAddress = data[PreferencesKey.shouldSaveRecipientAddressKey] as bool?;
270 final isAppSecure = data[PreferencesKey.isAppSecureKey] as bool?;
271 - final disableBuy = data[PreferencesKey.disableBuyKey] as bool?;
272 - final disableSell = data[PreferencesKey.disableSellKey] as bool?;
273 - final defaultBuyProvider = data[PreferencesKey.defaultBuyProvider] as int?;
271 + final disableTradeOption = data[PreferencesKey.disableTradeOption] as bool?;
272 final currentTransactionPriorityKeyLegacy =
273 data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?;
274 final currentBitcoinElectrumSererId =
@@ -323,14 +321,8 @@ class BackupService {
321 if (isAppSecure != null)
322 await _sharedPreferences.setBool(PreferencesKey.isAppSecureKey, isAppSecure);
323
326 - if (disableBuy != null)
327 - await _sharedPreferences.setBool(PreferencesKey.disableBuyKey, disableBuy);
328 -
329 - if (disableSell != null)
330 - await _sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell);
331 -
332 - if (defaultBuyProvider != null)
333 - await _sharedPreferences.setInt(PreferencesKey.defaultBuyProvider, defaultBuyProvider);
324 + if (disableTradeOption != null)
325 + await _sharedPreferences.setBool(PreferencesKey.disableTradeOption, disableTradeOption);
326
327 if (currentTransactionPriorityKeyLegacy != null)
328 await _sharedPreferences.setInt(
@@ -516,10 +508,7 @@ class BackupService {
508 _sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey),
509 PreferencesKey.shouldSaveRecipientAddressKey:
510 _sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey),
519 - PreferencesKey.disableBuyKey: _sharedPreferences.getBool(PreferencesKey.disableBuyKey),
520 - PreferencesKey.disableSellKey: _sharedPreferences.getBool(PreferencesKey.disableSellKey),
521 - PreferencesKey.defaultBuyProvider:
522 - _sharedPreferences.getInt(PreferencesKey.defaultBuyProvider),
511 + PreferencesKey.disableTradeOption: _sharedPreferences.getBool(PreferencesKey.disableTradeOption),
512 PreferencesKey.currentPinLength: _sharedPreferences.getInt(PreferencesKey.currentPinLength),
513 PreferencesKey.currentTransactionPriorityKeyLegacy:
514 _sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy),
lib/core/selectable_option.dart new
+47
@@ -0,0 +1,47 @@
1 +abstract class SelectableItem {
2 + SelectableItem({required this.title});
3 + final String title;
4 +}
5 +
6 +class OptionTitle extends SelectableItem {
7 + OptionTitle({required String title}) : super(title: title);
8 +
9 +}
10 +
11 +abstract class SelectableOption extends SelectableItem {
12 + SelectableOption({required String title}) : super(title: title);
13 +
14 + String get lightIconPath;
15 +
16 + String get darkIconPath;
17 +
18 + String? get description => null;
19 +
20 + String? get topLeftSubTitle => null;
21 +
22 + String? get topLeftSubTitleIconPath => null;
23 +
24 + String? get topRightSubTitle => null;
25 +
26 + String? get topRightSubTitleLightIconPath => null;
27 +
28 + String? get topRightSubTitleDarkIconPath => null;
29 +
30 + String? get bottomLeftSubTitle => null;
31 +
32 + String? get bottomLeftSubTitleIconPath => null;
33 +
34 + String? get bottomRightSubTitle => null;
35 +
36 + String? get bottomRightSubTitleLightIconPath => null;
37 +
38 + String? get bottomRightSubTitleDarkIconPath => null;
39 +
40 + List<String> get badges => [];
41 +
42 + bool get isOptionSelected => false;
43 +
44 + set isOptionSelected(bool isSelected) => false;
45 +}
46 +
47 +
lib/di.dart
+29 -3
@@ -19,6 +19,7 @@ import 'package:cake_wallet/core/backup_service.dart';
19 import 'package:cake_wallet/core/key_service.dart';
20 import 'package:cake_wallet/core/new_wallet_type_arguments.dart';
21 import 'package:cake_wallet/core/secure_storage.dart';
22 +import 'package:cake_wallet/core/selectable_option.dart';
23 import 'package:cake_wallet/core/totp_request_details.dart';
24 import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart';
25 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';
35 import 'package:cake_wallet/entities/parse_address_from_domain.dart';
36 import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart';
37 import 'package:cake_wallet/entities/wallet_manager.dart';
38 +import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart';
39 +import 'package:cake_wallet/src/screens/buy/payment_method_options_page.dart';
40 import 'package:cake_wallet/src/screens/receive/address_list_page.dart';
41 import 'package:cake_wallet/src/screens/settings/mweb_logs_page.dart';
42 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
64 import 'package:cake_wallet/src/screens/auth/auth_page.dart';
65 import 'package:cake_wallet/src/screens/backup/backup_page.dart';
66 import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart';
64 -import 'package:cake_wallet/src/screens/buy/buy_options_page.dart';
67 import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart';
68 import 'package:cake_wallet/src/screens/buy/webview_page.dart';
69 import 'package:cake_wallet/src/screens/contact/contact_list_page.dart';
@@ -135,6 +137,7 @@ import 'package:cake_wallet/utils/device_info.dart';
137 import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart';
138 import 'package:cake_wallet/utils/payment_request.dart';
139 import 'package:cake_wallet/utils/responsive_layout_util.dart';
140 +import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart';
141 import 'package:cake_wallet/view_model/animated_ur_model.dart';
142 import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart';
143 import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart';
@@ -248,6 +251,8 @@ import 'package:get_it/get_it.dart';
251 import 'package:hive/hive.dart';
252 import 'package:mobx/mobx.dart';
253 import 'package:shared_preferences/shared_preferences.dart';
254 +import 'buy/meld/meld_buy_provider.dart';
255 +import 'src/screens/buy/buy_sell_page.dart';
256 import 'cake_pay/cake_pay_payment_credantials.dart';
257
258 final getIt = GetIt.instance;
@@ -1004,6 +1009,10 @@ Future<void> setup({
1009 wallet: getIt.get<AppStore>().wallet!,
1010 ));
1011
1012 + getIt.registerFactory<MeldBuyProvider>(() => MeldBuyProvider(
1013 + wallet: getIt.get<AppStore>().wallet!,
1014 + ));
1015 +
1016 getIt.registerFactoryParam<WebViewPage, String, Uri>((title, uri) => WebViewPage(title, uri));
1017
1018 getIt.registerFactory<PayfuraBuyProvider>(() => PayfuraBuyProvider(
@@ -1193,8 +1202,25 @@ Future<void> setup({
1202
1203 getIt.registerFactory(() => BuyAmountViewModel());
1204
1196 - getIt.registerFactoryParam<BuySellOptionsPage, bool, void>(
1197 - (isBuyOption, _) => BuySellOptionsPage(getIt.get<DashboardViewModel>(), isBuyOption));
1205 + getIt.registerFactory(() => BuySellViewModel(getIt.get<AppStore>()));
1206 +
1207 + getIt.registerFactory(() => BuySellPage(getIt.get<BuySellViewModel>()));
1208 +
1209 + getIt.registerFactoryParam<BuyOptionsPage, List<dynamic>, void>((List<dynamic> args, _) {
1210 + final items = args.first as List<SelectableItem>;
1211 + final pickAnOption = args[1] as void Function(SelectableOption option)?;
1212 + final confirmOption = args[2] as void Function(BuildContext contex)?;
1213 + return BuyOptionsPage(
1214 + items: items, pickAnOption: pickAnOption, confirmOption: confirmOption);
1215 + });
1216 +
1217 + getIt.registerFactoryParam<PaymentMethodOptionsPage, List<dynamic>, void>((List<dynamic> args, _) {
1218 + final items = args.first as List<SelectableOption>;
1219 + final pickAnOption = args[1] as void Function(SelectableOption option)?;
1220 +
1221 + return PaymentMethodOptionsPage(
1222 + items: items, pickAnOption: pickAnOption);
1223 + });
1224
1225 getIt.registerFactory(() {
1226 final wallet = getIt.get<AppStore>().wallet;
lib/entities/main_actions.dart
+14 -51
@@ -23,31 +23,18 @@ class MainActions {
23 });
24
25 static List<MainActions> all = [
26 - buyAction,
26 + showWalletsAction,
27 receiveAction,
28 exchangeAction,
29 sendAction,
30 - sellAction,
30 + tradeAction,
31 ];
32
33 - static MainActions buyAction = MainActions._(
34 - name: (context) => S.of(context).buy,
35 - image: 'assets/images/buy.png',
36 - isEnabled: (viewModel) => viewModel.isEnabledBuyAction,
37 - canShow: (viewModel) => viewModel.hasBuyAction,
33 + static MainActions showWalletsAction = MainActions._(
34 + name: (context) => S.of(context).wallets,
35 + image: 'assets/images/wallet_new.png',
36 onTap: (BuildContext context, DashboardViewModel viewModel) async {
39 - if (!viewModel.isEnabledBuyAction) {
40 - return;
41 - }
42 -
43 - final defaultBuyProvider = viewModel.defaultBuyProvider;
44 - try {
45 - defaultBuyProvider != null
46 - ? await defaultBuyProvider.launchProvider(context, true)
47 - : await Navigator.of(context).pushNamed(Routes.buySellPage, arguments: true);
48 - } catch (e) {
49 - await _showErrorDialog(context, defaultBuyProvider.toString(), e.toString());
50 - }
37 + Navigator.pushNamed(context, Routes.walletList);
38 },
39 );
40
@@ -79,39 +66,15 @@ class MainActions {
66 },
67 );
68
82 - static MainActions sellAction = MainActions._(
83 - name: (context) => S.of(context).sell,
84 - image: 'assets/images/sell.png',
85 - isEnabled: (viewModel) => viewModel.isEnabledSellAction,
86 - canShow: (viewModel) => viewModel.hasSellAction,
87 - onTap: (BuildContext context, DashboardViewModel viewModel) async {
88 - if (!viewModel.isEnabledSellAction) {
89 - return;
90 - }
69
92 - final defaultSellProvider = viewModel.defaultSellProvider;
93 - try {
94 - defaultSellProvider != null
95 - ? await defaultSellProvider.launchProvider(context, false)
96 - : await Navigator.of(context).pushNamed(Routes.buySellPage, arguments: false);
97 - } catch (e) {
98 - await _showErrorDialog(context, defaultSellProvider.toString(), e.toString());
99 - }
70 + static MainActions tradeAction = MainActions._(
71 + name: (context) => '${S.of(context).buy} / ${S.of(context).sell}',
72 + image: 'assets/images/buy_sell.png',
73 + isEnabled: (viewModel) => viewModel.isEnabledTradeAction,
74 + canShow: (viewModel) => viewModel.hasTradeAction,
75 + onTap: (BuildContext context, DashboardViewModel viewModel) async {
76 + if (!viewModel.isEnabledTradeAction) return;
77 + await Navigator.of(context).pushNamed(Routes.buySellPage, arguments: false);
78 },
79 );
102 -
103 - static Future<void> _showErrorDialog(
104 - BuildContext context, String title, String errorMessage) async {
105 - await showPopUp<void>(
106 - context: context,
107 - builder: (BuildContext context) {
108 - return AlertWithOneAction(
109 - alertTitle: title,
110 - alertContent: errorMessage,
111 - buttonText: S.of(context).ok,
112 - buttonAction: () => Navigator.of(context).pop(),
113 - );
114 - },
115 - );
116 - }
80 }
\ No newline at end of file
lib/entities/preferences_key.dart
+1 -3
@@ -21,10 +21,8 @@ class PreferencesKey {
21 static const currentBalanceDisplayModeKey = 'current_balance_display_mode';
22 static const shouldSaveRecipientAddressKey = 'save_recipient_address';
23 static const isAppSecureKey = 'is_app_secure';
24 - static const disableBuyKey = 'disable_buy';
25 - static const disableSellKey = 'disable_sell';
24 + static const disableTradeOption = 'disable_buy';
25 static const disableBulletinKey = 'disable_bulletin';
27 - static const defaultBuyProvider = 'default_buy_provider';
26 static const walletListOrder = 'wallet_list_order';
27 static const contactListOrder = 'contact_list_order';
28 static const walletListAscending = 'wallet_list_ascending';
lib/entities/provider_types.dart
+19 -23
@@ -1,24 +1,18 @@
1 import 'package:cake_wallet/buy/buy_provider.dart';
2 import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart';
3 +import 'package:cake_wallet/buy/meld/meld_buy_provider.dart';
4 import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart';
5 import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart';
6 import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart';
7 import 'package:cake_wallet/di.dart';
8 import 'package:cw_core/wallet_type.dart';
9 +import 'package:http/http.dart';
10
9 -enum ProviderType {
10 - askEachTime,
11 - robinhood,
12 - dfx,
13 - onramper,
14 - moonpay,
15 -}
11 +enum ProviderType { robinhood, dfx, onramper, moonpay, meld }
12
13 extension ProviderTypeName on ProviderType {
14 String get title {
15 switch (this) {
20 - case ProviderType.askEachTime:
21 - return 'Ask each time';
16 case ProviderType.robinhood:
17 return 'Robinhood Connect';
18 case ProviderType.dfx:
@@ -27,13 +21,13 @@ extension ProviderTypeName on ProviderType {
21 return 'Onramper';
22 case ProviderType.moonpay:
23 return 'MoonPay';
24 + case ProviderType.meld:
25 + return 'Meld';
26 }
27 }
28
29 String get id {
30 switch (this) {
35 - case ProviderType.askEachTime:
36 - return 'ask_each_time_provider';
31 case ProviderType.robinhood:
32 return 'robinhood_connect_provider';
33 case ProviderType.dfx:
@@ -42,6 +36,8 @@ extension ProviderTypeName on ProviderType {
36 return 'onramper_provider';
37 case ProviderType.moonpay:
38 return 'moonpay_provider';
39 + case ProviderType.meld:
40 + return 'meld_provider';
41 }
42 }
43 }
@@ -52,14 +48,13 @@ class ProvidersHelper {
48 case WalletType.nano:
49 case WalletType.banano:
50 case WalletType.wownero:
55 - return [ProviderType.askEachTime, ProviderType.onramper];
51 + return [ProviderType.onramper];
52 case WalletType.monero:
57 - return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx];
53 + return [ProviderType.onramper, ProviderType.dfx];
54 case WalletType.bitcoin:
55 case WalletType.polygon:
56 case WalletType.ethereum:
57 return [
62 - ProviderType.askEachTime,
58 ProviderType.onramper,
59 ProviderType.dfx,
60 ProviderType.robinhood,
@@ -68,10 +63,13 @@ class ProvidersHelper {
63 case WalletType.litecoin:
64 case WalletType.bitcoinCash:
65 case WalletType.solana:
71 - return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood, ProviderType.moonpay];
66 + return [
67 + ProviderType.onramper,
68 + ProviderType.robinhood,
69 + ProviderType.moonpay
70 + ];
71 case WalletType.tron:
72 return [
74 - ProviderType.askEachTime,
73 ProviderType.onramper,
74 ProviderType.robinhood,
75 ProviderType.moonpay,
@@ -88,28 +86,24 @@ class ProvidersHelper {
86 case WalletType.ethereum:
87 case WalletType.polygon:
88 return [
91 - ProviderType.askEachTime,
89 ProviderType.onramper,
90 ProviderType.moonpay,
91 ProviderType.dfx,
92 ];
93 case WalletType.litecoin:
94 case WalletType.bitcoinCash:
98 - return [ProviderType.askEachTime, ProviderType.moonpay];
95 + return [ProviderType.moonpay];
96 case WalletType.solana:
97 return [
101 - ProviderType.askEachTime,
98 ProviderType.onramper,
103 - ProviderType.robinhood,
99 ProviderType.moonpay,
100 ];
101 case WalletType.tron:
102 return [
108 - ProviderType.askEachTime,
109 - ProviderType.robinhood,
103 ProviderType.moonpay,
104 ];
105 case WalletType.monero:
106 + return [ProviderType.dfx];
107 case WalletType.nano:
108 case WalletType.banano:
109 case WalletType.none:
@@ -129,7 +123,9 @@ class ProvidersHelper {
123 return getIt.get<OnRamperBuyProvider>();
124 case ProviderType.moonpay:
125 return getIt.get<MoonPayProvider>();
132 - case ProviderType.askEachTime:
126 + case ProviderType.meld:
127 + return getIt.get<MeldBuyProvider>();
128 + default:
129 return null;
130 }
131 }
lib/router.dart
+13 -3
@@ -17,8 +17,9 @@ import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dar
17 import 'package:cake_wallet/src/screens/auth/auth_page.dart';
18 import 'package:cake_wallet/src/screens/backup/backup_page.dart';
19 import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart';
20 -import 'package:cake_wallet/src/screens/buy/buy_options_page.dart';
20 +import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart';
21 import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart';
22 +import 'package:cake_wallet/src/screens/buy/payment_method_options_page.dart';
23 import 'package:cake_wallet/src/screens/buy/webview_page.dart';
24 import 'package:cake_wallet/src/screens/cake_pay/auth/cake_pay_account_page.dart';
25 import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart';
@@ -129,7 +130,8 @@ import 'package:cw_core/wallet_type.dart';
130 import 'package:flutter/cupertino.dart';
131 import 'package:flutter/material.dart';
132 import 'package:flutter/services.dart';
132 -
133 +import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart';
134 +import 'src/screens/buy/buy_sell_page.dart';
135 import 'src/screens/dashboard/pages/nft_import_page.dart';
136
137 late RouteSettings currentRouteSettings;
@@ -571,7 +573,15 @@ Route<dynamic> createRoute(RouteSettings settings) {
573
574 case Routes.buySellPage:
575 final args = settings.arguments as bool;
574 - return MaterialPageRoute<void>(builder: (_) => getIt.get<BuySellOptionsPage>(param1: args));
576 + return MaterialPageRoute<void>(builder: (_) => getIt.get<BuySellPage>(param1: args));
577 +
578 + case Routes.buyOptionsPage:
579 + final args = settings.arguments as List;
580 + return MaterialPageRoute<void>(builder: (_) => getIt.get<BuyOptionsPage>(param1: args));
581 +
582 + case Routes.paymentMethodOptionsPage:
583 + final args = settings.arguments as List;
584 + return MaterialPageRoute<void>(builder: (_) => getIt.get<PaymentMethodOptionsPage>(param1: args));
585
586 case Routes.buyWebView:
587 final args = settings.arguments as List;
lib/routes.dart
+2
@@ -59,6 +59,8 @@ class Routes {
59 static const supportOtherLinks = '/support/other';
60 static const orderDetails = '/order_details';
61 static const buySellPage = '/buy_sell_page';
62 + static const buyOptionsPage = '/buy_sell_options';
63 + static const paymentMethodOptionsPage = '/payment_method_options';
64 static const buyWebView = '/buy_web_view';
65 static const unspentCoinsList = '/unspent_coins_list';
66 static const unspentCoinsDetails = '/unspent_coins_details';
lib/src/screens/Info_page.dart renamed
lib/src/screens/buy/buy_options_page.dart deleted
-74
@@ -1,74 +0,0 @@
1 -import 'package:cake_wallet/generated/i18n.dart';
2 -import 'package:cake_wallet/src/screens/base_page.dart';
3 -import 'package:cake_wallet/src/widgets/option_tile.dart';
4 -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
5 -import 'package:cake_wallet/themes/extensions/option_tile_theme.dart';
6 -import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
7 -import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
8 -import 'package:flutter/material.dart';
9 -
10 -class BuySellOptionsPage extends BasePage {
11 - BuySellOptionsPage(this.dashboardViewModel, this.isBuyAction);
12 -
13 - final DashboardViewModel dashboardViewModel;
14 - final bool isBuyAction;
15 -
16 - @override
17 - String get title => isBuyAction ? S.current.buy : S.current.sell;
18 -
19 - @override
20 - AppBarStyle get appBarStyle => AppBarStyle.regular;
21 -
22 - @override
23 - Widget body(BuildContext context) {
24 - final isLightMode = Theme.of(context).extension<OptionTileTheme>()?.useDarkImage ?? false;
25 - final availableProviders = isBuyAction
26 - ? dashboardViewModel.availableBuyProviders
27 - : dashboardViewModel.availableSellProviders;
28 -
29 - return ScrollableWithBottomSection(
30 - content: Container(
31 - child: Center(
32 - child: ConstrainedBox(
33 - constraints: BoxConstraints(maxWidth: 330),
34 - child: Column(
35 - children: [
36 - ...availableProviders.map((provider) {
37 - final icon = Image.asset(
38 - isLightMode ? provider.lightIcon : provider.darkIcon,
39 - height: 40,
40 - width: 40,
41 - );
42 -
43 - return Padding(
44 - padding: EdgeInsets.only(top: 24),
45 - child: OptionTile(
46 - image: icon,
47 - title: provider.toString(),
48 - description: provider.providerDescription,
49 - onPressed: () => provider.launchProvider(context, isBuyAction),
50 - ),
51 - );
52 - }).toList(),
53 - ],
54 - ),
55 - ),
56 - ),
57 - ),
58 - bottomSection: Padding(
59 - padding: EdgeInsets.fromLTRB(24, 24, 24, 32),
60 - child: Text(
61 - isBuyAction
62 - ? S.of(context).select_buy_provider_notice
63 - : S.of(context).select_sell_provider_notice,
64 - textAlign: TextAlign.center,
65 - style: TextStyle(
66 - fontSize: 14,
67 - fontWeight: FontWeight.normal,
68 - color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
69 - ),
70 - ),
71 - ),
72 - );
73 - }
74 -}
lib/src/screens/buy/buy_sell_options_page.dart new
+48
@@ -0,0 +1,48 @@
1 +import 'package:cake_wallet/core/selectable_option.dart';
2 +import 'package:cake_wallet/generated/i18n.dart';
3 +import 'package:cake_wallet/src/screens/select_options_page.dart';
4 +import 'package:flutter/cupertino.dart';
5 +
6 +class BuyOptionsPage extends SelectOptionsPage {
7 + BuyOptionsPage({required this.items, this.pickAnOption, this.confirmOption});
8 +
9 + final List<SelectableItem> items;
10 + final Function(SelectableOption option)? pickAnOption;
11 + final Function(BuildContext context)? confirmOption;
12 +
13 + @override
14 + String get pageTitle => S.current.choose_a_provider;
15 +
16 + @override
17 + EdgeInsets? get contentPadding => null;
18 +
19 + @override
20 + EdgeInsets? get tilePadding => EdgeInsets.only(top: 8);
21 +
22 + @override
23 + EdgeInsets? get innerPadding => EdgeInsets.symmetric(horizontal: 24, vertical: 8);
24 +
25 + @override
26 + double? get imageHeight => 40;
27 +
28 + @override
29 + double? get imageWidth => 40;
30 +
31 + @override
32 + Color? get selectedBackgroundColor => null;
33 +
34 + @override
35 + double? get tileBorderRadius => 30;
36 +
37 + @override
38 + String get bottomSectionText => '';
39 +
40 + @override
41 + void Function(SelectableOption option)? get onOptionTap => pickAnOption;
42 +
43 + @override
44 + String get primaryButtonText => S.current.confirm;
45 +
46 + @override
47 + void Function(BuildContext context)? get primaryButtonAction => confirmOption;
48 +}
lib/src/screens/buy/buy_sell_page.dart new
+469
@@ -0,0 +1,469 @@
1 +import 'package:cake_wallet/buy/sell_buy_states.dart';
2 +import 'package:cake_wallet/core/address_validator.dart';
3 +import 'package:cake_wallet/di.dart';
4 +import 'package:cake_wallet/entities/fiat_currency.dart';
5 +import 'package:cake_wallet/entities/parse_address_from_domain.dart';
6 +import 'package:cake_wallet/generated/i18n.dart';
7 +import 'package:cake_wallet/routes.dart';
8 +import 'package:cake_wallet/src/screens/base_page.dart';
9 +import 'package:cake_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart';
10 +import 'package:cake_wallet/src/screens/exchange/widgets/exchange_card.dart';
11 +import 'package:cake_wallet/src/screens/exchange/widgets/mobile_exchange_cards_section.dart';
12 +import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
13 +import 'package:cake_wallet/src/widgets/primary_button.dart';
14 +import 'package:cake_wallet/src/widgets/provider_optoin_tile.dart';
15 +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
16 +import 'package:cake_wallet/src/widgets/trail_button.dart';
17 +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
18 +import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart';
19 +import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
20 +import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
21 +import 'package:cake_wallet/themes/theme_base.dart';
22 +import 'package:cake_wallet/typography.dart';
23 +import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
24 +import 'package:cake_wallet/utils/responsive_layout_util.dart';
25 +import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart';
26 +import 'package:cw_core/crypto_currency.dart';
27 +import 'package:cw_core/currency.dart';
28 +import 'package:cw_core/wallet_type.dart';
29 +import 'package:flutter/material.dart';
30 +import 'package:flutter_mobx/flutter_mobx.dart';
31 +import 'package:keyboard_actions/keyboard_actions.dart';
32 +import 'package:mobx/mobx.dart';
33 +
34 +class BuySellPage extends BasePage {
35 + BuySellPage(this.buySellViewModel);
36 +
37 + final BuySellViewModel buySellViewModel;
38 + final cryptoCurrencyKey = GlobalKey<ExchangeCardState>();
39 + final fiatCurrencyKey = GlobalKey<ExchangeCardState>();
40 + final _formKey = GlobalKey<FormState>();
41 + final _fiatAmountFocus = FocusNode();
42 + final _cryptoAmountFocus = FocusNode();
43 + final _cryptoAddressFocus = FocusNode();
44 + var _isReactionsSet = false;
45 +
46 + final arrowBottomPurple = Image.asset(
47 + 'assets/images/arrow_bottom_purple_icon.png',
48 + color: Colors.white,
49 + height: 8,
50 + );
51 + final arrowBottomCakeGreen = Image.asset(
52 + 'assets/images/arrow_bottom_cake_green.png',
53 + color: Colors.white,
54 + height: 8,
55 + );
56 +
57 + late final String? depositWalletName;
58 + late final String? receiveWalletName;
59 +
60 + @override
61 + String get title => S.current.buy + '/' + S.current.sell;
62 +
63 + @override
64 + bool get gradientBackground => true;
65 +
66 + @override
67 + bool get gradientAll => true;
68 +
69 + @override
70 + bool get resizeToAvoidBottomInset => false;
71 +
72 + @override
73 + bool get extendBodyBehindAppBar => true;
74 +
75 + @override
76 + AppBarStyle get appBarStyle => AppBarStyle.transparent;
77 +
78 + @override
79 + Function(BuildContext)? get pushToNextWidget => (context) {
80 + FocusScopeNode currentFocus = FocusScope.of(context);
81 + if (!currentFocus.hasPrimaryFocus) {
82 + currentFocus.focusedChild?.unfocus();
83 + }
84 + };
85 +
86 + @override
87 + Widget trailing(BuildContext context) => TrailButton(
88 + caption: S.of(context).clear,
89 + onPressed: () {
90 + _formKey.currentState?.reset();
91 + buySellViewModel.reset();
92 + });
93 +
94 + @override
95 + Widget? leading(BuildContext context) {
96 + final _backButton = Icon(
97 + Icons.arrow_back_ios,
98 + color: titleColor(context),
99 + size: 16,
100 + );
101 + final _closeButton =
102 + currentTheme.type == ThemeType.dark ? closeButtonImageDarkTheme : closeButtonImage;
103 +
104 + bool isMobileView = responsiveLayoutUtil.shouldRenderMobileUI;
105 +
106 + return MergeSemantics(
107 + child: SizedBox(
108 + height: isMobileView ? 37 : 45,
109 + width: isMobileView ? 37 : 45,
110 + child: ButtonTheme(
111 + minWidth: double.minPositive,
112 + child: Semantics(
113 + label: !isMobileView ? S.of(context).close : S.of(context).seed_alert_back,
114 + child: TextButton(
115 + style: ButtonStyle(
116 + overlayColor: MaterialStateColor.resolveWith((states) => Colors.transparent),
117 + ),
118 + onPressed: () => onClose(context),
119 + child: !isMobileView ? _closeButton : _backButton,
120 + ),
121 + ),
122 + ),
123 + ),
124 + );
125 + }
126 +
127 + @override
128 + Widget body(BuildContext context) {
129 + WidgetsBinding.instance.addPostFrameCallback((_) => _setReactions(context, buySellViewModel));
130 +
131 + return KeyboardActions(
132 + disableScroll: true,
133 + config: KeyboardActionsConfig(
134 + keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
135 + keyboardBarColor: Theme.of(context).extension<KeyboardTheme>()!.keyboardBarColor,
136 + nextFocus: false,
137 + actions: [
138 + KeyboardActionsItem(
139 + focusNode: _fiatAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()]),
140 + KeyboardActionsItem(
141 + focusNode: _cryptoAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()])
142 + ]),
143 + child: Container(
144 + color: Theme.of(context).colorScheme.background,
145 + child: Form(
146 + key: _formKey,
147 + child: ScrollableWithBottomSection(
148 + contentPadding: EdgeInsets.only(bottom: 24),
149 + content: Observer(
150 + builder: (_) => Column(children: [
151 + _exchangeCardsSection(context),
152 + Padding(
153 + padding: EdgeInsets.symmetric(horizontal: 24),
154 + child: Column(
155 + children: [
156 + SizedBox(height: 12),
157 + _buildPaymentMethodTile(context),
158 + ],
159 + ),
160 + ),
161 + ])),
162 + bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
163 + bottomSection: Column(children: [
164 + Observer(
165 + builder: (_) => LoadingPrimaryButton(
166 + text: S.current.choose_a_provider,
167 + onPressed: () async {
168 + if(!_formKey.currentState!.validate()) return;
169 + buySellViewModel.onTapChoseProvider(context);
170 + },
171 + color: Theme.of(context).primaryColor,
172 + textColor: Colors.white,
173 + isDisabled: false,
174 + isLoading: !buySellViewModel.isReadyToTrade)),
175 + ]),
176 + )),
177 + ));
178 + }
179 +
180 + Widget _buildPaymentMethodTile(BuildContext context) {
181 + if (buySellViewModel.paymentMethodState is PaymentMethodLoading ||
182 + buySellViewModel.paymentMethodState is InitialPaymentMethod) {
183 + return OptionTilePlaceholder(
184 + withBadge: false,
185 + withSubtitle: false,
186 + borderRadius: 30,
187 + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8),
188 + leadingIcon: Icons.arrow_forward_ios,
189 + isDarkTheme: buySellViewModel.isDarkTheme);
190 + }
191 + if (buySellViewModel.paymentMethodState is PaymentMethodFailed) {
192 + return OptionTilePlaceholder(errorText: 'No payment methods available', borderRadius: 30);
193 + }
194 + if (buySellViewModel.paymentMethodState is PaymentMethodLoaded &&
195 + buySellViewModel.selectedPaymentMethod != null) {
196 + return Observer(builder: (_) {
197 + final selectedPaymentMethod = buySellViewModel.selectedPaymentMethod!;
198 + return ProviderOptionTile(
199 + lightImagePath: selectedPaymentMethod.lightIconPath,
200 + darkImagePath: selectedPaymentMethod.darkIconPath,
201 + title: selectedPaymentMethod.title,
202 + onPressed: () => _pickPaymentMethod(context),
203 + leadingIcon: Icons.arrow_forward_ios,
204 + isLightMode: !buySellViewModel.isDarkTheme,
205 + borderRadius: 30,
206 + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8),
207 + titleTextStyle:
208 + textLargeBold(color: Theme.of(context).extension<CakeTextTheme>()!.titleColor),
209 + );
210 + });
211 + }
212 + return OptionTilePlaceholder(errorText: 'No payment methods available', borderRadius: 30);
213 + }
214 +
215 + void _pickPaymentMethod(BuildContext context) async {
216 + final currentOption = buySellViewModel.selectedPaymentMethod;
217 + await Navigator.of(context).pushNamed(
218 + Routes.paymentMethodOptionsPage,
219 + arguments: [
220 + buySellViewModel.paymentMethods,
221 + buySellViewModel.changeOption,
222 + ],
223 + );
224 +
225 + buySellViewModel.selectedPaymentMethod;
226 + if (currentOption != null &&
227 + currentOption.paymentMethodType !=
228 + buySellViewModel.selectedPaymentMethod?.paymentMethodType) {
229 + await buySellViewModel.calculateBestRate();
230 + }
231 + }
232 +
233 + void _setReactions(BuildContext context, BuySellViewModel buySellViewModel) {
234 + if (_isReactionsSet) {
235 + return;
236 + }
237 +
238 + final fiatAmountController = fiatCurrencyKey.currentState!.amountController;
239 + final cryptoAmountController = cryptoCurrencyKey.currentState!.amountController;
240 + final cryptoAddressController = cryptoCurrencyKey.currentState!.addressController;
241 +
242 + _onCurrencyChange(buySellViewModel.cryptoCurrency, buySellViewModel, cryptoCurrencyKey);
243 + _onCurrencyChange(buySellViewModel.fiatCurrency, buySellViewModel, fiatCurrencyKey);
244 +
245 + reaction(
246 + (_) => buySellViewModel.wallet.name,
247 + (String _) =>
248 + _onWalletNameChange(buySellViewModel, buySellViewModel.cryptoCurrency, cryptoCurrencyKey));
249 +
250 + reaction(
251 + (_) => buySellViewModel.cryptoCurrency,
252 + (CryptoCurrency currency) =>
253 + _onCurrencyChange(currency, buySellViewModel, cryptoCurrencyKey));
254 +
255 + reaction(
256 + (_) => buySellViewModel.fiatCurrency,
257 + (FiatCurrency currency) =>
258 + _onCurrencyChange(currency, buySellViewModel, fiatCurrencyKey));
259 +
260 + reaction((_) => buySellViewModel.fiatAmount, (String amount) {
261 + if (fiatCurrencyKey.currentState!.amountController.text != amount) {
262 + fiatCurrencyKey.currentState!.amountController.text = amount;
263 + }
264 + });
265 +
266 + reaction((_) => buySellViewModel.isCryptoCurrencyAddressEnabled, (bool isEnabled) {
267 + cryptoCurrencyKey.currentState!.isAddressEditable(isEditable: isEnabled);
268 + });
269 +
270 + reaction((_) => buySellViewModel.cryptoAmount, (String amount) {
271 + if (cryptoCurrencyKey.currentState!.amountController.text != amount) {
272 + cryptoCurrencyKey.currentState!.amountController.text = amount;
273 + }
274 + });
275 +
276 + reaction((_) => buySellViewModel.cryptoCurrencyAddress, (String address) {
277 + if (cryptoAddressController != address) {
278 + cryptoCurrencyKey.currentState!.addressController.text = address;
279 + }
280 + });
281 +
282 + fiatAmountController.addListener(() {
283 + if (fiatAmountController.text != buySellViewModel.fiatAmount) {
284 + buySellViewModel.changeFiatAmount(amount: fiatAmountController.text);
285 + }
286 + });
287 +
288 + cryptoAmountController.addListener(() {
289 + if (cryptoAmountController.text != buySellViewModel.cryptoAmount) {
290 + buySellViewModel.changeCryptoAmount(amount: cryptoAmountController.text);
291 + }
292 + });
293 +
294 + cryptoAddressController.addListener(() {
295 + buySellViewModel.changeCryptoCurrencyAddress(cryptoAddressController.text);
296 + });
297 +
298 + _cryptoAddressFocus.addListener(() async {
299 + if (!_cryptoAddressFocus.hasFocus && cryptoAddressController.text.isNotEmpty) {
300 + final domain = cryptoAddressController.text;
301 + buySellViewModel.cryptoCurrencyAddress =
302 + await fetchParsedAddress(context, domain, buySellViewModel.cryptoCurrency);
303 + }
304 + });
305 +
306 + reaction((_) => buySellViewModel.wallet.walletAddresses.addressForExchange, (String address) {
307 + if (buySellViewModel.cryptoCurrency == CryptoCurrency.xmr) {
308 + cryptoCurrencyKey.currentState!.changeAddress(address: address);
309 + }
310 + });
311 +
312 + reaction((_) => buySellViewModel.isReadyToTrade, (bool isReady) {
313 + if (isReady) {
314 + if (cryptoAmountController.text.isNotEmpty &&
315 + cryptoAmountController.text != S.current.fetching) {
316 + buySellViewModel.changeCryptoAmount(amount: cryptoAmountController.text);
317 + } else if (fiatAmountController.text.isNotEmpty &&
318 + fiatAmountController.text != S.current.fetching) {
319 + buySellViewModel.changeFiatAmount(amount: fiatAmountController.text);
320 + }
321 + }
322 + });
323 +
324 + _isReactionsSet = true;
325 + }
326 +
327 + void _onCurrencyChange(Currency currency, BuySellViewModel buySellViewModel,
328 + GlobalKey<ExchangeCardState> key) {
329 + final isCurrentTypeWallet = currency == buySellViewModel.wallet.currency;
330 +
331 + key.currentState!.changeSelectedCurrency(currency);
332 + key.currentState!.changeWalletName(isCurrentTypeWallet ? buySellViewModel.wallet.name : '');
333 +
334 + key.currentState!.changeAddress(
335 + address: isCurrentTypeWallet ? buySellViewModel.wallet.walletAddresses.addressForExchange : '');
336 +
337 + key.currentState!.changeAmount(amount: '');
338 + }
339 +
340 + void _onWalletNameChange(BuySellViewModel buySellViewModel, CryptoCurrency currency,
341 + GlobalKey<ExchangeCardState> key) {
342 + final isCurrentTypeWallet = currency == buySellViewModel.wallet.currency;
343 +
344 + if (isCurrentTypeWallet) {
345 + key.currentState!.changeWalletName(buySellViewModel.wallet.name);
346 + key.currentState!.addressController.text = buySellViewModel.wallet.walletAddresses.addressForExchange;
347 + } else if (key.currentState!.addressController.text ==
348 + buySellViewModel.wallet.walletAddresses.addressForExchange) {
349 + key.currentState!.changeWalletName('');
350 + key.currentState!.addressController.text = '';
351 + }
352 + }
353 +
354 + void disposeBestRateSync() => {};
355 +
356 + Widget _exchangeCardsSection(BuildContext context) {
357 + final fiatExchangeCard = Observer(
358 + builder: (_) => ExchangeCard(
359 + cardInstanceName: 'fiat_currency_trade_card',
360 + onDispose: disposeBestRateSync,
361 + amountFocusNode: _fiatAmountFocus,
362 + key: fiatCurrencyKey,
363 + title: 'FIAT ${S.of(context).amount}',
364 + initialCurrency: buySellViewModel.fiatCurrency,
365 + initialWalletName: '',
366 + initialAddress: '',
367 + initialIsAmountEditable: true,
368 + isAmountEstimated: false,
369 + currencyRowPadding: EdgeInsets.zero,
370 + addressRowPadding: EdgeInsets.zero,
371 + isMoneroWallet: buySellViewModel.wallet == WalletType.monero,
372 + showAddressField: false,
373 + showLimitsField: false,
374 + currencies: buySellViewModel.fiatCurrencies,
375 + onCurrencySelected: (currency) =>
376 + buySellViewModel.changeFiatCurrency(currency: currency),
377 + imageArrow: arrowBottomPurple,
378 + currencyButtonColor: Colors.transparent,
379 + addressButtonsColor:
380 + Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
381 + borderColor:
382 + Theme.of(context).extension<ExchangePageTheme>()!.textFieldBorderTopPanelColor,
383 + onPushPasteButton: (context) async {},
384 + onPushAddressBookButton: (context) async {},
385 + ));
386 +
387 + final cryptoExchangeCard = Observer(
388 + builder: (_) => ExchangeCard(
389 + cardInstanceName: 'crypto_currency_trade_card',
390 + onDispose: disposeBestRateSync,
391 + amountFocusNode: _cryptoAmountFocus,
392 + addressFocusNode: _cryptoAddressFocus,
393 + key: cryptoCurrencyKey,
394 + title: 'Crypto ${S.of(context).amount}',
395 + initialCurrency: buySellViewModel.cryptoCurrency,
396 + initialWalletName: '',
397 + initialAddress: buySellViewModel.cryptoCurrency == buySellViewModel.wallet.currency
398 + ? buySellViewModel.wallet.walletAddresses.addressForExchange
399 + : buySellViewModel.cryptoCurrencyAddress,
400 + initialIsAmountEditable: true,
401 + isAmountEstimated: true,
402 + showLimitsField: false,
403 + currencyRowPadding: EdgeInsets.zero,
404 + addressRowPadding: EdgeInsets.zero,
405 + isMoneroWallet: buySellViewModel.wallet == WalletType.monero,
406 + currencies: buySellViewModel.cryptoCurrencies,
407 + onCurrencySelected: (currency) =>
408 + buySellViewModel.changeCryptoCurrency(currency: currency),
409 + imageArrow: arrowBottomCakeGreen,
410 + currencyButtonColor: Colors.transparent,
411 + addressButtonsColor:
412 + Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
413 + borderColor:
414 + Theme.of(context).extension<ExchangePageTheme>()!.textFieldBorderBottomPanelColor,
415 + addressTextFieldValidator: AddressValidator(type: buySellViewModel.cryptoCurrency),
416 + onPushPasteButton: (context) async {},
417 + onPushAddressBookButton: (context) async {},
418 + ));
419 +
420 + if (responsiveLayoutUtil.shouldRenderMobileUI) {
421 + return Observer(
422 + builder: (_) {
423 + if (buySellViewModel.isBuyAction) {
424 + return MobileExchangeCardsSection(
425 + firstExchangeCard: fiatExchangeCard,
426 + secondExchangeCard: cryptoExchangeCard,
427 + onBuyTap: () => null,
428 + onSellTap: () =>
429 + buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null,
430 + isBuySellOption: true,
431 + );
432 + } else {
433 + return MobileExchangeCardsSection(
434 + firstExchangeCard: cryptoExchangeCard,
435 + secondExchangeCard: fiatExchangeCard,
436 + onBuyTap: () =>
437 + !buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null,
438 + onSellTap: () => null,
439 + isBuySellOption: true,
440 + );
441 + }
442 + },
443 + );
444 + }
445 +
446 + return Observer(
447 + builder: (_) {
448 + if (buySellViewModel.isBuyAction) {
449 + return DesktopExchangeCardsSection(
450 + firstExchangeCard: fiatExchangeCard,
451 + secondExchangeCard: cryptoExchangeCard,
452 + );
453 + } else {
454 + return DesktopExchangeCardsSection(
455 + firstExchangeCard: cryptoExchangeCard,
456 + secondExchangeCard: fiatExchangeCard,
457 + );
458 + }
459 + },
460 + );
461 + }
462 +
463 + Future<String> fetchParsedAddress(
464 + BuildContext context, String domain, CryptoCurrency currency) async {
465 + final parsedAddress = await getIt.get<AddressResolver>().resolve(context, domain, currency);
466 + final address = await extractAddressFromParsed(context, parsedAddress);
467 + return address;
468 + }
469 +}
lib/src/screens/buy/payment_method_options_page.dart new
+47
@@ -0,0 +1,47 @@
1 +import 'package:cake_wallet/core/selectable_option.dart';
2 +import 'package:cake_wallet/generated/i18n.dart';
3 +import 'package:cake_wallet/src/screens/select_options_page.dart';
4 +import 'package:flutter/cupertino.dart';
5 +
6 +class PaymentMethodOptionsPage extends SelectOptionsPage {
7 + PaymentMethodOptionsPage({required this.items, this.pickAnOption});
8 +
9 + final List<SelectableItem> items;
10 + final Function(SelectableOption option)? pickAnOption;
11 +
12 + @override
13 + String get pageTitle => S.current.choose_a_payment_method;
14 +
15 + @override
16 + EdgeInsets? get contentPadding => null;
17 +
18 + @override
19 + EdgeInsets? get tilePadding => EdgeInsets.only(top: 12);
20 +
21 + @override
22 + EdgeInsets? get innerPadding => EdgeInsets.symmetric(horizontal: 24, vertical: 12);
23 +
24 + @override
25 + double? get imageHeight => null;
26 +
27 + @override
28 + double? get imageWidth => null;
29 +
30 + @override
31 + Color? get selectedBackgroundColor => null;
32 +
33 + @override
34 + double? get tileBorderRadius => 30;
35 +
36 + @override
37 + String get bottomSectionText => '';
38 +
39 + @override
40 + void Function(SelectableOption option)? get onOptionTap => pickAnOption;
41 +
42 + @override
43 + String get primaryButtonText => S.current.confirm;
44 +
45 + @override
46 + void Function(BuildContext context)? get primaryButtonAction => null;
47 +}
lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart
+13 -14
@@ -21,6 +21,14 @@ class DesktopDashboardActions extends StatelessWidget {
21 return Column(
22 children: [
23 const SizedBox(height: 16),
24 + DesktopActionButton(
25 + title: MainActions.showWalletsAction.name(context),
26 + image: MainActions.showWalletsAction.image,
27 + canShow: MainActions.showWalletsAction.canShow?.call(dashboardViewModel),
28 + isEnabled: MainActions.showWalletsAction.isEnabled?.call(dashboardViewModel),
29 + onTap: () async =>
30 + await MainActions.showWalletsAction.onTap(context, dashboardViewModel),
31 + ),
32 DesktopActionButton(
33 title: MainActions.exchangeAction.name(context),
34 image: MainActions.exchangeAction.image,
@@ -55,20 +63,11 @@ class DesktopDashboardActions extends StatelessWidget {
63 children: [
64 Expanded(
65 child: DesktopActionButton(
58 - title: MainActions.buyAction.name(context),
59 - image: MainActions.buyAction.image,
60 - canShow: MainActions.buyAction.canShow?.call(dashboardViewModel),
61 - isEnabled: MainActions.buyAction.isEnabled?.call(dashboardViewModel),
62 - onTap: () async => await MainActions.buyAction.onTap(context, dashboardViewModel),
63 - ),
64 - ),
65 - Expanded(
66 - child: DesktopActionButton(
67 - title: MainActions.sellAction.name(context),
68 - image: MainActions.sellAction.image,
69 - canShow: MainActions.sellAction.canShow?.call(dashboardViewModel),
70 - isEnabled: MainActions.sellAction.isEnabled?.call(dashboardViewModel),
71 - onTap: () async => await MainActions.sellAction.onTap(context, dashboardViewModel),
66 + title: MainActions.tradeAction.name(context),
67 + image: MainActions.tradeAction.image,
68 + canShow: MainActions.tradeAction.canShow?.call(dashboardViewModel),
69 + isEnabled: MainActions.tradeAction.isEnabled?.call(dashboardViewModel),
70 + onTap: () async => await MainActions.tradeAction.onTap(context, dashboardViewModel),
71 ),
72 ),
73 ],
lib/src/screens/exchange/widgets/exchange_card.dart
+38 -20
@@ -18,7 +18,7 @@ import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
18 import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
19 import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
20
21 -class ExchangeCard extends StatefulWidget {
21 +class ExchangeCard<T extends Currency> extends StatefulWidget {
22 ExchangeCard({
23 Key? key,
24 required this.initialCurrency,
@@ -40,19 +40,23 @@ class ExchangeCard extends StatefulWidget {
40 this.borderColor = Colors.transparent,
41 this.hasAllAmount = false,
42 this.isAllAmountEnabled = false,
43 + this.showAddressField = true,
44 + this.showLimitsField = true,
45 this.amountFocusNode,
46 this.addressFocusNode,
47 this.allAmount,
48 + this.currencyRowPadding,
49 + this.addressRowPadding,
50 this.onPushPasteButton,
51 this.onPushAddressBookButton,
52 this.onDispose,
53 required this.cardInstanceName,
54 }) : super(key: key);
55
52 - final List<CryptoCurrency> currencies;
53 - final Function(CryptoCurrency) onCurrencySelected;
56 + final List<T> currencies;
57 + final Function(T) onCurrencySelected;
58 final String title;
55 - final CryptoCurrency initialCurrency;
59 + final T initialCurrency;
60 final String initialWalletName;
61 final String initialAddress;
62 final bool initialIsAmountEditable;
@@ -70,18 +74,22 @@ class ExchangeCard extends StatefulWidget {
74 final FocusNode? amountFocusNode;
75 final FocusNode? addressFocusNode;
76 final bool hasAllAmount;
77 + final bool showAddressField;
78 + final bool showLimitsField;
79 final bool isAllAmountEnabled;
80 final VoidCallback? allAmount;
81 + final EdgeInsets? currencyRowPadding;
82 + final EdgeInsets? addressRowPadding;
83 final void Function(BuildContext context)? onPushPasteButton;
84 final void Function(BuildContext context)? onPushAddressBookButton;
85 final Function()? onDispose;
86 final String cardInstanceName;
87
88 @override
81 - ExchangeCardState createState() => ExchangeCardState();
89 + ExchangeCardState<T> createState() => ExchangeCardState<T>();
90 }
91
84 -class ExchangeCardState extends State<ExchangeCard> {
92 +class ExchangeCardState<T extends Currency> extends State<ExchangeCard<T>> {
93 ExchangeCardState()
94 : _title = '',
95 _min = '',
@@ -89,7 +97,6 @@ class ExchangeCardState extends State<ExchangeCard> {
97 _isAmountEditable = false,
98 _isAddressEditable = false,
99 _walletName = '',
92 - _selectedCurrency = CryptoCurrency.btc,
100 _isAmountEstimated = false,
101 _isMoneroWallet = false,
102 _cardInstanceName = '';
@@ -101,7 +108,7 @@ class ExchangeCardState extends State<ExchangeCard> {
108 String _title;
109 String? _min;
110 String? _max;
104 - CryptoCurrency _selectedCurrency;
111 + late T _selectedCurrency;
112 String _walletName;
113 bool _isAmountEditable;
114 bool _isAddressEditable;
@@ -118,7 +125,8 @@ class ExchangeCardState extends State<ExchangeCard> {
125 _selectedCurrency = widget.initialCurrency;
126 _isAmountEstimated = widget.isAmountEstimated;
127 _isMoneroWallet = widget.isMoneroWallet;
121 - addressController.text = widget.initialAddress;
128 + addressController.text = _normalizeAddressFormat(widget.initialAddress);
129 +
130 super.initState();
131 }
132
@@ -136,7 +144,7 @@ class ExchangeCardState extends State<ExchangeCard> {
144 });
145 }
146
139 - void changeSelectedCurrency(CryptoCurrency currency) {
147 + void changeSelectedCurrency(T currency) {
148 setState(() => _selectedCurrency = currency);
149 }
150
@@ -157,7 +165,7 @@ class ExchangeCardState extends State<ExchangeCard> {
165 }
166
167 void changeAddress({required String address}) {
160 - setState(() => addressController.text = address);
168 + setState(() => addressController.text = _normalizeAddressFormat(address));
169 }
170
171 void changeAmount({required String amount}) {
@@ -222,7 +230,7 @@ class ExchangeCardState extends State<ExchangeCard> {
230 Divider(height: 1, color: Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor),
231 Padding(
232 padding: EdgeInsets.only(top: 5),
225 - child: Container(
233 + child: widget.showLimitsField ? Container(
234 height: 15,
235 child: Row(mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[
236 _min != null
@@ -247,7 +255,7 @@ class ExchangeCardState extends State<ExchangeCard> {
255 ),
256 )
257 : Offstage(),
250 - ])),
258 + ])) : Offstage(),
259 ),
260 !_isAddressEditable && widget.hasRefundAddress
261 ? Padding(
@@ -261,10 +269,11 @@ class ExchangeCardState extends State<ExchangeCard> {
269 ))
270 : Offstage(),
271 _isAddressEditable
272 + ? widget.showAddressField
273 ? FocusTraversalOrder(
274 order: NumericFocusOrder(2),
275 child: Padding(
267 - padding: EdgeInsets.only(top: 20),
276 + padding: widget.addressRowPadding ?? EdgeInsets.only(top: 20),
277 child: AddressTextField(
278 addressKey: ValueKey('${_cardInstanceName}_editable_address_textfield_key'),
279 focusNode: widget.addressFocusNode,
@@ -280,26 +289,29 @@ class ExchangeCardState extends State<ExchangeCard> {
289 widget.amountFocusNode?.requestFocus();
290 amountController.text = paymentRequest.amount;
291 },
283 - placeholder: widget.hasRefundAddress ? S.of(context).refund_address : null,
292 + placeholder:
293 + widget.hasRefundAddress ? S.of(context).refund_address : null,
294 options: [
295 AddressTextFieldOption.paste,
296 AddressTextFieldOption.qrCode,
297 AddressTextFieldOption.addressBook,
298 ],
299 isBorderExist: false,
290 - textStyle:
291 - TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
300 + textStyle: TextStyle(
301 + fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
302 hintStyle: TextStyle(
303 fontSize: 16,
304 fontWeight: FontWeight.w600,
295 - color: Theme.of(context).extension<ExchangePageTheme>()!.hintTextColor),
305 + color:
306 + Theme.of(context).extension<ExchangePageTheme>()!.hintTextColor),
307 buttonColor: widget.addressButtonsColor,
308 validator: widget.addressTextFieldValidator,
309 onPushPasteButton: widget.onPushPasteButton,
310 onPushAddressBookButton: widget.onPushAddressBookButton,
311 selectedCurrency: _selectedCurrency),
312 ),
302 - )
313 + )
314 + : Offstage()
315 : Padding(
316 padding: EdgeInsets.only(top: 10),
317 child: Builder(
@@ -402,7 +414,7 @@ class ExchangeCardState extends State<ExchangeCard> {
414 hintText: S.of(context).search_currency,
415 isMoneroWallet: _isMoneroWallet,
416 isConvertFrom: widget.hasRefundAddress,
405 - onItemSelected: (Currency item) => widget.onCurrencySelected(item as CryptoCurrency),
417 + onItemSelected: (Currency item) => widget.onCurrencySelected(item as T),
418 ),
419 );
420 }
@@ -424,4 +436,10 @@ class ExchangeCardState extends State<ExchangeCard> {
436 actionLeftButton: () => Navigator.of(dialogContext).pop());
437 });
438 }
439 +
440 + String _normalizeAddressFormat(String address) {
441 + if (address.startsWith('bitcoincash:')) address = address.substring(12);
442 + return address;
443 + }
444 }
445 +
lib/src/screens/exchange/widgets/mobile_exchange_cards_section.dart
+88 -3
@@ -1,20 +1,29 @@
1 +import 'package:cake_wallet/generated/i18n.dart';
2 +import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart';
3 import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart';
4 +import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
5 import 'package:flutter/material.dart';
6
7 class MobileExchangeCardsSection extends StatelessWidget {
8 final Widget firstExchangeCard;
9 final Widget secondExchangeCard;
10 + final bool isBuySellOption;
11 + final VoidCallback? onBuyTap;
12 + final VoidCallback? onSellTap;
13
14 const MobileExchangeCardsSection({
15 Key? key,
16 required this.firstExchangeCard,
17 required this.secondExchangeCard,
18 + this.isBuySellOption = false,
19 + this.onBuyTap,
20 + this.onSellTap,
21 }) : super(key: key);
22
23 @override
24 Widget build(BuildContext context) {
25 return Container(
17 - padding: EdgeInsets.only(bottom: 32),
26 + padding: EdgeInsets.only(bottom: isBuySellOption ? 8 : 32),
27 decoration: BoxDecoration(
28 borderRadius: BorderRadius.only(
29 bottomLeft: Radius.circular(24),
@@ -45,8 +54,18 @@ class MobileExchangeCardsSection extends StatelessWidget {
54 end: Alignment.bottomRight,
55 ),
56 ),
48 - padding: EdgeInsets.fromLTRB(24, 100, 24, 32),
49 - child: firstExchangeCard,
57 + padding: EdgeInsets.fromLTRB(24, 90, 24, isBuySellOption ? 8 : 32),
58 + child: Column(
59 + children: [
60 + if (isBuySellOption) Column(
61 + children: [
62 + const SizedBox(height: 16),
63 + BuySellOptionButtons(onBuyTap: onBuyTap, onSellTap: onSellTap),
64 + ],
65 + ),
66 + firstExchangeCard,
67 + ],
68 + ),
69 ),
70 Padding(
71 padding: EdgeInsets.only(top: 29, left: 24, right: 24),
@@ -57,3 +76,69 @@ class MobileExchangeCardsSection extends StatelessWidget {
76 );
77 }
78 }
79 +
80 +class BuySellOptionButtons extends StatefulWidget {
81 + final VoidCallback? onBuyTap;
82 + final VoidCallback? onSellTap;
83 +
84 + const BuySellOptionButtons({this.onBuyTap, this.onSellTap});
85 +
86 + @override
87 + _BuySellOptionButtonsState createState() => _BuySellOptionButtonsState();
88 +}
89 +
90 +class _BuySellOptionButtonsState extends State<BuySellOptionButtons> {
91 + bool isBuySelected = true;
92 +
93 + @override
94 + Widget build(BuildContext context) {
95 + return Padding(
96 + padding: const EdgeInsets.only(bottom: 24),
97 + child: Row(
98 + children: [
99 + Expanded(flex: 2, child: SizedBox()),
100 + Expanded(
101 + flex: 5,
102 + child: SelectButton(
103 + height: 44,
104 + text: S.of(context).buy,
105 + isSelected: isBuySelected,
106 + showTrailingIcon: false,
107 + textColor: Colors.white,
108 + image: Image.asset('assets/images/buy.png', height: 25, width: 25),
109 + padding: EdgeInsets.only(left: 10, right: 30),
110 + color: isBuySelected
111 + ? null
112 + : Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
113 + onTap: () {
114 + setState(() => isBuySelected = true);
115 + if (widget.onBuyTap != null) widget.onBuyTap!();
116 + },
117 + ),
118 + ),
119 + Expanded(child: const SizedBox()),
120 + Expanded(
121 + flex: 5,
122 + child: SelectButton(
123 + height: 44,
124 + text: S.of(context).sell,
125 + isSelected: !isBuySelected,
126 + showTrailingIcon: false,
127 + textColor: Colors.white,
128 + image: Image.asset('assets/images/sell.png', height: 25, width: 25),
129 + padding: EdgeInsets.only(left: 10, right: 30),
130 + color: !isBuySelected
131 + ? null
132 + : Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
133 + onTap: () {
134 + setState(() => isBuySelected = false);
135 + if (widget.onSellTap != null) widget.onSellTap!();
136 + },
137 + ),
138 + ),
139 + Expanded(flex: 2, child: SizedBox()),
140 + ],
141 + ),
142 + );
143 + }
144 +}
lib/src/screens/seed/pre_seed_page.dart
+1 -1
@@ -1,6 +1,6 @@
1 import 'package:cake_wallet/generated/i18n.dart';
2 import 'package:cake_wallet/routes.dart';
3 -import 'package:cake_wallet/src/screens/InfoPage.dart';
3 +import 'package:cake_wallet/src/screens/Info_page.dart';
4 import 'package:flutter/cupertino.dart';
5
6 class PreSeedPage extends InfoPage {
lib/src/screens/select_options_page.dart new
+199
@@ -0,0 +1,199 @@
1 +import 'package:cake_wallet/core/selectable_option.dart';
2 +import 'package:cake_wallet/src/screens/base_page.dart';
3 +import 'package:cake_wallet/src/widgets/primary_button.dart';
4 +import 'package:cake_wallet/src/widgets/provider_optoin_tile.dart';
5 +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
6 +import 'package:cake_wallet/themes/extensions/option_tile_theme.dart';
7 +import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
8 +import 'package:flutter/material.dart';
9 +
10 +abstract class SelectOptionsPage extends BasePage {
11 + SelectOptionsPage();
12 +
13 + String get pageTitle;
14 +
15 + EdgeInsets? get contentPadding;
16 +
17 + EdgeInsets? get tilePadding;
18 +
19 + EdgeInsets? get innerPadding;
20 +
21 + double? get imageHeight;
22 +
23 + double? get imageWidth;
24 +
25 + Color? get selectedBackgroundColor;
26 +
27 + double? get tileBorderRadius;
28 +
29 + String get bottomSectionText;
30 +
31 + bool get primaryButtonEnabled => true;
32 +
33 + String get primaryButtonText => '';
34 +
35 + List<SelectableItem> get items;
36 +
37 + void Function(SelectableOption option)? get onOptionTap;
38 +
39 + void Function(BuildContext context)? get primaryButtonAction;
40 +
41 + @override
42 + String get title => pageTitle;
43 +
44 + @override
45 + Widget body(BuildContext context) {
46 + return ScrollableWithBottomSection(
47 + content: BodySelectOptionsPage(
48 + items: items,
49 + onOptionTap: onOptionTap,
50 + tilePadding: tilePadding,
51 + tileBorderRadius: tileBorderRadius,
52 + imageHeight: imageHeight,
53 + imageWidth: imageWidth,
54 + innerPadding: innerPadding),
55 + bottomSection: Padding(
56 + padding: contentPadding ?? EdgeInsets.zero,
57 + child: Column(
58 + children: [
59 + Text(
60 + bottomSectionText,
61 + textAlign: TextAlign.center,
62 + style: TextStyle(
63 + fontSize: 14,
64 + fontWeight: FontWeight.normal,
65 + color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
66 + ),
67 + ),
68 + if (primaryButtonEnabled)
69 + LoadingPrimaryButton(
70 + text: primaryButtonText,
71 + onPressed: () {
72 + primaryButtonAction != null
73 + ? primaryButtonAction!(context)
74 + : Navigator.pop(context);
75 + },
76 + color: Theme.of(context).primaryColor,
77 + textColor: Colors.white,
78 + isDisabled: false,
79 + isLoading: false)
80 + ],
81 + ),
82 + ),
83 + );
84 + }
85 +}
86 +
87 +class BodySelectOptionsPage extends StatefulWidget {
88 + const BodySelectOptionsPage({
89 + required this.items,
90 + this.onOptionTap,
91 + this.tilePadding,
92 + this.tileBorderRadius,
93 + this.imageHeight,
94 + this.imageWidth,
95 + this.innerPadding,
96 + });
97 +
98 + final List<SelectableItem> items;
99 + final void Function(SelectableOption option)? onOptionTap;
100 + final EdgeInsets? tilePadding;
101 + final double? tileBorderRadius;
102 + final double? imageHeight;
103 + final double? imageWidth;
104 + final EdgeInsets? innerPadding;
105 +
106 + @override
107 + _BodySelectOptionsPageState createState() => _BodySelectOptionsPageState();
108 +}
109 +
110 +class _BodySelectOptionsPageState extends State<BodySelectOptionsPage> {
111 + late List<SelectableItem> _items;
112 +
113 + @override
114 + void initState() {
115 + super.initState();
116 + _items = widget.items;
117 + }
118 +
119 + void _handleOptionTap(SelectableOption option) {
120 + setState(() {
121 + for (var item in _items) {
122 + if (item is SelectableOption) {
123 + item.isOptionSelected = false;
124 + }
125 + }
126 + option.isOptionSelected = true;
127 + });
128 + widget.onOptionTap?.call(option);
129 + }
130 +
131 + @override
132 + Widget build(BuildContext context) {
133 + final isLightMode = Theme.of(context).extension<OptionTileTheme>()?.useDarkImage ?? false;
134 +
135 + Color titleColor =
136 + isLightMode ? Theme.of(context).appBarTheme.titleTextStyle!.color! : Colors.white;
137 +
138 + return Center(
139 + child: ConstrainedBox(
140 + constraints: const BoxConstraints(maxWidth: 350),
141 + child: Column(
142 + children: _items.map((item) {
143 + if (item is OptionTitle) {
144 + return Padding(
145 + padding: const EdgeInsets.only(top: 18, bottom: 8),
146 + child: Container(
147 + width: double.infinity,
148 + decoration: BoxDecoration(
149 + border: Border(
150 + bottom: BorderSide(
151 + color: titleColor,
152 + width: 1,
153 + ),
154 + ),
155 + ),
156 + child: Padding(
157 + padding: const EdgeInsets.only(bottom: 8.0),
158 + child: Text(
159 + item.title,
160 + style: TextStyle(
161 + fontSize: 18,
162 + fontWeight: FontWeight.w700,
163 + color: titleColor,
164 + ),
165 + ),
166 + ),
167 + ),
168 + );
169 + } else if (item is SelectableOption) {
170 + return Padding(
171 + padding: widget.tilePadding ?? const EdgeInsets.only(top: 24),
172 + child: ProviderOptionTile(
173 + title: item.title,
174 + lightImagePath: item.lightIconPath,
175 + darkImagePath: item.darkIconPath,
176 + imageHeight: widget.imageHeight,
177 + imageWidth: widget.imageWidth,
178 + padding: widget.innerPadding,
179 + description: item.description,
180 + topLeftSubTitle: item.topLeftSubTitle,
181 + topRightSubTitle: item.topRightSubTitle,
182 + rightSubTitleLightIconPath: item.topRightSubTitleLightIconPath,
183 + rightSubTitleDarkIconPath: item.topRightSubTitleDarkIconPath,
184 + bottomLeftSubTitle: item.bottomLeftSubTitle,
185 + badges: item.badges,
186 + isSelected: item.isOptionSelected,
187 + borderRadius: widget.tileBorderRadius,
188 + isLightMode: isLightMode,
189 + onPressed: () => _handleOptionTap(item),
190 + ),
191 + );
192 + }
193 + return const SizedBox.shrink();
194 + }).toList(),
195 + ),
196 + ),
197 + );
198 + }
199 +}
lib/src/screens/settings/other_settings_page.dart
-16
@@ -57,22 +57,6 @@ class OtherSettingsPage extends BasePage {
57 handler: (BuildContext context) =>
58 Navigator.of(context).pushNamed(Routes.changeRep),
59 ),
60 - if(_otherSettingsViewModel.isEnabledBuyAction)
61 - SettingsPickerCell(
62 - title: S.current.default_buy_provider,
63 - items: _otherSettingsViewModel.availableBuyProvidersTypes,
64 - displayItem: _otherSettingsViewModel.getBuyProviderType,
65 - selectedItem: _otherSettingsViewModel.buyProviderType,
66 - onItemSelected: _otherSettingsViewModel.onBuyProviderTypeSelected
67 - ),
68 - if(_otherSettingsViewModel.isEnabledSellAction)
69 - SettingsPickerCell(
70 - title: S.current.default_sell_provider,
71 - items: _otherSettingsViewModel.availableSellProvidersTypes,
72 - displayItem: _otherSettingsViewModel.getSellProviderType,
73 - selectedItem: _otherSettingsViewModel.sellProviderType,
74 - onItemSelected: _otherSettingsViewModel.onSellProviderTypeSelected,
75 - ),
60 SettingsCellWithArrow(
61 title: S.current.settings_terms_and_conditions,
62 handler: (BuildContext context) =>
lib/src/screens/settings/privacy_page.dart
+3 -9
@@ -73,16 +73,10 @@ class PrivacyPage extends BasePage {
73 _privacySettingsViewModel.setIsAppSecure(value);
74 }),
75 SettingsSwitcherCell(
76 - title: S.current.disable_buy,
77 - value: _privacySettingsViewModel.disableBuy,
76 + title: S.current.disable_trade_option,
77 + value: _privacySettingsViewModel.disableTradeOption,
78 onValueChange: (BuildContext _, bool value) {
79 - _privacySettingsViewModel.setDisableBuy(value);
80 - }),
81 - SettingsSwitcherCell(
82 - title: S.current.disable_sell,
83 - value: _privacySettingsViewModel.disableSell,
84 - onValueChange: (BuildContext _, bool value) {
85 - _privacySettingsViewModel.setDisableSell(value);
79 + _privacySettingsViewModel.setDisableTradeOption(value);
80 }),
81 SettingsSwitcherCell(
82 title: S.current.disable_bulletin,
lib/src/screens/setup_2fa/setup_2fa_info_page.dart
+1 -1
@@ -1,6 +1,6 @@
1 import 'package:cake_wallet/generated/i18n.dart';
2 import 'package:cake_wallet/routes.dart';
3 -import 'package:cake_wallet/src/screens/InfoPage.dart';
3 +import 'package:cake_wallet/src/screens/Info_page.dart';
4 import 'package:flutter/cupertino.dart';
5
6 class Setup2FAInfoPage extends InfoPage {
lib/src/widgets/address_text_field.dart
+4 -3
@@ -1,20 +1,21 @@
1 import 'package:cake_wallet/utils/device_info.dart';
2 import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
3 import 'package:cake_wallet/utils/responsive_layout_util.dart';
4 +import 'package:cw_core/currency.dart';
5 import 'package:flutter/services.dart';
6 import 'package:flutter/material.dart';
7 import 'package:cake_wallet/routes.dart';
8 import 'package:cake_wallet/generated/i18n.dart';
9 import 'package:cake_wallet/entities/qr_scanner.dart';
10 import 'package:cake_wallet/entities/contact_base.dart';
10 -import 'package:cw_core/crypto_currency.dart';
11 import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
12 import 'package:cake_wallet/utils/permission_handler.dart';
13 import 'package:permission_handler/permission_handler.dart';
14
15 enum AddressTextFieldOption { paste, qrCode, addressBook, walletAddresses }
16
17 -class AddressTextField extends StatelessWidget {
17 +
18 +class AddressTextField<T extends Currency> extends StatelessWidget{
19 AddressTextField({
20 required this.controller,
21 this.isActive = true,
@@ -58,7 +59,7 @@ class AddressTextField extends StatelessWidget {
59 final Function(BuildContext context)? onPushAddressBookButton;
60 final Function(BuildContext context)? onPushAddressPickerButton;
61 final Function(ContactBase contact)? onSelectedContact;
61 - final CryptoCurrency? selectedCurrency;
62 + final T? selectedCurrency;
63 final Key? addressKey;
64
65 @override
lib/src/widgets/provider_optoin_tile.dart new
+527
@@ -0,0 +1,527 @@
1 +import 'package:cake_wallet/themes/extensions/option_tile_theme.dart';
2 +import 'package:cake_wallet/themes/extensions/receive_page_theme.dart';
3 +import 'package:cake_wallet/typography.dart';
4 +import 'package:flutter/material.dart';
5 +import 'package:flutter_svg/svg.dart';
6 +
7 +class ProviderOptionTile extends StatelessWidget {
8 + const ProviderOptionTile({
9 + required this.onPressed,
10 + required this.lightImagePath,
11 + required this.darkImagePath,
12 + required this.title,
13 + this.topLeftSubTitle,
14 + this.topRightSubTitle,
15 + this.bottomLeftSubTitle,
16 + this.bottomRightSubTitle,
17 + this.leftSubTitleIconPath,
18 + this.rightSubTitleLightIconPath,
19 + this.rightSubTitleDarkIconPath,
20 + this.description,
21 + this.badges,
22 + this.borderRadius,
23 + this.imageHeight,
24 + this.imageWidth,
25 + this.padding,
26 + this.titleTextStyle,
27 + this.firstSubTitleTextStyle,
28 + this.secondSubTitleTextStyle,
29 + this.leadingIcon,
30 + this.selectedBackgroundColor,
31 + this.isSelected = false,
32 + required this.isLightMode,
33 + });
34 +
35 + final VoidCallback onPressed;
36 + final String lightImagePath;
37 + final String darkImagePath;
38 + final String title;
39 + final String? topLeftSubTitle;
40 + final String? topRightSubTitle;
41 + final String? bottomLeftSubTitle;
42 + final String? bottomRightSubTitle;
43 + final String? leftSubTitleIconPath;
44 + final String? rightSubTitleLightIconPath;
45 + final String? rightSubTitleDarkIconPath;
46 + final String? description;
47 + final List<String>? badges;
48 + final double? borderRadius;
49 + final double? imageHeight;
50 + final double? imageWidth;
51 + final EdgeInsets? padding;
52 + final TextStyle? titleTextStyle;
53 + final TextStyle? firstSubTitleTextStyle;
54 + final TextStyle? secondSubTitleTextStyle;
55 + final IconData? leadingIcon;
56 + final Color? selectedBackgroundColor;
57 + final bool isSelected;
58 + final bool isLightMode;
59 +
60 + @override
61 + Widget build(BuildContext context) {
62 + final backgroundColor = isSelected
63 + ? isLightMode
64 + ? Theme.of(context).extension<ReceivePageTheme>()!.currentTileBackgroundColor
65 + : Theme.of(context).extension<OptionTileTheme>()!.titleColor
66 + : Theme.of(context).cardColor;
67 +
68 + final textColor = isSelected
69 + ? isLightMode
70 + ? Colors.white
71 + : Theme.of(context).cardColor
72 + : Theme.of(context).extension<OptionTileTheme>()!.titleColor;
73 +
74 + final badgeColor = isSelected
75 + ? Theme.of(context).cardColor
76 + : Theme.of(context).extension<OptionTileTheme>()!.titleColor;
77 +
78 + final badgeTextColor = isSelected
79 + ? Theme.of(context).extension<OptionTileTheme>()!.titleColor
80 + : Theme.of(context).cardColor;
81 +
82 + final imagePath = isSelected
83 + ? isLightMode
84 + ? darkImagePath
85 + : lightImagePath
86 + : isLightMode
87 + ? lightImagePath
88 + : darkImagePath;
89 +
90 + final rightSubTitleIconPath = isSelected
91 + ? isLightMode
92 + ? rightSubTitleDarkIconPath
93 + : rightSubTitleLightIconPath
94 + : isLightMode
95 + ? rightSubTitleLightIconPath
96 + : rightSubTitleDarkIconPath;
97 +
98 + return GestureDetector(
99 + onTap: onPressed,
100 + child: Container(
101 + width: double.infinity,
102 + alignment: Alignment.center,
103 + decoration: BoxDecoration(
104 + borderRadius: BorderRadius.all(Radius.circular(borderRadius ?? 12)),
105 + border: isSelected && !isLightMode ? Border.all(color: textColor) : null,
106 + color: backgroundColor,
107 + ),
108 + child: Padding(
109 + padding: padding ?? const EdgeInsets.all(16),
110 + child: Column(
111 + crossAxisAlignment: CrossAxisAlignment.center,
112 + children: [
113 + Row(
114 + children: [
115 + getImage(imagePath, height: imageHeight, width: imageWidth),
116 + SizedBox(width: 8),
117 + Expanded(
118 + child: Container(
119 + child: Row(
120 + children: [
121 + Expanded(
122 + child: Text(title,
123 + style: titleTextStyle ?? textLargeBold(color: textColor))),
124 + Row(
125 + children: [
126 + if (leadingIcon != null)
127 + Icon(leadingIcon, size: 16, color: textColor),
128 + ],
129 + )
130 + ],
131 + ),
132 + ),
133 + ),
134 + ],
135 + ),
136 + if (topLeftSubTitle != null || topRightSubTitle != null)
137 + subTitleWidget(
138 + leftSubTitle: topLeftSubTitle,
139 + subTitleIconPath: leftSubTitleIconPath,
140 + textColor: textColor,
141 + rightSubTitle: topRightSubTitle,
142 + rightSubTitleIconPath: rightSubTitleIconPath),
143 + if (bottomLeftSubTitle != null || bottomRightSubTitle != null)
144 + subTitleWidget(
145 + leftSubTitle: bottomLeftSubTitle,
146 + textColor: textColor,
147 + subTitleFontSize: 12),
148 + if (badges != null && badges!.isNotEmpty)
149 + Padding(
150 + padding: const EdgeInsets.only(top: 12),
151 + child: Row(children: [
152 + ...badges!
153 + .map((badge) => Badge(
154 + title: badge, textColor: badgeTextColor, backgroundColor: badgeColor))
155 + .toList()
156 + ]),
157 + )
158 + ],
159 + ),
160 + ),
161 + ),
162 + );
163 + }
164 +}
165 +
166 +class subTitleWidget extends StatelessWidget {
167 + const subTitleWidget({
168 + super.key,
169 + this.leftSubTitle,
170 + this.subTitleIconPath,
171 + required this.textColor,
172 + this.rightSubTitle,
173 + this.rightSubTitleIconPath,
174 + this.subTitleFontSize = 16,
175 + });
176 +
177 + final String? leftSubTitle;
178 + final String? subTitleIconPath;
179 + final Color textColor;
180 + final String? rightSubTitle;
181 + final String? rightSubTitleIconPath;
182 + final double subTitleFontSize;
183 +
184 + @override
185 + Widget build(BuildContext context) {
186 + return Row(
187 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
188 + children: [
189 + leftSubTitle != null || subTitleIconPath != null
190 + ? Row(
191 + children: [
192 + if (subTitleIconPath != null && subTitleIconPath!.isNotEmpty)
193 + Padding(
194 + padding: const EdgeInsets.only(right: 6),
195 + child: getImage(subTitleIconPath!),
196 + ),
197 + Text(
198 + leftSubTitle ?? '',
199 + style: TextStyle(
200 + fontSize: subTitleFontSize,
201 + fontWeight: FontWeight.w700,
202 + color: textColor),
203 + ),
204 + ],
205 + )
206 + : Offstage(),
207 + rightSubTitle != null || rightSubTitleIconPath != null
208 + ? Row(
209 + children: [
210 + if (rightSubTitleIconPath != null && rightSubTitleIconPath!.isNotEmpty)
211 + Padding(
212 + padding: const EdgeInsets.only(right: 4),
213 + child: getImage(rightSubTitleIconPath!, imageColor: textColor),
214 + ),
215 + Text(
216 + rightSubTitle ?? '',
217 + style: TextStyle(
218 + fontSize: subTitleFontSize, fontWeight: FontWeight.w700, color: textColor),
219 + ),
220 + ],
221 + )
222 + : Offstage(),
223 + ],
224 + );
225 + }
226 +}
227 +
228 +class Badge extends StatelessWidget {
229 + Badge({required this.textColor, required this.backgroundColor, required this.title});
230 +
231 + final String title;
232 + final Color textColor;
233 + final Color backgroundColor;
234 +
235 + @override
236 + Widget build(BuildContext context) {
237 + return Padding(
238 + padding: const EdgeInsets.only(right: 8),
239 + child: FittedBox(
240 + fit: BoxFit.fitHeight,
241 + child: Container(
242 + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4),
243 + decoration: BoxDecoration(
244 + borderRadius: BorderRadius.all(Radius.circular(24)), color: backgroundColor),
245 + alignment: Alignment.center,
246 + child: Text(
247 + title,
248 + style: TextStyle(
249 + color: textColor,
250 + fontSize: 12,
251 + fontWeight: FontWeight.w600,
252 + ),
253 + ),
254 + ),
255 + ),
256 + );
257 + }
258 +}
259 +
260 +Widget getImage(String imagePath, {double? height, double? width, Color? imageColor}) {
261 + final bool isNetworkImage = imagePath.startsWith('http') || imagePath.startsWith('https');
262 + final bool isSvg = imagePath.endsWith('.svg');
263 + final double imageHeight = height ?? 35;
264 + final double imageWidth = width ?? 35;
265 +
266 + if (isNetworkImage) {
267 + return isSvg
268 + ? SvgPicture.network(
269 + imagePath,
270 + height: imageHeight,
271 + width: imageWidth,
272 + colorFilter: imageColor != null ? ColorFilter.mode(imageColor, BlendMode.srcIn) : null,
273 + placeholderBuilder: (BuildContext context) => Container(
274 + height: imageHeight,
275 + width: imageWidth,
276 + child: Center(
277 + child: CircularProgressIndicator(),
278 + ),
279 + ),
280 + )
281 + : Image.network(
282 + imagePath,
283 + height: imageHeight,
284 + width: imageWidth,
285 + loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
286 + if (loadingProgress == null) {
287 + return child;
288 + }
289 + return Container(
290 + height: imageHeight,
291 + width: imageWidth,
292 + child: Center(
293 + child: CircularProgressIndicator(
294 + value: loadingProgress.expectedTotalBytes != null
295 + ? loadingProgress.cumulativeBytesLoaded /
296 + loadingProgress.expectedTotalBytes!
297 + : null,
298 + ),
299 + ),
300 + );
301 + },
302 + errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) {
303 + return Container(
304 + height: imageHeight,
305 + width: imageWidth,
306 + );
307 + },
308 + );
309 + } else {
310 + return isSvg
311 + ? SvgPicture.asset(
312 + imagePath,
313 + height: imageHeight,
314 + width: imageWidth,
315 + colorFilter: imageColor != null ? ColorFilter.mode(imageColor, BlendMode.srcIn) : null,
316 + )
317 + : Image.asset(imagePath, height: imageHeight, width: imageWidth);
318 + }
319 +}
320 +
321 +class OptionTilePlaceholder extends StatefulWidget {
322 + OptionTilePlaceholder({
323 + this.borderRadius,
324 + this.imageHeight,
325 + this.imageWidth,
326 + this.padding,
327 + this.leadingIcon,
328 + this.withBadge = true,
329 + this.withSubtitle = true,
330 + this.isDarkTheme = false,
331 + this.errorText,
332 + });
333 +
334 + final double? borderRadius;
335 + final double? imageHeight;
336 + final double? imageWidth;
337 + final EdgeInsets? padding;
338 + final IconData? leadingIcon;
339 + final bool withBadge;
340 + final bool withSubtitle;
341 + final bool isDarkTheme;
342 + final String? errorText;
343 +
344 + @override
345 + _OptionTilePlaceholderState createState() => _OptionTilePlaceholderState();
346 +}
347 +
348 +class _OptionTilePlaceholderState extends State<OptionTilePlaceholder>
349 + with SingleTickerProviderStateMixin {
350 + late AnimationController _controller;
351 + late Animation<double> _animation;
352 +
353 + @override
354 + void initState() {
355 + super.initState();
356 +
357 + _controller = AnimationController(
358 + duration: const Duration(seconds: 2),
359 + vsync: this,
360 + )..repeat();
361 +
362 + _animation = CurvedAnimation(
363 + parent: _controller,
364 + curve: Curves.linear,
365 + );
366 + }
367 +
368 + @override
369 + void dispose() {
370 + _controller.dispose();
371 + super.dispose();
372 + }
373 +
374 + @override
375 + Widget build(BuildContext context) {
376 + final backgroundColor = Theme.of(context).cardColor;
377 + final titleColor = Theme.of(context).extension<OptionTileTheme>()!.titleColor.withOpacity(0.4);
378 +
379 + return widget.errorText != null
380 + ? Container(
381 + width: double.infinity,
382 + padding: widget.padding ?? EdgeInsets.all(16),
383 + alignment: Alignment.center,
384 + decoration: BoxDecoration(
385 + borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius ?? 12)),
386 + color: backgroundColor,
387 + ),
388 + child: Column(
389 + children: [
390 + Text(
391 + widget.errorText!,
392 + style: TextStyle(
393 + color: titleColor,
394 + fontSize: 16,
395 + ),
396 + ),
397 + if (widget.withSubtitle) SizedBox(height: 8),
398 + Text(
399 + '',
400 + style: TextStyle(
401 + color: titleColor,
402 + fontSize: 16,
403 + ),
404 + ),
405 + ],
406 + ),
407 + )
408 + : AnimatedBuilder(
409 + animation: _animation,
410 + builder: (context, child) {
411 + return Stack(
412 + children: [
413 + Container(
414 + width: double.infinity,
415 + padding: widget.padding ?? EdgeInsets.all(16),
416 + alignment: Alignment.center,
417 + decoration: BoxDecoration(
418 + borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius ?? 12)),
419 + color: backgroundColor,
420 + ),
421 + child: Column(
422 + crossAxisAlignment: CrossAxisAlignment.center,
423 + children: [
424 + Row(
425 + children: [
426 + Container(
427 + height: widget.imageHeight ?? 35,
428 + width: widget.imageWidth ?? 35,
429 + decoration: BoxDecoration(
430 + color: titleColor,
431 + shape: BoxShape.circle,
432 + ),
433 + ),
434 + SizedBox(width: 8),
435 + Expanded(
436 + child: Container(
437 + child: Row(
438 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
439 + children: [
440 + Container(
441 + height: 20,
442 + width: 70,
443 + decoration: BoxDecoration(
444 + color: titleColor,
445 + borderRadius: BorderRadius.all(Radius.circular(8)),
446 + ),
447 + ),
448 + if (widget.leadingIcon != null)
449 + Icon(widget.leadingIcon, size: 16, color: titleColor),
450 + ],
451 + ),
452 + ),
453 + ),
454 + ],
455 + ),
456 + if (widget.withSubtitle)
457 + Padding(
458 + padding: EdgeInsets.symmetric(vertical: 8),
459 + child: Row(
460 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
461 + children: [
462 + Container(
463 + height: 20,
464 + width: 170,
465 + decoration: BoxDecoration(
466 + color: titleColor,
467 + borderRadius: BorderRadius.all(Radius.circular(8)),
468 + ),
469 + ),
470 + ],
471 + ),
472 + ),
473 + if (widget.withBadge)
474 + Padding(
475 + padding: const EdgeInsets.only(top: 16.0),
476 + child: Row(
477 + children: [
478 + Container(
479 + height: 30,
480 + width: 70,
481 + decoration: BoxDecoration(
482 + color: titleColor,
483 + borderRadius: BorderRadius.all(Radius.circular(20)),
484 + ),
485 + ),
486 + SizedBox(width: 8),
487 + Container(
488 + height: 30,
489 + width: 70,
490 + decoration: BoxDecoration(
491 + color: titleColor,
492 + borderRadius: BorderRadius.all(Radius.circular(20)),
493 + ),
494 + ),
495 + ],
496 + ),
497 + ),
498 + ],
499 + ),
500 + ),
501 + Positioned.fill(
502 + child: Container(
503 + decoration: BoxDecoration(
504 + borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius ?? 12)),
505 + gradient: LinearGradient(
506 + begin: Alignment(-2, -4),
507 + end: Alignment(2, 4),
508 + stops: [
509 + _animation.value - 0.2,
510 + _animation.value,
511 + _animation.value + 0.2,
512 + ],
513 + colors: [
514 + backgroundColor.withOpacity(widget.isDarkTheme ? 0.4 : 0.7),
515 + backgroundColor.withOpacity(widget.isDarkTheme ? 0.7 : 0.4),
516 + backgroundColor.withOpacity(widget.isDarkTheme ? 0.4 : 0.7),
517 + ],
518 + ),
519 + ),
520 + ),
521 + ),
522 + ],
523 + );
524 + },
525 + );
526 + }
527 +}
lib/store/settings_store.dart
+10 -69
@@ -61,8 +61,7 @@ abstract class SettingsStoreBase with Store {
61 required BitcoinSeedType initialBitcoinSeedType,
62 required NanoSeedType initialNanoSeedType,
63 required bool initialAppSecure,
64 - required bool initialDisableBuy,
65 - required bool initialDisableSell,
64 + required bool initialDisableTrade,
65 required FilterListOrderType initialWalletListOrder,
66 required FilterListOrderType initialContactListOrder,
67 required bool initialDisableBulletin,
@@ -150,8 +149,7 @@ abstract class SettingsStoreBase with Store {
149 useTOTP2FA = initialUseTOTP2FA,
150 numberOfFailedTokenTrials = initialFailedTokenTrial,
151 isAppSecure = initialAppSecure,
153 - disableBuy = initialDisableBuy,
154 - disableSell = initialDisableSell,
152 + disableTradeOption = initialDisableTrade,
153 disableBulletin = initialDisableBulletin,
154 walletListOrder = initialWalletListOrder,
155 contactListOrder = initialContactListOrder,
@@ -178,9 +176,7 @@ abstract class SettingsStoreBase with Store {
176 initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings,
177 currentSyncMode = initialSyncMode,
178 currentSyncAll = initialSyncAll,
181 - priority = ObservableMap<WalletType, TransactionPriority>(),
182 - defaultBuyProviders = ObservableMap<WalletType, ProviderType>(),
183 - defaultSellProviders = ObservableMap<WalletType, ProviderType>() {
179 + priority = ObservableMap<WalletType, TransactionPriority>() {
180 //this.nodes = ObservableMap<WalletType, Node>.of(nodes);
181
182 if (initialMoneroTransactionPriority != null) {
@@ -221,30 +217,6 @@ abstract class SettingsStoreBase with Store {
217
218 initializeTrocadorProviderStates();
219
224 - WalletType.values.forEach((walletType) {
225 - final key = 'buyProvider_${walletType.toString()}';
226 - final providerId = sharedPreferences.getString(key);
227 - if (providerId != null) {
228 - defaultBuyProviders[walletType] = ProviderType.values.firstWhere(
229 - (provider) => provider.id == providerId,
230 - orElse: () => ProviderType.askEachTime);
231 - } else {
232 - defaultBuyProviders[walletType] = ProviderType.askEachTime;
233 - }
234 - });
235 -
236 - WalletType.values.forEach((walletType) {
237 - final key = 'sellProvider_${walletType.toString()}';
238 - final providerId = sharedPreferences.getString(key);
239 - if (providerId != null) {
240 - defaultSellProviders[walletType] = ProviderType.values.firstWhere(
241 - (provider) => provider.id == providerId,
242 - orElse: () => ProviderType.askEachTime);
243 - } else {
244 - defaultSellProviders[walletType] = ProviderType.askEachTime;
245 - }
246 - });
247 -
220 reaction(
221 (_) => fiatCurrency,
222 (FiatCurrency fiatCurrency) => sharedPreferences.setString(
@@ -267,20 +239,6 @@ abstract class SettingsStoreBase with Store {
239 reaction((_) => shouldShowRepWarning,
240 (bool val) => sharedPreferences.setBool(PreferencesKey.shouldShowRepWarning, val));
241
270 - defaultBuyProviders.observe((change) {
271 - final String key = 'buyProvider_${change.key.toString()}';
272 - if (change.newValue != null) {
273 - sharedPreferences.setString(key, change.newValue!.id);
274 - }
275 - });
276 -
277 - defaultSellProviders.observe((change) {
278 - final String key = 'sellProvider_${change.key.toString()}';
279 - if (change.newValue != null) {
280 - sharedPreferences.setString(key, change.newValue!.id);
281 - }
282 - });
283 -
242 priority.observe((change) {
243 final String? key;
244 switch (change.key) {
@@ -329,14 +287,9 @@ abstract class SettingsStoreBase with Store {
287 });
288 }
289
332 - reaction((_) => disableBuy,
333 - (bool disableBuy) => sharedPreferences.setBool(PreferencesKey.disableBuyKey, disableBuy));
334 -
335 - reaction(
336 - (_) => disableSell,
337 - (bool disableSell) =>
338 - sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell));
339 -
290 + reaction((_) => disableTradeOption,
291 + (bool disableTradeOption) => sharedPreferences.setBool(PreferencesKey.disableTradeOption, disableTradeOption));
292 +
293 reaction(
294 (_) => disableBulletin,
295 (bool disableBulletin) =>
@@ -680,10 +633,7 @@ abstract class SettingsStoreBase with Store {
633 bool isAppSecure;
634
635 @observable
683 - bool disableBuy;
684 -
685 - @observable
686 - bool disableSell;
636 + bool disableTradeOption;
637
638 @observable
639 FilterListOrderType contactListOrder;
@@ -769,12 +719,6 @@ abstract class SettingsStoreBase with Store {
719 @observable
720 ObservableMap<String, bool> trocadorProviderStates = ObservableMap<String, bool>();
721
772 - @observable
773 - ObservableMap<WalletType, ProviderType> defaultBuyProviders;
774 -
775 - @observable
776 - ObservableMap<WalletType, ProviderType> defaultSellProviders;
777 -
722 @observable
723 SortBalanceBy sortBalanceBy;
724
@@ -956,8 +900,7 @@ abstract class SettingsStoreBase with Store {
900 final shouldSaveRecipientAddress =
901 sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ?? false;
902 final isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? false;
959 - final disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? false;
960 - final disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? false;
903 + final disableTradeOption = sharedPreferences.getBool(PreferencesKey.disableTradeOption) ?? false;
904 final disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? false;
905 final walletListOrder =
906 FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0];
@@ -1255,8 +1198,7 @@ abstract class SettingsStoreBase with Store {
1198 initialBitcoinSeedType: bitcoinSeedType,
1199 initialNanoSeedType: nanoSeedType,
1200 initialAppSecure: isAppSecure,
1258 - initialDisableBuy: disableBuy,
1259 - initialDisableSell: disableSell,
1201 + initialDisableTrade: disableTradeOption,
1202 initialDisableBulletin: disableBulletin,
1203 initialWalletListOrder: walletListOrder,
1204 initialWalletListAscending: walletListAscending,
@@ -1406,8 +1348,7 @@ abstract class SettingsStoreBase with Store {
1348 numberOfFailedTokenTrials =
1349 sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? numberOfFailedTokenTrials;
1350 isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? isAppSecure;
1409 - disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? disableBuy;
1410 - disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? disableSell;
1351 + disableTradeOption = sharedPreferences.getBool(PreferencesKey.disableTradeOption) ?? disableTradeOption;
1352 disableBulletin =
1353 sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? disableBulletin;
1354 walletListOrder =
lib/typography.dart
+2
@@ -22,6 +22,8 @@ TextStyle textMediumSemiBold({Color? color}) => _cakeSemiBold(22, color);
22
23 TextStyle textLarge({Color? color}) => _cakeRegular(18, color);
24
25 +TextStyle textLargeBold({Color? color}) => _cakeBold(18, color);
26 +
27 TextStyle textLargeSemiBold({Color? color}) => _cakeSemiBold(24, color);
28
29 TextStyle textXLarge({Color? color}) => _cakeRegular(32, color);
lib/view_model/buy/buy_sell_view_model.dart new
+446
@@ -0,0 +1,446 @@
1 +import 'dart:async';
2 +
3 +import 'package:cake_wallet/buy/buy_provider.dart';
4 +import 'package:cake_wallet/buy/buy_quote.dart';
5 +import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart';
6 +import 'package:cake_wallet/buy/payment_method.dart';
7 +import 'package:cake_wallet/buy/sell_buy_states.dart';
8 +import 'package:cake_wallet/core/selectable_option.dart';
9 +import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
10 +import 'package:cake_wallet/entities/fiat_currency.dart';
11 +import 'package:cake_wallet/entities/provider_types.dart';
12 +import 'package:cake_wallet/generated/i18n.dart';
13 +import 'package:cake_wallet/routes.dart';
14 +import 'package:cake_wallet/store/app_store.dart';
15 +import 'package:cake_wallet/store/settings_store.dart';
16 +import 'package:cake_wallet/themes/theme_base.dart';
17 +import 'package:cw_core/crypto_currency.dart';
18 +import 'package:cw_core/currency_for_wallet_type.dart';
19 +import 'package:flutter/cupertino.dart';
20 +import 'package:intl/intl.dart';
21 +import 'package:mobx/mobx.dart';
22 +
23 +part 'buy_sell_view_model.g.dart';
24 +
25 +class BuySellViewModel = BuySellViewModelBase with _$BuySellViewModel;
26 +
27 +abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with Store {
28 + BuySellViewModelBase(
29 + AppStore appStore,
30 + ) : _cryptoNumberFormat = NumberFormat(),
31 + cryptoAmount = '',
32 + fiatAmount = '',
33 + cryptoCurrencyAddress = '',
34 + isCryptoCurrencyAddressEnabled = false,
35 + cryptoCurrencies = <CryptoCurrency>[],
36 + fiatCurrencies = <FiatCurrency>[],
37 + paymentMethodState = InitialPaymentMethod(),
38 + buySellQuotState = InitialBuySellQuotState(),
39 + cryptoCurrency = appStore.wallet!.currency,
40 + fiatCurrency = appStore.settingsStore.fiatCurrency,
41 + providerList = [],
42 + sortedRecommendedQuotes = ObservableList<Quote>(),
43 + sortedQuotes = ObservableList<Quote>(),
44 + paymentMethods = ObservableList<PaymentMethod>(),
45 + settingsStore = appStore.settingsStore,
46 + super(appStore: appStore) {
47 + const excludeFiatCurrencies = [];
48 + const excludeCryptoCurrencies = [];
49 +
50 + fiatCurrencies =
51 + FiatCurrency.all.where((currency) => !excludeFiatCurrencies.contains(currency)).toList();
52 + cryptoCurrencies = CryptoCurrency.all
53 + .where((currency) => !excludeCryptoCurrencies.contains(currency))
54 + .toList();
55 + _initialize();
56 +
57 + isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency);
58 + }
59 +
60 + final NumberFormat _cryptoNumberFormat;
61 + late Timer bestRateSync;
62 +
63 + List<BuyProvider> get availableBuyProviders {
64 + final providerTypes = ProvidersHelper.getAvailableBuyProviderTypes(
65 + walletTypeForCurrency(cryptoCurrency) ?? wallet.type);
66 + return providerTypes
67 + .map((type) => ProvidersHelper.getProviderByType(type))
68 + .where((provider) => provider != null)
69 + .cast<BuyProvider>()
70 + .toList();
71 + }
72 +
73 + List<BuyProvider> get availableSellProviders {
74 + final providerTypes = ProvidersHelper.getAvailableSellProviderTypes(
75 + walletTypeForCurrency(cryptoCurrency) ?? wallet.type);
76 + return providerTypes
77 + .map((type) => ProvidersHelper.getProviderByType(type))
78 + .where((provider) => provider != null)
79 + .cast<BuyProvider>()
80 + .toList();
81 + }
82 +
83 + @override
84 + void onWalletChange(wallet) {
85 + cryptoCurrency = wallet.currency;
86 + }
87 +
88 + bool get isDarkTheme => settingsStore.currentTheme.type == ThemeType.dark;
89 +
90 + double get amount {
91 + final formattedFiatAmount = double.tryParse(fiatAmount) ?? 200.0;
92 + final formattedCryptoAmount =
93 + double.tryParse(cryptoAmount) ?? (cryptoCurrency == CryptoCurrency.btc ? 0.001 : 1);
94 +
95 + return isBuyAction ? formattedFiatAmount : formattedCryptoAmount;
96 + }
97 +
98 + SettingsStore settingsStore;
99 +
100 + Quote? bestRateQuote;
101 +
102 + Quote? selectedQuote;
103 +
104 + @observable
105 + List<CryptoCurrency> cryptoCurrencies;
106 +
107 + @observable
108 + List<FiatCurrency> fiatCurrencies;
109 +
110 + @observable
111 + bool isBuyAction = true;
112 +
113 + @observable
114 + List<BuyProvider> providerList;
115 +
116 + @observable
117 + ObservableList<Quote> sortedRecommendedQuotes;
118 +
119 + @observable
120 + ObservableList<Quote> sortedQuotes;
121 +
122 + @observable
123 + ObservableList<PaymentMethod> paymentMethods;
124 +
125 + @observable
126 + FiatCurrency fiatCurrency;
127 +
128 + @observable
129 + CryptoCurrency cryptoCurrency;
130 +
131 + @observable
132 + String cryptoAmount;
133 +
134 + @observable
135 + String fiatAmount;
136 +
137 + @observable
138 + String cryptoCurrencyAddress;
139 +
140 + @observable
141 + bool isCryptoCurrencyAddressEnabled;
142 +
143 + @observable
144 + PaymentMethod? selectedPaymentMethod;
145 +
146 + @observable
147 + PaymentMethodLoadingState paymentMethodState;
148 +
149 + @observable
150 + BuySellQuotLoadingState buySellQuotState;
151 +
152 + @computed
153 + bool get isReadyToTrade {
154 + final hasSelectedQuote = selectedQuote != null;
155 + final hasSelectedPaymentMethod = selectedPaymentMethod != null;
156 + final isPaymentMethodLoaded = paymentMethodState is PaymentMethodLoaded;
157 + final isBuySellQuotLoaded = buySellQuotState is BuySellQuotLoaded;
158 +
159 + return hasSelectedQuote &&
160 + hasSelectedPaymentMethod &&
161 + isPaymentMethodLoaded &&
162 + isBuySellQuotLoaded;
163 + }
164 +
165 + @action
166 + void reset() {
167 + cryptoCurrency = wallet.currency;
168 + fiatCurrency = settingsStore.fiatCurrency;
169 + isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency);
170 + _initialize();
171 + }
172 +
173 + @action
174 + void changeBuySellAction() {
175 + isBuyAction = !isBuyAction;
176 + _initialize();
177 + }
178 +
179 + @action
180 + void changeFiatCurrency({required FiatCurrency currency}) {
181 + fiatCurrency = currency;
182 + _onPairChange();
183 + }
184 +
185 + @action
186 + void changeCryptoCurrency({required CryptoCurrency currency}) {
187 + cryptoCurrency = currency;
188 + _onPairChange();
189 + isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency);
190 + }
191 +
192 + @action
193 + void changeCryptoCurrencyAddress(String address) => cryptoCurrencyAddress = address;
194 +
195 + @action
196 + Future<void> changeFiatAmount({required String amount}) async {
197 + fiatAmount = amount;
198 +
199 + if (amount.isEmpty) {
200 + fiatAmount = '';
201 + cryptoAmount = '';
202 + return;
203 + }
204 +
205 + final enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0;
206 +
207 + if (!isReadyToTrade) {
208 + cryptoAmount = S.current.fetching;
209 + return;
210 + }
211 +
212 + if (bestRateQuote != null) {
213 + _cryptoNumberFormat.maximumFractionDigits = cryptoCurrency.decimals;
214 + cryptoAmount = _cryptoNumberFormat
215 + .format(enteredAmount / bestRateQuote!.rate)
216 + .toString()
217 + .replaceAll(RegExp('\\,'), '');
218 + } else {
219 + await calculateBestRate();
220 + }
221 + }
222 +
223 + @action
224 + Future<void> changeCryptoAmount({required String amount}) async {
225 + cryptoAmount = amount;
226 +
227 + if (amount.isEmpty) {
228 + fiatAmount = '';
229 + cryptoAmount = '';
230 + return;
231 + }
232 +
233 + final enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0;
234 +
235 + if (!isReadyToTrade) {
236 + fiatAmount = S.current.fetching;
237 + }
238 +
239 + if (bestRateQuote != null) {
240 + fiatAmount = _cryptoNumberFormat
241 + .format(enteredAmount * bestRateQuote!.rate)
242 + .toString()
243 + .replaceAll(RegExp('\\,'), '');
244 + } else {
245 + await calculateBestRate();
246 + }
247 + }
248 +
249 + @action
250 + void changeOption(SelectableOption option) {
251 + if (option is Quote) {
252 + sortedRecommendedQuotes.forEach((element) => element.setIsSelected = false);
253 + sortedQuotes.forEach((element) => element.setIsSelected = false);
254 + option.setIsSelected = true;
255 + selectedQuote = option;
256 + } else if (option is PaymentMethod) {
257 + paymentMethods.forEach((element) => element.isSelected = false);
258 + option.isSelected = true;
259 + selectedPaymentMethod = option;
260 + } else {
261 + throw ArgumentError('Unknown option type');
262 + }
263 + }
264 +
265 + void onTapChoseProvider(BuildContext context) async {
266 + final initialQuotes = List<Quote>.from(sortedRecommendedQuotes + sortedQuotes);
267 + await calculateBestRate();
268 + final newQuotes = (sortedRecommendedQuotes + sortedQuotes);
269 +
270 + for (var quote in newQuotes) quote.limits = null;
271 +
272 + final newQuoteProviders = newQuotes
273 + .map((quote) => quote.provider.isAggregator ? quote.rampName : quote.provider.title)
274 + .toSet();
275 +
276 + final outOfLimitQuotes = initialQuotes.where((initialQuote) {
277 + return !newQuoteProviders.contains(
278 + initialQuote.provider.isAggregator ? initialQuote.rampName : initialQuote.provider.title);
279 + }).map((missingQuote) {
280 + final quote = Quote(
281 + rate: missingQuote.rate,
282 + feeAmount: missingQuote.feeAmount,
283 + networkFee: missingQuote.networkFee,
284 + transactionFee: missingQuote.transactionFee,
285 + payout: missingQuote.payout,
286 + rampId: missingQuote.rampId,
287 + rampName: missingQuote.rampName,
288 + rampIconPath: missingQuote.rampIconPath,
289 + paymentType: missingQuote.paymentType,
290 + quoteId: missingQuote.quoteId,
291 + recommendations: missingQuote.recommendations,
292 + provider: missingQuote.provider,
293 + isBuyAction: missingQuote.isBuyAction,
294 + limits: missingQuote.limits,
295 + );
296 + quote.setFiatCurrency = missingQuote.fiatCurrency;
297 + quote.setCryptoCurrency = missingQuote.cryptoCurrency;
298 + return quote;
299 + }).toList();
300 +
301 + final updatedQuoteOptions = List<SelectableItem>.from([
302 + OptionTitle(title: 'Recommended'),
303 + ...sortedRecommendedQuotes,
304 + if (sortedQuotes.isNotEmpty) OptionTitle(title: 'All Providers'),
305 + ...sortedQuotes,
306 + if (outOfLimitQuotes.isNotEmpty) OptionTitle(title: 'Out of Limits'),
307 + ...outOfLimitQuotes,
308 + ]);
309 +
310 + await Navigator.of(context).pushNamed(
311 + Routes.buyOptionsPage,
312 + arguments: [
313 + updatedQuoteOptions,
314 + changeOption,
315 + launchTrade,
316 + ],
317 + ).then((value) => calculateBestRate());
318 + }
319 +
320 + void _onPairChange() {
321 + _initialize();
322 + }
323 +
324 + void _setProviders() =>
325 + providerList = isBuyAction ? availableBuyProviders : availableSellProviders;
326 +
327 + Future<void> _initialize() async {
328 + _setProviders();
329 + cryptoAmount = '';
330 + fiatAmount = '';
331 + cryptoCurrencyAddress = _getInitialCryptoCurrencyAddress();
332 + paymentMethodState = InitialPaymentMethod();
333 + buySellQuotState = InitialBuySellQuotState();
334 + await _getAvailablePaymentTypes();
335 + await calculateBestRate();
336 + }
337 +
338 + String _getInitialCryptoCurrencyAddress() {
339 + return cryptoCurrency == wallet.currency ? wallet.walletAddresses.address : '';
340 + }
341 +
342 + @action
343 + Future<void> _getAvailablePaymentTypes() async {
344 + paymentMethodState = PaymentMethodLoading();
345 + selectedPaymentMethod = null;
346 + final result = await Future.wait(providerList.map((element) => element
347 + .getAvailablePaymentTypes(fiatCurrency.title, cryptoCurrency.title, isBuyAction)
348 + .timeout(
349 + Duration(seconds: 10),
350 + onTimeout: () => [],
351 + )));
352 +
353 + final Map<PaymentType, PaymentMethod> uniquePaymentMethods = {};
354 + for (var methods in result) {
355 + for (var method in methods) {
356 + uniquePaymentMethods[method.paymentMethodType] = method;
357 + }
358 + }
359 +
360 + paymentMethods = ObservableList<PaymentMethod>.of(uniquePaymentMethods.values);
361 + if (paymentMethods.isNotEmpty) {
362 + paymentMethods.insert(0, PaymentMethod.all());
363 + selectedPaymentMethod = paymentMethods.first;
364 + selectedPaymentMethod!.isSelected = true;
365 + paymentMethodState = PaymentMethodLoaded();
366 + } else {
367 + paymentMethodState = PaymentMethodFailed();
368 + }
369 + }
370 +
371 + @action
372 + Future<void> calculateBestRate() async {
373 + buySellQuotState = BuySellQuotLoading();
374 +
375 + final result = await Future.wait<List<Quote>?>(providerList.map((element) => element
376 + .fetchQuote(
377 + cryptoCurrency: cryptoCurrency,
378 + fiatCurrency: fiatCurrency,
379 + amount: amount,
380 + paymentType: selectedPaymentMethod?.paymentMethodType,
381 + isBuyAction: isBuyAction,
382 + walletAddress: wallet.walletAddresses.address,
383 + )
384 + .timeout(
385 + Duration(seconds: 10),
386 + onTimeout: () => null,
387 + )));
388 +
389 + sortedRecommendedQuotes.clear();
390 + sortedQuotes.clear();
391 +
392 + final validQuotes = result
393 + .where((element) => element != null && element.isNotEmpty)
394 + .expand((element) => element!)
395 + .toList();
396 +
397 + if (validQuotes.isEmpty) {
398 + buySellQuotState = BuySellQuotFailed();
399 + return;
400 + }
401 +
402 + validQuotes.sort((a, b) => a.rate.compareTo(b.rate));
403 +
404 + final Set<String> addedProviders = {};
405 + final List<Quote> uniqueProviderQuotes = validQuotes.where((element) {
406 + if (addedProviders.contains(element.provider.title)) return false;
407 + addedProviders.add(element.provider.title);
408 + return true;
409 + }).toList();
410 +
411 + sortedRecommendedQuotes.addAll(uniqueProviderQuotes);
412 +
413 + sortedQuotes = ObservableList.of(
414 + validQuotes.where((element) => !uniqueProviderQuotes.contains(element)).toList());
415 +
416 + if (sortedRecommendedQuotes.isNotEmpty) {
417 + sortedRecommendedQuotes.first
418 + ..setIsBestRate = true
419 + ..recommendations.insert(0, ProviderRecommendation.bestRate);
420 + bestRateQuote = sortedRecommendedQuotes.first;
421 +
422 + sortedRecommendedQuotes.sort((a, b) {
423 + if (a.provider is OnRamperBuyProvider) return -1;
424 + if (b.provider is OnRamperBuyProvider) return 1;
425 + return 0;
426 + });
427 +
428 + selectedQuote = sortedRecommendedQuotes.first;
429 + sortedRecommendedQuotes.first.setIsSelected = true;
430 + }
431 +
432 + buySellQuotState = BuySellQuotLoaded();
433 + }
434 +
435 + @action
436 + Future<void> launchTrade(BuildContext context) async {
437 + final provider = selectedQuote!.provider;
438 + await provider.launchProvider(
439 + context: context,
440 + quote: selectedQuote!,
441 + amount: amount,
442 + isBuyAction: isBuyAction,
443 + cryptoCurrencyAddress: cryptoCurrencyAddress,
444 + );
445 + }
446 +}
lib/view_model/dashboard/dashboard_view_model.dart
+4 -41
@@ -71,8 +71,7 @@ abstract class DashboardViewModelBase with Store {
71 required this.anonpayTransactionsStore,
72 required this.sharedPreferences,
73 required this.keyService})
74 - : hasSellAction = false,
75 - hasBuyAction = false,
74 + : hasTradeAction = false,
75 hasExchangeAction = false,
76 isShowFirstYatIntroduction = false,
77 isShowSecondYatIntroduction = false,
@@ -521,37 +520,8 @@ abstract class DashboardViewModelBase with Store {
520
521 Map<String, List<FilterItem>> filterItems;
522
524 - BuyProvider? get defaultBuyProvider => ProvidersHelper.getProviderByType(
525 - settingsStore.defaultBuyProviders[wallet.type] ?? ProviderType.askEachTime);
526 -
527 - BuyProvider? get defaultSellProvider => ProvidersHelper.getProviderByType(
528 - settingsStore.defaultSellProviders[wallet.type] ?? ProviderType.askEachTime);
529 -
523 bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled;
524
532 - List<BuyProvider> get availableBuyProviders {
533 - final providerTypes = ProvidersHelper.getAvailableBuyProviderTypes(wallet.type);
534 - return providerTypes
535 - .map((type) => ProvidersHelper.getProviderByType(type))
536 - .where((provider) => provider != null)
537 - .cast<BuyProvider>()
538 - .toList();
539 - }
540 -
541 - bool get hasBuyProviders => ProvidersHelper.getAvailableBuyProviderTypes(wallet.type).isNotEmpty;
542 -
543 - List<BuyProvider> get availableSellProviders {
544 - final providerTypes = ProvidersHelper.getAvailableSellProviderTypes(wallet.type);
545 - return providerTypes
546 - .map((type) => ProvidersHelper.getProviderByType(type))
547 - .where((provider) => provider != null)
548 - .cast<BuyProvider>()
549 - .toList();
550 - }
551 -
552 - bool get hasSellProviders =>
553 - ProvidersHelper.getAvailableSellProviderTypes(wallet.type).isNotEmpty;
554 -
525 bool get shouldShowYatPopup => settingsStore.shouldShowYatPopup;
526
527 @action
@@ -564,16 +534,10 @@ abstract class DashboardViewModelBase with Store {
534 bool hasExchangeAction;
535
536 @computed
567 - bool get isEnabledBuyAction => !settingsStore.disableBuy && hasBuyProviders;
568 -
569 - @observable
570 - bool hasBuyAction;
571 -
572 - @computed
573 - bool get isEnabledSellAction => !settingsStore.disableSell && hasSellProviders;
537 + bool get isEnabledTradeAction => !settingsStore.disableTradeOption;
538
539 @observable
576 - bool hasSellAction;
540 + bool hasTradeAction;
541
542 @computed
543 bool get isEnabledBulletinAction => !settingsStore.disableBulletin;
@@ -776,8 +740,7 @@ abstract class DashboardViewModelBase with Store {
740
741 void updateActions() {
742 hasExchangeAction = !isHaven;
779 - hasBuyAction = !isHaven;
780 - hasSellAction = !isHaven;
743 + hasTradeAction = !isHaven;
744 }
745
746 @computed
lib/view_model/settings/other_settings_view_model.dart
-45
@@ -65,29 +65,6 @@ abstract class OtherSettingsViewModelBase with Store {
65 _wallet.type == WalletType.solana ||
66 _wallet.type == WalletType.tron);
67
68 - @computed
69 - bool get isEnabledBuyAction =>
70 - !_settingsStore.disableBuy && _wallet.type != WalletType.haven;
71 -
72 - @computed
73 - bool get isEnabledSellAction =>
74 - !_settingsStore.disableSell && _wallet.type != WalletType.haven;
75 -
76 - List<ProviderType> get availableBuyProvidersTypes {
77 - return ProvidersHelper.getAvailableBuyProviderTypes(walletType);
78 - }
79 -
80 - List<ProviderType> get availableSellProvidersTypes =>
81 - ProvidersHelper.getAvailableSellProviderTypes(walletType);
82 -
83 - ProviderType get buyProviderType =>
84 - _settingsStore.defaultBuyProviders[walletType] ??
85 - ProviderType.askEachTime;
86 -
87 - ProviderType get sellProviderType =>
88 - _settingsStore.defaultSellProviders[walletType] ??
89 - ProviderType.askEachTime;
90 -
68 String getDisplayPriority(dynamic priority) {
69 final _priority = priority as TransactionPriority;
70
@@ -115,20 +92,6 @@ abstract class OtherSettingsViewModelBase with Store {
92 return priority.toString();
93 }
94
118 - String getBuyProviderType(dynamic buyProviderType) {
119 - final _buyProviderType = buyProviderType as ProviderType;
120 - return _buyProviderType == ProviderType.askEachTime
121 - ? S.current.ask_each_time
122 - : _buyProviderType.title;
123 - }
124 -
125 - String getSellProviderType(dynamic sellProviderType) {
126 - final _sellProviderType = sellProviderType as ProviderType;
127 - return _sellProviderType == ProviderType.askEachTime
128 - ? S.current.ask_each_time
129 - : _sellProviderType.title;
130 - }
131 -
95 void onDisplayPrioritySelected(TransactionPriority priority) =>
96 _settingsStore.priority[walletType] = priority;
97
@@ -157,12 +120,4 @@ abstract class OtherSettingsViewModelBase with Store {
120 }
121 return null;
122 }
160 -
161 - @action
162 - ProviderType onBuyProviderTypeSelected(ProviderType buyProviderType) =>
163 - _settingsStore.defaultBuyProviders[walletType] = buyProviderType;
164 -
165 - @action
166 - ProviderType onSellProviderTypeSelected(ProviderType sellProviderType) =>
167 - _settingsStore.defaultSellProviders[walletType] = sellProviderType;
123 }
lib/view_model/settings/privacy_settings_view_model.dart
+2 -8
@@ -59,10 +59,7 @@ abstract class PrivacySettingsViewModelBase with Store {
59 bool get isAppSecure => _settingsStore.isAppSecure;
60
61 @computed
62 - bool get disableBuy => _settingsStore.disableBuy;
63 -
64 - @computed
65 - bool get disableSell => _settingsStore.disableSell;
62 + bool get disableTradeOption => _settingsStore.disableTradeOption;
63
64 @computed
65 bool get disableBulletin => _settingsStore.disableBulletin;
@@ -119,10 +116,7 @@ abstract class PrivacySettingsViewModelBase with Store {
116 void setIsAppSecure(bool value) => _settingsStore.isAppSecure = value;
117
118 @action
122 - void setDisableBuy(bool value) => _settingsStore.disableBuy = value;
123 -
124 - @action
125 - void setDisableSell(bool value) => _settingsStore.disableSell = value;
119 + void setDisableTradeOption(bool value) => _settingsStore.disableTradeOption = value;
120
121 @action
122 void setDisableBulletin(bool value) => _settingsStore.disableBulletin = value;
res/values/strings_ar.arb
+3
@@ -123,6 +123,8 @@
123 "change_rep_successful": "تم تغيير ممثل بنجاح",
124 "change_wallet_alert_content": "هل تريد تغيير المحفظة الحالية إلى ${wallet_name}؟",
125 "change_wallet_alert_title": "تغيير المحفظة الحالية",
126 + "choose_a_payment_method": "اختر طريقة الدفع",
127 + "choose_a_provider": "اختر مزودًا",
128 "choose_account": "اختر حساب",
129 "choose_address": "\n\nالرجاء اختيار عنوان:",
130 "choose_card_value": "اختر قيمة بطاقة",
@@ -217,6 +219,7 @@
219 "disable_fee_api_warning": "من خلال إيقاف تشغيل هذا ، قد تكون معدلات الرسوم غير دقيقة في بعض الحالات ، لذلك قد ينتهي بك الأمر إلى دفع مبالغ زائدة أو دفع رسوم المعاملات الخاصة بك",
220 "disable_fiat": "تعطيل fiat",
221 "disable_sell": "قم بتعطيل إجراء البيع",
222 + "disable_trade_option": "تعطيل خيار التجارة",
223 "disableBatteryOptimization": "تعطيل تحسين البطارية",
224 "disableBatteryOptimizationDescription": "هل تريد تعطيل تحسين البطارية من أجل جعل الخلفية مزامنة تعمل بحرية وسلاسة؟",
225 "disabled": "معطلة",
res/values/strings_bg.arb
+3
@@ -123,6 +123,8 @@
123 "change_rep_successful": "Успешно промени представител",
124 "change_wallet_alert_content": "Искате ли да смените сегашния портфейл на ${wallet_name}?",
125 "change_wallet_alert_title": "Смяна на сегашния портфейл",
126 + "choose_a_payment_method": "Изберете начин на плащане",
127 + "choose_a_provider": "Изберете доставчик",
128 "choose_account": "Избиране на профил",
129 "choose_address": "\n\nМоля, изберете адреса:",
130 "choose_card_value": "Изберете стойност на картата",
@@ -217,6 +219,7 @@
219 "disable_fee_api_warning": "Като изключите това, таксите могат да бъдат неточни в някои случаи, така че може да се препланите или да не плащате таксите за вашите транзакции",
220 "disable_fiat": "Деактивиране на fiat",
221 "disable_sell": "Деактивирайте действието за продажба",
222 + "disable_trade_option": "Деактивирайте опцията за търговия",
223 "disableBatteryOptimization": "Деактивирайте оптимизацията на батерията",
224 "disableBatteryOptimizationDescription": "Искате ли да деактивирате оптимизацията на батерията, за да направите синхронизирането на фона да работи по -свободно и гладко?",
225 "disabled": "Деактивирано",
res/values/strings_cs.arb
+3
@@ -123,6 +123,8 @@
123 "change_rep_successful": "Úspěšně změnil zástupce",
124 "change_wallet_alert_content": "Opravdu chcete změnit aktivní peněženku na ${wallet_name}?",
125 "change_wallet_alert_title": "Přepnout peněženku",
126 + "choose_a_payment_method": "Vyberte metodu platby",
127 + "choose_a_provider": "Vyberte poskytovatele",
128 "choose_account": "Zvolte částku",
129 "choose_address": "\n\nProsím vyberte adresu:",
130 "choose_card_value": "Vyberte hodnotu karty",
@@ -217,6 +219,7 @@
219 "disable_fee_api_warning": "Tímto vypnutím by sazby poplatků mohly být v některých případech nepřesné, takže byste mohli skončit přepláváním nebo nedoplatkem poplatků za vaše transakce",
220 "disable_fiat": "Zakázat fiat",
221 "disable_sell": "Zakázat akci prodeje",
222 + "disable_trade_option": "Zakázat možnost TRADE",
223 "disableBatteryOptimization": "Zakázat optimalizaci baterie",
224 "disableBatteryOptimizationDescription": "Chcete deaktivovat optimalizaci baterie, aby se synchronizovala pozadí volně a hladce?",
225 "disabled": "Zakázáno",
res/values/strings_de.arb
+3
@@ -123,6 +123,8 @@
123 "change_rep_successful": "Vertreter erfolgreich gerändert",
124 "change_wallet_alert_content": "Möchten Sie die aktuelle Wallet zu ${wallet_name} ändern?",
125 "change_wallet_alert_title": "Aktuelle Wallet ändern",
126 + "choose_a_payment_method": "Wählen Sie eine Zahlungsmethode",
127 + "choose_a_provider": "Wählen Sie einen Anbieter",
128 "choose_account": "Konto auswählen",
129 "choose_address": "\n\nBitte wählen Sie die Adresse:",
130 "choose_card_value": "Wählen Sie einen Kartenwert",
@@ -217,6 +219,7 @@
219 "disable_fee_api_warning": "Wenn dies ausgeschaltet wird, sind die Gebührenquoten in einigen Fällen möglicherweise ungenau, sodass Sie die Gebühren für Ihre Transaktionen möglicherweise überbezahlt oder unterzahlt",
220 "disable_fiat": "Fiat deaktivieren",
221 "disable_sell": "Verkaufsaktion deaktivieren",
222 + "disable_trade_option": "Handelsoption deaktivieren",
223 "disableBatteryOptimization": "Batterieoptimierung deaktivieren",
224 "disableBatteryOptimizationDescription": "Möchten Sie die Batterieoptimierung deaktivieren, um die Hintergrundsynchronisierung reibungsloser zu gestalten?",
225 "disabled": "Deaktiviert",
res/values/strings_en.arb
+3
@@ -123,6 +123,8 @@
123 "change_rep_successful": "Successfully changed representative",
124 "change_wallet_alert_content": "Do you want to change current wallet to ${wallet_name}?",
125 "change_wallet_alert_title": "Change current wallet",
126 + "choose_a_payment_method": "Choose a payment method",
127 + "choose_a_provider": "Choose a provider",
128 "choose_account": "Choose account",
129 "choose_address": "\n\nPlease choose the address:",
130 "choose_card_value": "Choose a card value",
@@ -217,6 +219,7 @@
219 "disable_fee_api_warning": "By turning this off, the fee rates might be inaccurate in some cases, so you might end up overpaying or underpaying the fees for your transactions",
220 "disable_fiat": "Disable fiat",
221 "disable_sell": "Disable sell action",
222 + "disable_trade_option": "Disable trade option",
223 "disableBatteryOptimization": "Disable Battery Optimization",
224 "disableBatteryOptimizationDescription": "Do you want to disable battery optimization in order to make background sync run more freely and smoothly?",
225 "disabled": "Disabled",
res/values/strings_es.arb
+3
@@ -123,6 +123,8 @@
123 "change_rep_successful": "Representante cambiado con éxito",
124 "change_wallet_alert_content": "¿Quieres cambiar la billetera actual a ${wallet_name}?",
125 "change_wallet_alert_title": "Cambiar billetera actual",
126 + "choose_a_payment_method": "Elija un método de pago",
127 + "choose_a_provider": "Elija un proveedor",
128 "choose_account": "Elegir cuenta",
129 "choose_address": "\n\nPor favor elija la dirección:",
130 "choose_card_value": "Elige un valor de tarjeta",
@@ -217,6 +219,7 @@
219 "disable_fee_api_warning": "Al apagar esto, las tasas de tarifas pueden ser inexactas en algunos casos, por lo que puede terminar pagando en exceso o pagando menos las tarifas por sus transacciones",
220 "disable_fiat": "Deshabilitar fiat",
221 "disable_sell": "Desactivar acción de venta",
222 + "disable_trade_option": "Deshabilitar la opción de comercio",
223 "disableBatteryOptimization": "Deshabilitar la optimización de la batería",
224 "disableBatteryOptimizationDescription": "¿Desea deshabilitar la optimización de la batería para que la sincronización de fondo se ejecute más libremente y sin problemas?",
225 "disabled": "Desactivado",
res/values/strings_fr.arb
+3
@@ -123,6 +123,8 @@
123 "change_rep_successful": "Représentant changé avec succès",
124 "change_wallet_alert_content": "Souhaitez-vous changer le portefeuille (wallet) actuel vers ${wallet_name} ?",
125 "change_wallet_alert_title": "Changer le portefeuille (wallet) actuel",
126 + "choose_a_payment_method": "Choisissez un mode de paiement",
127 + "choose_a_provider": "Choisissez un fournisseur",
128 "choose_account": "Choisir le compte",
129 "choose_address": "\n\nMerci de choisir l'adresse :",
130 "choose_card_value": "Choisissez une valeur de carte",
@@ -217,6 +219,7 @@
219 "disable_fee_api_warning": "En désactivant cela, les taux de frais peuvent être inexacts dans certains cas, vous pourriez donc finir par payer trop ou sous-paiement les frais pour vos transactions",
220 "disable_fiat": "Désactiver les montants en fiat",
221 "disable_sell": "Désactiver l'action de vente",
222 + "disable_trade_option": "Désactiver l'option de commerce",
223 "disableBatteryOptimization": "Désactiver l'optimisation de la batterie",
224 "disableBatteryOptimizationDescription": "Voulez-vous désactiver l'optimisation de la batterie afin de faire fonctionner la synchronisation d'arrière-plan plus librement et en douceur?",
225 "disabled": "Désactivé",
res/values/strings_ha.arb
+3
@@ -123,6 +123,8 @@
123 "change_rep_successful": "An samu nasarar canzawa wakilin",
124 "change_wallet_alert_content": "Kana so ka canja walat yanzu zuwa ${wallet_name}?",
125 "change_wallet_alert_title": "Canja walat yanzu",
126 + "choose_a_payment_method": "Zabi hanyar biyan kuɗi",
127 + "choose_a_provider": "Zabi mai bada",
128 "choose_account": "Zaɓi asusu",
129 "choose_address": "\n\n Da fatan za a zaɓi adireshin:",
130 "choose_card_value": "Zabi darajar katin",
@@ -217,6 +219,7 @@
219 "disable_fee_api_warning": "Ta hanyar juya wannan kashe, kudaden da zai iya zama ba daidai ba a wasu halaye, saboda haka zaku iya ƙare da overpaying ko a ƙarƙashin kudaden don ma'amaloli",
220 "disable_fiat": "Dakatar da fiat",
221 "disable_sell": "Kashe karbuwa",
222 + "disable_trade_option": "Musaki zaɓi na kasuwanci",
223 "disableBatteryOptimization": "Kashe ingantawa baturi",
224 "disableBatteryOptimizationDescription": "Shin kana son kashe ingantawa baturi don yin setnc bankwali gudu da yar kyauta da kyau?",
225 "disabled": "tsaya",
res/values/strings_hi.arb
+3
@@ -123,6 +123,8 @@
123 "change_rep_successful": "सफलतापूर्वक बदलकर प्रतिनिधि",
124 "change_wallet_alert_content": "क्या आप करंट वॉलेट को बदलना चाहते हैं ${wallet_name}?",
125 "change_wallet_alert_title": "वर्तमान बटुआ बदलें",
126 + "choose_a_payment_method": "एक भुगतान विधि का चयन करें",
127 + "choose_a_provider": "एक प्रदाता चुनें",
128 "choose_account": "खाता चुनें",
129 "choose_address": "\n\nकृपया पता चुनें:",
130 "choose_card_value": "एक कार्ड मूल्य चुनें",
@@ -217,6 +219,7 @@
219 "disable_fee_api_warning": "इसे बंद करने से, कुछ मामलों में शुल्क दरें गलत हो सकती हैं, इसलिए आप अपने लेनदेन के लिए फीस को कम कर सकते हैं या कम कर सकते हैं",
220 "disable_fiat": "िएट को अक्षम करें",
221 "disable_sell": "बेचने की कार्रवाई अक्षम करें",
222 + "disable_trade_option": "व्यापार विकल्प अक्षम करें",
223 "disableBatteryOptimization": "बैटरी अनुकूलन अक्षम करें",
224 "disableBatteryOptimizationDescription": "क्या आप बैकग्राउंड सिंक को अधिक स्वतंत्र और सुचारू रूप से चलाने के लिए बैटरी ऑप्टिमाइज़ेशन को अक्षम करना चाहते हैं?",
225 "disabled": "अक्षम",
res/values/strings_hr.arb
+3
@@ -123,6 +123,8 @@
123 "change_rep_successful": "Uspješno promijenjena reprezentativna",
124 "change_wallet_alert_content": "Želite li promijeniti trenutni novčanik u ${wallet_name}?",
125 "change_wallet_alert_title": "Izmijeni trenutni novčanik",
126 + "choose_a_payment_method": "Odaberite način plaćanja",
127 + "choose_a_provider": "Odaberite davatelja usluga",
128 "choose_account": "Odaberi račun",
129 "choose_address": "\n\nOdaberite adresu:",
130 "choose_card_value": "Odaberite vrijednost kartice",
@@ -217,6 +219,7 @@
219 "disable_fee_api_warning": "Isključivanjem ovoga, stope naknade u nekim bi slučajevima mogle biti netočne, tako da biste mogli preplatiti ili predati naknadu za vaše transakcije",
220 "disable_fiat": "Isključi, fiat",
221 "disable_sell": "Onemogući akciju prodaje",
222 + "disable_trade_option": "Onemogući trgovinsku opciju",
223 "disableBatteryOptimization": "Onemogući optimizaciju baterije",
224 "disableBatteryOptimizationDescription": "Želite li onemogućiti optimizaciju baterije kako bi se pozadinska sinkronizacija radila slobodnije i glatko?",
225 "disabled": "Onemogućeno",
res/values/strings_hy.arb
+3
@@ -123,6 +123,8 @@
123 "change_rep_successful": "Ներկայացուցչի փոփոխությունը հաջողությամբ կատարվեց",
124 "change_wallet_alert_content": "Ցանկանում եք փոխել ընթացիկ դրամապանակը ${wallet_name}?",
125 "change_wallet_alert_title": "Փոխել ընթացիկ դրամապանակը",
126 + "choose_a_payment_method": "Ընտրեք վճարման եղանակ",
127 + "choose_a_provider": "Ընտրեք մատակարար",
128 "choose_account": "Ընտրեք հաշիվը",
129 "choose_address": "\n\nԽնդրում ենք ընտրեք հասցեն",
130 "choose_card_value": "Ընտրեք քարտի արժեք",
@@ -217,6 +219,7 @@
219 "disable_fee_api_warning": "Դրանից անջատելով, վճարների տեմպերը որոշ դեպքերում կարող են անճիշտ լինել, այնպես որ դուք կարող եք վերջ տալ ձեր գործարքների համար վճարների գերավճարների կամ գերավճարների վրա",
220 "disable_fiat": "Անջատել ֆիատ",
221 "disable_sell": "Անջատել վաճառք գործողությունը",
222 + "disable_trade_option": "Անջատեք առեւտրի տարբերակը",
223 "disableBatteryOptimization": "Անջատել մարտկոցի օպտիմիզացիան",
224 "disableBatteryOptimizationDescription": "Դուք ցանկանում եք անջատել մարտկոցի օպտիմիզացիան ֆոնային համաժամացման ավելի ազատ և հարթ ընթացքի համար?",
225 "disabled": "Անջատված",
res/values/strings_id.arb
+3
@@ -123,6 +123,8 @@
123 "change_rep_successful": "Berhasil mengubah perwakilan",
124 "change_wallet_alert_content": "Apakah Anda ingin mengganti dompet saat ini ke ${wallet_name}?",
125 "change_wallet_alert_title": "Ganti dompet saat ini",
126 + "choose_a_payment_method": "Pilih metode pembayaran",
127 + "choose_a_provider": "Pilih penyedia",
128 "choose_account": "Pilih akun",
129 "choose_address": "\n\nSilakan pilih alamat:",
130 "choose_card_value": "Pilih nilai kartu",
@@ -217,6 +219,7 @@
219 "disable_fee_api_warning": "Dengan mematikan ini, tarif biaya mungkin tidak akurat dalam beberapa kasus, jadi Anda mungkin akan membayar lebih atau membayar biaya untuk transaksi Anda",
220 "disable_fiat": "Nonaktifkan fiat",
221 "disable_sell": "Nonaktifkan aksi jual",
222 + "disable_trade_option": "Nonaktifkan opsi perdagangan",
223 "disableBatteryOptimization": "Nonaktifkan optimasi baterai",
224 "disableBatteryOptimizationDescription": "Apakah Anda ingin menonaktifkan optimasi baterai untuk membuat sinkronisasi latar belakang berjalan lebih bebas dan lancar?",
225 "disabled": "Dinonaktifkan",
res/values/strings_it.arb
+3
@@ -123,6 +123,8 @@
123 "change_rep_successful": "Rappresentante modificato con successo",
124 "change_wallet_alert_content": "Sei sicuro di voler cambiare il portafoglio attuale con ${wallet_name}?",
125 "change_wallet_alert_title": "Cambia portafoglio attuale",
126 + "choose_a_payment_method": "Scegli un metodo di pagamento",
127 + "choose_a_provider": "Scegli un fornitore",
128 "choose_account": "Scegli account",
129 "choose_address": "\n\nSi prega di scegliere l'indirizzo:",
130 "choose_card_value": "Scegli un valore della carta",
@@ -218,6 +220,7 @@
220 "disable_fee_api_warning": "Disattivando questo, i tassi delle commissioni potrebbero essere inaccurati in alcuni casi, quindi potresti finire in eccesso o sostenere le commissioni per le transazioni",
221 "disable_fiat": "Disabilita fiat",
222 "disable_sell": "Disabilita l'azione di vendita",
223 + "disable_trade_option": "Disabilita l'opzione commerciale",
224 "disableBatteryOptimization": "Disabilita l'ottimizzazione della batteria",
225 "disableBatteryOptimizationDescription": "Vuoi disabilitare l'ottimizzazione della batteria per far funzionare la sincronizzazione in background più libera e senza intoppi?",
226 "disabled": "Disabilitato",
res/values/strings_ja.arb
+3
@@ -123,6 +123,8 @@
123 "change_rep_successful": "代表者の変更に成功しました",
124 "change_wallet_alert_content": "現在のウォレットをに変更しますか ${wallet_name}?",
125 "change_wallet_alert_title": "現在のウォレットを変更する",
126 + "choose_a_payment_method": "支払い方法を選択します",
127 + "choose_a_provider": "プロバイダーを選択します",
128 "choose_account": "アカウントを選択",
129 "choose_address": "\n\n住所を選択してください:",
130 "choose_card_value": "カード値を選択します",
@@ -217,6 +219,7 @@
219 "disable_fee_api_warning": "これをオフにすることで、料金金利は場合によっては不正確になる可能性があるため、取引の費用が過払いまたは不足している可能性があります",
220 "disable_fiat": "フィアットを無効にする",
221 "disable_sell": "販売アクションを無効にする",
222 + "disable_trade_option": "取引オプションを無効にします",
223 "disableBatteryOptimization": "バッテリーの最適化を無効にします",
224 "disableBatteryOptimizationDescription": "バックグラウンドシンクをより自由かつスムーズに実行するために、バッテリーの最適化を無効にしたいですか?",
225 "disabled": "無効",
res/values/strings_ko.arb
+3
@@ -123,6 +123,8 @@
123 "change_rep_successful": "대리인이 성공적으로 변경되었습니다",
124 "change_wallet_alert_content": "현재 지갑을 다음으로 변경 하시겠습니까 ${wallet_name}?",
125 "change_wallet_alert_title": "현재 지갑 변경",
126 + "choose_a_payment_method": "결제 방법을 선택하십시오",
127 + "choose_a_provider": "제공자를 선택하십시오",
128 "choose_account": "계정을 선택하십시오",
129 "choose_address": "\n\n주소를 선택하십시오:",
130 "choose_card_value": "카드 값을 선택하십시오",
@@ -217,6 +219,7 @@
219 "disable_fee_api_warning": "이것을 끄면 경우에 따라 수수료가 부정확 할 수 있으므로 거래 수수료를 초과 지불하거나 지불 할 수 있습니다.",
220 "disable_fiat": "법정화폐 비활성화",
221 "disable_sell": "판매 조치 비활성화",
222 + "disable_trade_option": "거래 옵션 비활성화",
223 "disableBatteryOptimization": "배터리 최적화를 비활성화합니다",
224 "disableBatteryOptimizationDescription": "백그라운드 동기화를보다 자유롭고 매끄럽게 실행하기 위해 배터리 최적화를 비활성화하고 싶습니까?",
225 "disabled": "장애가 있는",
res/values/strings_my.arb
+3
@@ -123,6 +123,8 @@
123 "change_rep_successful": "အောင်မြင်စွာကိုယ်စားလှယ်ပြောင်းလဲသွားတယ်",
124 "change_wallet_alert_content": "လက်ရှိပိုက်ဆံအိတ်ကို ${wallet_name} သို့ ပြောင်းလိုပါသလား။",
125 "change_wallet_alert_title": "လက်ရှိပိုက်ဆံအိတ်ကို ပြောင်းပါ။",
126 + "choose_a_payment_method": "ငွေပေးချေမှုနည်းလမ်းကိုရွေးချယ်ပါ",
127 + "choose_a_provider": "ပံ့ပိုးပေးရွေးချယ်ပါ",
128 "choose_account": "အကောင့်ကို ရွေးပါ။",
129 "choose_address": "\n\nလိပ်စာကို ရွေးပါ-",
130 "choose_card_value": "ကဒ်တန်ဖိုးတစ်ခုရွေးပါ",
@@ -217,6 +219,7 @@
219 "disable_fee_api_warning": "ဤအရာကိုဖွင့်ခြင်းအားဖြင့်အချို့သောကိစ္စရပ်များတွင်အခကြေးငွေနှုန်းထားများသည်တိကျမှုရှိနိုင်သည်,",
220 "disable_fiat": "Fiat ကိုပိတ်ပါ။",
221 "disable_sell": "ရောင်းချခြင်းလုပ်ဆောင်ချက်ကို ပိတ်ပါ။",
222 + "disable_trade_option": "ကုန်သွယ်ရေး option ကိုပိတ်ပါ",
223 "disableBatteryOptimization": "ဘက်ထရီ optimization ကိုပိတ်ပါ",
224 "disableBatteryOptimizationDescription": "နောက်ခံထပ်တူပြုခြင်းနှင့်ချောချောမွေ့မွေ့ပြုလုပ်နိုင်ရန်ဘက်ထရီ optimization ကိုသင်ပိတ်ထားလိုပါသလား။",
225 "disabled": "မသန်စွမ်း",
res/values/strings_nl.arb
+3
@@ -123,6 +123,8 @@
123 "change_rep_successful": "Met succes veranderde vertegenwoordiger",
124 "change_wallet_alert_content": "Wilt u de huidige portemonnee wijzigen in ${wallet_name}?",
125 "change_wallet_alert_title": "Wijzig huidige portemonnee",
126 + "choose_a_payment_method": "Kies een betaalmethode",
127 + "choose_a_provider": "Kies een provider",
128 "choose_account": "Kies account",
129 "choose_address": "\n\nKies het adres:",
130 "choose_card_value": "Kies een kaartwaarde",
@@ -217,6 +219,7 @@
219 "disable_fee_api_warning": "Door dit uit te schakelen, kunnen de tarieven in sommige gevallen onnauwkeurig zijn, dus u kunt de vergoedingen voor uw transacties te veel betalen of te weinig betalen",
220 "disable_fiat": "Schakel Fiat uit",
221 "disable_sell": "Verkoopactie uitschakelen",
222 + "disable_trade_option": "Schakel handelsoptie uit",
223 "disableBatteryOptimization": "Schakel de batterijoptimalisatie uit",
224 "disableBatteryOptimizationDescription": "Wilt u de optimalisatie van de batterij uitschakelen om achtergrondsynchronisatie te laten werken, vrijer en soepeler?",
225 "disabled": "Gehandicapt",
res/values/strings_pl.arb
+3
@@ -123,6 +123,8 @@
123 "change_rep_successful": "Pomyślnie zmienił przedstawiciela",
124 "change_wallet_alert_content": "Czy chcesz zmienić obecny portfel na ${wallet_name}?",
125 "change_wallet_alert_title": "Zmień obecny portfel",
126 + "choose_a_payment_method": "Wybierz metodę płatności",
127 + "choose_a_provider": "Wybierz dostawcę",
128 "choose_account": "Wybierz konto",
129 "choose_address": "\n\nWybierz adres:",
130 "choose_card_value": "Wybierz wartość karty",
@@ -217,6 +219,7 @@
219 "disable_fee_api_warning": "Wyłączając to, stawki opłaty mogą być w niektórych przypadkach niedokładne, więc możesz skończyć się przepłaceniem lub wynagrodzeniem opłat za transakcje",
220 "disable_fiat": "Wyłącz waluty FIAT",
221 "disable_sell": "Wyłącz akcję sprzedaży",
222 + "disable_trade_option": "Wyłącz opcję handlu",
223 "disableBatteryOptimization": "Wyłącz optymalizację baterii",
224 "disableBatteryOptimizationDescription": "Czy chcesz wyłączyć optymalizację baterii, aby synchronizacja tła działała swobodniej i płynnie?",
225 "disabled": "Wyłączone",
res/values/strings_pt.arb
+3
@@ -123,6 +123,8 @@
123 "change_rep_successful": "Mudou com sucesso o representante",
124 "change_wallet_alert_content": "Quer mudar a carteira atual para ${wallet_name}?",
125 "change_wallet_alert_title": "Alterar carteira atual",
126 + "choose_a_payment_method": "Escolha um método de pagamento",
127 + "choose_a_provider": "Escolha um provedor",
128 "choose_account": "Escolha uma conta",
129 "choose_address": "\n\nEscolha o endereço:",
130 "choose_card_value": "Escolha um valor de cartão",
@@ -217,6 +219,7 @@
219 "disable_fee_api_warning": "Ao desativar isso, as taxas de taxas podem ser imprecisas em alguns casos, para que você possa acabar pagando demais ou pagando as taxas por suas transações",
220 "disable_fiat": "Desativar fiat",
221 "disable_sell": "Desativar ação de venda",
222 + "disable_trade_option": "Desativar a opção comercial",
223 "disableBatteryOptimization": "Desative a otimização da bateria",
224 "disableBatteryOptimizationDescription": "Deseja desativar a otimização da bateria para fazer a sincronização de fundo funcionar de forma mais livre e suave?",
225 "disabled": "Desabilitado",
res/values/strings_ru.arb
+3
@@ -123,6 +123,8 @@
123 "change_rep_successful": "Успешно изменил представитель",
124 "change_wallet_alert_content": "Вы хотите изменить текущий кошелек на ${wallet_name}?",
125 "change_wallet_alert_title": "Изменить текущий кошелек",
126 + "choose_a_payment_method": "Выберите способ оплаты",
127 + "choose_a_provider": "Выберите поставщика",
128 "choose_account": "Выберите аккаунт",
129 "choose_address": "\n\nПожалуйста, выберите адрес:",
130 "choose_card_value": "Выберите значение карты",
@@ -217,6 +219,7 @@
219 "disable_fee_api_warning": "Выключив это, в некоторых случаях ставки платы могут быть неточными, так что вы можете в конечном итоге переплачивать или недоплачивать сборы за ваши транзакции",
220 "disable_fiat": "Отключить фиат",
221 "disable_sell": "Отключить действие продажи",
222 + "disable_trade_option": "Отключить возможность торговли",
223 "disableBatteryOptimization": "Отключить оптимизацию батареи",
224 "disableBatteryOptimizationDescription": "Вы хотите отключить оптимизацию батареи, чтобы сделать фона синхронизации более свободно и плавно?",
225 "disabled": "Отключено",
res/values/strings_th.arb
+3
@@ -123,6 +123,8 @@
123 "change_rep_successful": "เปลี่ยนตัวแทนสำเร็จ",
124 "change_wallet_alert_content": "คุณต้องการเปลี่ยนกระเป๋าปัจจุบันเป็น ${wallet_name} หรือไม่?",
125 "change_wallet_alert_title": "เปลี่ยนกระเป๋าปัจจุบัน",
126 + "choose_a_payment_method": "เลือกวิธีการชำระเงิน",
127 + "choose_a_provider": "เลือกผู้ให้บริการ",
128 "choose_account": "เลือกบัญชี",
129 "choose_address": "\n\nโปรดเลือกที่อยู่:",
130 "choose_card_value": "เลือกค่าบัตร",
@@ -217,6 +219,7 @@
219 "disable_fee_api_warning": "โดยการปิดสิ่งนี้อัตราค่าธรรมเนียมอาจไม่ถูกต้องในบางกรณีดังนั้นคุณอาจจบลงด้วยการจ่ายเงินมากเกินไปหรือจ่ายค่าธรรมเนียมสำหรับการทำธุรกรรมของคุณมากเกินไป",
220 "disable_fiat": "ปิดใช้งานสกุลเงินตรา",
221 "disable_sell": "ปิดการใช้งานการขาย",
222 + "disable_trade_option": "ปิดใช้งานตัวเลือกการค้า",
223 "disableBatteryOptimization": "ปิดใช้งานการเพิ่มประสิทธิภาพแบตเตอรี่",
224 "disableBatteryOptimizationDescription": "คุณต้องการปิดใช้งานการเพิ่มประสิทธิภาพแบตเตอรี่เพื่อให้การซิงค์พื้นหลังทำงานได้อย่างอิสระและราบรื่นมากขึ้นหรือไม่?",
225 "disabled": "ปิดใช้งาน",
res/values/strings_tl.arb
+3
@@ -123,6 +123,8 @@
123 "change_rep_successful": "Matagumpay na nagbago ng representative",
124 "change_wallet_alert_content": "Gusto mo bang palitan ang kasalukuyang wallet sa ${wallet_name}?",
125 "change_wallet_alert_title": "Baguhin ang kasalukuyang wallet",
126 + "choose_a_payment_method": "Pumili ng isang paraan ng pagbabayad",
127 + "choose_a_provider": "Pumili ng isang provider",
128 "choose_account": "Pumili ng account",
129 "choose_address": "Mangyaring piliin ang address:",
130 "choose_card_value": "Pumili ng isang halaga ng card",
@@ -217,6 +219,7 @@
219 "disable_fee_api_warning": "Sa pamamagitan ng pag -off nito, ang mga rate ng bayad ay maaaring hindi tumpak sa ilang mga kaso, kaya maaari mong tapusin ang labis na bayad o pagsuporta sa mga bayarin para sa iyong mga transaksyon",
220 "disable_fiat": "Huwag paganahin ang fiat",
221 "disable_sell": "Huwag paganahin ang pagkilos ng pagbebenta",
222 + "disable_trade_option": "Huwag paganahin ang pagpipilian sa kalakalan",
223 "disableBatteryOptimization": "Huwag Paganahin ang Pag-optimize ng Baterya",
224 "disableBatteryOptimizationDescription": "Nais mo bang huwag paganahin ang pag-optimize ng baterya upang gawing mas malaya at maayos ang background sync?",
225 "disabled": "Hindi pinagana",
res/values/strings_tr.arb
+3
@@ -123,6 +123,8 @@
123 "change_rep_successful": "Temsilciyi başarıyla değiştirdi",
124 "change_wallet_alert_content": "Şimdiki cüzdanı ${wallet_name} cüzdanı ile değiştirmek istediğinden emin misin?",
125 "change_wallet_alert_title": "Şimdiki cüzdanı değiştir",
126 + "choose_a_payment_method": "Bir Ödeme Yöntemi Seçin",
127 + "choose_a_provider": "Bir Sağlayıcı Seçin",
128 "choose_account": "Hesabı seç",
129 "choose_address": "\n\nLütfen adresi seçin:",
130 "choose_card_value": "Bir kart değeri seçin",
@@ -217,6 +219,7 @@
219 "disable_fee_api_warning": "Bunu kapatarak, ücret oranları bazı durumlarda yanlış olabilir, bu nedenle işlemleriniz için ücretleri fazla ödeyebilir veya az ödeyebilirsiniz.",
220 "disable_fiat": "İtibari paraları devre dışı bırak",
221 "disable_sell": "Satış işlemini devre dışı bırak",
222 + "disable_trade_option": "Ticaret seçeneğini devre dışı bırakın",
223 "disableBatteryOptimization": "Pil optimizasyonunu devre dışı bırakın",
224 "disableBatteryOptimizationDescription": "Arka plan senkronizasyonunu daha özgür ve sorunsuz bir şekilde çalıştırmak için pil optimizasyonunu devre dışı bırakmak istiyor musunuz?",
225 "disabled": "Devre dışı",
res/values/strings_uk.arb
+8 -5
@@ -123,6 +123,8 @@
123 "change_rep_successful": "Успішно змінив представник",
124 "change_wallet_alert_content": "Ви хочете змінити поточний гаманець на ${wallet_name}?",
125 "change_wallet_alert_title": "Змінити поточний гаманець",
126 + "choose_a_payment_method": "Виберіть метод оплати",
127 + "choose_a_provider": "Виберіть постачальника",
128 "choose_account": "Оберіть акаунт",
129 "choose_address": "\n\nБудь ласка, оберіть адресу:",
130 "choose_card_value": "Виберіть значення картки",
@@ -185,7 +187,7 @@
187 "creating_new_wallet_error": "Помилка: ${description}",
188 "creation_date": "Дата створення",
189 "custom": "на замовлення",
188 - "custom_drag": "На замовлення (утримуйте та перетягується)",
190 + "custom_drag": "На замовлення (утримуйте та перетягуйте)",
191 "custom_redeem_amount": "Власна сума викупу",
192 "custom_value": "Спеціальне значення",
193 "dark_theme": "Темна",
@@ -206,17 +208,18 @@
208 "description": "опис",
209 "destination_tag": "Тег призначення:",
210 "dfx_option_description": "Купуйте криптовалюту з EUR & CHF. Для роздрібних та корпоративних клієнтів у Європі",
209 - "didnt_get_code": "Не отримуєте код?",
211 + "didnt_get_code": "Не отримали код?",
212 "digit_pin": "-значний PIN",
213 "digital_and_physical_card": " цифрова та фізична передплачена дебетова картка",
214 "disable": "Вимкнути",
215 "disable_bulletin": "Вимкнути статус послуги",
216 "disable_buy": "Вимкнути дію покупки",
217 "disable_cake_2fa": "Вимкнути Cake 2FA",
216 - "disable_exchange": "Вимкнути exchange",
218 + "disable_exchange": "Вимкнути можливість обміну",
219 "disable_fee_api_warning": "Вимкнувши це, ставки плати в деяких випадках можуть бути неточними, тому ви можете переплатити або недооплатити плату за свої транзакції",
220 "disable_fiat": "Вимкнути фиат",
221 "disable_sell": "Вимкнути дію продажу",
222 + "disable_trade_option": "Вимкнути можливість торгівлі",
223 "disableBatteryOptimization": "Вимкнути оптимізацію акумулятора",
224 "disableBatteryOptimizationDescription": "Ви хочете відключити оптимізацію акумулятора, щоб зробити фонову синхронізацію більш вільно та плавно?",
225 "disabled": "Вимкнено",
@@ -226,14 +229,14 @@
229 "do_not_have_enough_gas_asset": "У вас недостатньо ${currency}, щоб здійснити трансакцію з поточними умовами мережі блокчейн. Вам потрібно більше ${currency}, щоб сплатити комісію мережі блокчейн, навіть якщо ви надсилаєте інший актив.",
230 "do_not_send": "Не надсилайте",
231 "do_not_share_warning_text": "Не діліться цим нікому, включно зі службою підтримки.\n\nВаші кошти можуть і будуть вкрадені!",
229 - "do_not_show_me": "Не показуй мені це знову",
232 + "do_not_show_me": "Не показувати це знову",
233 "domain_looks_up": "Пошук доменів",
234 "donation_link_details": "Деталі посилання для пожертв",
235 "e_sign_consent": "Згода електронного підпису",
236 "edit": "Редагувати",
237 "edit_backup_password": "Змінити пароль резервної копії",
238 "edit_node": "Редагувати вузол",
236 - "edit_token": "Редагувати маркер",
239 + "edit_token": "Редагувати токен",
240 "electrum_address_disclaimer": "Ми створюємо нові адреси щоразу, коли ви використовуєте їх, але попередні адреси продовжують працювати",
241 "email_address": "Адреса електронної пошти",
242 "enable": "Ввімкнути",
res/values/strings_ur.arb
+3
@@ -123,6 +123,8 @@
123 "change_rep_successful": "نمائندہ کو کامیابی کے ساتھ تبدیل کیا",
124 "change_wallet_alert_content": "کیا آپ موجودہ والیٹ کو ${wallet_name} میں تبدیل کرنا چاہتے ہیں؟",
125 "change_wallet_alert_title": "موجودہ پرس تبدیل کریں۔",
126 + "choose_a_payment_method": "ادائیگی کا طریقہ منتخب کریں",
127 + "choose_a_provider": "فراہم کنندہ کا انتخاب کریں",
128 "choose_account": "اکاؤنٹ کا انتخاب کریں۔",
129 "choose_address": "\\n\\nبراہ کرم پتہ منتخب کریں:",
130 "choose_card_value": "کارڈ کی قیمت کا انتخاب کریں",
@@ -217,6 +219,7 @@
219 "disable_fee_api_warning": "اس کو بند کرنے سے ، کچھ معاملات میں فیس کی شرح غلط ہوسکتی ہے ، لہذا آپ اپنے لین دین کے لئے فیسوں کو زیادہ ادائیگی یا ادائیگی ختم کرسکتے ہیں۔",
220 "disable_fiat": "فیاٹ کو غیر فعال کریں۔",
221 "disable_sell": "فروخت کی کارروائی کو غیر فعال کریں۔",
222 + "disable_trade_option": "تجارت کے آپشن کو غیر فعال کریں",
223 "disableBatteryOptimization": "بیٹری کی اصلاح کو غیر فعال کریں",
224 "disableBatteryOptimizationDescription": "کیا آپ پس منظر کی مطابقت پذیری کو زیادہ آزادانہ اور آسانی سے چلانے کے لئے بیٹری کی اصلاح کو غیر فعال کرنا چاہتے ہیں؟",
225 "disabled": "معذور",
res/values/strings_vi.arb
+1
@@ -218,6 +218,7 @@
218 "disable_fee_api_warning": "Khi tắt chức năng này, tỉ lệ phí có thể không chính xác trong một số trường hợp, dẫn đến bạn trả quá hoặc không đủ phí cho giao dịch của mình.",
219 "disable_fiat": "Vô hiệu hóa tiền tệ fiat",
220 "disable_sell": "Vô hiệu hóa chức năng bán",
221 + "disable_trade_option": "Tắt tùy chọn thương mại",
222 "disableBatteryOptimization": "Vô hiệu hóa Tối ưu hóa Pin",
223 "disableBatteryOptimizationDescription": "Bạn có muốn vô hiệu hóa tối ưu hóa pin để đồng bộ hóa nền hoạt động mượt mà hơn không?",
224 "disabled": "Đã vô hiệu hóa",
res/values/strings_yo.arb
+3
@@ -123,6 +123,8 @@
123 "change_rep_successful": "Ni ifijišẹ yipada aṣoju",
124 "change_wallet_alert_content": "Ṣe ẹ fẹ́ pààrọ̀ àpamọ́wọ́ yìí sí ${wallet_name}?",
125 "change_wallet_alert_title": "Ẹ pààrọ̀ àpamọ́wọ́ yìí",
126 + "choose_a_payment_method": "Yan ọna isanwo kan",
127 + "choose_a_provider": "Yan olupese",
128 "choose_account": "Yan àkáǹtì",
129 "choose_address": "\n\nẸ jọ̀wọ́ yan àdírẹ́sì:",
130 "choose_card_value": "Yan iye kaadi",
@@ -217,6 +219,7 @@
219 "disable_fee_api_warning": "Nipa yiyi eyi kuro, awọn oṣuwọn owo naa le jẹ aiṣe deede ni awọn ọrọ kan, nitorinaa o le pari apọju tabi awọn idiyele ti o ni agbara fun awọn iṣowo rẹ",
220 "disable_fiat": "Pa owó tí ìjọba pàṣẹ wa lò",
221 "disable_sell": "Ko iṣọrọ iṣọrọ",
222 + "disable_trade_option": "Mu aṣayan iṣowo ṣiṣẹ",
223 "disableBatteryOptimization": "Mu Ifasi batiri",
224 "disableBatteryOptimizationDescription": "Ṣe o fẹ lati mu iṣapelo batiri si lati le ṣiṣe ayẹwo ẹhin ati laisiyonu?",
225 "disabled": "Wọ́n tí a ti pa",
res/values/strings_zh.arb
+3
@@ -123,6 +123,8 @@
123 "change_rep_successful": "成功改变了代表",
124 "change_wallet_alert_content": "您是否想将当前钱包改为 ${wallet_name}?",
125 "change_wallet_alert_title": "更换当前钱包",
126 + "choose_a_payment_method": "选择付款方式",
127 + "choose_a_provider": "选择一个提供商",
128 "choose_account": "选择账户",
129 "choose_address": "\n\n請選擇地址:",
130 "choose_card_value": "选择卡值",
@@ -217,6 +219,7 @@
219 "disable_fee_api_warning": "通过将其关闭,在某些情况下,收费率可能不准确,因此您最终可能会超额付款或支付交易费用",
220 "disable_fiat": "禁用法令",
221 "disable_sell": "禁用卖出操作",
222 + "disable_trade_option": "禁用贸易选项",
223 "disableBatteryOptimization": "禁用电池优化",
224 "disableBatteryOptimizationDescription": "您是否要禁用电池优化以使背景同步更加自由,平稳地运行?",
225 "disabled": "禁用",
tool/utils/secret_key.dart
+2
@@ -44,6 +44,8 @@ class SecretKey {
44 SecretKey('cakePayApiKey', () => ''),
45 SecretKey('CSRFToken', () => ''),
46 SecretKey('authorization', () => ''),
47 + SecretKey('meldTestApiKey', () => ''),
48 + SecretKey('meldTestPublicKey', () => ''),
49 SecretKey('moneroTestWalletSeeds', () => ''),
50 SecretKey('moneroLegacyTestWalletSeeds ', () => ''),
51 SecretKey('bitcoinTestWalletSeeds', () => ''),