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>
This commit is contained in:
Serhii 2024-11-09 21:00:56 +02:00 committed by GitHub
parent cea3084bb3
commit 02f53055b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
88 changed files with 4082 additions and 686 deletions

View file

@ -194,6 +194,8 @@ jobs:
echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
echo "const meldTestApiKey = '${{ secrets.MELD_TEST_API_KEY }}';" >> lib/.secrets.g.dart
echo "const meldTestPublicKey = '${{ secrets.MELD_TEST_PUBLIC_KEY}}';" >> lib/.secrets.g.dar
echo "const letsExchangeBearerToken = '${{ secrets.LETS_EXCHANGE_TOKEN }}';" >> lib/.secrets.g.dart
echo "const letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart
echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart

View file

@ -175,6 +175,8 @@ jobs:
echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
echo "const meldTestApiKey = '${{ secrets.MELD_TEST_API_KEY }}';" >> lib/.secrets.g.dart
echo "const meldTestPublicKey = '${{ secrets.MELD_TEST_PUBLIC_KEY}}';" >> lib/.secrets.g.dar
echo "const letsExchangeBearerToken = '${{ secrets.LETS_EXCHANGE_TOKEN }}';" >> lib/.secrets.g.dart
echo "const letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart
echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View file

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
<defs>
</defs>
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
<path d="M 24.144 34.985 c -0.77 0.912 -2.003 1.631 -3.236 1.528 c -0.154 -1.233 0.449 -2.542 1.156 -3.351 c 0.77 -0.937 2.119 -1.605 3.21 -1.656 C 25.402 32.789 24.902 34.047 24.144 34.985 M 25.261 36.757 c -1.785 -0.103 -3.313 1.014 -4.16 1.014 c -0.86 0 -2.157 -0.963 -3.57 -0.937 c -1.836 0.026 -3.544 1.066 -4.481 2.722 c -1.926 3.313 -0.501 8.218 1.361 10.914 c 0.912 1.335 2.003 2.799 3.441 2.748 c 1.361 -0.051 1.9 -0.886 3.544 -0.886 c 1.656 0 2.131 0.886 3.57 0.86 c 1.489 -0.026 2.427 -1.335 3.338 -2.671 c 1.04 -1.515 1.464 -2.992 1.489 -3.069 c -0.026 -0.026 -2.876 -1.117 -2.902 -4.404 c -0.026 -2.748 2.247 -4.057 2.35 -4.134 C 27.958 37.014 25.954 36.808 25.261 36.757 M 35.572 33.033 v 20.018 h 3.107 v -6.844 h 4.301 c 3.929 0 6.69 -2.696 6.69 -6.6 s -2.709 -6.574 -6.587 -6.574 H 35.572 L 35.572 33.033 z M 38.679 35.652 h 3.582 c 2.696 0 4.237 1.438 4.237 3.968 c 0 2.529 -1.541 3.98 -4.25 3.98 h -3.57 V 35.652 z M 55.345 53.205 c 1.952 0 3.762 -0.989 4.584 -2.555 h 0.064 v 2.401 h 2.876 v -9.964 c 0 -2.889 -2.311 -4.751 -5.868 -4.751 c -3.3 0 -5.739 1.887 -5.829 4.481 h 2.799 c 0.231 -1.233 1.374 -2.042 2.94 -2.042 c 1.9 0 2.966 0.886 2.966 2.517 v 1.104 L 56 44.628 c -3.608 0.218 -5.56 1.695 -5.56 4.263 C 50.44 51.484 52.456 53.205 55.345 53.205 z M 56.18 50.829 c -1.656 0 -2.709 -0.796 -2.709 -2.016 c 0 -1.258 1.014 -1.99 2.953 -2.106 l 3.454 -0.218 v 1.13 C 59.878 49.494 58.286 50.829 56.18 50.829 z M 66.709 58.495 c 3.03 0 4.455 -1.156 5.701 -4.661 l 5.457 -15.305 h -3.159 l -3.659 11.826 h -0.064 l -3.659 -11.826 h -3.249 l 5.264 14.573 l -0.282 0.886 c -0.475 1.502 -1.245 2.08 -2.619 2.08 c -0.244 0 -0.719 -0.026 -0.912 -0.051 v 2.401 C 65.707 58.469 66.478 58.495 66.709 58.495 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: white; fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
<path d="M 45 90 C 20.187 90 0 69.813 0 45 C 0 20.187 20.187 0 45 0 c 24.813 0 45 20.187 45 45 C 90 69.813 69.813 90 45 90 z M 45 3 C 21.841 3 3 21.841 3 45 c 0 23.159 18.841 42 42 42 c 23.159 0 42 -18.841 42 -42 C 87 21.841 68.159 3 45 3 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: white; fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
<defs>
</defs>
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
<path d="M 24.144 34.985 c -0.77 0.912 -2.003 1.631 -3.236 1.528 c -0.154 -1.233 0.449 -2.542 1.156 -3.351 c 0.77 -0.937 2.119 -1.605 3.21 -1.656 C 25.402 32.789 24.902 34.047 24.144 34.985 M 25.261 36.757 c -1.785 -0.103 -3.313 1.014 -4.16 1.014 c -0.86 0 -2.157 -0.963 -3.57 -0.937 c -1.836 0.026 -3.544 1.066 -4.481 2.722 c -1.926 3.313 -0.501 8.218 1.361 10.914 c 0.912 1.335 2.003 2.799 3.441 2.748 c 1.361 -0.051 1.9 -0.886 3.544 -0.886 c 1.656 0 2.131 0.886 3.57 0.86 c 1.489 -0.026 2.427 -1.335 3.338 -2.671 c 1.04 -1.515 1.464 -2.992 1.489 -3.069 c -0.026 -0.026 -2.876 -1.117 -2.902 -4.404 c -0.026 -2.748 2.247 -4.057 2.35 -4.134 C 27.958 37.014 25.954 36.808 25.261 36.757 M 35.572 33.033 v 20.018 h 3.107 v -6.844 h 4.301 c 3.929 0 6.69 -2.696 6.69 -6.6 s -2.709 -6.574 -6.587 -6.574 H 35.572 L 35.572 33.033 z M 38.679 35.652 h 3.582 c 2.696 0 4.237 1.438 4.237 3.968 c 0 2.529 -1.541 3.98 -4.25 3.98 h -3.57 V 35.652 z M 55.345 53.205 c 1.952 0 3.762 -0.989 4.584 -2.555 h 0.064 v 2.401 h 2.876 v -9.964 c 0 -2.889 -2.311 -4.751 -5.868 -4.751 c -3.3 0 -5.739 1.887 -5.829 4.481 h 2.799 c 0.231 -1.233 1.374 -2.042 2.94 -2.042 c 1.9 0 2.966 0.886 2.966 2.517 v 1.104 L 56 44.628 c -3.608 0.218 -5.56 1.695 -5.56 4.263 C 50.44 51.484 52.456 53.205 55.345 53.205 z M 56.18 50.829 c -1.656 0 -2.709 -0.796 -2.709 -2.016 c 0 -1.258 1.014 -1.99 2.953 -2.106 l 3.454 -0.218 v 1.13 C 59.878 49.494 58.286 50.829 56.18 50.829 z M 66.709 58.495 c 3.03 0 4.455 -1.156 5.701 -4.661 l 5.457 -15.305 h -3.159 l -3.659 11.826 h -0.064 l -3.659 -11.826 h -3.249 l 5.264 14.573 l -0.282 0.886 c -0.475 1.502 -1.245 2.08 -2.619 2.08 c -0.244 0 -0.719 -0.026 -0.912 -0.051 v 2.401 C 65.707 58.469 66.478 58.495 66.709 58.495 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
<path d="M 45 90 C 20.187 90 0 69.813 0 45 C 0 20.187 20.187 0 45 0 c 24.813 0 45 20.187 45 45 C 90 69.813 69.813 90 45 90 z M 45 3 C 21.841 3 3 21.841 3 45 c 0 23.159 18.841 42 42 42 c 23.159 0 42 -18.841 42 -42 C 87 21.841 68.159 3 45 3 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
assets/images/bank.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
<defs>
</defs>
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: white; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
<path d="M 84.668 38.004 v -6.27 H 90 V 20 L 45 3.034 L 0 20 v 11.734 h 5.332 v 6.27 h 4.818 v 30.892 H 5.332 v 6.271 H 0 v 11.8 h 90 v -11.8 h -5.332 v -6.271 H 79.85 V 38.004 H 84.668 z M 81.668 35.004 H 66.332 v -3.27 h 15.336 V 35.004 z M 63.332 68.896 v 6.271 h -7.664 v -6.271 H 50.85 V 38.004 h 4.818 v -6.27 h 7.664 v 6.27 h 4.818 v 30.892 H 63.332 z M 26.668 38.004 v -6.27 h 7.664 v 6.27 h 4.818 v 30.892 h -4.818 v 6.271 h -7.664 v -6.271 H 21.85 V 38.004 H 26.668 z M 42.15 68.896 V 38.004 h 5.7 v 30.892 H 42.15 z M 37.332 35.004 v -3.27 h 15.336 v 3.27 H 37.332 z M 37.332 71.896 h 15.336 v 3.271 H 37.332 V 71.896 z M 3 22.075 L 45 6.24 l 42 15.835 v 6.659 H 3 V 22.075 z M 8.332 31.734 h 15.336 v 3.27 H 8.332 V 31.734 z M 13.15 38.004 h 5.7 v 30.892 h -5.7 V 38.004 z M 8.332 71.896 h 15.336 v 3.271 H 8.332 V 71.896 z M 87 83.966 H 3 v -5.8 h 84 V 83.966 z M 81.668 75.166 H 66.332 v -3.271 h 15.336 V 75.166 z M 76.85 68.896 H 71.15 V 38.004 h 5.699 V 68.896 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: white; fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
<defs>
</defs>
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: black; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
<path d="M 84.668 38.004 v -6.27 H 90 V 20 L 45 3.034 L 0 20 v 11.734 h 5.332 v 6.27 h 4.818 v 30.892 H 5.332 v 6.271 H 0 v 11.8 h 90 v -11.8 h -5.332 v -6.271 H 79.85 V 38.004 H 84.668 z M 81.668 35.004 H 66.332 v -3.27 h 15.336 V 35.004 z M 63.332 68.896 v 6.271 h -7.664 v -6.271 H 50.85 V 38.004 h 4.818 v -6.27 h 7.664 v 6.27 h 4.818 v 30.892 H 63.332 z M 26.668 38.004 v -6.27 h 7.664 v 6.27 h 4.818 v 30.892 h -4.818 v 6.271 h -7.664 v -6.271 H 21.85 V 38.004 H 26.668 z M 42.15 68.896 V 38.004 h 5.7 v 30.892 H 42.15 z M 37.332 35.004 v -3.27 h 15.336 v 3.27 H 37.332 z M 37.332 71.896 h 15.336 v 3.271 H 37.332 V 71.896 z M 3 22.075 L 45 6.24 l 42 15.835 v 6.659 H 3 V 22.075 z M 8.332 31.734 h 15.336 v 3.27 H 8.332 V 31.734 z M 13.15 38.004 h 5.7 v 30.892 h -5.7 V 38.004 z M 8.332 71.896 h 15.336 v 3.271 H 8.332 V 71.896 z M 87 83.966 H 3 v -5.8 h 84 V 83.966 z M 81.668 75.166 H 66.332 v -3.271 h 15.336 V 75.166 z M 76.85 68.896 H 71.15 V 38.004 h 5.699 V 68.896 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: black; fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
assets/images/buy_sell.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

1
assets/images/card.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="iso-8859-1"?><!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 50 50" width="100px" height="100px"><path style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;" d="M43,40H7c-2.209,0-4-1.791-4-4V14c0-2.209,1.791-4,4-4h36c2.209,0,4,1.791,4,4v22C47,38.209,45.209,40,43,40z"/><rect x="3" y="16" width="44" height="5"/><line style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:10;" x1="9" y1="25" x2="25" y2="25"/></svg>

After

Width:  |  Height:  |  Size: 633 B

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 50 50" width="100px" height="100px">
<path style="fill:none;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;" d="M43,40H7c-2.209,0-4-1.791-4-4V14c0-2.209,1.791-4,4-4h36c2.209,0,4,1.791,4,4v22C47,38.209,45.209,40,43,40z"/>
<rect x="3" y="16" width="44" height="5" style="fill:white;stroke:#ffffff;stroke-width:2;"/>
<line style="fill:none;stroke:#ffffff;stroke-width:2;stroke-miterlimit:10;" x1="9" y1="25" x2="25" y2="25"/>
</svg>

After

Width:  |  Height:  |  Size: 701 B

View file

@ -0,0 +1,12 @@
<?xml version="1.0" ?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<g id="Outline">
<g data-name="Outline" id="Outline-2">
<path d="M39,36.852a6.8,6.8,0,0,0-6.793-6.793h-.319A3.716,3.716,0,1,1,35.6,26.344a1,1,0,0,0,2,0,5.725,5.725,0,0,0-4.561-5.6V18.09a1,1,0,0,0-2,0V20.7a5.712,5.712,0,0,0,.846,11.361h.319a4.793,4.793,0,1,1-4.793,4.793,1,1,0,0,0-2,0A6.8,6.8,0,0,0,31.451,43.6v2.947a1,1,0,0,0,2,0v-3c0-.008,0-.014,0-.021A6.8,6.8,0,0,0,39,36.852Z"/>
<path d="M32,2A30,30,0,1,0,62,32,30.034,30.034,0,0,0,32,2Zm0,58A28,28,0,1,1,60,32,28.032,28.032,0,0,1,32,60Z"/>
<path d="M49.655,16.793a3.172,3.172,0,1,0-3.172,3.172,3.137,3.137,0,0,0,1.263-.266A19.994,19.994,0,0,1,22.692,49.707a1,1,0,0,0-.933,1.769,21.986,21.986,0,0,0,27.47-33.124A3.141,3.141,0,0,0,49.655,16.793Zm-4.344,0a1.172,1.172,0,1,1,1.172,1.172A1.172,1.172,0,0,1,45.311,16.793Z"/>
<path d="M16.793,44.035a3.164,3.164,0,0,0-.692.081A19.779,19.779,0,0,1,12,32,20.023,20.023,0,0,1,32,12a19.811,19.811,0,0,1,8.463,1.874,1,1,0,0,0,.848-1.812A21.989,21.989,0,0,0,14.39,45.16a3.141,3.141,0,0,0-.769,2.047,3.172,3.172,0,1,0,3.172-3.172Zm0,4.344a1.172,1.172,0,1,1,1.172-1.172A1.172,1.172,0,0,1,16.793,48.379Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,30 @@
<svg width="40" height="42" viewBox="0 0 40 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.7" d="M19.9526 41.9076L7.3877 34.655V26.1226L19.9526 33.3751V41.9076Z" fill="url(#paint0_linear_2113_32117)"/>
<path opacity="0.7" d="M19.9521 41.9076L32.5171 34.655V26.1226L19.9521 33.3751V41.9076Z" fill="url(#paint1_linear_2113_32117)"/>
<path opacity="0.7" d="M39.9095 7.34521V21.8562L32.5166 26.1225V11.6114L39.9095 7.34521Z" fill="url(#paint2_linear_2113_32117)"/>
<path d="M39.9099 7.34536L27.345 0.0927734L19.9521 4.359L32.5171 11.6116L39.9099 7.34536Z" fill="url(#paint3_linear_2113_32117)"/>
<path d="M0 7.34536L12.5649 0.0927734L19.9519 4.359L7.387 11.6116L0 7.34536Z" fill="#F969D3"/>
<path opacity="0.7" d="M0 7.34521V21.8562L7.387 26.1225V11.6114L0 7.34521Z" fill="url(#paint4_linear_2113_32117)"/>
<defs>
<linearGradient id="paint0_linear_2113_32117" x1="18.6099" y1="41.8335" x2="7.73529" y2="8.31842" gradientUnits="userSpaceOnUse">
<stop stop-color="#E98ADA"/>
<stop offset="1" stop-color="#7E4DBD"/>
</linearGradient>
<linearGradient id="paint1_linear_2113_32117" x1="26.2346" y1="26.1226" x2="26.2346" y2="41.9076" gradientUnits="userSpaceOnUse">
<stop stop-color="#719DED"/>
<stop offset="1" stop-color="#2545BE"/>
</linearGradient>
<linearGradient id="paint2_linear_2113_32117" x1="36.213" y1="7.34521" x2="36.213" y2="26.1225" gradientUnits="userSpaceOnUse">
<stop stop-color="#93EBFF"/>
<stop offset="1" stop-color="#197DDB"/>
</linearGradient>
<linearGradient id="paint3_linear_2113_32117" x1="29.931" y1="0.0927734" x2="38.2156" y2="14.8448" gradientUnits="userSpaceOnUse">
<stop stop-color="#F969D3"/>
<stop offset="1" stop-color="#4F51C0"/>
</linearGradient>
<linearGradient id="paint4_linear_2113_32117" x1="18.1251" y1="44.2539" x2="-7.06792" y2="15.2763" gradientUnits="userSpaceOnUse">
<stop stop-color="#E98ADA"/>
<stop offset="1" stop-color="#7E4DBD"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
assets/images/revolut.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

15
assets/images/skrill.svg Normal file
View file

@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
<defs>
</defs>
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
<circle cx="45" cy="45" r="45" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(127,33,99); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) "/>
<polygon points="69.59,36.9 69.59,54.86 74.5,54.86 74.5,36.02 " style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(249,249,249); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) "/>
<polygon points="62.42,36.9 67.33,36.02 67.33,54.87 62.42,54.87 " style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(249,249,249); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) "/>
<rect x="55.43" y="41.08" rx="0" ry="0" width="4.91" height="13.78" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(249,249,249); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) "/>
<path d="M 57.879 39.76 c 1.332 0 2.425 -1.082 2.425 -2.415 s -1.082 -2.425 -2.425 -2.425 c -1.332 0 -2.415 1.082 -2.415 2.425 C 55.465 38.677 56.547 39.76 57.879 39.76 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(249,249,249); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
<path d="M 52.665 40.884 c -4.538 0.146 -6.838 2.186 -6.838 6.234 v 7.754 h 4.954 v -6.328 c 0 -2.425 0.312 -3.466 3.195 -3.559 v -4.038 C 53.477 40.853 52.665 40.884 52.665 40.884 L 52.665 40.884 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(249,249,249); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
<path d="M 39.344 41.071 c -0.104 0.271 -0.895 2.498 -2.8 4.798 v -9.845 l -5.068 0.999 v 17.838 h 5.068 v -5.516 c 1.467 2.206 2.196 5.516 2.196 5.516 h 6.068 c -0.604 -2.498 -3.226 -7.098 -3.226 -7.098 c 2.352 -2.987 3.393 -6.172 3.559 -6.702 h -5.797 L 39.344 41.071 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(249,249,249); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
<path d="M 22.973 43.028 c -0.624 -0.042 -2.061 -0.135 -2.061 -1.426 c 0 -1.561 2.071 -1.561 2.841 -1.561 c 1.363 0 3.133 0.406 4.392 0.781 c 0 0 0.708 0.25 1.301 0.5 l 0.052 0.01 v -4.267 l -0.073 -0.021 c -1.488 -0.52 -3.216 -1.02 -6.432 -1.02 c -5.537 0 -7.493 3.226 -7.493 5.984 c 0 1.592 0.687 5.339 7.025 5.776 c 0.541 0.031 1.967 0.114 1.967 1.457 c 0 1.103 -1.166 1.759 -3.133 1.759 c -2.154 0 -4.236 -0.552 -5.506 -1.072 v 4.402 c 1.894 0.5 4.038 0.749 6.546 0.749 c 5.412 0 7.837 -3.049 7.837 -6.078 C 30.237 45.567 27.531 43.34 22.973 43.028 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(249,249,249); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 24 24" width="512" height="512">
<path d="M12,0C5.383,0,0,5.383,0,12s5.383,12,12,12,12-5.383,12-12S18.617,0,12,0Zm0,23c-6.065,0-11-4.935-11-11S5.935,1,12,1s11,4.935,11,11-4.935,11-11,11Zm4-8.626c0,1.448-1.178,2.626-2.626,2.626h-.874v1.5c0,.276-.224,.5-.5,.5s-.5-.224-.5-.5v-1.5h-.926c-.979,0-1.893-.526-2.382-1.375-.139-.239-.057-.545,.183-.683,.238-.14,.544-.057,.683,.183,.312,.54,.894,.875,1.517,.875h2.8c.896,0,1.626-.729,1.626-1.626,0-.803-.575-1.478-1.368-1.605l-3.422-.55c-1.28-.206-2.21-1.296-2.21-2.593,0-1.448,1.178-2.626,2.626-2.626h.874v-1.5c0-.276,.224-.5,.5-.5s.5,.224,.5,.5v1.5h.926c.979,0,1.892,.527,2.382,1.375,.139,.239,.057,.545-.183,.683-.236,.136-.545,.057-.683-.183-.312-.54-.894-.875-1.517-.875h-2.8c-.896,0-1.626,.729-1.626,1.626,0,.803,.575,1.478,1.368,1.605l3.422,.55c1.28,.206,2.21,1.297,2.21,2.593Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 978 B

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 24 24" width="512" height="512"><path d="M12,0C5.383,0,0,5.383,0,12s5.383,12,12,12,12-5.383,12-12S18.617,0,12,0Zm0,23c-6.065,0-11-4.935-11-11S5.935,1,12,1s11,4.935,11,11-4.935,11-11,11Zm4-8.626c0,1.448-1.178,2.626-2.626,2.626h-.874v1.5c0,.276-.224,.5-.5,.5s-.5-.224-.5-.5v-1.5h-.926c-.979,0-1.893-.526-2.382-1.375-.139-.239-.057-.545,.183-.683,.238-.14,.544-.057,.683,.183,.312,.54,.894,.875,1.517,.875h2.8c.896,0,1.626-.729,1.626-1.626,0-.803-.575-1.478-1.368-1.605l-3.422-.55c-1.28-.206-2.21-1.296-2.21-2.593,0-1.448,1.178-2.626,2.626-2.626h.874v-1.5c0-.276,.224-.5,.5-.5s.5,.224,.5,.5v1.5h.926c.979,0,1.892,.527,2.382,1.375,.139,.239,.057,.545-.183,.683-.236,.136-.545,.057-.683-.183-.312-.54-.894-.875-1.517-.875h-2.8c-.896,0-1.626,.729-1.626,1.626,0,.803,.575,1.478,1.368,1.605l3.422,.55c1.28,.206,2.21,1.297,2.21,2.593Z"/></svg>

After

Width:  |  Height:  |  Size: 960 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -35,3 +35,34 @@ CryptoCurrency currencyForWalletType(WalletType type, {bool? isTestnet}) {
'Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType');
}
}
WalletType? walletTypeForCurrency(CryptoCurrency currency) {
switch (currency) {
case CryptoCurrency.btc:
return WalletType.bitcoin;
case CryptoCurrency.xmr:
return WalletType.monero;
case CryptoCurrency.ltc:
return WalletType.litecoin;
case CryptoCurrency.xhv:
return WalletType.haven;
case CryptoCurrency.eth:
return WalletType.ethereum;
case CryptoCurrency.bch:
return WalletType.bitcoinCash;
case CryptoCurrency.nano:
return WalletType.nano;
case CryptoCurrency.banano:
return WalletType.banano;
case CryptoCurrency.maticpoly:
return WalletType.polygon;
case CryptoCurrency.sol:
return WalletType.solana;
case CryptoCurrency.trx:
return WalletType.tron;
case CryptoCurrency.wow:
return WalletType.wownero;
default:
return null;
}
}

View file

@ -1,6 +1,10 @@
import 'package:cake_wallet/buy/buy_amount.dart';
import 'package:cake_wallet/buy/buy_quote.dart';
import 'package:cake_wallet/buy/order.dart';
import 'package:cake_wallet/buy/payment_method.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:flutter/material.dart';
@ -23,14 +27,38 @@ abstract class BuyProvider {
String get darkIcon;
bool get isAggregator;
@override
String toString() => title;
Future<void> launchProvider(BuildContext context, bool? isBuyAction);
Future<void>? launchProvider(
{required BuildContext context,
required Quote quote,
required double amount,
required bool isBuyAction,
required String cryptoCurrencyAddress,
String? countryCode}) =>
null;
Future<String> requestUrl(String amount, String sourceCurrency) => throw UnimplementedError();
Future<Order> findOrderById(String id) => throw UnimplementedError();
Future<BuyAmount> calculateAmount(String amount, String sourceCurrency) => throw UnimplementedError();
Future<BuyAmount> calculateAmount(String amount, String sourceCurrency) =>
throw UnimplementedError();
Future<List<PaymentMethod>> getAvailablePaymentTypes(
String fiatCurrency, String cryptoCurrency, bool isBuyAction) async =>
[];
Future<List<Quote>?> fetchQuote(
{required CryptoCurrency cryptoCurrency,
required FiatCurrency fiatCurrency,
required double amount,
required bool isBuyAction,
required String walletAddress,
PaymentType? paymentType,
String? countryCode}) async =>
null;
}

302
lib/buy/buy_quote.dart Normal file
View file

@ -0,0 +1,302 @@
import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/payment_method.dart';
import 'package:cake_wallet/core/selectable_option.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/provider_types.dart';
import 'package:cake_wallet/exchange/limits.dart';
import 'package:cw_core/crypto_currency.dart';
enum ProviderRecommendation { bestRate, lowKyc, successRate }
extension RecommendationTitle on ProviderRecommendation {
String get title {
switch (this) {
case ProviderRecommendation.bestRate:
return 'BEST RATE';
case ProviderRecommendation.lowKyc:
return 'LOW KYC';
case ProviderRecommendation.successRate:
return 'SUCCESS RATE';
}
}
}
ProviderRecommendation? getRecommendationFromString(String title) {
switch (title) {
case 'BEST RATE':
return ProviderRecommendation.bestRate;
case 'LowKyc':
return ProviderRecommendation.lowKyc;
case 'SuccessRate':
return ProviderRecommendation.successRate;
default:
return null;
}
}
class Quote extends SelectableOption {
Quote({
required this.rate,
required this.feeAmount,
required this.networkFee,
required this.transactionFee,
required this.payout,
required this.provider,
required this.paymentType,
required this.recommendations,
this.isBuyAction = true,
this.quoteId,
this.rampId,
this.rampName,
this.rampIconPath,
this.limits,
}) : super(title: provider.isAggregator ? rampName ?? '' : provider.title);
final double rate;
final double feeAmount;
final double networkFee;
final double transactionFee;
final double payout;
final PaymentType paymentType;
final BuyProvider provider;
final String? quoteId;
final List<ProviderRecommendation> recommendations;
String? rampId;
String? rampName;
String? rampIconPath;
bool _isSelected = false;
bool _isBestRate = false;
bool isBuyAction;
Limits? limits;
late FiatCurrency _fiatCurrency;
late CryptoCurrency _cryptoCurrency;
bool get isSelected => _isSelected;
bool get isBestRate => _isBestRate;
FiatCurrency get fiatCurrency => _fiatCurrency;
CryptoCurrency get cryptoCurrency => _cryptoCurrency;
@override
bool get isOptionSelected => this._isSelected;
@override
String get lightIconPath =>
provider.isAggregator ? rampIconPath ?? provider.lightIcon : provider.lightIcon;
@override
String get darkIconPath =>
provider.isAggregator ? rampIconPath ?? provider.darkIcon : provider.darkIcon;
@override
List<String> get badges => recommendations.map((e) => e.title).toList();
@override
String get topLeftSubTitle =>
this.rate > 0 ? '1 $cryptoName = ${rate.toStringAsFixed(2)} $fiatName' : '';
@override
String get bottomLeftSubTitle {
if (limits != null) {
final min = limits!.min;
final max = limits!.max;
return 'min: ${min} ${fiatCurrency.toString()} | max: ${max == double.infinity ? '' : '${max} ${fiatCurrency.toString()}'}';
}
return '';
}
String get fiatName => isBuyAction ? fiatCurrency.toString() : cryptoCurrency.toString();
String get cryptoName => isBuyAction ? cryptoCurrency.toString() : fiatCurrency.toString();
@override
String? get topRightSubTitle => '';
@override
String get topRightSubTitleLightIconPath => provider.isAggregator ? provider.lightIcon : '';
@override
String get topRightSubTitleDarkIconPath => provider.isAggregator ? provider.darkIcon : '';
String get quoteTitle => '${provider.title} - ${paymentType.name}';
String get formatedFee => '$feeAmount ${isBuyAction ? fiatCurrency : cryptoCurrency}';
set setIsSelected(bool isSelected) => _isSelected = isSelected;
set setIsBestRate(bool isBestRate) => _isBestRate = isBestRate;
set setFiatCurrency(FiatCurrency fiatCurrency) => _fiatCurrency = fiatCurrency;
set setCryptoCurrency(CryptoCurrency cryptoCurrency) => _cryptoCurrency = cryptoCurrency;
set setLimits(Limits limits) => this.limits = limits;
factory Quote.fromOnramperJson(Map<String, dynamic> json, bool isBuyAction,
Map<String, dynamic> metaData, PaymentType paymentType) {
final rate = _toDouble(json['rate']) ?? 0.0;
final networkFee = _toDouble(json['networkFee']) ?? 0.0;
final transactionFee = _toDouble(json['transactionFee']) ?? 0.0;
final feeAmount = double.parse((networkFee + transactionFee).toStringAsFixed(2));
final rampId = json['ramp'] as String? ?? '';
final rampData = metaData[rampId] ?? {};
final rampName = rampData['displayName'] as String? ?? '';
final rampIconPath = rampData['svg'] as String? ?? '';
final recommendations = json['recommendations'] != null
? List<String>.from(json['recommendations'] as List<dynamic>)
: <String>[];
final enumRecommendations = recommendations
.map((e) => getRecommendationFromString(e))
.whereType<ProviderRecommendation>()
.toList();
final availablePaymentMethods = json['availablePaymentMethods'] as List<dynamic>? ?? [];
double minLimit = 0.0;
double maxLimit = double.infinity;
for (var paymentMethod in availablePaymentMethods) {
if (paymentMethod is Map<String, dynamic>) {
final details = paymentMethod['details'] as Map<String, dynamic>?;
if (details != null) {
final limits = details['limits'] as Map<String, dynamic>?;
if (limits != null && limits.isNotEmpty) {
final firstLimitEntry = limits.values.first as Map<String, dynamic>?;
if (firstLimitEntry != null) {
minLimit = _toDouble(firstLimitEntry['min'])?.roundToDouble() ?? 0.0;
maxLimit = _toDouble(firstLimitEntry['max'])?.roundToDouble() ?? double.infinity;
break;
}
}
}
}
}
return Quote(
rate: rate,
feeAmount: feeAmount,
networkFee: networkFee,
transactionFee: transactionFee,
payout: json['payout'] as double? ?? 0.0,
rampId: rampId,
rampName: rampName,
rampIconPath: rampIconPath,
paymentType: paymentType,
quoteId: json['quoteId'] as String? ?? '',
recommendations: enumRecommendations,
provider: ProvidersHelper.getProviderByType(ProviderType.onramper)!,
isBuyAction: isBuyAction,
limits: Limits(min: minLimit, max: maxLimit),
);
}
factory Quote.fromMoonPayJson(
Map<String, dynamic> json, bool isBuyAction, PaymentType paymentType) {
final rate = isBuyAction
? json['quoteCurrencyPrice'] as double? ?? 0.0
: json['baseCurrencyPrice'] as double? ?? 0.0;
final fee = _toDouble(json['feeAmount']) ?? 0.0;
final networkFee = _toDouble(json['networkFeeAmount']) ?? 0.0;
final transactionFee = _toDouble(json['extraFeeAmount']) ?? 0.0;
final feeAmount = double.parse((fee + networkFee + transactionFee).toStringAsFixed(2));
final baseCurrency = json['baseCurrency'] as Map<String, dynamic>?;
double minLimit = 0.0;
double maxLimit = double.infinity;
if (baseCurrency != null) {
minLimit = _toDouble(baseCurrency['minAmount']) ?? minLimit;
maxLimit = _toDouble(baseCurrency['maxAmount']) ?? maxLimit;
}
return Quote(
rate: rate,
feeAmount: feeAmount,
networkFee: networkFee,
transactionFee: transactionFee,
payout: _toDouble(json['quoteCurrencyAmount']) ?? 0.0,
paymentType: paymentType,
recommendations: [],
quoteId: json['signature'] as String? ?? '',
provider: ProvidersHelper.getProviderByType(ProviderType.moonpay)!,
isBuyAction: isBuyAction,
limits: Limits(min: minLimit, max: maxLimit),
);
}
factory Quote.fromDFXJson(
Map<String, dynamic> json,
bool isBuyAction,
PaymentType paymentType,
) {
final rate = _toDouble(json['exchangeRate']) ?? 0.0;
final fees = json['fees'] as Map<String, dynamic>;
final minVolume = _toDouble(json['minVolume']) ?? 0.0;
final maxVolume = _toDouble(json['maxVolume']) ?? double.infinity;
return Quote(
rate: isBuyAction ? rate : 1 / rate,
feeAmount: _toDouble(json['feeAmount']) ?? 0.0,
networkFee: _toDouble(fees['network']) ?? 0.0,
transactionFee: _toDouble(fees['rate']) ?? 0.0,
payout: _toDouble(json['payout']) ?? 0.0,
paymentType: paymentType,
recommendations: [ProviderRecommendation.lowKyc],
provider: ProvidersHelper.getProviderByType(ProviderType.dfx)!,
isBuyAction: isBuyAction,
limits: Limits(min: minVolume, max: maxVolume),
);
}
factory Quote.fromRobinhoodJson(
Map<String, dynamic> json, bool isBuyAction, PaymentType paymentType) {
final networkFee = json['networkFee'] as Map<String, dynamic>;
final processingFee = json['processingFee'] as Map<String, dynamic>;
final networkFeeAmount = _toDouble(networkFee['fiatAmount']) ?? 0.0;
final transactionFeeAmount = _toDouble(processingFee['fiatAmount']) ?? 0.0;
final feeAmount = double.parse((networkFeeAmount + transactionFeeAmount).toStringAsFixed(2));
return Quote(
rate: _toDouble(json['price']) ?? 0.0,
feeAmount: feeAmount,
networkFee: _toDouble(networkFee['fiatAmount']) ?? 0.0,
transactionFee: _toDouble(processingFee['fiatAmount']) ?? 0.0,
payout: _toDouble(json['cryptoAmount']) ?? 0.0,
paymentType: paymentType,
recommendations: [],
provider: ProvidersHelper.getProviderByType(ProviderType.robinhood)!,
isBuyAction: isBuyAction,
limits: Limits(min: 0.0, max: double.infinity),
);
}
factory Quote.fromMeldJson(Map<String, dynamic> json, bool isBuyAction, PaymentType paymentType) {
final quotes = json['quotes'][0] as Map<String, dynamic>;
return Quote(
rate: quotes['exchangeRate'] as double? ?? 0.0,
feeAmount: quotes['totalFee'] as double? ?? 0.0,
networkFee: quotes['networkFee'] as double? ?? 0.0,
transactionFee: quotes['transactionFee'] as double? ?? 0.0,
payout: quotes['payout'] as double? ?? 0.0,
paymentType: paymentType,
recommendations: [],
provider: ProvidersHelper.getProviderByType(ProviderType.meld)!,
isBuyAction: isBuyAction,
limits: Limits(min: 0.0, max: double.infinity),
);
}
static double? _toDouble(dynamic value) {
if (value is int) {
return value.toDouble();
} else if (value is double) {
return value;
} else if (value is String) {
return double.tryParse(value);
}
return null;
}
}

View file

@ -1,13 +1,17 @@
import 'dart:convert';
import 'dart:developer';
import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/buy_quote.dart';
import 'package:cake_wallet/buy/payment_method.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
@ -15,10 +19,12 @@ import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart';
class DFXBuyProvider extends BuyProvider {
DFXBuyProvider({required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM})
DFXBuyProvider(
{required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM})
: super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: ledgerVM);
static const _baseUrl = 'api.dfx.swiss';
// static const _signMessagePath = '/v1/auth/signMessage';
static const _authPath = '/v1/auth';
static const walletName = 'CakeWallet';
@ -35,24 +41,8 @@ class DFXBuyProvider extends BuyProvider {
@override
String get darkIcon => 'assets/images/dfx_dark.png';
String get assetOut {
switch (wallet.type) {
case WalletType.bitcoin:
return 'BTC';
case WalletType.bitcoinCash:
return 'BCH';
case WalletType.litecoin:
return 'LTC';
case WalletType.monero:
return 'XMR';
case WalletType.ethereum:
return 'ETH';
case WalletType.polygon:
return 'MATIC';
default:
throw Exception("WalletType is not available for DFX ${wallet.type}");
}
}
@override
bool get isAggregator => false;
String get blockchain {
switch (wallet.type) {
@ -60,21 +50,13 @@ class DFXBuyProvider extends BuyProvider {
case WalletType.bitcoinCash:
case WalletType.litecoin:
return 'Bitcoin';
case WalletType.monero:
return 'Monero';
case WalletType.ethereum:
return 'Ethereum';
case WalletType.polygon:
return 'Polygon';
default:
throw Exception("WalletType is not available for DFX ${wallet.type}");
return walletTypeToString(wallet.type);
}
}
String get walletAddress =>
wallet.walletAddresses.primaryAddress ?? wallet.walletAddresses.address;
Future<String> getSignMessage() async =>
Future<String> getSignMessage(String walletAddress) async =>
"By_signing_this_message,_you_confirm_that_you_are_the_sole_owner_of_the_provided_Blockchain_address._Your_ID:_$walletAddress";
// // Lets keep this just in case, but we can avoid this API Call
@ -92,8 +74,9 @@ class DFXBuyProvider extends BuyProvider {
// }
// }
Future<String> auth() async {
final signMessage = await getSignature(await getSignMessage());
Future<String> auth(String walletAddress) async {
final signMessage = await getSignature(
await getSignMessage(walletAddress), walletAddress);
final requestBody = jsonEncode({
'wallet': walletName,
@ -120,7 +103,7 @@ class DFXBuyProvider extends BuyProvider {
}
}
Future<String> getSignature(String message) async {
Future<String> getSignature(String message, String walletAddress) async {
switch (wallet.type) {
case WalletType.ethereum:
case WalletType.polygon:
@ -135,8 +118,178 @@ class DFXBuyProvider extends BuyProvider {
}
}
Future<Map<String, dynamic>> fetchFiatCredentials(String fiatCurrency) async {
final url = Uri.https(_baseUrl, '/v1/fiat');
try {
final response = await http.get(url, headers: {'accept': 'application/json'});
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as List<dynamic>;
for (final item in data) {
if (item['name'] == fiatCurrency) return item as Map<String, dynamic>;
}
log('DFX does not support fiat: $fiatCurrency');
return {};
} else {
log('DFX Failed to fetch fiat currencies: ${response.statusCode}');
return {};
}
} catch (e) {
print('DFX Error fetching fiat currencies: $e');
return {};
}
}
Future<Map<String, dynamic>> fetchAssetCredential(String assetsName) async {
final url = Uri.https(_baseUrl, '/v1/asset', {'blockchains': blockchain});
try {
final response = await http.get(url, headers: {'accept': 'application/json'});
if (response.statusCode == 200) {
final responseData = jsonDecode(response.body);
if (responseData is List && responseData.isNotEmpty) {
return responseData.first as Map<String, dynamic>;
} else if (responseData is Map<String, dynamic>) {
return responseData;
} else {
log('DFX: Does not support this asset name : ${blockchain}');
}
} else {
log('DFX: Failed to fetch assets: ${response.statusCode}');
}
} catch (e) {
log('DFX: Error fetching assets: $e');
}
return {};
}
Future<List<PaymentMethod>> getAvailablePaymentTypes(
String fiatCurrency, String cryptoCurrency, bool isBuyAction) async {
final List<PaymentMethod> paymentMethods = [];
if (isBuyAction) {
final fiatBuyCredentials = await fetchFiatCredentials(fiatCurrency);
if (fiatBuyCredentials.isNotEmpty) {
fiatBuyCredentials.forEach((key, value) {
if (key == 'limits') {
final limits = value as Map<String, dynamic>;
limits.forEach((paymentMethodKey, paymentMethodValue) {
final min = _toDouble(paymentMethodValue['minVolume']);
final max = _toDouble(paymentMethodValue['maxVolume']);
if (min != null && max != null && min > 0 && max > 0) {
final paymentMethod = PaymentMethod.fromDFX(
paymentMethodKey, _getPaymentTypeByString(paymentMethodKey));
paymentMethods.add(paymentMethod);
}
});
}
});
}
} else {
final assetCredentials = await fetchAssetCredential(cryptoCurrency);
if (assetCredentials.isNotEmpty) {
if (assetCredentials['sellable'] == true) {
final availablePaymentTypes = [
PaymentType.bankTransfer,
PaymentType.creditCard,
PaymentType.sepa
];
availablePaymentTypes.forEach((element) {
final paymentMethod = PaymentMethod.fromDFX(normalizePaymentMethod(element)!, element);
paymentMethods.add(paymentMethod);
});
}
}
}
return paymentMethods;
}
@override
Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
Future<List<Quote>?> fetchQuote(
{required CryptoCurrency cryptoCurrency,
required FiatCurrency fiatCurrency,
required double amount,
required bool isBuyAction,
required String walletAddress,
PaymentType? paymentType,
String? countryCode}) async {
/// if buying with any currency other than eur or chf then DFX is not supported
if (isBuyAction && (fiatCurrency != FiatCurrency.eur && fiatCurrency != FiatCurrency.chf)) {
return null;
}
String? paymentMethod;
if (paymentType != null && paymentType != PaymentType.all) {
paymentMethod = normalizePaymentMethod(paymentType);
if (paymentMethod == null) paymentMethod = paymentType.name;
} else {
paymentMethod = 'Bank';
}
final action = isBuyAction ? 'buy' : 'sell';
if (isBuyAction && cryptoCurrency != wallet.currency) return null;
final fiatCredentials = await fetchFiatCredentials(fiatCurrency.name.toString());
if (fiatCredentials['id'] == null) return null;
final assetCredentials = await fetchAssetCredential(cryptoCurrency.title.toString());
if (assetCredentials['id'] == null) return null;
log('DFX: Fetching $action quote: ${isBuyAction ? cryptoCurrency : fiatCurrency} -> ${isBuyAction ? fiatCurrency : cryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod');
final url = Uri.https(_baseUrl, '/v1/$action/quote');
final headers = {'accept': 'application/json', 'Content-Type': 'application/json'};
final body = jsonEncode({
'currency': {'id': fiatCredentials['id'] as int},
'asset': {'id': assetCredentials['id']},
'amount': amount,
'targetAmount': 0,
'paymentMethod': paymentMethod,
'discountCode': ''
});
try {
final response = await http.put(url, headers: headers, body: body);
final responseData = jsonDecode(response.body);
if (response.statusCode == 200) {
if (responseData is Map<String, dynamic>) {
final paymentType = _getPaymentTypeByString(responseData['paymentMethod'] as String?);
final quote = Quote.fromDFXJson(responseData, isBuyAction, paymentType);
quote.setFiatCurrency = fiatCurrency;
quote.setCryptoCurrency = cryptoCurrency;
return [quote];
} else {
print('DFX: Unexpected data type: ${responseData.runtimeType}');
return null;
}
} else {
if (responseData is Map<String, dynamic> && responseData.containsKey('message')) {
print('DFX Error: ${responseData['message']}');
} else {
print('DFX Failed to fetch buy quote: ${response.statusCode}');
}
return null;
}
} catch (e) {
print('DFX Error fetching buy quote: $e');
return null;
}
}
Future<void>? launchProvider(
{required BuildContext context,
required Quote quote,
required double amount,
required bool isBuyAction,
required String cryptoCurrencyAddress,
String? countryCode}) async {
if (wallet.isHardwareWallet) {
if (!ledgerVM!.isConnected) {
await Navigator.of(context).pushNamed(Routes.connectDevices,
@ -152,26 +305,21 @@ class DFXBuyProvider extends BuyProvider {
}
try {
final assetOut = this.assetOut;
final blockchain = this.blockchain;
final actionType = isBuyAction == true ? '/buy' : '/sell';
final actionType = isBuyAction ? '/buy' : '/sell';
final accessToken = await auth();
final accessToken = await auth(cryptoCurrencyAddress);
final uri = Uri.https('services.dfx.swiss', actionType, {
'session': accessToken,
'lang': 'en',
'asset-out': assetOut,
'asset-out': isBuyAction ? quote.cryptoCurrency.toString() : quote.fiatCurrency.toString(),
'blockchain': blockchain,
'asset-in': 'EUR',
'asset-in': isBuyAction ? quote.fiatCurrency.toString() : quote.cryptoCurrency.toString(),
'amount': amount.toString() //TODO: Amount does not work
});
if (await canLaunchUrl(uri)) {
if (DeviceInfo.instance.isMobile) {
Navigator.of(context).pushNamed(Routes.webViewPage, arguments: [title, uri]);
} else {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
throw Exception('Could not launch URL');
}
@ -187,4 +335,39 @@ class DFXBuyProvider extends BuyProvider {
});
}
}
String? normalizePaymentMethod(PaymentType paymentMethod) {
switch (paymentMethod) {
case PaymentType.bankTransfer:
return 'Bank';
case PaymentType.creditCard:
return 'Card';
case PaymentType.sepa:
return 'Instant';
default:
return null;
}
}
PaymentType _getPaymentTypeByString(String? paymentMethod) {
switch (paymentMethod) {
case 'Bank':
return PaymentType.bankTransfer;
case 'Card':
return PaymentType.creditCard;
case 'Instant':
return PaymentType.sepa;
default:
return PaymentType.all;
}
}
double? _toDouble(dynamic value) {
if (value is int) {
return value.toDouble();
} else if (value is double) {
return value;
}
return null;
}
}

View file

@ -0,0 +1,266 @@
import 'dart:convert';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/buy_quote.dart';
import 'package:cake_wallet/buy/payment_method.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/provider_types.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:flutter/material.dart';
import 'dart:developer';
import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart';
class MeldBuyProvider extends BuyProvider {
MeldBuyProvider({required WalletBase wallet, bool isTestEnvironment = false})
: super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: null);
static const _isProduction = false;
static const _baseUrl = _isProduction ? 'api.meld.io' : 'api-sb.meld.io';
static const _providersProperties = '/service-providers/properties';
static const _paymentMethodsPath = '/payment-methods';
static const _quotePath = '/payments/crypto/quote';
static const String sandboxUrl = 'sb.fluidmoney.xyz';
static const String productionUrl = 'fluidmoney.xyz';
static const String _baseWidgetUrl = _isProduction ? productionUrl : sandboxUrl;
static String get _testApiKey => secrets.meldTestApiKey;
static String get _testPublicKey => '' ; //secrets.meldTestPublicKey;
@override
String get title => 'Meld';
@override
String get providerDescription => 'Meld Buy Provider';
@override
String get lightIcon => 'assets/images/meld_logo.svg';
@override
String get darkIcon => 'assets/images/meld_logo.svg';
@override
bool get isAggregator => true;
@override
Future<List<PaymentMethod>> getAvailablePaymentTypes(
String fiatCurrency, String cryptoCurrency, bool isBuyAction) async {
final params = {'fiatCurrencies': fiatCurrency, 'statuses': 'LIVE,RECENTLY_ADDED,BUILDING'};
final path = '$_providersProperties$_paymentMethodsPath';
final url = Uri.https(_baseUrl, path, params);
try {
final response = await http.get(
url,
headers: {
'Authorization': _isProduction ? '' : _testApiKey,
'Meld-Version': '2023-12-19',
'accept': 'application/json',
'content-type': 'application/json',
},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as List<dynamic>;
final paymentMethods =
data.map((e) => PaymentMethod.fromMeldJson(e as Map<String, dynamic>)).toList();
return paymentMethods;
} else {
print('Meld: Failed to fetch payment types');
return List<PaymentMethod>.empty();
}
} catch (e) {
print('Meld: Failed to fetch payment types: $e');
return List<PaymentMethod>.empty();
}
}
@override
Future<List<Quote>?> fetchQuote(
{required CryptoCurrency cryptoCurrency,
required FiatCurrency fiatCurrency,
required double amount,
required bool isBuyAction,
required String walletAddress,
PaymentType? paymentType,
String? countryCode}) async {
String? paymentMethod;
if (paymentType != null && paymentType != PaymentType.all) {
paymentMethod = normalizePaymentMethod(paymentType);
if (paymentMethod == null) paymentMethod = paymentType.name;
}
log('Meld: Fetching buy quote: ${isBuyAction ? cryptoCurrency : fiatCurrency} -> ${isBuyAction ? fiatCurrency : cryptoCurrency}, amount: $amount');
final url = Uri.https(_baseUrl, _quotePath);
final headers = {
'Authorization': _testApiKey,
'Meld-Version': '2023-12-19',
'accept': 'application/json',
'content-type': 'application/json',
};
final body = jsonEncode({
'countryCode': countryCode,
'destinationCurrencyCode': isBuyAction ? fiatCurrency.name : cryptoCurrency.title,
'sourceAmount': amount,
'sourceCurrencyCode': isBuyAction ? cryptoCurrency.title : fiatCurrency.name,
if (paymentMethod != null) 'paymentMethod': paymentMethod,
});
try {
final response = await http.post(url, headers: headers, body: body);
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as Map<String, dynamic>;
final paymentType = _getPaymentTypeByString(data['paymentMethodType'] as String?);
final quote = Quote.fromMeldJson(data, isBuyAction, paymentType);
quote.setFiatCurrency = fiatCurrency;
quote.setCryptoCurrency = cryptoCurrency;
return [quote];
} else {
return null;
}
} catch (e) {
print('Error fetching buy quote: $e');
return null;
}
}
Future<void>? launchProvider(
{required BuildContext context,
required Quote quote,
required double amount,
required bool isBuyAction,
required String cryptoCurrencyAddress,
String? countryCode}) async {
final actionType = isBuyAction ? 'BUY' : 'SELL';
final params = {
'publicKey': _isProduction ? '' : _testPublicKey,
'countryCode': countryCode,
//'paymentMethodType': normalizePaymentMethod(paymentMethod.paymentMethodType),
'sourceAmount': amount.toString(),
'sourceCurrencyCode': quote.fiatCurrency,
'destinationCurrencyCode': quote.cryptoCurrency,
'walletAddress': cryptoCurrencyAddress,
'transactionType': actionType
};
final uri = Uri.https(_baseWidgetUrl, '', params);
try {
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
throw Exception('Could not launch URL');
}
} catch (e) {
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: "Meld",
alertContent: S.of(context).buy_provider_unavailable + ': $e',
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
}
}
String? normalizePaymentMethod(PaymentType paymentType) {
switch (paymentType) {
case PaymentType.creditCard:
return 'CREDIT_DEBIT_CARD';
case PaymentType.applePay:
return 'APPLE_PAY';
case PaymentType.googlePay:
return 'GOOGLE_PAY';
case PaymentType.neteller:
return 'NETELLER';
case PaymentType.skrill:
return 'SKRILL';
case PaymentType.sepa:
return 'SEPA';
case PaymentType.sepaInstant:
return 'SEPA_INSTANT';
case PaymentType.ach:
return 'ACH';
case PaymentType.achInstant:
return 'INSTANT_ACH';
case PaymentType.Khipu:
return 'KHIPU';
case PaymentType.ovo:
return 'OVO';
case PaymentType.zaloPay:
return 'ZALOPAY';
case PaymentType.zaloBankTransfer:
return 'ZA_BANK_TRANSFER';
case PaymentType.gcash:
return 'GCASH';
case PaymentType.imps:
return 'IMPS';
case PaymentType.dana:
return 'DANA';
case PaymentType.ideal:
return 'IDEAL';
default:
return null;
}
}
PaymentType _getPaymentTypeByString(String? paymentMethod) {
switch (paymentMethod?.toUpperCase()) {
case 'CREDIT_DEBIT_CARD':
return PaymentType.creditCard;
case 'APPLE_PAY':
return PaymentType.applePay;
case 'GOOGLE_PAY':
return PaymentType.googlePay;
case 'NETELLER':
return PaymentType.neteller;
case 'SKRILL':
return PaymentType.skrill;
case 'SEPA':
return PaymentType.sepa;
case 'SEPA_INSTANT':
return PaymentType.sepaInstant;
case 'ACH':
return PaymentType.ach;
case 'INSTANT_ACH':
return PaymentType.achInstant;
case 'KHIPU':
return PaymentType.Khipu;
case 'OVO':
return PaymentType.ovo;
case 'ZALOPAY':
return PaymentType.zaloPay;
case 'ZA_BANK_TRANSFER':
return PaymentType.zaloBankTransfer;
case 'GCASH':
return PaymentType.gcash;
case 'IMPS':
return PaymentType.imps;
case 'DANA':
return PaymentType.dana;
case 'IDEAL':
return PaymentType.ideal;
default:
return PaymentType.all;
}
}
}

View file

@ -1,19 +1,20 @@
import 'dart:convert';
import 'dart:developer';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/buy/buy_amount.dart';
import 'package:cake_wallet/buy/buy_exception.dart';
import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/buy_provider_description.dart';
import 'package:cake_wallet/buy/buy_quote.dart';
import 'package:cake_wallet/buy/order.dart';
import 'package:cake_wallet/buy/payment_method.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
@ -39,6 +40,15 @@ class MoonPayProvider extends BuyProvider {
static const _baseBuyProductUrl = 'buy.moonpay.com';
static const _cIdBaseUrl = 'exchange-helper.cakewallet.com';
static const _apiUrl = 'https://api.moonpay.com';
static const _baseUrl = 'api.moonpay.com';
static const _currenciesPath = '/v3/currencies';
static const _buyQuote = '/buy_quote';
static const _sellQuote = '/sell_quote';
static const _transactionsSuffix = '/v1/transactions';
final String baseBuyUrl;
final String baseSellUrl;
@override
String get providerDescription =>
@ -53,6 +63,17 @@ class MoonPayProvider extends BuyProvider {
@override
String get darkIcon => 'assets/images/moonpay_dark.png';
@override
bool get isAggregator => false;
static String get _apiKey => secrets.moonPayApiKey;
String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase();
String get trackUrl => baseBuyUrl + '/transaction_receipt?transactionId=';
static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey;
static String themeToMoonPayTheme(ThemeBase theme) {
switch (theme.type) {
case ThemeType.bright:
@ -63,28 +84,12 @@ class MoonPayProvider extends BuyProvider {
}
}
static String get _apiKey => secrets.moonPayApiKey;
final String baseBuyUrl;
final String baseSellUrl;
String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase();
String get trackUrl => baseBuyUrl + '/transaction_receipt?transactionId=';
static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey;
Future<String> getMoonpaySignature(String query) async {
final uri = Uri.https(_cIdBaseUrl, "/api/moonpay");
final response = await post(
uri,
headers: {
'Content-Type': 'application/json',
'x-api-key': _exchangeHelperApiKey,
},
body: json.encode({'query': query}),
);
final response = await post(uri,
headers: {'Content-Type': 'application/json', 'x-api-key': _exchangeHelperApiKey},
body: json.encode({'query': query}));
if (response.statusCode == 200) {
return (jsonDecode(response.body) as Map<String, dynamic>)['signature'] as String;
@ -94,85 +99,195 @@ class MoonPayProvider extends BuyProvider {
}
}
Future<Uri> requestSellMoonPayUrl({
required CryptoCurrency currency,
required String refundWalletAddress,
required SettingsStore settingsStore,
}) async {
final params = {
'theme': themeToMoonPayTheme(settingsStore.currentTheme),
'language': settingsStore.languageCode,
'colorCode': settingsStore.currentTheme.type == ThemeType.dark
? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}'
: '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}',
'defaultCurrencyCode': _normalizeCurrency(currency),
'refundWalletAddress': refundWalletAddress,
};
Future<Map<String, dynamic>> fetchFiatCredentials(
String fiatCurrency, String cryptocurrency, String? paymentMethod) async {
final params = {'baseCurrencyCode': fiatCurrency.toLowerCase(), 'apiKey': _apiKey};
if (_apiKey.isNotEmpty) {
params['apiKey'] = _apiKey;
if (paymentMethod != null) params['paymentMethod'] = paymentMethod;
final path = '$_currenciesPath/${cryptocurrency.toLowerCase()}/limits';
final url = Uri.https(_baseUrl, path, params);
try {
final response = await get(url, headers: {'accept': 'application/json'});
if (response.statusCode == 200) {
return jsonDecode(response.body) as Map<String, dynamic>;
} else {
print('MoonPay does not support fiat: $fiatCurrency');
return {};
}
} catch (e) {
print('MoonPay Error fetching fiat currencies: $e');
return {};
}
final originalUri = Uri.https(
baseSellUrl,
'',
params,
);
if (isTestEnvironment) {
return originalUri;
}
final signature = await getMoonpaySignature('?${originalUri.query}');
final query = Map<String, dynamic>.from(originalUri.queryParameters);
query['signature'] = signature;
final signedUri = originalUri.replace(queryParameters: query);
return signedUri;
}
// BUY:
static const _currenciesSuffix = '/v3/currencies';
static const _quoteSuffix = '/buy_quote';
static const _transactionsSuffix = '/v1/transactions';
static const _ipAddressSuffix = '/v4/ip_address';
Future<List<PaymentMethod>> getAvailablePaymentTypes(
String fiatCurrency, String cryptoCurrency, bool isBuyAction) async {
final List<PaymentMethod> paymentMethods = [];
if (isBuyAction) {
final fiatBuyCredentials = await fetchFiatCredentials(fiatCurrency, cryptoCurrency, null);
if (fiatBuyCredentials.isNotEmpty) {
final paymentMethod = fiatBuyCredentials['paymentMethod'] as String?;
paymentMethods.add(PaymentMethod.fromMoonPayJson(
fiatBuyCredentials, _getPaymentTypeByString(paymentMethod)));
return paymentMethods;
}
}
return paymentMethods;
}
@override
Future<List<Quote>?> fetchQuote(
{required CryptoCurrency cryptoCurrency,
required FiatCurrency fiatCurrency,
required double amount,
required bool isBuyAction,
required String walletAddress,
PaymentType? paymentType,
String? countryCode}) async {
String? paymentMethod;
if (paymentType != null && paymentType != PaymentType.all) {
paymentMethod = normalizePaymentMethod(paymentType);
if (paymentMethod == null) paymentMethod = paymentType.name;
} else {
paymentMethod = 'credit_debit_card';
}
final action = isBuyAction ? 'buy' : 'sell';
final formattedCryptoCurrency = _normalizeCurrency(cryptoCurrency);
final baseCurrencyCode =
isBuyAction ? fiatCurrency.name.toLowerCase() : cryptoCurrency.title.toLowerCase();
Future<Uri> requestBuyMoonPayUrl({
required CryptoCurrency currency,
required SettingsStore settingsStore,
required String walletAddress,
String? amount,
}) async {
final params = {
'theme': themeToMoonPayTheme(settingsStore.currentTheme),
'language': settingsStore.languageCode,
'colorCode': settingsStore.currentTheme.type == ThemeType.dark
'baseCurrencyCode': baseCurrencyCode,
'baseCurrencyAmount': amount.toString(),
'amount': amount.toString(),
'paymentMethod': paymentMethod,
'areFeesIncluded': 'false',
'apiKey': _apiKey
};
log('MoonPay: Fetching $action quote: ${isBuyAction ? formattedCryptoCurrency : fiatCurrency.name.toLowerCase()} -> ${isBuyAction ? baseCurrencyCode : formattedCryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod');
final quotePath = isBuyAction ? _buyQuote : _sellQuote;
final path = '$_currenciesPath/$formattedCryptoCurrency$quotePath';
final url = Uri.https(_baseUrl, path, params);
try {
final response = await get(url);
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as Map<String, dynamic>;
// Check if the response is for the correct fiat currency
if (isBuyAction) {
final fiatCurrencyCode = data['baseCurrencyCode'] as String?;
if (fiatCurrencyCode == null || fiatCurrencyCode != fiatCurrency.name.toLowerCase())
return null;
} else {
final quoteCurrency = data['quoteCurrency'] as Map<String, dynamic>?;
if (quoteCurrency == null || quoteCurrency['code'] != fiatCurrency.name.toLowerCase())
return null;
}
final paymentMethods = data['paymentMethod'] as String?;
final quote =
Quote.fromMoonPayJson(data, isBuyAction, _getPaymentTypeByString(paymentMethods));
quote.setFiatCurrency = fiatCurrency;
quote.setCryptoCurrency = cryptoCurrency;
return [quote];
} else {
print('Moon Pay: Error fetching buy quote: ');
return null;
}
} catch (e) {
print('Moon Pay: Error fetching buy quote: $e');
return null;
}
}
@override
Future<void>? launchProvider(
{required BuildContext context,
required Quote quote,
required double amount,
required bool isBuyAction,
required String cryptoCurrencyAddress,
String? countryCode}) async {
final Map<String, String> params = {
'theme': themeToMoonPayTheme(_settingsStore.currentTheme),
'language': _settingsStore.languageCode,
'colorCode': _settingsStore.currentTheme.type == ThemeType.dark
? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}'
: '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}',
'baseCurrencyCode': settingsStore.fiatCurrency.title,
'baseCurrencyAmount': amount ?? '0',
'currencyCode': _normalizeCurrency(currency),
'walletAddress': walletAddress,
'baseCurrencyCode': isBuyAction ? quote.fiatCurrency.name : quote.cryptoCurrency.name,
'baseCurrencyAmount': amount.toString(),
'walletAddress': cryptoCurrencyAddress,
'lockAmount': 'false',
'showAllCurrencies': 'false',
'showWalletAddressForm': 'false',
'enabledPaymentMethods':
'credit_debit_card,apple_pay,google_pay,samsung_pay,sepa_bank_transfer,gbp_bank_transfer,gbp_open_banking_payment',
if (isBuyAction)
'enabledPaymentMethods': normalizePaymentMethod(quote.paymentType) ??
'credit_debit_card,apple_pay,google_pay,samsung_pay,sepa_bank_transfer,gbp_bank_transfer,gbp_open_banking_payment',
if (!isBuyAction) 'refundWalletAddress': cryptoCurrencyAddress
};
if (_apiKey.isNotEmpty) {
params['apiKey'] = _apiKey;
}
if (isBuyAction) params['currencyCode'] = quote.cryptoCurrency.name;
if (!isBuyAction) params['quoteCurrencyCode'] = quote.cryptoCurrency.name;
final originalUri = Uri.https(
baseBuyUrl,
'',
params,
);
try {
{
final uri = await requestMoonPayUrl(
walletAddress: cryptoCurrencyAddress,
settingsStore: _settingsStore,
isBuyAction: isBuyAction,
amount: amount.toString(),
params: params);
if (isTestEnvironment) {
return originalUri;
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
throw Exception('Could not launch URL');
}
}
} catch (e) {
if (context.mounted) {
await showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: 'MoonPay',
alertContent: 'The MoonPay service is currently unavailable: $e',
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
);
},
);
}
}
}
Future<Uri> requestMoonPayUrl({
required String walletAddress,
required SettingsStore settingsStore,
required bool isBuyAction,
required Map<String, String> params,
String? amount,
}) async {
if (_apiKey.isNotEmpty) params['apiKey'] = _apiKey;
final baseUrl = isBuyAction ? baseBuyUrl : baseSellUrl;
final originalUri = Uri.https(baseUrl, '', params);
if (isTestEnvironment) return originalUri;
final signature = await getMoonpaySignature('?${originalUri.query}');
final query = Map<String, dynamic>.from(originalUri.queryParameters);
@ -181,33 +296,6 @@ class MoonPayProvider extends BuyProvider {
return signedUri;
}
Future<BuyAmount> calculateAmount(String amount, String sourceCurrency) async {
final url = _apiUrl +
_currenciesSuffix +
'/$currencyCode' +
_quoteSuffix +
'/?apiKey=' +
_apiKey +
'&baseCurrencyAmount=' +
amount +
'&baseCurrencyCode=' +
sourceCurrency.toLowerCase();
final uri = Uri.parse(url);
final response = await get(uri);
if (response.statusCode != 200) {
throw BuyException(title: providerDescription, content: 'Quote is not found!');
}
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final sourceAmount = responseJSON['totalAmount'] as double;
final destAmount = responseJSON['quoteCurrencyAmount'] as double;
final minSourceAmount = responseJSON['baseCurrency']['minAmount'] as int;
return BuyAmount(
sourceAmount: sourceAmount, destAmount: destAmount, minAmount: minSourceAmount);
}
Future<Order> findOrderById(String id) async {
final url = _apiUrl + _transactionsSuffix + '/$id' + '?apiKey=' + _apiKey;
final uri = Uri.parse(url);
@ -235,74 +323,83 @@ class MoonPayProvider extends BuyProvider {
walletId: wallet.id);
}
static Future<bool> onEnabled() async {
final url = _apiUrl + _ipAddressSuffix + '?apiKey=' + _apiKey;
var isBuyEnable = false;
final uri = Uri.parse(url);
final response = await get(uri);
try {
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
isBuyEnable = responseJSON['isBuyAllowed'] as bool;
} catch (e) {
isBuyEnable = false;
print(e.toString());
}
return isBuyEnable;
}
@override
Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
try {
late final Uri uri;
if (isBuyAction ?? true) {
uri = await requestBuyMoonPayUrl(
currency: wallet.currency,
walletAddress: wallet.walletAddresses.address,
settingsStore: _settingsStore,
);
} else {
uri = await requestSellMoonPayUrl(
currency: wallet.currency,
refundWalletAddress: wallet.walletAddresses.address,
settingsStore: _settingsStore,
);
}
if (await canLaunchUrl(uri)) {
if (DeviceInfo.instance.isMobile) {
Navigator.of(context).pushNamed(Routes.webViewPage, arguments: ['MoonPay', uri]);
} else {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
} else {
throw Exception('Could not launch URL');
}
} catch (e) {
if (context.mounted) {
await showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: 'MoonPay',
alertContent: 'The MoonPay service is currently unavailable: $e',
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
);
},
);
}
}
}
String _normalizeCurrency(CryptoCurrency currency) {
if (currency == CryptoCurrency.maticpoly) {
return "POL_POLYGON";
} else if (currency == CryptoCurrency.matic) {
return "POL";
if (currency.tag == 'POLY') {
return '${currency.title.toLowerCase()}_polygon';
}
if (currency.tag == 'TRX') {
return '${currency.title.toLowerCase()}_trx';
}
return currency.toString().toLowerCase();
}
String? normalizePaymentMethod(PaymentType paymentMethod) {
switch (paymentMethod) {
case PaymentType.creditCard:
return 'credit_debit_card';
case PaymentType.debitCard:
return 'credit_debit_card';
case PaymentType.ach:
return 'ach_bank_transfer';
case PaymentType.applePay:
return 'apple_pay';
case PaymentType.googlePay:
return 'google_pay';
case PaymentType.sepa:
return 'sepa_bank_transfer';
case PaymentType.paypal:
return 'paypal';
case PaymentType.sepaOpenBankingPayment:
return 'sepa_open_banking_payment';
case PaymentType.gbpOpenBankingPayment:
return 'gbp_open_banking_payment';
case PaymentType.lowCostAch:
return 'low_cost_ach';
case PaymentType.mobileWallet:
return 'mobile_wallet';
case PaymentType.pixInstantPayment:
return 'pix_instant_payment';
case PaymentType.yellowCardBankTransfer:
return 'yellow_card_bank_transfer';
case PaymentType.fiatBalance:
return 'fiat_balance';
default:
return null;
}
}
PaymentType _getPaymentTypeByString(String? paymentMethod) {
switch (paymentMethod) {
case 'ach_bank_transfer':
return PaymentType.ach;
case 'apple_pay':
return PaymentType.applePay;
case 'credit_debit_card':
return PaymentType.creditCard;
case 'fiat_balance':
return PaymentType.fiatBalance;
case 'gbp_open_banking_payment':
return PaymentType.gbpOpenBankingPayment;
case 'google_pay':
return PaymentType.googlePay;
case 'low_cost_ach':
return PaymentType.lowCostAch;
case 'mobile_wallet':
return PaymentType.mobileWallet;
case 'paypal':
return PaymentType.paypal;
case 'pix_instant_payment':
return PaymentType.pixInstantPayment;
case 'sepa_bank_transfer':
return PaymentType.sepa;
case 'sepa_open_banking_payment':
return PaymentType.sepaOpenBankingPayment;
case 'yellow_card_bank_transfer':
return PaymentType.yellowCardBankTransfer;
default:
return PaymentType.all;
}
}
}

View file

@ -1,13 +1,19 @@
import 'dart:convert';
import 'dart:developer';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/buy_quote.dart';
import 'package:cake_wallet/buy/payment_method.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart';
class OnRamperBuyProvider extends BuyProvider {
@ -16,9 +22,15 @@ class OnRamperBuyProvider extends BuyProvider {
: super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: null);
static const _baseUrl = 'buy.onramper.com';
static const _baseApiUrl = 'api.onramper.com';
static const quotes = '/quotes';
static const paymentTypes = '/payment-types';
static const supported = '/supported';
final SettingsStore _settingsStore;
String get _apiKey => secrets.onramperApiKey;
@override
String get title => 'Onramper';
@ -31,74 +43,327 @@ class OnRamperBuyProvider extends BuyProvider {
@override
String get darkIcon => 'assets/images/onramper_dark.png';
String get _apiKey => secrets.onramperApiKey;
@override
bool get isAggregator => true;
String get _normalizeCryptoCurrency {
switch (wallet.currency) {
case CryptoCurrency.ltc:
return "LTC_LITECOIN";
case CryptoCurrency.xmr:
return "XMR_MONERO";
case CryptoCurrency.bch:
return "BCH_BITCOINCASH";
case CryptoCurrency.nano:
return "XNO_NANO";
default:
return wallet.currency.title;
Future<List<PaymentMethod>> getAvailablePaymentTypes(
String fiatCurrency, String cryptoCurrency, bool isBuyAction) async {
final params = {
'fiatCurrency': fiatCurrency,
'type': isBuyAction ? 'buy' : 'sell',
'isRecurringPayment': 'false'
};
final url = Uri.https(_baseApiUrl, '$supported$paymentTypes/$fiatCurrency', params);
try {
final response =
await http.get(url, headers: {'Authorization': _apiKey, 'accept': 'application/json'});
if (response.statusCode == 200) {
final Map<String, dynamic> data = jsonDecode(response.body) as Map<String, dynamic>;
final List<dynamic> message = data['message'] as List<dynamic>;
return message
.map((item) => PaymentMethod.fromOnramperJson(item as Map<String, dynamic>))
.toList();
} else {
print('Failed to fetch available payment types');
return [];
}
} catch (e) {
print('Failed to fetch available payment types: $e');
return [];
}
}
String getColorStr(Color color) {
return color.value.toRadixString(16).replaceAll(RegExp(r'^ff'), "");
Future<Map<String, dynamic>> getOnrampMetadata() async {
final url = Uri.https(_baseApiUrl, '$supported/onramps/all');
try {
final response =
await http.get(url, headers: {'Authorization': _apiKey, 'accept': 'application/json'});
if (response.statusCode == 200) {
final Map<String, dynamic> data = jsonDecode(response.body) as Map<String, dynamic>;
final List<dynamic> onramps = data['message'] as List<dynamic>;
final Map<String, dynamic> result = {
for (var onramp in onramps)
(onramp['id'] as String): {
'displayName': onramp['displayName'] as String,
'svg': onramp['icons']['svg'] as String
}
};
return result;
} else {
print('Failed to fetch onramp metadata');
return {};
}
} catch (e) {
print('Error occurred: $e');
return {};
}
}
Uri requestOnramperUrl(BuildContext context, bool? isBuyAction) {
String primaryColor,
secondaryColor,
primaryTextColor,
secondaryTextColor,
containerColor,
cardColor;
@override
Future<List<Quote>?> fetchQuote(
{required CryptoCurrency cryptoCurrency,
required FiatCurrency fiatCurrency,
required double amount,
required bool isBuyAction,
required String walletAddress,
PaymentType? paymentType,
String? countryCode}) async {
String? paymentMethod;
primaryColor = getColorStr(Theme.of(context).primaryColor);
secondaryColor = getColorStr(Theme.of(context).colorScheme.background);
primaryTextColor =
getColorStr(Theme.of(context).extension<CakeTextTheme>()!.titleColor);
secondaryTextColor = getColorStr(
Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor);
containerColor = getColorStr(Theme.of(context).colorScheme.background);
cardColor = getColorStr(Theme.of(context).cardColor);
if (paymentType != null && paymentType != PaymentType.all) {
paymentMethod = normalizePaymentMethod(paymentType);
if (paymentMethod == null) paymentMethod = paymentType.name;
}
final actionType = isBuyAction ? 'buy' : 'sell';
final normalizedCryptoCurrency = _getNormalizeCryptoCurrency(cryptoCurrency);
final params = {
'amount': amount.toString(),
if (paymentMethod != null) 'paymentMethod': paymentMethod,
'clientName': 'CakeWallet',
'type': actionType,
'walletAddress': walletAddress,
'isRecurringPayment': 'false',
'input': 'source',
};
log('Onramper: Fetching $actionType quote: ${isBuyAction ? normalizedCryptoCurrency : fiatCurrency.name} -> ${isBuyAction ? fiatCurrency.name : normalizedCryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod');
final sourceCurrency = isBuyAction ? fiatCurrency.name : normalizedCryptoCurrency;
final destinationCurrency = isBuyAction ? normalizedCryptoCurrency : fiatCurrency.name;
final url = Uri.https(_baseApiUrl, '$quotes/${sourceCurrency}/${destinationCurrency}', params);
final headers = {'Authorization': _apiKey, 'accept': 'application/json'};
try {
final response = await http.get(url, headers: headers);
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as List<dynamic>;
if (data.isEmpty) return null;
List<Quote> validQuotes = [];
final onrampMetadata = await getOnrampMetadata();
for (var item in data) {
if (item['errors'] != null) continue;
final paymentMethod = (item as Map<String, dynamic>)['paymentMethod'] as String;
final rampId = item['ramp'] as String?;
final rampMetaData = onrampMetadata[rampId] as Map<String, dynamic>?;
if (rampMetaData == null) continue;
final quote = Quote.fromOnramperJson(
item, isBuyAction, onrampMetadata, _getPaymentTypeByString(paymentMethod));
quote.setFiatCurrency = fiatCurrency;
quote.setCryptoCurrency = cryptoCurrency;
validQuotes.add(quote);
}
if (validQuotes.isEmpty) return null;
return validQuotes;
} else {
print('Onramper: Failed to fetch rate');
return null;
}
} catch (e) {
print('Onramper: Failed to fetch rate $e');
return null;
}
}
Future<void>? launchProvider(
{required BuildContext context,
required Quote quote,
required double amount,
required bool isBuyAction,
required String cryptoCurrencyAddress,
String? countryCode}) async {
final actionType = isBuyAction ? 'buy' : 'sell';
final prefix = actionType == 'sell' ? actionType + '_' : '';
final primaryColor = getColorStr(Theme.of(context).primaryColor);
final secondaryColor = getColorStr(Theme.of(context).colorScheme.background);
final primaryTextColor = getColorStr(Theme.of(context).extension<CakeTextTheme>()!.titleColor);
final secondaryTextColor =
getColorStr(Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor);
final containerColor = getColorStr(Theme.of(context).colorScheme.background);
var cardColor = getColorStr(Theme.of(context).cardColor);
if (_settingsStore.currentTheme.title == S.current.high_contrast_theme) {
cardColor = getColorStr(Colors.white);
}
final networkName =
wallet.currency.fullName?.toUpperCase().replaceAll(" ", "");
final defaultCrypto = _getNormalizeCryptoCurrency(quote.cryptoCurrency);
return Uri.https(_baseUrl, '', <String, dynamic>{
final paymentMethod = normalizePaymentMethod(quote.paymentType);
final uri = Uri.https(_baseUrl, '', {
'apiKey': _apiKey,
'defaultCrypto': _normalizeCryptoCurrency,
'sell_defaultCrypto': _normalizeCryptoCurrency,
'networkWallets': '${networkName}:${wallet.walletAddresses.address}',
'mode': actionType,
'${prefix}defaultFiat': quote.fiatCurrency.name,
'${prefix}defaultCrypto': quote.cryptoCurrency.name,
'${prefix}defaultAmount': amount.toString(),
if (paymentMethod != null) '${prefix}defaultPaymentMethod': paymentMethod,
'onlyOnramps': quote.rampId,
'networkWallets': '$defaultCrypto:$cryptoCurrencyAddress',
'walletAddress': cryptoCurrencyAddress,
'supportSwap': "false",
'primaryColor': primaryColor,
'secondaryColor': secondaryColor,
'containerColor': containerColor,
'primaryTextColor': primaryTextColor,
'secondaryTextColor': secondaryTextColor,
'containerColor': containerColor,
'cardColor': cardColor,
'mode': isBuyAction == true ? 'buy' : 'sell',
});
}
Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
final uri = requestOnramperUrl(context, isBuyAction);
if (DeviceInfo.instance.isMobile) {
Navigator.of(context)
.pushNamed(Routes.webViewPage, arguments: [title, uri]);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
await launchUrl(uri);
throw Exception('Could not launch URL');
}
}
List<CryptoCurrency> mainCurrency = [
CryptoCurrency.btc,
CryptoCurrency.eth,
CryptoCurrency.sol,
];
String _tagToNetwork(String tag) {
switch (tag) {
case 'OMNI':
return tag;
case 'POL':
return 'POLYGON';
default:
return CryptoCurrency.fromString(tag).fullName ?? tag;
}
}
String _getNormalizeCryptoCurrency(Currency currency) {
if (currency is CryptoCurrency) {
if (!mainCurrency.contains(currency)) {
final network = currency.tag == null ? currency.fullName : _tagToNetwork(currency.tag!);
return '${currency.title}_${network?.replaceAll(' ', '')}'.toUpperCase();
}
return currency.title.toUpperCase();
}
return currency.name.toUpperCase();
}
String? normalizePaymentMethod(PaymentType paymentType) {
switch (paymentType) {
case PaymentType.bankTransfer:
return 'banktransfer';
case PaymentType.creditCard:
return 'creditcard';
case PaymentType.debitCard:
return 'debitcard';
case PaymentType.applePay:
return 'applepay';
case PaymentType.googlePay:
return 'googlepay';
case PaymentType.revolutPay:
return 'revolutpay';
case PaymentType.neteller:
return 'neteller';
case PaymentType.skrill:
return 'skrill';
case PaymentType.sepa:
return 'sepabanktransfer';
case PaymentType.sepaInstant:
return 'sepainstant';
case PaymentType.ach:
return 'ach';
case PaymentType.achInstant:
return 'iach';
case PaymentType.Khipu:
return 'khipu';
case PaymentType.palomaBanktTansfer:
return 'palomabanktransfer';
case PaymentType.ovo:
return 'ovo';
case PaymentType.zaloPay:
return 'zalopay';
case PaymentType.zaloBankTransfer:
return 'zalobanktransfer';
case PaymentType.gcash:
return 'gcash';
case PaymentType.imps:
return 'imps';
case PaymentType.dana:
return 'dana';
case PaymentType.ideal:
return 'ideal';
default:
return null;
}
}
PaymentType _getPaymentTypeByString(String paymentMethod) {
switch (paymentMethod.toLowerCase()) {
case 'banktransfer':
return PaymentType.bankTransfer;
case 'creditcard':
return PaymentType.creditCard;
case 'debitcard':
return PaymentType.debitCard;
case 'applepay':
return PaymentType.applePay;
case 'googlepay':
return PaymentType.googlePay;
case 'revolutpay':
return PaymentType.revolutPay;
case 'neteller':
return PaymentType.neteller;
case 'skrill':
return PaymentType.skrill;
case 'sepabanktransfer':
return PaymentType.sepa;
case 'sepainstant':
return PaymentType.sepaInstant;
case 'ach':
return PaymentType.ach;
case 'iach':
return PaymentType.achInstant;
case 'khipu':
return PaymentType.Khipu;
case 'palomabanktransfer':
return PaymentType.palomaBanktTansfer;
case 'ovo':
return PaymentType.ovo;
case 'zalopay':
return PaymentType.zaloPay;
case 'zalobanktransfer':
return PaymentType.zaloBankTransfer;
case 'gcash':
return PaymentType.gcash;
case 'imps':
return PaymentType.imps;
case 'dana':
return PaymentType.dana;
case 'ideal':
return PaymentType.ideal;
default:
return PaymentType.all;
}
}
String getColorStr(Color color) => color.value.toRadixString(16).replaceAll(RegExp(r'^ff'), "");
}

287
lib/buy/payment_method.dart Normal file
View file

@ -0,0 +1,287 @@
import 'dart:ui';
import 'package:cake_wallet/core/selectable_option.dart';
enum PaymentType {
all,
bankTransfer,
creditCard,
debitCard,
applePay,
googlePay,
revolutPay,
neteller,
skrill,
sepa,
sepaInstant,
ach,
achInstant,
Khipu,
palomaBanktTansfer,
ovo,
zaloPay,
zaloBankTransfer,
gcash,
imps,
dana,
ideal,
paypal,
sepaOpenBankingPayment,
gbpOpenBankingPayment,
lowCostAch,
mobileWallet,
pixInstantPayment,
yellowCardBankTransfer,
fiatBalance,
bancontact,
}
extension PaymentTypeTitle on PaymentType {
String? get title {
switch (this) {
case PaymentType.all:
return 'All Payment Methods';
case PaymentType.bankTransfer:
return 'Bank Transfer';
case PaymentType.creditCard:
return 'Credit Card';
case PaymentType.debitCard:
return 'Debit Card';
case PaymentType.applePay:
return 'Apple Pay';
case PaymentType.googlePay:
return 'Google Pay';
case PaymentType.revolutPay:
return 'Revolut Pay';
case PaymentType.neteller:
return 'Neteller';
case PaymentType.skrill:
return 'Skrill';
case PaymentType.sepa:
return 'SEPA';
case PaymentType.sepaInstant:
return 'SEPA Instant';
case PaymentType.ach:
return 'ACH';
case PaymentType.achInstant:
return 'ACH Instant';
case PaymentType.Khipu:
return 'Khipu';
case PaymentType.palomaBanktTansfer:
return 'Paloma Bank Transfer';
case PaymentType.ovo:
return 'OVO';
case PaymentType.zaloPay:
return 'Zalo Pay';
case PaymentType.zaloBankTransfer:
return 'Zalo Bank Transfer';
case PaymentType.gcash:
return 'GCash';
case PaymentType.imps:
return 'IMPS';
case PaymentType.dana:
return 'DANA';
case PaymentType.ideal:
return 'iDEAL';
case PaymentType.paypal:
return 'PayPal';
case PaymentType.sepaOpenBankingPayment:
return 'SEPA Open Banking Payment';
case PaymentType.gbpOpenBankingPayment:
return 'GBP Open Banking Payment';
case PaymentType.lowCostAch:
return 'Low Cost ACH';
case PaymentType.mobileWallet:
return 'Mobile Wallet';
case PaymentType.pixInstantPayment:
return 'PIX Instant Payment';
case PaymentType.yellowCardBankTransfer:
return 'Yellow Card Bank Transfer';
case PaymentType.fiatBalance:
return 'Fiat Balance';
case PaymentType.bancontact:
return 'Bancontact';
default:
return null;
}
}
String? get lightIconPath {
switch (this) {
case PaymentType.all:
return 'assets/images/usd_round_light.svg';
case PaymentType.creditCard:
case PaymentType.debitCard:
case PaymentType.yellowCardBankTransfer:
return 'assets/images/card.svg';
case PaymentType.bankTransfer:
return 'assets/images/bank_light.svg';
case PaymentType.skrill:
return 'assets/images/skrill.svg';
case PaymentType.applePay:
return 'assets/images/apple_pay_round_light.svg';
default:
return null;
}
}
String? get darkIconPath {
switch (this) {
case PaymentType.all:
return 'assets/images/usd_round_dark.svg';
case PaymentType.creditCard:
case PaymentType.debitCard:
case PaymentType.yellowCardBankTransfer:
return 'assets/images/card_dark.svg';
case PaymentType.bankTransfer:
return 'assets/images/bank_dark.svg';
case PaymentType.skrill:
return 'assets/images/skrill.svg';
case PaymentType.applePay:
return 'assets/images/apple_pay_round_dark.svg';
default:
return null;
}
}
String? get description {
switch (this) {
default:
return null;
}
}
}
class PaymentMethod extends SelectableOption {
PaymentMethod({
required this.paymentMethodType,
required this.customTitle,
required this.customIconPath,
this.customDescription,
}) : super(title: paymentMethodType.title ?? customTitle);
final PaymentType paymentMethodType;
final String customTitle;
final String customIconPath;
final String? customDescription;
bool isSelected = false;
@override
String? get description => paymentMethodType.description ?? customDescription;
@override
String get lightIconPath => paymentMethodType.lightIconPath ?? customIconPath;
@override
String get darkIconPath => paymentMethodType.darkIconPath ?? customIconPath;
@override
bool get isOptionSelected => isSelected;
factory PaymentMethod.all() {
return PaymentMethod(
paymentMethodType: PaymentType.all,
customTitle: 'All Payment Methods',
customIconPath: 'assets/images/dollar_coin.svg');
}
factory PaymentMethod.fromOnramperJson(Map<String, dynamic> json) {
final type = PaymentMethod.getPaymentTypeId(json['paymentTypeId'] as String?);
return PaymentMethod(
paymentMethodType: type,
customTitle: json['name'] as String? ?? 'Unknown',
customIconPath: json['icon'] as String? ?? 'assets/images/card.png',
customDescription: json['description'] as String?);
}
factory PaymentMethod.fromDFX(String paymentMethod, PaymentType paymentType) {
return PaymentMethod(
paymentMethodType: paymentType,
customTitle: paymentMethod,
customIconPath: 'assets/images/card.png');
}
factory PaymentMethod.fromMoonPayJson(Map<String, dynamic> json, PaymentType paymentType) {
return PaymentMethod(
paymentMethodType: paymentType,
customTitle: json['paymentMethod'] as String,
customIconPath: 'assets/images/card.png');
}
factory PaymentMethod.fromMeldJson(Map<String, dynamic> json) {
final type = PaymentMethod.getPaymentTypeId(json['paymentMethod'] as String?);
final logos = json['logos'] as Map<String, dynamic>;
return PaymentMethod(
paymentMethodType: type,
customTitle: json['name'] as String? ?? 'Unknown',
customIconPath: logos['dark'] as String? ?? 'assets/images/card.png',
customDescription: json['description'] as String?);
}
static PaymentType getPaymentTypeId(String? type) {
switch (type?.toLowerCase()) {
case 'banktransfer':
case 'bank':
case 'yellow_card_bank_transfer':
return PaymentType.bankTransfer;
case 'creditcard':
case 'card':
case 'credit_debit_card':
return PaymentType.creditCard;
case 'debitcard':
return PaymentType.debitCard;
case 'applepay':
case 'apple_pay':
return PaymentType.applePay;
case 'googlepay':
case 'google_pay':
return PaymentType.googlePay;
case 'revolutpay':
return PaymentType.revolutPay;
case 'neteller':
return PaymentType.neteller;
case 'skrill':
return PaymentType.skrill;
case 'sepabanktransfer':
case 'sepa':
case 'sepa_bank_transfer':
return PaymentType.sepa;
case 'sepainstant':
case 'sepa_instant':
return PaymentType.sepaInstant;
case 'ach':
case 'ach_bank_transfer':
return PaymentType.ach;
case 'iach':
case 'instant_ach':
return PaymentType.achInstant;
case 'khipu':
return PaymentType.Khipu;
case 'palomabanktransfer':
return PaymentType.palomaBanktTansfer;
case 'ovo':
return PaymentType.ovo;
case 'zalopay':
return PaymentType.zaloPay;
case 'zalobanktransfer':
case 'za_bank_transfer':
return PaymentType.zaloBankTransfer;
case 'gcash':
return PaymentType.gcash;
case 'imps':
return PaymentType.imps;
case 'dana':
return PaymentType.dana;
case 'ideal':
return PaymentType.ideal;
case 'paypal':
return PaymentType.paypal;
case 'sepa_open_banking_payment':
return PaymentType.sepaOpenBankingPayment;
case 'bancontact':
return PaymentType.bancontact;
default:
return PaymentType.all;
}
}
}

View file

@ -1,13 +1,18 @@
import 'dart:convert';
import 'dart:developer';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/buy_quote.dart';
import 'package:cake_wallet/buy/payment_method.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
@ -15,7 +20,8 @@ import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart';
class RobinhoodBuyProvider extends BuyProvider {
RobinhoodBuyProvider({required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM})
RobinhoodBuyProvider(
{required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM})
: super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: ledgerVM);
static const _baseUrl = 'applink.robinhood.com';
@ -33,6 +39,9 @@ class RobinhoodBuyProvider extends BuyProvider {
@override
String get darkIcon => 'assets/images/robinhood_dark.png';
@override
bool get isAggregator => false;
String get _applicationId => secrets.robinhoodApplicationId;
String get _apiSecret => secrets.exchangeHelperApiKey;
@ -86,7 +95,13 @@ class RobinhoodBuyProvider extends BuyProvider {
});
}
Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
Future<void>? launchProvider(
{required BuildContext context,
required Quote quote,
required double amount,
required bool isBuyAction,
required String cryptoCurrencyAddress,
String? countryCode}) async {
if (wallet.isHardwareWallet) {
if (!ledgerVM!.isConnected) {
await Navigator.of(context).pushNamed(Routes.connectDevices,
@ -116,4 +131,87 @@ class RobinhoodBuyProvider extends BuyProvider {
});
}
}
@override
Future<List<Quote>?> fetchQuote(
{required CryptoCurrency cryptoCurrency,
required FiatCurrency fiatCurrency,
required double amount,
required bool isBuyAction,
required String walletAddress,
PaymentType? paymentType,
String? countryCode}) async {
String? paymentMethod;
if (paymentType != null && paymentType != PaymentType.all) {
paymentMethod = normalizePaymentMethod(paymentType);
if (paymentMethod == null) paymentMethod = paymentType.name;
}
final action = isBuyAction ? 'buy' : 'sell';
log('Robinhood: Fetching $action quote: ${isBuyAction ? cryptoCurrency.title : fiatCurrency.name.toUpperCase()} -> ${isBuyAction ? fiatCurrency.name.toUpperCase() : cryptoCurrency.title}, amount: $amount paymentMethod: $paymentMethod');
final queryParams = {
'applicationId': _applicationId,
'fiatCode': fiatCurrency.name,
'assetCode': cryptoCurrency.title,
'fiatAmount': amount.toString(),
if (paymentMethod != null) 'paymentMethod': paymentMethod,
};
final uri =
Uri.https('api.robinhood.com', '/catpay/v1/${cryptoCurrency.title}/quote/', queryParams);
try {
final response = await http.get(uri, headers: {'accept': 'application/json'});
final responseData = jsonDecode(response.body) as Map<String, dynamic>;
if (response.statusCode == 200) {
final paymentType = _getPaymentTypeByString(responseData['paymentMethod'] as String?);
final quote = Quote.fromRobinhoodJson(responseData, isBuyAction, paymentType);
quote.setFiatCurrency = fiatCurrency;
quote.setCryptoCurrency = cryptoCurrency;
return [quote];
} else {
if (responseData.containsKey('message')) {
log('Robinhood Error: ${responseData['message']}');
} else {
print('Robinhood Failed to fetch $action quote: ${response.statusCode}');
}
return null;
}
} catch (e) {
log('Robinhood: Failed to fetch $action quote: $e');
return null;
}
// buying_power
// crypto_balance
// debit_card
// bank_transfer
}
String? normalizePaymentMethod(PaymentType paymentMethod) {
switch (paymentMethod) {
case PaymentType.creditCard:
return 'debit_card';
case PaymentType.debitCard:
return 'debit_card';
case PaymentType.bankTransfer:
return 'bank_transfer';
default:
return null;
}
}
PaymentType _getPaymentTypeByString(String? paymentMethod) {
switch (paymentMethod) {
case 'debit_card':
return PaymentType.debitCard;
case 'bank_transfer':
return PaymentType.bankTransfer;
default:
return PaymentType.all;
}
}
}

View file

@ -0,0 +1,20 @@
abstract class PaymentMethodLoadingState {}
class InitialPaymentMethod extends PaymentMethodLoadingState {}
class PaymentMethodLoading extends PaymentMethodLoadingState {}
class PaymentMethodLoaded extends PaymentMethodLoadingState {}
class PaymentMethodFailed extends PaymentMethodLoadingState {}
abstract class BuySellQuotLoadingState {}
class InitialBuySellQuotState extends BuySellQuotLoadingState {}
class BuySellQuotLoading extends BuySellQuotLoadingState {}
class BuySellQuotLoaded extends BuySellQuotLoadingState {}
class BuySellQuotFailed extends BuySellQuotLoadingState {}

View file

@ -42,6 +42,9 @@ class WyreBuyProvider extends BuyProvider {
@override
String get darkIcon => 'assets/images/robinhood_dark.png';
@override
bool get isAggregator => false;
String get trackUrl => isTestEnvironment ? _trackTestUrl : _trackProductUrl;
String baseApiUrl;
@ -148,10 +151,4 @@ class WyreBuyProvider extends BuyProvider {
receiveAddress: wallet.walletAddresses.address,
walletId: wallet.id);
}
@override
Future<void> launchProvider(BuildContext context, bool? isBuyAction) {
// TODO: implement launchProvider
throw UnimplementedError();
}
}

View file

@ -268,9 +268,7 @@ class BackupService {
final currentFiatCurrency = data[PreferencesKey.currentFiatCurrencyKey] as String?;
final shouldSaveRecipientAddress = data[PreferencesKey.shouldSaveRecipientAddressKey] as bool?;
final isAppSecure = data[PreferencesKey.isAppSecureKey] as bool?;
final disableBuy = data[PreferencesKey.disableBuyKey] as bool?;
final disableSell = data[PreferencesKey.disableSellKey] as bool?;
final defaultBuyProvider = data[PreferencesKey.defaultBuyProvider] as int?;
final disableTradeOption = data[PreferencesKey.disableTradeOption] as bool?;
final currentTransactionPriorityKeyLegacy =
data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?;
final currentBitcoinElectrumSererId =
@ -323,14 +321,8 @@ class BackupService {
if (isAppSecure != null)
await _sharedPreferences.setBool(PreferencesKey.isAppSecureKey, isAppSecure);
if (disableBuy != null)
await _sharedPreferences.setBool(PreferencesKey.disableBuyKey, disableBuy);
if (disableSell != null)
await _sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell);
if (defaultBuyProvider != null)
await _sharedPreferences.setInt(PreferencesKey.defaultBuyProvider, defaultBuyProvider);
if (disableTradeOption != null)
await _sharedPreferences.setBool(PreferencesKey.disableTradeOption, disableTradeOption);
if (currentTransactionPriorityKeyLegacy != null)
await _sharedPreferences.setInt(
@ -516,10 +508,7 @@ class BackupService {
_sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey),
PreferencesKey.shouldSaveRecipientAddressKey:
_sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey),
PreferencesKey.disableBuyKey: _sharedPreferences.getBool(PreferencesKey.disableBuyKey),
PreferencesKey.disableSellKey: _sharedPreferences.getBool(PreferencesKey.disableSellKey),
PreferencesKey.defaultBuyProvider:
_sharedPreferences.getInt(PreferencesKey.defaultBuyProvider),
PreferencesKey.disableTradeOption: _sharedPreferences.getBool(PreferencesKey.disableTradeOption),
PreferencesKey.currentPinLength: _sharedPreferences.getInt(PreferencesKey.currentPinLength),
PreferencesKey.currentTransactionPriorityKeyLegacy:
_sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy),

View file

@ -0,0 +1,47 @@
abstract class SelectableItem {
SelectableItem({required this.title});
final String title;
}
class OptionTitle extends SelectableItem {
OptionTitle({required String title}) : super(title: title);
}
abstract class SelectableOption extends SelectableItem {
SelectableOption({required String title}) : super(title: title);
String get lightIconPath;
String get darkIconPath;
String? get description => null;
String? get topLeftSubTitle => null;
String? get topLeftSubTitleIconPath => null;
String? get topRightSubTitle => null;
String? get topRightSubTitleLightIconPath => null;
String? get topRightSubTitleDarkIconPath => null;
String? get bottomLeftSubTitle => null;
String? get bottomLeftSubTitleIconPath => null;
String? get bottomRightSubTitle => null;
String? get bottomRightSubTitleLightIconPath => null;
String? get bottomRightSubTitleDarkIconPath => null;
List<String> get badges => [];
bool get isOptionSelected => false;
set isOptionSelected(bool isSelected) => false;
}

View file

@ -19,6 +19,7 @@ import 'package:cake_wallet/core/backup_service.dart';
import 'package:cake_wallet/core/key_service.dart';
import 'package:cake_wallet/core/new_wallet_type_arguments.dart';
import 'package:cake_wallet/core/secure_storage.dart';
import 'package:cake_wallet/core/selectable_option.dart';
import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart';
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
@ -34,6 +35,8 @@ import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart';
import 'package:cake_wallet/entities/wallet_manager.dart';
import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart';
import 'package:cake_wallet/src/screens/buy/payment_method_options_page.dart';
import 'package:cake_wallet/src/screens/receive/address_list_page.dart';
import 'package:cake_wallet/src/screens/settings/mweb_logs_page.dart';
import 'package:cake_wallet/src/screens/settings/mweb_node_page.dart';
@ -61,7 +64,6 @@ import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dar
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
import 'package:cake_wallet/src/screens/backup/backup_page.dart';
import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart';
import 'package:cake_wallet/src/screens/buy/buy_options_page.dart';
import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart';
import 'package:cake_wallet/src/screens/buy/webview_page.dart';
import 'package:cake_wallet/src/screens/contact/contact_list_page.dart';
@ -135,6 +137,7 @@ import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart';
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart';
import 'package:cake_wallet/view_model/animated_ur_model.dart';
import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart';
import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart';
@ -248,6 +251,8 @@ import 'package:get_it/get_it.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'buy/meld/meld_buy_provider.dart';
import 'src/screens/buy/buy_sell_page.dart';
import 'cake_pay/cake_pay_payment_credantials.dart';
final getIt = GetIt.instance;
@ -1004,6 +1009,10 @@ Future<void> setup({
wallet: getIt.get<AppStore>().wallet!,
));
getIt.registerFactory<MeldBuyProvider>(() => MeldBuyProvider(
wallet: getIt.get<AppStore>().wallet!,
));
getIt.registerFactoryParam<WebViewPage, String, Uri>((title, uri) => WebViewPage(title, uri));
getIt.registerFactory<PayfuraBuyProvider>(() => PayfuraBuyProvider(
@ -1193,8 +1202,25 @@ Future<void> setup({
getIt.registerFactory(() => BuyAmountViewModel());
getIt.registerFactoryParam<BuySellOptionsPage, bool, void>(
(isBuyOption, _) => BuySellOptionsPage(getIt.get<DashboardViewModel>(), isBuyOption));
getIt.registerFactory(() => BuySellViewModel(getIt.get<AppStore>()));
getIt.registerFactory(() => BuySellPage(getIt.get<BuySellViewModel>()));
getIt.registerFactoryParam<BuyOptionsPage, List<dynamic>, void>((List<dynamic> args, _) {
final items = args.first as List<SelectableItem>;
final pickAnOption = args[1] as void Function(SelectableOption option)?;
final confirmOption = args[2] as void Function(BuildContext contex)?;
return BuyOptionsPage(
items: items, pickAnOption: pickAnOption, confirmOption: confirmOption);
});
getIt.registerFactoryParam<PaymentMethodOptionsPage, List<dynamic>, void>((List<dynamic> args, _) {
final items = args.first as List<SelectableOption>;
final pickAnOption = args[1] as void Function(SelectableOption option)?;
return PaymentMethodOptionsPage(
items: items, pickAnOption: pickAnOption);
});
getIt.registerFactory(() {
final wallet = getIt.get<AppStore>().wallet;

View file

@ -23,31 +23,18 @@ class MainActions {
});
static List<MainActions> all = [
buyAction,
showWalletsAction,
receiveAction,
exchangeAction,
sendAction,
sellAction,
tradeAction,
];
static MainActions buyAction = MainActions._(
name: (context) => S.of(context).buy,
image: 'assets/images/buy.png',
isEnabled: (viewModel) => viewModel.isEnabledBuyAction,
canShow: (viewModel) => viewModel.hasBuyAction,
static MainActions showWalletsAction = MainActions._(
name: (context) => S.of(context).wallets,
image: 'assets/images/wallet_new.png',
onTap: (BuildContext context, DashboardViewModel viewModel) async {
if (!viewModel.isEnabledBuyAction) {
return;
}
final defaultBuyProvider = viewModel.defaultBuyProvider;
try {
defaultBuyProvider != null
? await defaultBuyProvider.launchProvider(context, true)
: await Navigator.of(context).pushNamed(Routes.buySellPage, arguments: true);
} catch (e) {
await _showErrorDialog(context, defaultBuyProvider.toString(), e.toString());
}
Navigator.pushNamed(context, Routes.walletList);
},
);
@ -79,39 +66,15 @@ class MainActions {
},
);
static MainActions sellAction = MainActions._(
name: (context) => S.of(context).sell,
image: 'assets/images/sell.png',
isEnabled: (viewModel) => viewModel.isEnabledSellAction,
canShow: (viewModel) => viewModel.hasSellAction,
onTap: (BuildContext context, DashboardViewModel viewModel) async {
if (!viewModel.isEnabledSellAction) {
return;
}
final defaultSellProvider = viewModel.defaultSellProvider;
try {
defaultSellProvider != null
? await defaultSellProvider.launchProvider(context, false)
: await Navigator.of(context).pushNamed(Routes.buySellPage, arguments: false);
} catch (e) {
await _showErrorDialog(context, defaultSellProvider.toString(), e.toString());
}
static MainActions tradeAction = MainActions._(
name: (context) => '${S.of(context).buy} / ${S.of(context).sell}',
image: 'assets/images/buy_sell.png',
isEnabled: (viewModel) => viewModel.isEnabledTradeAction,
canShow: (viewModel) => viewModel.hasTradeAction,
onTap: (BuildContext context, DashboardViewModel viewModel) async {
if (!viewModel.isEnabledTradeAction) return;
await Navigator.of(context).pushNamed(Routes.buySellPage, arguments: false);
},
);
static Future<void> _showErrorDialog(
BuildContext context, String title, String errorMessage) async {
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: title,
alertContent: errorMessage,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
);
},
);
}
}

View file

@ -21,10 +21,8 @@ class PreferencesKey {
static const currentBalanceDisplayModeKey = 'current_balance_display_mode';
static const shouldSaveRecipientAddressKey = 'save_recipient_address';
static const isAppSecureKey = 'is_app_secure';
static const disableBuyKey = 'disable_buy';
static const disableSellKey = 'disable_sell';
static const disableTradeOption = 'disable_buy';
static const disableBulletinKey = 'disable_bulletin';
static const defaultBuyProvider = 'default_buy_provider';
static const walletListOrder = 'wallet_list_order';
static const contactListOrder = 'contact_list_order';
static const walletListAscending = 'wallet_list_ascending';

View file

@ -1,24 +1,18 @@
import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart';
import 'package:cake_wallet/buy/meld/meld_buy_provider.dart';
import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart';
import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart';
import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart';
import 'package:cake_wallet/di.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:http/http.dart';
enum ProviderType {
askEachTime,
robinhood,
dfx,
onramper,
moonpay,
}
enum ProviderType { robinhood, dfx, onramper, moonpay, meld }
extension ProviderTypeName on ProviderType {
String get title {
switch (this) {
case ProviderType.askEachTime:
return 'Ask each time';
case ProviderType.robinhood:
return 'Robinhood Connect';
case ProviderType.dfx:
@ -27,13 +21,13 @@ extension ProviderTypeName on ProviderType {
return 'Onramper';
case ProviderType.moonpay:
return 'MoonPay';
case ProviderType.meld:
return 'Meld';
}
}
String get id {
switch (this) {
case ProviderType.askEachTime:
return 'ask_each_time_provider';
case ProviderType.robinhood:
return 'robinhood_connect_provider';
case ProviderType.dfx:
@ -42,6 +36,8 @@ extension ProviderTypeName on ProviderType {
return 'onramper_provider';
case ProviderType.moonpay:
return 'moonpay_provider';
case ProviderType.meld:
return 'meld_provider';
}
}
}
@ -52,14 +48,13 @@ class ProvidersHelper {
case WalletType.nano:
case WalletType.banano:
case WalletType.wownero:
return [ProviderType.askEachTime, ProviderType.onramper];
return [ProviderType.onramper];
case WalletType.monero:
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx];
return [ProviderType.onramper, ProviderType.dfx];
case WalletType.bitcoin:
case WalletType.polygon:
case WalletType.ethereum:
return [
ProviderType.askEachTime,
ProviderType.onramper,
ProviderType.dfx,
ProviderType.robinhood,
@ -68,10 +63,13 @@ class ProvidersHelper {
case WalletType.litecoin:
case WalletType.bitcoinCash:
case WalletType.solana:
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood, ProviderType.moonpay];
return [
ProviderType.onramper,
ProviderType.robinhood,
ProviderType.moonpay
];
case WalletType.tron:
return [
ProviderType.askEachTime,
ProviderType.onramper,
ProviderType.robinhood,
ProviderType.moonpay,
@ -88,28 +86,24 @@ class ProvidersHelper {
case WalletType.ethereum:
case WalletType.polygon:
return [
ProviderType.askEachTime,
ProviderType.onramper,
ProviderType.moonpay,
ProviderType.dfx,
];
case WalletType.litecoin:
case WalletType.bitcoinCash:
return [ProviderType.askEachTime, ProviderType.moonpay];
return [ProviderType.moonpay];
case WalletType.solana:
return [
ProviderType.askEachTime,
ProviderType.onramper,
ProviderType.robinhood,
ProviderType.moonpay,
];
case WalletType.tron:
return [
ProviderType.askEachTime,
ProviderType.robinhood,
ProviderType.moonpay,
];
case WalletType.monero:
return [ProviderType.dfx];
case WalletType.nano:
case WalletType.banano:
case WalletType.none:
@ -129,7 +123,9 @@ class ProvidersHelper {
return getIt.get<OnRamperBuyProvider>();
case ProviderType.moonpay:
return getIt.get<MoonPayProvider>();
case ProviderType.askEachTime:
case ProviderType.meld:
return getIt.get<MeldBuyProvider>();
default:
return null;
}
}

View file

@ -17,8 +17,9 @@ import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dar
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
import 'package:cake_wallet/src/screens/backup/backup_page.dart';
import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart';
import 'package:cake_wallet/src/screens/buy/buy_options_page.dart';
import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart';
import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart';
import 'package:cake_wallet/src/screens/buy/payment_method_options_page.dart';
import 'package:cake_wallet/src/screens/buy/webview_page.dart';
import 'package:cake_wallet/src/screens/cake_pay/auth/cake_pay_account_page.dart';
import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart';
@ -129,7 +130,8 @@ import 'package:cw_core/wallet_type.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart';
import 'src/screens/buy/buy_sell_page.dart';
import 'src/screens/dashboard/pages/nft_import_page.dart';
late RouteSettings currentRouteSettings;
@ -571,7 +573,15 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.buySellPage:
final args = settings.arguments as bool;
return MaterialPageRoute<void>(builder: (_) => getIt.get<BuySellOptionsPage>(param1: args));
return MaterialPageRoute<void>(builder: (_) => getIt.get<BuySellPage>(param1: args));
case Routes.buyOptionsPage:
final args = settings.arguments as List;
return MaterialPageRoute<void>(builder: (_) => getIt.get<BuyOptionsPage>(param1: args));
case Routes.paymentMethodOptionsPage:
final args = settings.arguments as List;
return MaterialPageRoute<void>(builder: (_) => getIt.get<PaymentMethodOptionsPage>(param1: args));
case Routes.buyWebView:
final args = settings.arguments as List;

View file

@ -59,6 +59,8 @@ class Routes {
static const supportOtherLinks = '/support/other';
static const orderDetails = '/order_details';
static const buySellPage = '/buy_sell_page';
static const buyOptionsPage = '/buy_sell_options';
static const paymentMethodOptionsPage = '/payment_method_options';
static const buyWebView = '/buy_web_view';
static const unspentCoinsList = '/unspent_coins_list';
static const unspentCoinsDetails = '/unspent_coins_details';

View file

@ -1,74 +0,0 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/option_tile.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/themes/extensions/option_tile_theme.dart';
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:flutter/material.dart';
class BuySellOptionsPage extends BasePage {
BuySellOptionsPage(this.dashboardViewModel, this.isBuyAction);
final DashboardViewModel dashboardViewModel;
final bool isBuyAction;
@override
String get title => isBuyAction ? S.current.buy : S.current.sell;
@override
AppBarStyle get appBarStyle => AppBarStyle.regular;
@override
Widget body(BuildContext context) {
final isLightMode = Theme.of(context).extension<OptionTileTheme>()?.useDarkImage ?? false;
final availableProviders = isBuyAction
? dashboardViewModel.availableBuyProviders
: dashboardViewModel.availableSellProviders;
return ScrollableWithBottomSection(
content: Container(
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 330),
child: Column(
children: [
...availableProviders.map((provider) {
final icon = Image.asset(
isLightMode ? provider.lightIcon : provider.darkIcon,
height: 40,
width: 40,
);
return Padding(
padding: EdgeInsets.only(top: 24),
child: OptionTile(
image: icon,
title: provider.toString(),
description: provider.providerDescription,
onPressed: () => provider.launchProvider(context, isBuyAction),
),
);
}).toList(),
],
),
),
),
),
bottomSection: Padding(
padding: EdgeInsets.fromLTRB(24, 24, 24, 32),
child: Text(
isBuyAction
? S.of(context).select_buy_provider_notice
: S.of(context).select_sell_provider_notice,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
),
),
),
);
}
}

View file

@ -0,0 +1,48 @@
import 'package:cake_wallet/core/selectable_option.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/select_options_page.dart';
import 'package:flutter/cupertino.dart';
class BuyOptionsPage extends SelectOptionsPage {
BuyOptionsPage({required this.items, this.pickAnOption, this.confirmOption});
final List<SelectableItem> items;
final Function(SelectableOption option)? pickAnOption;
final Function(BuildContext context)? confirmOption;
@override
String get pageTitle => S.current.choose_a_provider;
@override
EdgeInsets? get contentPadding => null;
@override
EdgeInsets? get tilePadding => EdgeInsets.only(top: 8);
@override
EdgeInsets? get innerPadding => EdgeInsets.symmetric(horizontal: 24, vertical: 8);
@override
double? get imageHeight => 40;
@override
double? get imageWidth => 40;
@override
Color? get selectedBackgroundColor => null;
@override
double? get tileBorderRadius => 30;
@override
String get bottomSectionText => '';
@override
void Function(SelectableOption option)? get onOptionTap => pickAnOption;
@override
String get primaryButtonText => S.current.confirm;
@override
void Function(BuildContext context)? get primaryButtonAction => confirmOption;
}

View file

@ -0,0 +1,469 @@
import 'package:cake_wallet/buy/sell_buy_states.dart';
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/exchange_card.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/mobile_exchange_cards_section.dart';
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/provider_optoin_tile.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/src/widgets/trail_button.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart';
import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:keyboard_actions/keyboard_actions.dart';
import 'package:mobx/mobx.dart';
class BuySellPage extends BasePage {
BuySellPage(this.buySellViewModel);
final BuySellViewModel buySellViewModel;
final cryptoCurrencyKey = GlobalKey<ExchangeCardState>();
final fiatCurrencyKey = GlobalKey<ExchangeCardState>();
final _formKey = GlobalKey<FormState>();
final _fiatAmountFocus = FocusNode();
final _cryptoAmountFocus = FocusNode();
final _cryptoAddressFocus = FocusNode();
var _isReactionsSet = false;
final arrowBottomPurple = Image.asset(
'assets/images/arrow_bottom_purple_icon.png',
color: Colors.white,
height: 8,
);
final arrowBottomCakeGreen = Image.asset(
'assets/images/arrow_bottom_cake_green.png',
color: Colors.white,
height: 8,
);
late final String? depositWalletName;
late final String? receiveWalletName;
@override
String get title => S.current.buy + '/' + S.current.sell;
@override
bool get gradientBackground => true;
@override
bool get gradientAll => true;
@override
bool get resizeToAvoidBottomInset => false;
@override
bool get extendBodyBehindAppBar => true;
@override
AppBarStyle get appBarStyle => AppBarStyle.transparent;
@override
Function(BuildContext)? get pushToNextWidget => (context) {
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus) {
currentFocus.focusedChild?.unfocus();
}
};
@override
Widget trailing(BuildContext context) => TrailButton(
caption: S.of(context).clear,
onPressed: () {
_formKey.currentState?.reset();
buySellViewModel.reset();
});
@override
Widget? leading(BuildContext context) {
final _backButton = Icon(
Icons.arrow_back_ios,
color: titleColor(context),
size: 16,
);
final _closeButton =
currentTheme.type == ThemeType.dark ? closeButtonImageDarkTheme : closeButtonImage;
bool isMobileView = responsiveLayoutUtil.shouldRenderMobileUI;
return MergeSemantics(
child: SizedBox(
height: isMobileView ? 37 : 45,
width: isMobileView ? 37 : 45,
child: ButtonTheme(
minWidth: double.minPositive,
child: Semantics(
label: !isMobileView ? S.of(context).close : S.of(context).seed_alert_back,
child: TextButton(
style: ButtonStyle(
overlayColor: MaterialStateColor.resolveWith((states) => Colors.transparent),
),
onPressed: () => onClose(context),
child: !isMobileView ? _closeButton : _backButton,
),
),
),
),
);
}
@override
Widget body(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) => _setReactions(context, buySellViewModel));
return KeyboardActions(
disableScroll: true,
config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
keyboardBarColor: Theme.of(context).extension<KeyboardTheme>()!.keyboardBarColor,
nextFocus: false,
actions: [
KeyboardActionsItem(
focusNode: _fiatAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()]),
KeyboardActionsItem(
focusNode: _cryptoAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()])
]),
child: Container(
color: Theme.of(context).colorScheme.background,
child: Form(
key: _formKey,
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(bottom: 24),
content: Observer(
builder: (_) => Column(children: [
_exchangeCardsSection(context),
Padding(
padding: EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
SizedBox(height: 12),
_buildPaymentMethodTile(context),
],
),
),
])),
bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
bottomSection: Column(children: [
Observer(
builder: (_) => LoadingPrimaryButton(
text: S.current.choose_a_provider,
onPressed: () async {
if(!_formKey.currentState!.validate()) return;
buySellViewModel.onTapChoseProvider(context);
},
color: Theme.of(context).primaryColor,
textColor: Colors.white,
isDisabled: false,
isLoading: !buySellViewModel.isReadyToTrade)),
]),
)),
));
}
Widget _buildPaymentMethodTile(BuildContext context) {
if (buySellViewModel.paymentMethodState is PaymentMethodLoading ||
buySellViewModel.paymentMethodState is InitialPaymentMethod) {
return OptionTilePlaceholder(
withBadge: false,
withSubtitle: false,
borderRadius: 30,
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8),
leadingIcon: Icons.arrow_forward_ios,
isDarkTheme: buySellViewModel.isDarkTheme);
}
if (buySellViewModel.paymentMethodState is PaymentMethodFailed) {
return OptionTilePlaceholder(errorText: 'No payment methods available', borderRadius: 30);
}
if (buySellViewModel.paymentMethodState is PaymentMethodLoaded &&
buySellViewModel.selectedPaymentMethod != null) {
return Observer(builder: (_) {
final selectedPaymentMethod = buySellViewModel.selectedPaymentMethod!;
return ProviderOptionTile(
lightImagePath: selectedPaymentMethod.lightIconPath,
darkImagePath: selectedPaymentMethod.darkIconPath,
title: selectedPaymentMethod.title,
onPressed: () => _pickPaymentMethod(context),
leadingIcon: Icons.arrow_forward_ios,
isLightMode: !buySellViewModel.isDarkTheme,
borderRadius: 30,
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8),
titleTextStyle:
textLargeBold(color: Theme.of(context).extension<CakeTextTheme>()!.titleColor),
);
});
}
return OptionTilePlaceholder(errorText: 'No payment methods available', borderRadius: 30);
}
void _pickPaymentMethod(BuildContext context) async {
final currentOption = buySellViewModel.selectedPaymentMethod;
await Navigator.of(context).pushNamed(
Routes.paymentMethodOptionsPage,
arguments: [
buySellViewModel.paymentMethods,
buySellViewModel.changeOption,
],
);
buySellViewModel.selectedPaymentMethod;
if (currentOption != null &&
currentOption.paymentMethodType !=
buySellViewModel.selectedPaymentMethod?.paymentMethodType) {
await buySellViewModel.calculateBestRate();
}
}
void _setReactions(BuildContext context, BuySellViewModel buySellViewModel) {
if (_isReactionsSet) {
return;
}
final fiatAmountController = fiatCurrencyKey.currentState!.amountController;
final cryptoAmountController = cryptoCurrencyKey.currentState!.amountController;
final cryptoAddressController = cryptoCurrencyKey.currentState!.addressController;
_onCurrencyChange(buySellViewModel.cryptoCurrency, buySellViewModel, cryptoCurrencyKey);
_onCurrencyChange(buySellViewModel.fiatCurrency, buySellViewModel, fiatCurrencyKey);
reaction(
(_) => buySellViewModel.wallet.name,
(String _) =>
_onWalletNameChange(buySellViewModel, buySellViewModel.cryptoCurrency, cryptoCurrencyKey));
reaction(
(_) => buySellViewModel.cryptoCurrency,
(CryptoCurrency currency) =>
_onCurrencyChange(currency, buySellViewModel, cryptoCurrencyKey));
reaction(
(_) => buySellViewModel.fiatCurrency,
(FiatCurrency currency) =>
_onCurrencyChange(currency, buySellViewModel, fiatCurrencyKey));
reaction((_) => buySellViewModel.fiatAmount, (String amount) {
if (fiatCurrencyKey.currentState!.amountController.text != amount) {
fiatCurrencyKey.currentState!.amountController.text = amount;
}
});
reaction((_) => buySellViewModel.isCryptoCurrencyAddressEnabled, (bool isEnabled) {
cryptoCurrencyKey.currentState!.isAddressEditable(isEditable: isEnabled);
});
reaction((_) => buySellViewModel.cryptoAmount, (String amount) {
if (cryptoCurrencyKey.currentState!.amountController.text != amount) {
cryptoCurrencyKey.currentState!.amountController.text = amount;
}
});
reaction((_) => buySellViewModel.cryptoCurrencyAddress, (String address) {
if (cryptoAddressController != address) {
cryptoCurrencyKey.currentState!.addressController.text = address;
}
});
fiatAmountController.addListener(() {
if (fiatAmountController.text != buySellViewModel.fiatAmount) {
buySellViewModel.changeFiatAmount(amount: fiatAmountController.text);
}
});
cryptoAmountController.addListener(() {
if (cryptoAmountController.text != buySellViewModel.cryptoAmount) {
buySellViewModel.changeCryptoAmount(amount: cryptoAmountController.text);
}
});
cryptoAddressController.addListener(() {
buySellViewModel.changeCryptoCurrencyAddress(cryptoAddressController.text);
});
_cryptoAddressFocus.addListener(() async {
if (!_cryptoAddressFocus.hasFocus && cryptoAddressController.text.isNotEmpty) {
final domain = cryptoAddressController.text;
buySellViewModel.cryptoCurrencyAddress =
await fetchParsedAddress(context, domain, buySellViewModel.cryptoCurrency);
}
});
reaction((_) => buySellViewModel.wallet.walletAddresses.addressForExchange, (String address) {
if (buySellViewModel.cryptoCurrency == CryptoCurrency.xmr) {
cryptoCurrencyKey.currentState!.changeAddress(address: address);
}
});
reaction((_) => buySellViewModel.isReadyToTrade, (bool isReady) {
if (isReady) {
if (cryptoAmountController.text.isNotEmpty &&
cryptoAmountController.text != S.current.fetching) {
buySellViewModel.changeCryptoAmount(amount: cryptoAmountController.text);
} else if (fiatAmountController.text.isNotEmpty &&
fiatAmountController.text != S.current.fetching) {
buySellViewModel.changeFiatAmount(amount: fiatAmountController.text);
}
}
});
_isReactionsSet = true;
}
void _onCurrencyChange(Currency currency, BuySellViewModel buySellViewModel,
GlobalKey<ExchangeCardState> key) {
final isCurrentTypeWallet = currency == buySellViewModel.wallet.currency;
key.currentState!.changeSelectedCurrency(currency);
key.currentState!.changeWalletName(isCurrentTypeWallet ? buySellViewModel.wallet.name : '');
key.currentState!.changeAddress(
address: isCurrentTypeWallet ? buySellViewModel.wallet.walletAddresses.addressForExchange : '');
key.currentState!.changeAmount(amount: '');
}
void _onWalletNameChange(BuySellViewModel buySellViewModel, CryptoCurrency currency,
GlobalKey<ExchangeCardState> key) {
final isCurrentTypeWallet = currency == buySellViewModel.wallet.currency;
if (isCurrentTypeWallet) {
key.currentState!.changeWalletName(buySellViewModel.wallet.name);
key.currentState!.addressController.text = buySellViewModel.wallet.walletAddresses.addressForExchange;
} else if (key.currentState!.addressController.text ==
buySellViewModel.wallet.walletAddresses.addressForExchange) {
key.currentState!.changeWalletName('');
key.currentState!.addressController.text = '';
}
}
void disposeBestRateSync() => {};
Widget _exchangeCardsSection(BuildContext context) {
final fiatExchangeCard = Observer(
builder: (_) => ExchangeCard(
cardInstanceName: 'fiat_currency_trade_card',
onDispose: disposeBestRateSync,
amountFocusNode: _fiatAmountFocus,
key: fiatCurrencyKey,
title: 'FIAT ${S.of(context).amount}',
initialCurrency: buySellViewModel.fiatCurrency,
initialWalletName: '',
initialAddress: '',
initialIsAmountEditable: true,
isAmountEstimated: false,
currencyRowPadding: EdgeInsets.zero,
addressRowPadding: EdgeInsets.zero,
isMoneroWallet: buySellViewModel.wallet == WalletType.monero,
showAddressField: false,
showLimitsField: false,
currencies: buySellViewModel.fiatCurrencies,
onCurrencySelected: (currency) =>
buySellViewModel.changeFiatCurrency(currency: currency),
imageArrow: arrowBottomPurple,
currencyButtonColor: Colors.transparent,
addressButtonsColor:
Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
borderColor:
Theme.of(context).extension<ExchangePageTheme>()!.textFieldBorderTopPanelColor,
onPushPasteButton: (context) async {},
onPushAddressBookButton: (context) async {},
));
final cryptoExchangeCard = Observer(
builder: (_) => ExchangeCard(
cardInstanceName: 'crypto_currency_trade_card',
onDispose: disposeBestRateSync,
amountFocusNode: _cryptoAmountFocus,
addressFocusNode: _cryptoAddressFocus,
key: cryptoCurrencyKey,
title: 'Crypto ${S.of(context).amount}',
initialCurrency: buySellViewModel.cryptoCurrency,
initialWalletName: '',
initialAddress: buySellViewModel.cryptoCurrency == buySellViewModel.wallet.currency
? buySellViewModel.wallet.walletAddresses.addressForExchange
: buySellViewModel.cryptoCurrencyAddress,
initialIsAmountEditable: true,
isAmountEstimated: true,
showLimitsField: false,
currencyRowPadding: EdgeInsets.zero,
addressRowPadding: EdgeInsets.zero,
isMoneroWallet: buySellViewModel.wallet == WalletType.monero,
currencies: buySellViewModel.cryptoCurrencies,
onCurrencySelected: (currency) =>
buySellViewModel.changeCryptoCurrency(currency: currency),
imageArrow: arrowBottomCakeGreen,
currencyButtonColor: Colors.transparent,
addressButtonsColor:
Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
borderColor:
Theme.of(context).extension<ExchangePageTheme>()!.textFieldBorderBottomPanelColor,
addressTextFieldValidator: AddressValidator(type: buySellViewModel.cryptoCurrency),
onPushPasteButton: (context) async {},
onPushAddressBookButton: (context) async {},
));
if (responsiveLayoutUtil.shouldRenderMobileUI) {
return Observer(
builder: (_) {
if (buySellViewModel.isBuyAction) {
return MobileExchangeCardsSection(
firstExchangeCard: fiatExchangeCard,
secondExchangeCard: cryptoExchangeCard,
onBuyTap: () => null,
onSellTap: () =>
buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null,
isBuySellOption: true,
);
} else {
return MobileExchangeCardsSection(
firstExchangeCard: cryptoExchangeCard,
secondExchangeCard: fiatExchangeCard,
onBuyTap: () =>
!buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null,
onSellTap: () => null,
isBuySellOption: true,
);
}
},
);
}
return Observer(
builder: (_) {
if (buySellViewModel.isBuyAction) {
return DesktopExchangeCardsSection(
firstExchangeCard: fiatExchangeCard,
secondExchangeCard: cryptoExchangeCard,
);
} else {
return DesktopExchangeCardsSection(
firstExchangeCard: cryptoExchangeCard,
secondExchangeCard: fiatExchangeCard,
);
}
},
);
}
Future<String> fetchParsedAddress(
BuildContext context, String domain, CryptoCurrency currency) async {
final parsedAddress = await getIt.get<AddressResolver>().resolve(context, domain, currency);
final address = await extractAddressFromParsed(context, parsedAddress);
return address;
}
}

View file

@ -0,0 +1,47 @@
import 'package:cake_wallet/core/selectable_option.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/select_options_page.dart';
import 'package:flutter/cupertino.dart';
class PaymentMethodOptionsPage extends SelectOptionsPage {
PaymentMethodOptionsPage({required this.items, this.pickAnOption});
final List<SelectableItem> items;
final Function(SelectableOption option)? pickAnOption;
@override
String get pageTitle => S.current.choose_a_payment_method;
@override
EdgeInsets? get contentPadding => null;
@override
EdgeInsets? get tilePadding => EdgeInsets.only(top: 12);
@override
EdgeInsets? get innerPadding => EdgeInsets.symmetric(horizontal: 24, vertical: 12);
@override
double? get imageHeight => null;
@override
double? get imageWidth => null;
@override
Color? get selectedBackgroundColor => null;
@override
double? get tileBorderRadius => 30;
@override
String get bottomSectionText => '';
@override
void Function(SelectableOption option)? get onOptionTap => pickAnOption;
@override
String get primaryButtonText => S.current.confirm;
@override
void Function(BuildContext context)? get primaryButtonAction => null;
}

View file

@ -21,6 +21,14 @@ class DesktopDashboardActions extends StatelessWidget {
return Column(
children: [
const SizedBox(height: 16),
DesktopActionButton(
title: MainActions.showWalletsAction.name(context),
image: MainActions.showWalletsAction.image,
canShow: MainActions.showWalletsAction.canShow?.call(dashboardViewModel),
isEnabled: MainActions.showWalletsAction.isEnabled?.call(dashboardViewModel),
onTap: () async =>
await MainActions.showWalletsAction.onTap(context, dashboardViewModel),
),
DesktopActionButton(
title: MainActions.exchangeAction.name(context),
image: MainActions.exchangeAction.image,
@ -55,20 +63,11 @@ class DesktopDashboardActions extends StatelessWidget {
children: [
Expanded(
child: DesktopActionButton(
title: MainActions.buyAction.name(context),
image: MainActions.buyAction.image,
canShow: MainActions.buyAction.canShow?.call(dashboardViewModel),
isEnabled: MainActions.buyAction.isEnabled?.call(dashboardViewModel),
onTap: () async => await MainActions.buyAction.onTap(context, dashboardViewModel),
),
),
Expanded(
child: DesktopActionButton(
title: MainActions.sellAction.name(context),
image: MainActions.sellAction.image,
canShow: MainActions.sellAction.canShow?.call(dashboardViewModel),
isEnabled: MainActions.sellAction.isEnabled?.call(dashboardViewModel),
onTap: () async => await MainActions.sellAction.onTap(context, dashboardViewModel),
title: MainActions.tradeAction.name(context),
image: MainActions.tradeAction.image,
canShow: MainActions.tradeAction.canShow?.call(dashboardViewModel),
isEnabled: MainActions.tradeAction.isEnabled?.call(dashboardViewModel),
onTap: () async => await MainActions.tradeAction.onTap(context, dashboardViewModel),
),
),
],

View file

@ -18,7 +18,7 @@ import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
class ExchangeCard extends StatefulWidget {
class ExchangeCard<T extends Currency> extends StatefulWidget {
ExchangeCard({
Key? key,
required this.initialCurrency,
@ -40,19 +40,23 @@ class ExchangeCard extends StatefulWidget {
this.borderColor = Colors.transparent,
this.hasAllAmount = false,
this.isAllAmountEnabled = false,
this.showAddressField = true,
this.showLimitsField = true,
this.amountFocusNode,
this.addressFocusNode,
this.allAmount,
this.currencyRowPadding,
this.addressRowPadding,
this.onPushPasteButton,
this.onPushAddressBookButton,
this.onDispose,
required this.cardInstanceName,
}) : super(key: key);
final List<CryptoCurrency> currencies;
final Function(CryptoCurrency) onCurrencySelected;
final List<T> currencies;
final Function(T) onCurrencySelected;
final String title;
final CryptoCurrency initialCurrency;
final T initialCurrency;
final String initialWalletName;
final String initialAddress;
final bool initialIsAmountEditable;
@ -70,18 +74,22 @@ class ExchangeCard extends StatefulWidget {
final FocusNode? amountFocusNode;
final FocusNode? addressFocusNode;
final bool hasAllAmount;
final bool showAddressField;
final bool showLimitsField;
final bool isAllAmountEnabled;
final VoidCallback? allAmount;
final EdgeInsets? currencyRowPadding;
final EdgeInsets? addressRowPadding;
final void Function(BuildContext context)? onPushPasteButton;
final void Function(BuildContext context)? onPushAddressBookButton;
final Function()? onDispose;
final String cardInstanceName;
@override
ExchangeCardState createState() => ExchangeCardState();
ExchangeCardState<T> createState() => ExchangeCardState<T>();
}
class ExchangeCardState extends State<ExchangeCard> {
class ExchangeCardState<T extends Currency> extends State<ExchangeCard<T>> {
ExchangeCardState()
: _title = '',
_min = '',
@ -89,7 +97,6 @@ class ExchangeCardState extends State<ExchangeCard> {
_isAmountEditable = false,
_isAddressEditable = false,
_walletName = '',
_selectedCurrency = CryptoCurrency.btc,
_isAmountEstimated = false,
_isMoneroWallet = false,
_cardInstanceName = '';
@ -101,7 +108,7 @@ class ExchangeCardState extends State<ExchangeCard> {
String _title;
String? _min;
String? _max;
CryptoCurrency _selectedCurrency;
late T _selectedCurrency;
String _walletName;
bool _isAmountEditable;
bool _isAddressEditable;
@ -118,7 +125,8 @@ class ExchangeCardState extends State<ExchangeCard> {
_selectedCurrency = widget.initialCurrency;
_isAmountEstimated = widget.isAmountEstimated;
_isMoneroWallet = widget.isMoneroWallet;
addressController.text = widget.initialAddress;
addressController.text = _normalizeAddressFormat(widget.initialAddress);
super.initState();
}
@ -136,7 +144,7 @@ class ExchangeCardState extends State<ExchangeCard> {
});
}
void changeSelectedCurrency(CryptoCurrency currency) {
void changeSelectedCurrency(T currency) {
setState(() => _selectedCurrency = currency);
}
@ -157,7 +165,7 @@ class ExchangeCardState extends State<ExchangeCard> {
}
void changeAddress({required String address}) {
setState(() => addressController.text = address);
setState(() => addressController.text = _normalizeAddressFormat(address));
}
void changeAmount({required String amount}) {
@ -222,7 +230,7 @@ class ExchangeCardState extends State<ExchangeCard> {
Divider(height: 1, color: Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor),
Padding(
padding: EdgeInsets.only(top: 5),
child: Container(
child: widget.showLimitsField ? Container(
height: 15,
child: Row(mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[
_min != null
@ -247,7 +255,7 @@ class ExchangeCardState extends State<ExchangeCard> {
),
)
: Offstage(),
])),
])) : Offstage(),
),
!_isAddressEditable && widget.hasRefundAddress
? Padding(
@ -261,10 +269,11 @@ class ExchangeCardState extends State<ExchangeCard> {
))
: Offstage(),
_isAddressEditable
? widget.showAddressField
? FocusTraversalOrder(
order: NumericFocusOrder(2),
child: Padding(
padding: EdgeInsets.only(top: 20),
padding: widget.addressRowPadding ?? EdgeInsets.only(top: 20),
child: AddressTextField(
addressKey: ValueKey('${_cardInstanceName}_editable_address_textfield_key'),
focusNode: widget.addressFocusNode,
@ -280,26 +289,29 @@ class ExchangeCardState extends State<ExchangeCard> {
widget.amountFocusNode?.requestFocus();
amountController.text = paymentRequest.amount;
},
placeholder: widget.hasRefundAddress ? S.of(context).refund_address : null,
placeholder:
widget.hasRefundAddress ? S.of(context).refund_address : null,
options: [
AddressTextFieldOption.paste,
AddressTextFieldOption.qrCode,
AddressTextFieldOption.addressBook,
],
isBorderExist: false,
textStyle:
TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
textStyle: TextStyle(
fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
hintStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context).extension<ExchangePageTheme>()!.hintTextColor),
color:
Theme.of(context).extension<ExchangePageTheme>()!.hintTextColor),
buttonColor: widget.addressButtonsColor,
validator: widget.addressTextFieldValidator,
onPushPasteButton: widget.onPushPasteButton,
onPushAddressBookButton: widget.onPushAddressBookButton,
selectedCurrency: _selectedCurrency),
),
)
)
: Offstage()
: Padding(
padding: EdgeInsets.only(top: 10),
child: Builder(
@ -402,7 +414,7 @@ class ExchangeCardState extends State<ExchangeCard> {
hintText: S.of(context).search_currency,
isMoneroWallet: _isMoneroWallet,
isConvertFrom: widget.hasRefundAddress,
onItemSelected: (Currency item) => widget.onCurrencySelected(item as CryptoCurrency),
onItemSelected: (Currency item) => widget.onCurrencySelected(item as T),
),
);
}
@ -424,4 +436,10 @@ class ExchangeCardState extends State<ExchangeCard> {
actionLeftButton: () => Navigator.of(dialogContext).pop());
});
}
String _normalizeAddressFormat(String address) {
if (address.startsWith('bitcoincash:')) address = address.substring(12);
return address;
}
}

View file

@ -1,20 +1,29 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart';
import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
import 'package:flutter/material.dart';
class MobileExchangeCardsSection extends StatelessWidget {
final Widget firstExchangeCard;
final Widget secondExchangeCard;
final bool isBuySellOption;
final VoidCallback? onBuyTap;
final VoidCallback? onSellTap;
const MobileExchangeCardsSection({
Key? key,
required this.firstExchangeCard,
required this.secondExchangeCard,
this.isBuySellOption = false,
this.onBuyTap,
this.onSellTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(bottom: 32),
padding: EdgeInsets.only(bottom: isBuySellOption ? 8 : 32),
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24),
@ -45,8 +54,18 @@ class MobileExchangeCardsSection extends StatelessWidget {
end: Alignment.bottomRight,
),
),
padding: EdgeInsets.fromLTRB(24, 100, 24, 32),
child: firstExchangeCard,
padding: EdgeInsets.fromLTRB(24, 90, 24, isBuySellOption ? 8 : 32),
child: Column(
children: [
if (isBuySellOption) Column(
children: [
const SizedBox(height: 16),
BuySellOptionButtons(onBuyTap: onBuyTap, onSellTap: onSellTap),
],
),
firstExchangeCard,
],
),
),
Padding(
padding: EdgeInsets.only(top: 29, left: 24, right: 24),
@ -57,3 +76,69 @@ class MobileExchangeCardsSection extends StatelessWidget {
);
}
}
class BuySellOptionButtons extends StatefulWidget {
final VoidCallback? onBuyTap;
final VoidCallback? onSellTap;
const BuySellOptionButtons({this.onBuyTap, this.onSellTap});
@override
_BuySellOptionButtonsState createState() => _BuySellOptionButtonsState();
}
class _BuySellOptionButtonsState extends State<BuySellOptionButtons> {
bool isBuySelected = true;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 24),
child: Row(
children: [
Expanded(flex: 2, child: SizedBox()),
Expanded(
flex: 5,
child: SelectButton(
height: 44,
text: S.of(context).buy,
isSelected: isBuySelected,
showTrailingIcon: false,
textColor: Colors.white,
image: Image.asset('assets/images/buy.png', height: 25, width: 25),
padding: EdgeInsets.only(left: 10, right: 30),
color: isBuySelected
? null
: Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
onTap: () {
setState(() => isBuySelected = true);
if (widget.onBuyTap != null) widget.onBuyTap!();
},
),
),
Expanded(child: const SizedBox()),
Expanded(
flex: 5,
child: SelectButton(
height: 44,
text: S.of(context).sell,
isSelected: !isBuySelected,
showTrailingIcon: false,
textColor: Colors.white,
image: Image.asset('assets/images/sell.png', height: 25, width: 25),
padding: EdgeInsets.only(left: 10, right: 30),
color: !isBuySelected
? null
: Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
onTap: () {
setState(() => isBuySelected = false);
if (widget.onSellTap != null) widget.onSellTap!();
},
),
),
Expanded(flex: 2, child: SizedBox()),
],
),
);
}
}

View file

@ -1,6 +1,6 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/InfoPage.dart';
import 'package:cake_wallet/src/screens/Info_page.dart';
import 'package:flutter/cupertino.dart';
class PreSeedPage extends InfoPage {

View file

@ -0,0 +1,199 @@
import 'package:cake_wallet/core/selectable_option.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/provider_optoin_tile.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/themes/extensions/option_tile_theme.dart';
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
import 'package:flutter/material.dart';
abstract class SelectOptionsPage extends BasePage {
SelectOptionsPage();
String get pageTitle;
EdgeInsets? get contentPadding;
EdgeInsets? get tilePadding;
EdgeInsets? get innerPadding;
double? get imageHeight;
double? get imageWidth;
Color? get selectedBackgroundColor;
double? get tileBorderRadius;
String get bottomSectionText;
bool get primaryButtonEnabled => true;
String get primaryButtonText => '';
List<SelectableItem> get items;
void Function(SelectableOption option)? get onOptionTap;
void Function(BuildContext context)? get primaryButtonAction;
@override
String get title => pageTitle;
@override
Widget body(BuildContext context) {
return ScrollableWithBottomSection(
content: BodySelectOptionsPage(
items: items,
onOptionTap: onOptionTap,
tilePadding: tilePadding,
tileBorderRadius: tileBorderRadius,
imageHeight: imageHeight,
imageWidth: imageWidth,
innerPadding: innerPadding),
bottomSection: Padding(
padding: contentPadding ?? EdgeInsets.zero,
child: Column(
children: [
Text(
bottomSectionText,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
),
),
if (primaryButtonEnabled)
LoadingPrimaryButton(
text: primaryButtonText,
onPressed: () {
primaryButtonAction != null
? primaryButtonAction!(context)
: Navigator.pop(context);
},
color: Theme.of(context).primaryColor,
textColor: Colors.white,
isDisabled: false,
isLoading: false)
],
),
),
);
}
}
class BodySelectOptionsPage extends StatefulWidget {
const BodySelectOptionsPage({
required this.items,
this.onOptionTap,
this.tilePadding,
this.tileBorderRadius,
this.imageHeight,
this.imageWidth,
this.innerPadding,
});
final List<SelectableItem> items;
final void Function(SelectableOption option)? onOptionTap;
final EdgeInsets? tilePadding;
final double? tileBorderRadius;
final double? imageHeight;
final double? imageWidth;
final EdgeInsets? innerPadding;
@override
_BodySelectOptionsPageState createState() => _BodySelectOptionsPageState();
}
class _BodySelectOptionsPageState extends State<BodySelectOptionsPage> {
late List<SelectableItem> _items;
@override
void initState() {
super.initState();
_items = widget.items;
}
void _handleOptionTap(SelectableOption option) {
setState(() {
for (var item in _items) {
if (item is SelectableOption) {
item.isOptionSelected = false;
}
}
option.isOptionSelected = true;
});
widget.onOptionTap?.call(option);
}
@override
Widget build(BuildContext context) {
final isLightMode = Theme.of(context).extension<OptionTileTheme>()?.useDarkImage ?? false;
Color titleColor =
isLightMode ? Theme.of(context).appBarTheme.titleTextStyle!.color! : Colors.white;
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 350),
child: Column(
children: _items.map((item) {
if (item is OptionTitle) {
return Padding(
padding: const EdgeInsets.only(top: 18, bottom: 8),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: titleColor,
width: 1,
),
),
),
child: Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
item.title,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: titleColor,
),
),
),
),
);
} else if (item is SelectableOption) {
return Padding(
padding: widget.tilePadding ?? const EdgeInsets.only(top: 24),
child: ProviderOptionTile(
title: item.title,
lightImagePath: item.lightIconPath,
darkImagePath: item.darkIconPath,
imageHeight: widget.imageHeight,
imageWidth: widget.imageWidth,
padding: widget.innerPadding,
description: item.description,
topLeftSubTitle: item.topLeftSubTitle,
topRightSubTitle: item.topRightSubTitle,
rightSubTitleLightIconPath: item.topRightSubTitleLightIconPath,
rightSubTitleDarkIconPath: item.topRightSubTitleDarkIconPath,
bottomLeftSubTitle: item.bottomLeftSubTitle,
badges: item.badges,
isSelected: item.isOptionSelected,
borderRadius: widget.tileBorderRadius,
isLightMode: isLightMode,
onPressed: () => _handleOptionTap(item),
),
);
}
return const SizedBox.shrink();
}).toList(),
),
),
);
}
}

View file

@ -57,22 +57,6 @@ class OtherSettingsPage extends BasePage {
handler: (BuildContext context) =>
Navigator.of(context).pushNamed(Routes.changeRep),
),
if(_otherSettingsViewModel.isEnabledBuyAction)
SettingsPickerCell(
title: S.current.default_buy_provider,
items: _otherSettingsViewModel.availableBuyProvidersTypes,
displayItem: _otherSettingsViewModel.getBuyProviderType,
selectedItem: _otherSettingsViewModel.buyProviderType,
onItemSelected: _otherSettingsViewModel.onBuyProviderTypeSelected
),
if(_otherSettingsViewModel.isEnabledSellAction)
SettingsPickerCell(
title: S.current.default_sell_provider,
items: _otherSettingsViewModel.availableSellProvidersTypes,
displayItem: _otherSettingsViewModel.getSellProviderType,
selectedItem: _otherSettingsViewModel.sellProviderType,
onItemSelected: _otherSettingsViewModel.onSellProviderTypeSelected,
),
SettingsCellWithArrow(
title: S.current.settings_terms_and_conditions,
handler: (BuildContext context) =>

View file

@ -73,16 +73,10 @@ class PrivacyPage extends BasePage {
_privacySettingsViewModel.setIsAppSecure(value);
}),
SettingsSwitcherCell(
title: S.current.disable_buy,
value: _privacySettingsViewModel.disableBuy,
title: S.current.disable_trade_option,
value: _privacySettingsViewModel.disableTradeOption,
onValueChange: (BuildContext _, bool value) {
_privacySettingsViewModel.setDisableBuy(value);
}),
SettingsSwitcherCell(
title: S.current.disable_sell,
value: _privacySettingsViewModel.disableSell,
onValueChange: (BuildContext _, bool value) {
_privacySettingsViewModel.setDisableSell(value);
_privacySettingsViewModel.setDisableTradeOption(value);
}),
SettingsSwitcherCell(
title: S.current.disable_bulletin,

View file

@ -1,6 +1,6 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/InfoPage.dart';
import 'package:cake_wallet/src/screens/Info_page.dart';
import 'package:flutter/cupertino.dart';
class Setup2FAInfoPage extends InfoPage {

View file

@ -1,20 +1,21 @@
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cw_core/currency.dart';
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/entities/qr_scanner.dart';
import 'package:cake_wallet/entities/contact_base.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
import 'package:cake_wallet/utils/permission_handler.dart';
import 'package:permission_handler/permission_handler.dart';
enum AddressTextFieldOption { paste, qrCode, addressBook, walletAddresses }
class AddressTextField extends StatelessWidget {
class AddressTextField<T extends Currency> extends StatelessWidget{
AddressTextField({
required this.controller,
this.isActive = true,
@ -58,7 +59,7 @@ class AddressTextField extends StatelessWidget {
final Function(BuildContext context)? onPushAddressBookButton;
final Function(BuildContext context)? onPushAddressPickerButton;
final Function(ContactBase contact)? onSelectedContact;
final CryptoCurrency? selectedCurrency;
final T? selectedCurrency;
final Key? addressKey;
@override

View file

@ -0,0 +1,527 @@
import 'package:cake_wallet/themes/extensions/option_tile_theme.dart';
import 'package:cake_wallet/themes/extensions/receive_page_theme.dart';
import 'package:cake_wallet/typography.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
class ProviderOptionTile extends StatelessWidget {
const ProviderOptionTile({
required this.onPressed,
required this.lightImagePath,
required this.darkImagePath,
required this.title,
this.topLeftSubTitle,
this.topRightSubTitle,
this.bottomLeftSubTitle,
this.bottomRightSubTitle,
this.leftSubTitleIconPath,
this.rightSubTitleLightIconPath,
this.rightSubTitleDarkIconPath,
this.description,
this.badges,
this.borderRadius,
this.imageHeight,
this.imageWidth,
this.padding,
this.titleTextStyle,
this.firstSubTitleTextStyle,
this.secondSubTitleTextStyle,
this.leadingIcon,
this.selectedBackgroundColor,
this.isSelected = false,
required this.isLightMode,
});
final VoidCallback onPressed;
final String lightImagePath;
final String darkImagePath;
final String title;
final String? topLeftSubTitle;
final String? topRightSubTitle;
final String? bottomLeftSubTitle;
final String? bottomRightSubTitle;
final String? leftSubTitleIconPath;
final String? rightSubTitleLightIconPath;
final String? rightSubTitleDarkIconPath;
final String? description;
final List<String>? badges;
final double? borderRadius;
final double? imageHeight;
final double? imageWidth;
final EdgeInsets? padding;
final TextStyle? titleTextStyle;
final TextStyle? firstSubTitleTextStyle;
final TextStyle? secondSubTitleTextStyle;
final IconData? leadingIcon;
final Color? selectedBackgroundColor;
final bool isSelected;
final bool isLightMode;
@override
Widget build(BuildContext context) {
final backgroundColor = isSelected
? isLightMode
? Theme.of(context).extension<ReceivePageTheme>()!.currentTileBackgroundColor
: Theme.of(context).extension<OptionTileTheme>()!.titleColor
: Theme.of(context).cardColor;
final textColor = isSelected
? isLightMode
? Colors.white
: Theme.of(context).cardColor
: Theme.of(context).extension<OptionTileTheme>()!.titleColor;
final badgeColor = isSelected
? Theme.of(context).cardColor
: Theme.of(context).extension<OptionTileTheme>()!.titleColor;
final badgeTextColor = isSelected
? Theme.of(context).extension<OptionTileTheme>()!.titleColor
: Theme.of(context).cardColor;
final imagePath = isSelected
? isLightMode
? darkImagePath
: lightImagePath
: isLightMode
? lightImagePath
: darkImagePath;
final rightSubTitleIconPath = isSelected
? isLightMode
? rightSubTitleDarkIconPath
: rightSubTitleLightIconPath
: isLightMode
? rightSubTitleLightIconPath
: rightSubTitleDarkIconPath;
return GestureDetector(
onTap: onPressed,
child: Container(
width: double.infinity,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(borderRadius ?? 12)),
border: isSelected && !isLightMode ? Border.all(color: textColor) : null,
color: backgroundColor,
),
child: Padding(
padding: padding ?? const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
children: [
getImage(imagePath, height: imageHeight, width: imageWidth),
SizedBox(width: 8),
Expanded(
child: Container(
child: Row(
children: [
Expanded(
child: Text(title,
style: titleTextStyle ?? textLargeBold(color: textColor))),
Row(
children: [
if (leadingIcon != null)
Icon(leadingIcon, size: 16, color: textColor),
],
)
],
),
),
),
],
),
if (topLeftSubTitle != null || topRightSubTitle != null)
subTitleWidget(
leftSubTitle: topLeftSubTitle,
subTitleIconPath: leftSubTitleIconPath,
textColor: textColor,
rightSubTitle: topRightSubTitle,
rightSubTitleIconPath: rightSubTitleIconPath),
if (bottomLeftSubTitle != null || bottomRightSubTitle != null)
subTitleWidget(
leftSubTitle: bottomLeftSubTitle,
textColor: textColor,
subTitleFontSize: 12),
if (badges != null && badges!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 12),
child: Row(children: [
...badges!
.map((badge) => Badge(
title: badge, textColor: badgeTextColor, backgroundColor: badgeColor))
.toList()
]),
)
],
),
),
),
);
}
}
class subTitleWidget extends StatelessWidget {
const subTitleWidget({
super.key,
this.leftSubTitle,
this.subTitleIconPath,
required this.textColor,
this.rightSubTitle,
this.rightSubTitleIconPath,
this.subTitleFontSize = 16,
});
final String? leftSubTitle;
final String? subTitleIconPath;
final Color textColor;
final String? rightSubTitle;
final String? rightSubTitleIconPath;
final double subTitleFontSize;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
leftSubTitle != null || subTitleIconPath != null
? Row(
children: [
if (subTitleIconPath != null && subTitleIconPath!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(right: 6),
child: getImage(subTitleIconPath!),
),
Text(
leftSubTitle ?? '',
style: TextStyle(
fontSize: subTitleFontSize,
fontWeight: FontWeight.w700,
color: textColor),
),
],
)
: Offstage(),
rightSubTitle != null || rightSubTitleIconPath != null
? Row(
children: [
if (rightSubTitleIconPath != null && rightSubTitleIconPath!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(right: 4),
child: getImage(rightSubTitleIconPath!, imageColor: textColor),
),
Text(
rightSubTitle ?? '',
style: TextStyle(
fontSize: subTitleFontSize, fontWeight: FontWeight.w700, color: textColor),
),
],
)
: Offstage(),
],
);
}
}
class Badge extends StatelessWidget {
Badge({required this.textColor, required this.backgroundColor, required this.title});
final String title;
final Color textColor;
final Color backgroundColor;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(right: 8),
child: FittedBox(
fit: BoxFit.fitHeight,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(24)), color: backgroundColor),
alignment: Alignment.center,
child: Text(
title,
style: TextStyle(
color: textColor,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
),
);
}
}
Widget getImage(String imagePath, {double? height, double? width, Color? imageColor}) {
final bool isNetworkImage = imagePath.startsWith('http') || imagePath.startsWith('https');
final bool isSvg = imagePath.endsWith('.svg');
final double imageHeight = height ?? 35;
final double imageWidth = width ?? 35;
if (isNetworkImage) {
return isSvg
? SvgPicture.network(
imagePath,
height: imageHeight,
width: imageWidth,
colorFilter: imageColor != null ? ColorFilter.mode(imageColor, BlendMode.srcIn) : null,
placeholderBuilder: (BuildContext context) => Container(
height: imageHeight,
width: imageWidth,
child: Center(
child: CircularProgressIndicator(),
),
),
)
: Image.network(
imagePath,
height: imageHeight,
width: imageWidth,
loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) {
return child;
}
return Container(
height: imageHeight,
width: imageWidth,
child: Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
),
);
},
errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) {
return Container(
height: imageHeight,
width: imageWidth,
);
},
);
} else {
return isSvg
? SvgPicture.asset(
imagePath,
height: imageHeight,
width: imageWidth,
colorFilter: imageColor != null ? ColorFilter.mode(imageColor, BlendMode.srcIn) : null,
)
: Image.asset(imagePath, height: imageHeight, width: imageWidth);
}
}
class OptionTilePlaceholder extends StatefulWidget {
OptionTilePlaceholder({
this.borderRadius,
this.imageHeight,
this.imageWidth,
this.padding,
this.leadingIcon,
this.withBadge = true,
this.withSubtitle = true,
this.isDarkTheme = false,
this.errorText,
});
final double? borderRadius;
final double? imageHeight;
final double? imageWidth;
final EdgeInsets? padding;
final IconData? leadingIcon;
final bool withBadge;
final bool withSubtitle;
final bool isDarkTheme;
final String? errorText;
@override
_OptionTilePlaceholderState createState() => _OptionTilePlaceholderState();
}
class _OptionTilePlaceholderState extends State<OptionTilePlaceholder>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
)..repeat();
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.linear,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final backgroundColor = Theme.of(context).cardColor;
final titleColor = Theme.of(context).extension<OptionTileTheme>()!.titleColor.withOpacity(0.4);
return widget.errorText != null
? Container(
width: double.infinity,
padding: widget.padding ?? EdgeInsets.all(16),
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius ?? 12)),
color: backgroundColor,
),
child: Column(
children: [
Text(
widget.errorText!,
style: TextStyle(
color: titleColor,
fontSize: 16,
),
),
if (widget.withSubtitle) SizedBox(height: 8),
Text(
'',
style: TextStyle(
color: titleColor,
fontSize: 16,
),
),
],
),
)
: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Stack(
children: [
Container(
width: double.infinity,
padding: widget.padding ?? EdgeInsets.all(16),
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius ?? 12)),
color: backgroundColor,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
children: [
Container(
height: widget.imageHeight ?? 35,
width: widget.imageWidth ?? 35,
decoration: BoxDecoration(
color: titleColor,
shape: BoxShape.circle,
),
),
SizedBox(width: 8),
Expanded(
child: Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
height: 20,
width: 70,
decoration: BoxDecoration(
color: titleColor,
borderRadius: BorderRadius.all(Radius.circular(8)),
),
),
if (widget.leadingIcon != null)
Icon(widget.leadingIcon, size: 16, color: titleColor),
],
),
),
),
],
),
if (widget.withSubtitle)
Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
height: 20,
width: 170,
decoration: BoxDecoration(
color: titleColor,
borderRadius: BorderRadius.all(Radius.circular(8)),
),
),
],
),
),
if (widget.withBadge)
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: Row(
children: [
Container(
height: 30,
width: 70,
decoration: BoxDecoration(
color: titleColor,
borderRadius: BorderRadius.all(Radius.circular(20)),
),
),
SizedBox(width: 8),
Container(
height: 30,
width: 70,
decoration: BoxDecoration(
color: titleColor,
borderRadius: BorderRadius.all(Radius.circular(20)),
),
),
],
),
),
],
),
),
Positioned.fill(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius ?? 12)),
gradient: LinearGradient(
begin: Alignment(-2, -4),
end: Alignment(2, 4),
stops: [
_animation.value - 0.2,
_animation.value,
_animation.value + 0.2,
],
colors: [
backgroundColor.withOpacity(widget.isDarkTheme ? 0.4 : 0.7),
backgroundColor.withOpacity(widget.isDarkTheme ? 0.7 : 0.4),
backgroundColor.withOpacity(widget.isDarkTheme ? 0.4 : 0.7),
],
),
),
),
),
],
);
},
);
}
}

View file

@ -61,8 +61,7 @@ abstract class SettingsStoreBase with Store {
required BitcoinSeedType initialBitcoinSeedType,
required NanoSeedType initialNanoSeedType,
required bool initialAppSecure,
required bool initialDisableBuy,
required bool initialDisableSell,
required bool initialDisableTrade,
required FilterListOrderType initialWalletListOrder,
required FilterListOrderType initialContactListOrder,
required bool initialDisableBulletin,
@ -150,8 +149,7 @@ abstract class SettingsStoreBase with Store {
useTOTP2FA = initialUseTOTP2FA,
numberOfFailedTokenTrials = initialFailedTokenTrial,
isAppSecure = initialAppSecure,
disableBuy = initialDisableBuy,
disableSell = initialDisableSell,
disableTradeOption = initialDisableTrade,
disableBulletin = initialDisableBulletin,
walletListOrder = initialWalletListOrder,
contactListOrder = initialContactListOrder,
@ -178,9 +176,7 @@ abstract class SettingsStoreBase with Store {
initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings,
currentSyncMode = initialSyncMode,
currentSyncAll = initialSyncAll,
priority = ObservableMap<WalletType, TransactionPriority>(),
defaultBuyProviders = ObservableMap<WalletType, ProviderType>(),
defaultSellProviders = ObservableMap<WalletType, ProviderType>() {
priority = ObservableMap<WalletType, TransactionPriority>() {
//this.nodes = ObservableMap<WalletType, Node>.of(nodes);
if (initialMoneroTransactionPriority != null) {
@ -221,30 +217,6 @@ abstract class SettingsStoreBase with Store {
initializeTrocadorProviderStates();
WalletType.values.forEach((walletType) {
final key = 'buyProvider_${walletType.toString()}';
final providerId = sharedPreferences.getString(key);
if (providerId != null) {
defaultBuyProviders[walletType] = ProviderType.values.firstWhere(
(provider) => provider.id == providerId,
orElse: () => ProviderType.askEachTime);
} else {
defaultBuyProviders[walletType] = ProviderType.askEachTime;
}
});
WalletType.values.forEach((walletType) {
final key = 'sellProvider_${walletType.toString()}';
final providerId = sharedPreferences.getString(key);
if (providerId != null) {
defaultSellProviders[walletType] = ProviderType.values.firstWhere(
(provider) => provider.id == providerId,
orElse: () => ProviderType.askEachTime);
} else {
defaultSellProviders[walletType] = ProviderType.askEachTime;
}
});
reaction(
(_) => fiatCurrency,
(FiatCurrency fiatCurrency) => sharedPreferences.setString(
@ -267,20 +239,6 @@ abstract class SettingsStoreBase with Store {
reaction((_) => shouldShowRepWarning,
(bool val) => sharedPreferences.setBool(PreferencesKey.shouldShowRepWarning, val));
defaultBuyProviders.observe((change) {
final String key = 'buyProvider_${change.key.toString()}';
if (change.newValue != null) {
sharedPreferences.setString(key, change.newValue!.id);
}
});
defaultSellProviders.observe((change) {
final String key = 'sellProvider_${change.key.toString()}';
if (change.newValue != null) {
sharedPreferences.setString(key, change.newValue!.id);
}
});
priority.observe((change) {
final String? key;
switch (change.key) {
@ -329,14 +287,9 @@ abstract class SettingsStoreBase with Store {
});
}
reaction((_) => disableBuy,
(bool disableBuy) => sharedPreferences.setBool(PreferencesKey.disableBuyKey, disableBuy));
reaction(
(_) => disableSell,
(bool disableSell) =>
sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell));
reaction((_) => disableTradeOption,
(bool disableTradeOption) => sharedPreferences.setBool(PreferencesKey.disableTradeOption, disableTradeOption));
reaction(
(_) => disableBulletin,
(bool disableBulletin) =>
@ -680,10 +633,7 @@ abstract class SettingsStoreBase with Store {
bool isAppSecure;
@observable
bool disableBuy;
@observable
bool disableSell;
bool disableTradeOption;
@observable
FilterListOrderType contactListOrder;
@ -769,12 +719,6 @@ abstract class SettingsStoreBase with Store {
@observable
ObservableMap<String, bool> trocadorProviderStates = ObservableMap<String, bool>();
@observable
ObservableMap<WalletType, ProviderType> defaultBuyProviders;
@observable
ObservableMap<WalletType, ProviderType> defaultSellProviders;
@observable
SortBalanceBy sortBalanceBy;
@ -956,8 +900,7 @@ abstract class SettingsStoreBase with Store {
final shouldSaveRecipientAddress =
sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ?? false;
final isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? false;
final disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? false;
final disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? false;
final disableTradeOption = sharedPreferences.getBool(PreferencesKey.disableTradeOption) ?? false;
final disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? false;
final walletListOrder =
FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0];
@ -1255,8 +1198,7 @@ abstract class SettingsStoreBase with Store {
initialBitcoinSeedType: bitcoinSeedType,
initialNanoSeedType: nanoSeedType,
initialAppSecure: isAppSecure,
initialDisableBuy: disableBuy,
initialDisableSell: disableSell,
initialDisableTrade: disableTradeOption,
initialDisableBulletin: disableBulletin,
initialWalletListOrder: walletListOrder,
initialWalletListAscending: walletListAscending,
@ -1406,8 +1348,7 @@ abstract class SettingsStoreBase with Store {
numberOfFailedTokenTrials =
sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? numberOfFailedTokenTrials;
isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? isAppSecure;
disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? disableBuy;
disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? disableSell;
disableTradeOption = sharedPreferences.getBool(PreferencesKey.disableTradeOption) ?? disableTradeOption;
disableBulletin =
sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? disableBulletin;
walletListOrder =

View file

@ -22,6 +22,8 @@ TextStyle textMediumSemiBold({Color? color}) => _cakeSemiBold(22, color);
TextStyle textLarge({Color? color}) => _cakeRegular(18, color);
TextStyle textLargeBold({Color? color}) => _cakeBold(18, color);
TextStyle textLargeSemiBold({Color? color}) => _cakeSemiBold(24, color);
TextStyle textXLarge({Color? color}) => _cakeRegular(32, color);

View file

@ -0,0 +1,446 @@
import 'dart:async';
import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/buy_quote.dart';
import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart';
import 'package:cake_wallet/buy/payment_method.dart';
import 'package:cake_wallet/buy/sell_buy_states.dart';
import 'package:cake_wallet/core/selectable_option.dart';
import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/provider_types.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:flutter/cupertino.dart';
import 'package:intl/intl.dart';
import 'package:mobx/mobx.dart';
part 'buy_sell_view_model.g.dart';
class BuySellViewModel = BuySellViewModelBase with _$BuySellViewModel;
abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with Store {
BuySellViewModelBase(
AppStore appStore,
) : _cryptoNumberFormat = NumberFormat(),
cryptoAmount = '',
fiatAmount = '',
cryptoCurrencyAddress = '',
isCryptoCurrencyAddressEnabled = false,
cryptoCurrencies = <CryptoCurrency>[],
fiatCurrencies = <FiatCurrency>[],
paymentMethodState = InitialPaymentMethod(),
buySellQuotState = InitialBuySellQuotState(),
cryptoCurrency = appStore.wallet!.currency,
fiatCurrency = appStore.settingsStore.fiatCurrency,
providerList = [],
sortedRecommendedQuotes = ObservableList<Quote>(),
sortedQuotes = ObservableList<Quote>(),
paymentMethods = ObservableList<PaymentMethod>(),
settingsStore = appStore.settingsStore,
super(appStore: appStore) {
const excludeFiatCurrencies = [];
const excludeCryptoCurrencies = [];
fiatCurrencies =
FiatCurrency.all.where((currency) => !excludeFiatCurrencies.contains(currency)).toList();
cryptoCurrencies = CryptoCurrency.all
.where((currency) => !excludeCryptoCurrencies.contains(currency))
.toList();
_initialize();
isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency);
}
final NumberFormat _cryptoNumberFormat;
late Timer bestRateSync;
List<BuyProvider> get availableBuyProviders {
final providerTypes = ProvidersHelper.getAvailableBuyProviderTypes(
walletTypeForCurrency(cryptoCurrency) ?? wallet.type);
return providerTypes
.map((type) => ProvidersHelper.getProviderByType(type))
.where((provider) => provider != null)
.cast<BuyProvider>()
.toList();
}
List<BuyProvider> get availableSellProviders {
final providerTypes = ProvidersHelper.getAvailableSellProviderTypes(
walletTypeForCurrency(cryptoCurrency) ?? wallet.type);
return providerTypes
.map((type) => ProvidersHelper.getProviderByType(type))
.where((provider) => provider != null)
.cast<BuyProvider>()
.toList();
}
@override
void onWalletChange(wallet) {
cryptoCurrency = wallet.currency;
}
bool get isDarkTheme => settingsStore.currentTheme.type == ThemeType.dark;
double get amount {
final formattedFiatAmount = double.tryParse(fiatAmount) ?? 200.0;
final formattedCryptoAmount =
double.tryParse(cryptoAmount) ?? (cryptoCurrency == CryptoCurrency.btc ? 0.001 : 1);
return isBuyAction ? formattedFiatAmount : formattedCryptoAmount;
}
SettingsStore settingsStore;
Quote? bestRateQuote;
Quote? selectedQuote;
@observable
List<CryptoCurrency> cryptoCurrencies;
@observable
List<FiatCurrency> fiatCurrencies;
@observable
bool isBuyAction = true;
@observable
List<BuyProvider> providerList;
@observable
ObservableList<Quote> sortedRecommendedQuotes;
@observable
ObservableList<Quote> sortedQuotes;
@observable
ObservableList<PaymentMethod> paymentMethods;
@observable
FiatCurrency fiatCurrency;
@observable
CryptoCurrency cryptoCurrency;
@observable
String cryptoAmount;
@observable
String fiatAmount;
@observable
String cryptoCurrencyAddress;
@observable
bool isCryptoCurrencyAddressEnabled;
@observable
PaymentMethod? selectedPaymentMethod;
@observable
PaymentMethodLoadingState paymentMethodState;
@observable
BuySellQuotLoadingState buySellQuotState;
@computed
bool get isReadyToTrade {
final hasSelectedQuote = selectedQuote != null;
final hasSelectedPaymentMethod = selectedPaymentMethod != null;
final isPaymentMethodLoaded = paymentMethodState is PaymentMethodLoaded;
final isBuySellQuotLoaded = buySellQuotState is BuySellQuotLoaded;
return hasSelectedQuote &&
hasSelectedPaymentMethod &&
isPaymentMethodLoaded &&
isBuySellQuotLoaded;
}
@action
void reset() {
cryptoCurrency = wallet.currency;
fiatCurrency = settingsStore.fiatCurrency;
isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency);
_initialize();
}
@action
void changeBuySellAction() {
isBuyAction = !isBuyAction;
_initialize();
}
@action
void changeFiatCurrency({required FiatCurrency currency}) {
fiatCurrency = currency;
_onPairChange();
}
@action
void changeCryptoCurrency({required CryptoCurrency currency}) {
cryptoCurrency = currency;
_onPairChange();
isCryptoCurrencyAddressEnabled = !(cryptoCurrency == wallet.currency);
}
@action
void changeCryptoCurrencyAddress(String address) => cryptoCurrencyAddress = address;
@action
Future<void> changeFiatAmount({required String amount}) async {
fiatAmount = amount;
if (amount.isEmpty) {
fiatAmount = '';
cryptoAmount = '';
return;
}
final enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0;
if (!isReadyToTrade) {
cryptoAmount = S.current.fetching;
return;
}
if (bestRateQuote != null) {
_cryptoNumberFormat.maximumFractionDigits = cryptoCurrency.decimals;
cryptoAmount = _cryptoNumberFormat
.format(enteredAmount / bestRateQuote!.rate)
.toString()
.replaceAll(RegExp('\\,'), '');
} else {
await calculateBestRate();
}
}
@action
Future<void> changeCryptoAmount({required String amount}) async {
cryptoAmount = amount;
if (amount.isEmpty) {
fiatAmount = '';
cryptoAmount = '';
return;
}
final enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0;
if (!isReadyToTrade) {
fiatAmount = S.current.fetching;
}
if (bestRateQuote != null) {
fiatAmount = _cryptoNumberFormat
.format(enteredAmount * bestRateQuote!.rate)
.toString()
.replaceAll(RegExp('\\,'), '');
} else {
await calculateBestRate();
}
}
@action
void changeOption(SelectableOption option) {
if (option is Quote) {
sortedRecommendedQuotes.forEach((element) => element.setIsSelected = false);
sortedQuotes.forEach((element) => element.setIsSelected = false);
option.setIsSelected = true;
selectedQuote = option;
} else if (option is PaymentMethod) {
paymentMethods.forEach((element) => element.isSelected = false);
option.isSelected = true;
selectedPaymentMethod = option;
} else {
throw ArgumentError('Unknown option type');
}
}
void onTapChoseProvider(BuildContext context) async {
final initialQuotes = List<Quote>.from(sortedRecommendedQuotes + sortedQuotes);
await calculateBestRate();
final newQuotes = (sortedRecommendedQuotes + sortedQuotes);
for (var quote in newQuotes) quote.limits = null;
final newQuoteProviders = newQuotes
.map((quote) => quote.provider.isAggregator ? quote.rampName : quote.provider.title)
.toSet();
final outOfLimitQuotes = initialQuotes.where((initialQuote) {
return !newQuoteProviders.contains(
initialQuote.provider.isAggregator ? initialQuote.rampName : initialQuote.provider.title);
}).map((missingQuote) {
final quote = Quote(
rate: missingQuote.rate,
feeAmount: missingQuote.feeAmount,
networkFee: missingQuote.networkFee,
transactionFee: missingQuote.transactionFee,
payout: missingQuote.payout,
rampId: missingQuote.rampId,
rampName: missingQuote.rampName,
rampIconPath: missingQuote.rampIconPath,
paymentType: missingQuote.paymentType,
quoteId: missingQuote.quoteId,
recommendations: missingQuote.recommendations,
provider: missingQuote.provider,
isBuyAction: missingQuote.isBuyAction,
limits: missingQuote.limits,
);
quote.setFiatCurrency = missingQuote.fiatCurrency;
quote.setCryptoCurrency = missingQuote.cryptoCurrency;
return quote;
}).toList();
final updatedQuoteOptions = List<SelectableItem>.from([
OptionTitle(title: 'Recommended'),
...sortedRecommendedQuotes,
if (sortedQuotes.isNotEmpty) OptionTitle(title: 'All Providers'),
...sortedQuotes,
if (outOfLimitQuotes.isNotEmpty) OptionTitle(title: 'Out of Limits'),
...outOfLimitQuotes,
]);
await Navigator.of(context).pushNamed(
Routes.buyOptionsPage,
arguments: [
updatedQuoteOptions,
changeOption,
launchTrade,
],
).then((value) => calculateBestRate());
}
void _onPairChange() {
_initialize();
}
void _setProviders() =>
providerList = isBuyAction ? availableBuyProviders : availableSellProviders;
Future<void> _initialize() async {
_setProviders();
cryptoAmount = '';
fiatAmount = '';
cryptoCurrencyAddress = _getInitialCryptoCurrencyAddress();
paymentMethodState = InitialPaymentMethod();
buySellQuotState = InitialBuySellQuotState();
await _getAvailablePaymentTypes();
await calculateBestRate();
}
String _getInitialCryptoCurrencyAddress() {
return cryptoCurrency == wallet.currency ? wallet.walletAddresses.address : '';
}
@action
Future<void> _getAvailablePaymentTypes() async {
paymentMethodState = PaymentMethodLoading();
selectedPaymentMethod = null;
final result = await Future.wait(providerList.map((element) => element
.getAvailablePaymentTypes(fiatCurrency.title, cryptoCurrency.title, isBuyAction)
.timeout(
Duration(seconds: 10),
onTimeout: () => [],
)));
final Map<PaymentType, PaymentMethod> uniquePaymentMethods = {};
for (var methods in result) {
for (var method in methods) {
uniquePaymentMethods[method.paymentMethodType] = method;
}
}
paymentMethods = ObservableList<PaymentMethod>.of(uniquePaymentMethods.values);
if (paymentMethods.isNotEmpty) {
paymentMethods.insert(0, PaymentMethod.all());
selectedPaymentMethod = paymentMethods.first;
selectedPaymentMethod!.isSelected = true;
paymentMethodState = PaymentMethodLoaded();
} else {
paymentMethodState = PaymentMethodFailed();
}
}
@action
Future<void> calculateBestRate() async {
buySellQuotState = BuySellQuotLoading();
final result = await Future.wait<List<Quote>?>(providerList.map((element) => element
.fetchQuote(
cryptoCurrency: cryptoCurrency,
fiatCurrency: fiatCurrency,
amount: amount,
paymentType: selectedPaymentMethod?.paymentMethodType,
isBuyAction: isBuyAction,
walletAddress: wallet.walletAddresses.address,
)
.timeout(
Duration(seconds: 10),
onTimeout: () => null,
)));
sortedRecommendedQuotes.clear();
sortedQuotes.clear();
final validQuotes = result
.where((element) => element != null && element.isNotEmpty)
.expand((element) => element!)
.toList();
if (validQuotes.isEmpty) {
buySellQuotState = BuySellQuotFailed();
return;
}
validQuotes.sort((a, b) => a.rate.compareTo(b.rate));
final Set<String> addedProviders = {};
final List<Quote> uniqueProviderQuotes = validQuotes.where((element) {
if (addedProviders.contains(element.provider.title)) return false;
addedProviders.add(element.provider.title);
return true;
}).toList();
sortedRecommendedQuotes.addAll(uniqueProviderQuotes);
sortedQuotes = ObservableList.of(
validQuotes.where((element) => !uniqueProviderQuotes.contains(element)).toList());
if (sortedRecommendedQuotes.isNotEmpty) {
sortedRecommendedQuotes.first
..setIsBestRate = true
..recommendations.insert(0, ProviderRecommendation.bestRate);
bestRateQuote = sortedRecommendedQuotes.first;
sortedRecommendedQuotes.sort((a, b) {
if (a.provider is OnRamperBuyProvider) return -1;
if (b.provider is OnRamperBuyProvider) return 1;
return 0;
});
selectedQuote = sortedRecommendedQuotes.first;
sortedRecommendedQuotes.first.setIsSelected = true;
}
buySellQuotState = BuySellQuotLoaded();
}
@action
Future<void> launchTrade(BuildContext context) async {
final provider = selectedQuote!.provider;
await provider.launchProvider(
context: context,
quote: selectedQuote!,
amount: amount,
isBuyAction: isBuyAction,
cryptoCurrencyAddress: cryptoCurrencyAddress,
);
}
}

View file

@ -71,8 +71,7 @@ abstract class DashboardViewModelBase with Store {
required this.anonpayTransactionsStore,
required this.sharedPreferences,
required this.keyService})
: hasSellAction = false,
hasBuyAction = false,
: hasTradeAction = false,
hasExchangeAction = false,
isShowFirstYatIntroduction = false,
isShowSecondYatIntroduction = false,
@ -521,37 +520,8 @@ abstract class DashboardViewModelBase with Store {
Map<String, List<FilterItem>> filterItems;
BuyProvider? get defaultBuyProvider => ProvidersHelper.getProviderByType(
settingsStore.defaultBuyProviders[wallet.type] ?? ProviderType.askEachTime);
BuyProvider? get defaultSellProvider => ProvidersHelper.getProviderByType(
settingsStore.defaultSellProviders[wallet.type] ?? ProviderType.askEachTime);
bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled;
List<BuyProvider> get availableBuyProviders {
final providerTypes = ProvidersHelper.getAvailableBuyProviderTypes(wallet.type);
return providerTypes
.map((type) => ProvidersHelper.getProviderByType(type))
.where((provider) => provider != null)
.cast<BuyProvider>()
.toList();
}
bool get hasBuyProviders => ProvidersHelper.getAvailableBuyProviderTypes(wallet.type).isNotEmpty;
List<BuyProvider> get availableSellProviders {
final providerTypes = ProvidersHelper.getAvailableSellProviderTypes(wallet.type);
return providerTypes
.map((type) => ProvidersHelper.getProviderByType(type))
.where((provider) => provider != null)
.cast<BuyProvider>()
.toList();
}
bool get hasSellProviders =>
ProvidersHelper.getAvailableSellProviderTypes(wallet.type).isNotEmpty;
bool get shouldShowYatPopup => settingsStore.shouldShowYatPopup;
@action
@ -564,16 +534,10 @@ abstract class DashboardViewModelBase with Store {
bool hasExchangeAction;
@computed
bool get isEnabledBuyAction => !settingsStore.disableBuy && hasBuyProviders;
bool get isEnabledTradeAction => !settingsStore.disableTradeOption;
@observable
bool hasBuyAction;
@computed
bool get isEnabledSellAction => !settingsStore.disableSell && hasSellProviders;
@observable
bool hasSellAction;
bool hasTradeAction;
@computed
bool get isEnabledBulletinAction => !settingsStore.disableBulletin;
@ -776,8 +740,7 @@ abstract class DashboardViewModelBase with Store {
void updateActions() {
hasExchangeAction = !isHaven;
hasBuyAction = !isHaven;
hasSellAction = !isHaven;
hasTradeAction = !isHaven;
}
@computed

View file

@ -65,29 +65,6 @@ abstract class OtherSettingsViewModelBase with Store {
_wallet.type == WalletType.solana ||
_wallet.type == WalletType.tron);
@computed
bool get isEnabledBuyAction =>
!_settingsStore.disableBuy && _wallet.type != WalletType.haven;
@computed
bool get isEnabledSellAction =>
!_settingsStore.disableSell && _wallet.type != WalletType.haven;
List<ProviderType> get availableBuyProvidersTypes {
return ProvidersHelper.getAvailableBuyProviderTypes(walletType);
}
List<ProviderType> get availableSellProvidersTypes =>
ProvidersHelper.getAvailableSellProviderTypes(walletType);
ProviderType get buyProviderType =>
_settingsStore.defaultBuyProviders[walletType] ??
ProviderType.askEachTime;
ProviderType get sellProviderType =>
_settingsStore.defaultSellProviders[walletType] ??
ProviderType.askEachTime;
String getDisplayPriority(dynamic priority) {
final _priority = priority as TransactionPriority;
@ -115,20 +92,6 @@ abstract class OtherSettingsViewModelBase with Store {
return priority.toString();
}
String getBuyProviderType(dynamic buyProviderType) {
final _buyProviderType = buyProviderType as ProviderType;
return _buyProviderType == ProviderType.askEachTime
? S.current.ask_each_time
: _buyProviderType.title;
}
String getSellProviderType(dynamic sellProviderType) {
final _sellProviderType = sellProviderType as ProviderType;
return _sellProviderType == ProviderType.askEachTime
? S.current.ask_each_time
: _sellProviderType.title;
}
void onDisplayPrioritySelected(TransactionPriority priority) =>
_settingsStore.priority[walletType] = priority;
@ -157,12 +120,4 @@ abstract class OtherSettingsViewModelBase with Store {
}
return null;
}
@action
ProviderType onBuyProviderTypeSelected(ProviderType buyProviderType) =>
_settingsStore.defaultBuyProviders[walletType] = buyProviderType;
@action
ProviderType onSellProviderTypeSelected(ProviderType sellProviderType) =>
_settingsStore.defaultSellProviders[walletType] = sellProviderType;
}

View file

@ -59,10 +59,7 @@ abstract class PrivacySettingsViewModelBase with Store {
bool get isAppSecure => _settingsStore.isAppSecure;
@computed
bool get disableBuy => _settingsStore.disableBuy;
@computed
bool get disableSell => _settingsStore.disableSell;
bool get disableTradeOption => _settingsStore.disableTradeOption;
@computed
bool get disableBulletin => _settingsStore.disableBulletin;
@ -119,10 +116,7 @@ abstract class PrivacySettingsViewModelBase with Store {
void setIsAppSecure(bool value) => _settingsStore.isAppSecure = value;
@action
void setDisableBuy(bool value) => _settingsStore.disableBuy = value;
@action
void setDisableSell(bool value) => _settingsStore.disableSell = value;
void setDisableTradeOption(bool value) => _settingsStore.disableTradeOption = value;
@action
void setDisableBulletin(bool value) => _settingsStore.disableBulletin = value;

View file

@ -123,6 +123,8 @@
"change_rep_successful": "تم تغيير ممثل بنجاح",
"change_wallet_alert_content": "هل تريد تغيير المحفظة الحالية إلى ${wallet_name}؟",
"change_wallet_alert_title": "تغيير المحفظة الحالية",
"choose_a_payment_method": "اختر طريقة الدفع",
"choose_a_provider": "اختر مزودًا",
"choose_account": "اختر حساب",
"choose_address": "\n\nالرجاء اختيار عنوان:",
"choose_card_value": "اختر قيمة بطاقة",
@ -217,6 +219,7 @@
"disable_fee_api_warning": "من خلال إيقاف تشغيل هذا ، قد تكون معدلات الرسوم غير دقيقة في بعض الحالات ، لذلك قد ينتهي بك الأمر إلى دفع مبالغ زائدة أو دفع رسوم المعاملات الخاصة بك",
"disable_fiat": "تعطيل fiat",
"disable_sell": "قم بتعطيل إجراء البيع",
"disable_trade_option": "تعطيل خيار التجارة",
"disableBatteryOptimization": "تعطيل تحسين البطارية",
"disableBatteryOptimizationDescription": "هل تريد تعطيل تحسين البطارية من أجل جعل الخلفية مزامنة تعمل بحرية وسلاسة؟",
"disabled": "معطلة",

View file

@ -123,6 +123,8 @@
"change_rep_successful": "Успешно промени представител",
"change_wallet_alert_content": "Искате ли да смените сегашния портфейл на ${wallet_name}?",
"change_wallet_alert_title": "Смяна на сегашния портфейл",
"choose_a_payment_method": "Изберете начин на плащане",
"choose_a_provider": "Изберете доставчик",
"choose_account": "Избиране на профил",
"choose_address": "\n\nМоля, изберете адреса:",
"choose_card_value": "Изберете стойност на картата",
@ -217,6 +219,7 @@
"disable_fee_api_warning": "Като изключите това, таксите могат да бъдат неточни в някои случаи, така че може да се препланите или да не плащате таксите за вашите транзакции",
"disable_fiat": "Деактивиране на fiat",
"disable_sell": "Деактивирайте действието за продажба",
"disable_trade_option": "Деактивирайте опцията за търговия",
"disableBatteryOptimization": "Деактивирайте оптимизацията на батерията",
"disableBatteryOptimizationDescription": "Искате ли да деактивирате оптимизацията на батерията, за да направите синхронизирането на фона да работи по -свободно и гладко?",
"disabled": "Деактивирано",

View file

@ -123,6 +123,8 @@
"change_rep_successful": "Úspěšně změnil zástupce",
"change_wallet_alert_content": "Opravdu chcete změnit aktivní peněženku na ${wallet_name}?",
"change_wallet_alert_title": "Přepnout peněženku",
"choose_a_payment_method": "Vyberte metodu platby",
"choose_a_provider": "Vyberte poskytovatele",
"choose_account": "Zvolte částku",
"choose_address": "\n\nProsím vyberte adresu:",
"choose_card_value": "Vyberte hodnotu karty",
@ -217,6 +219,7 @@
"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",
"disable_fiat": "Zakázat fiat",
"disable_sell": "Zakázat akci prodeje",
"disable_trade_option": "Zakázat možnost TRADE",
"disableBatteryOptimization": "Zakázat optimalizaci baterie",
"disableBatteryOptimizationDescription": "Chcete deaktivovat optimalizaci baterie, aby se synchronizovala pozadí volně a hladce?",
"disabled": "Zakázáno",

View file

@ -123,6 +123,8 @@
"change_rep_successful": "Vertreter erfolgreich gerändert",
"change_wallet_alert_content": "Möchten Sie die aktuelle Wallet zu ${wallet_name} ändern?",
"change_wallet_alert_title": "Aktuelle Wallet ändern",
"choose_a_payment_method": "Wählen Sie eine Zahlungsmethode",
"choose_a_provider": "Wählen Sie einen Anbieter",
"choose_account": "Konto auswählen",
"choose_address": "\n\nBitte wählen Sie die Adresse:",
"choose_card_value": "Wählen Sie einen Kartenwert",
@ -217,6 +219,7 @@
"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",
"disable_fiat": "Fiat deaktivieren",
"disable_sell": "Verkaufsaktion deaktivieren",
"disable_trade_option": "Handelsoption deaktivieren",
"disableBatteryOptimization": "Batterieoptimierung deaktivieren",
"disableBatteryOptimizationDescription": "Möchten Sie die Batterieoptimierung deaktivieren, um die Hintergrundsynchronisierung reibungsloser zu gestalten?",
"disabled": "Deaktiviert",

View file

@ -123,6 +123,8 @@
"change_rep_successful": "Successfully changed representative",
"change_wallet_alert_content": "Do you want to change current wallet to ${wallet_name}?",
"change_wallet_alert_title": "Change current wallet",
"choose_a_payment_method": "Choose a payment method",
"choose_a_provider": "Choose a provider",
"choose_account": "Choose account",
"choose_address": "\n\nPlease choose the address:",
"choose_card_value": "Choose a card value",
@ -217,6 +219,7 @@
"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",
"disable_fiat": "Disable fiat",
"disable_sell": "Disable sell action",
"disable_trade_option": "Disable trade option",
"disableBatteryOptimization": "Disable Battery Optimization",
"disableBatteryOptimizationDescription": "Do you want to disable battery optimization in order to make background sync run more freely and smoothly?",
"disabled": "Disabled",

View file

@ -123,6 +123,8 @@
"change_rep_successful": "Representante cambiado con éxito",
"change_wallet_alert_content": "¿Quieres cambiar la billetera actual a ${wallet_name}?",
"change_wallet_alert_title": "Cambiar billetera actual",
"choose_a_payment_method": "Elija un método de pago",
"choose_a_provider": "Elija un proveedor",
"choose_account": "Elegir cuenta",
"choose_address": "\n\nPor favor elija la dirección:",
"choose_card_value": "Elige un valor de tarjeta",
@ -217,6 +219,7 @@
"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",
"disable_fiat": "Deshabilitar fiat",
"disable_sell": "Desactivar acción de venta",
"disable_trade_option": "Deshabilitar la opción de comercio",
"disableBatteryOptimization": "Deshabilitar la optimización de la batería",
"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?",
"disabled": "Desactivado",

View file

@ -123,6 +123,8 @@
"change_rep_successful": "Représentant changé avec succès",
"change_wallet_alert_content": "Souhaitez-vous changer le portefeuille (wallet) actuel vers ${wallet_name} ?",
"change_wallet_alert_title": "Changer le portefeuille (wallet) actuel",
"choose_a_payment_method": "Choisissez un mode de paiement",
"choose_a_provider": "Choisissez un fournisseur",
"choose_account": "Choisir le compte",
"choose_address": "\n\nMerci de choisir l'adresse :",
"choose_card_value": "Choisissez une valeur de carte",
@ -217,6 +219,7 @@
"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",
"disable_fiat": "Désactiver les montants en fiat",
"disable_sell": "Désactiver l'action de vente",
"disable_trade_option": "Désactiver l'option de commerce",
"disableBatteryOptimization": "Désactiver l'optimisation de la batterie",
"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?",
"disabled": "Désactivé",

View file

@ -123,6 +123,8 @@
"change_rep_successful": "An samu nasarar canzawa wakilin",
"change_wallet_alert_content": "Kana so ka canja walat yanzu zuwa ${wallet_name}?",
"change_wallet_alert_title": "Canja walat yanzu",
"choose_a_payment_method": "Zabi hanyar biyan kuɗi",
"choose_a_provider": "Zabi mai bada",
"choose_account": "Zaɓi asusu",
"choose_address": "\n\n Da fatan za a zaɓi adireshin:",
"choose_card_value": "Zabi darajar katin",
@ -217,6 +219,7 @@
"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",
"disable_fiat": "Dakatar da fiat",
"disable_sell": "Kashe karbuwa",
"disable_trade_option": "Musaki zaɓi na kasuwanci",
"disableBatteryOptimization": "Kashe ingantawa baturi",
"disableBatteryOptimizationDescription": "Shin kana son kashe ingantawa baturi don yin setnc bankwali gudu da yar kyauta da kyau?",
"disabled": "tsaya",

View file

@ -123,6 +123,8 @@
"change_rep_successful": "सफलतापूर्वक बदलकर प्रतिनिधि",
"change_wallet_alert_content": "क्या आप करंट वॉलेट को बदलना चाहते हैं ${wallet_name}?",
"change_wallet_alert_title": "वर्तमान बटुआ बदलें",
"choose_a_payment_method": "एक भुगतान विधि का चयन करें",
"choose_a_provider": "एक प्रदाता चुनें",
"choose_account": "खाता चुनें",
"choose_address": "\n\nकृपया पता चुनें:",
"choose_card_value": "एक कार्ड मूल्य चुनें",
@ -217,6 +219,7 @@
"disable_fee_api_warning": "इसे बंद करने से, कुछ मामलों में शुल्क दरें गलत हो सकती हैं, इसलिए आप अपने लेनदेन के लिए फीस को कम कर सकते हैं या कम कर सकते हैं",
"disable_fiat": "िएट को अक्षम करें",
"disable_sell": "बेचने की कार्रवाई अक्षम करें",
"disable_trade_option": "व्यापार विकल्प अक्षम करें",
"disableBatteryOptimization": "बैटरी अनुकूलन अक्षम करें",
"disableBatteryOptimizationDescription": "क्या आप बैकग्राउंड सिंक को अधिक स्वतंत्र और सुचारू रूप से चलाने के लिए बैटरी ऑप्टिमाइज़ेशन को अक्षम करना चाहते हैं?",
"disabled": "अक्षम",

View file

@ -123,6 +123,8 @@
"change_rep_successful": "Uspješno promijenjena reprezentativna",
"change_wallet_alert_content": "Želite li promijeniti trenutni novčanik u ${wallet_name}?",
"change_wallet_alert_title": "Izmijeni trenutni novčanik",
"choose_a_payment_method": "Odaberite način plaćanja",
"choose_a_provider": "Odaberite davatelja usluga",
"choose_account": "Odaberi račun",
"choose_address": "\n\nOdaberite adresu:",
"choose_card_value": "Odaberite vrijednost kartice",
@ -217,6 +219,7 @@
"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",
"disable_fiat": "Isključi, fiat",
"disable_sell": "Onemogući akciju prodaje",
"disable_trade_option": "Onemogući trgovinsku opciju",
"disableBatteryOptimization": "Onemogući optimizaciju baterije",
"disableBatteryOptimizationDescription": "Želite li onemogućiti optimizaciju baterije kako bi se pozadinska sinkronizacija radila slobodnije i glatko?",
"disabled": "Onemogućeno",

View file

@ -123,6 +123,8 @@
"change_rep_successful": "Ներկայացուցչի փոփոխությունը հաջողությամբ կատարվեց",
"change_wallet_alert_content": "Ցանկանում եք փոխել ընթացիկ դրամապանակը ${wallet_name}?",
"change_wallet_alert_title": "Փոխել ընթացիկ դրամապանակը",
"choose_a_payment_method": "Ընտրեք վճարման եղանակ",
"choose_a_provider": "Ընտրեք մատակարար",
"choose_account": "Ընտրեք հաշիվը",
"choose_address": "\n\nԽնդրում ենք ընտրեք հասցեն",
"choose_card_value": "Ընտրեք քարտի արժեք",
@ -217,6 +219,7 @@
"disable_fee_api_warning": "Դրանից անջատելով, վճարների տեմպերը որոշ դեպքերում կարող են անճիշտ լինել, այնպես որ դուք կարող եք վերջ տալ ձեր գործարքների համար վճարների գերավճարների կամ գերավճարների վրա",
"disable_fiat": "Անջատել ֆիատ",
"disable_sell": "Անջատել վաճառք գործողությունը",
"disable_trade_option": "Անջատեք առեւտրի տարբերակը",
"disableBatteryOptimization": "Անջատել մարտկոցի օպտիմիզացիան",
"disableBatteryOptimizationDescription": "Դուք ցանկանում եք անջատել մարտկոցի օպտիմիզացիան ֆոնային համաժամացման ավելի ազատ և հարթ ընթացքի համար?",
"disabled": "Անջատված",

View file

@ -123,6 +123,8 @@
"change_rep_successful": "Berhasil mengubah perwakilan",
"change_wallet_alert_content": "Apakah Anda ingin mengganti dompet saat ini ke ${wallet_name}?",
"change_wallet_alert_title": "Ganti dompet saat ini",
"choose_a_payment_method": "Pilih metode pembayaran",
"choose_a_provider": "Pilih penyedia",
"choose_account": "Pilih akun",
"choose_address": "\n\nSilakan pilih alamat:",
"choose_card_value": "Pilih nilai kartu",
@ -217,6 +219,7 @@
"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",
"disable_fiat": "Nonaktifkan fiat",
"disable_sell": "Nonaktifkan aksi jual",
"disable_trade_option": "Nonaktifkan opsi perdagangan",
"disableBatteryOptimization": "Nonaktifkan optimasi baterai",
"disableBatteryOptimizationDescription": "Apakah Anda ingin menonaktifkan optimasi baterai untuk membuat sinkronisasi latar belakang berjalan lebih bebas dan lancar?",
"disabled": "Dinonaktifkan",

View file

@ -123,6 +123,8 @@
"change_rep_successful": "Rappresentante modificato con successo",
"change_wallet_alert_content": "Sei sicuro di voler cambiare il portafoglio attuale con ${wallet_name}?",
"change_wallet_alert_title": "Cambia portafoglio attuale",
"choose_a_payment_method": "Scegli un metodo di pagamento",
"choose_a_provider": "Scegli un fornitore",
"choose_account": "Scegli account",
"choose_address": "\n\nSi prega di scegliere l'indirizzo:",
"choose_card_value": "Scegli un valore della carta",
@ -218,6 +220,7 @@
"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",
"disable_fiat": "Disabilita fiat",
"disable_sell": "Disabilita l'azione di vendita",
"disable_trade_option": "Disabilita l'opzione commerciale",
"disableBatteryOptimization": "Disabilita l'ottimizzazione della batteria",
"disableBatteryOptimizationDescription": "Vuoi disabilitare l'ottimizzazione della batteria per far funzionare la sincronizzazione in background più libera e senza intoppi?",
"disabled": "Disabilitato",

View file

@ -123,6 +123,8 @@
"change_rep_successful": "代表者の変更に成功しました",
"change_wallet_alert_content": "現在のウォレットをに変更しますか ${wallet_name}?",
"change_wallet_alert_title": "現在のウォレットを変更する",
"choose_a_payment_method": "支払い方法を選択します",
"choose_a_provider": "プロバイダーを選択します",
"choose_account": "アカウントを選択",
"choose_address": "\n\n住所を選択してください",
"choose_card_value": "カード値を選択します",
@ -217,6 +219,7 @@
"disable_fee_api_warning": "これをオフにすることで、料金金利は場合によっては不正確になる可能性があるため、取引の費用が過払いまたは不足している可能性があります",
"disable_fiat": "フィアットを無効にする",
"disable_sell": "販売アクションを無効にする",
"disable_trade_option": "取引オプションを無効にします",
"disableBatteryOptimization": "バッテリーの最適化を無効にします",
"disableBatteryOptimizationDescription": "バックグラウンドシンクをより自由かつスムーズに実行するために、バッテリーの最適化を無効にしたいですか?",
"disabled": "無効",

View file

@ -123,6 +123,8 @@
"change_rep_successful": "대리인이 성공적으로 변경되었습니다",
"change_wallet_alert_content": "현재 지갑을 다음으로 변경 하시겠습니까 ${wallet_name}?",
"change_wallet_alert_title": "현재 지갑 변경",
"choose_a_payment_method": "결제 방법을 선택하십시오",
"choose_a_provider": "제공자를 선택하십시오",
"choose_account": "계정을 선택하십시오",
"choose_address": "\n\n주소를 선택하십시오:",
"choose_card_value": "카드 값을 선택하십시오",
@ -217,6 +219,7 @@
"disable_fee_api_warning": "이것을 끄면 경우에 따라 수수료가 부정확 할 수 있으므로 거래 수수료를 초과 지불하거나 지불 할 수 있습니다.",
"disable_fiat": "법정화폐 비활성화",
"disable_sell": "판매 조치 비활성화",
"disable_trade_option": "거래 옵션 비활성화",
"disableBatteryOptimization": "배터리 최적화를 비활성화합니다",
"disableBatteryOptimizationDescription": "백그라운드 동기화를보다 자유롭고 매끄럽게 실행하기 위해 배터리 최적화를 비활성화하고 싶습니까?",
"disabled": "장애가 있는",

View file

@ -123,6 +123,8 @@
"change_rep_successful": "အောင်မြင်စွာကိုယ်စားလှယ်ပြောင်းလဲသွားတယ်",
"change_wallet_alert_content": "လက်ရှိပိုက်ဆံအိတ်ကို ${wallet_name} သို့ ပြောင်းလိုပါသလား။",
"change_wallet_alert_title": "လက်ရှိပိုက်ဆံအိတ်ကို ပြောင်းပါ။",
"choose_a_payment_method": "ငွေပေးချေမှုနည်းလမ်းကိုရွေးချယ်ပါ",
"choose_a_provider": "ပံ့ပိုးပေးရွေးချယ်ပါ",
"choose_account": "အကောင့်ကို ရွေးပါ။",
"choose_address": "\n\nလိပ်စာကို ရွေးပါ-",
"choose_card_value": "ကဒ်တန်ဖိုးတစ်ခုရွေးပါ",
@ -217,6 +219,7 @@
"disable_fee_api_warning": "ဤအရာကိုဖွင့်ခြင်းအားဖြင့်အချို့သောကိစ္စရပ်များတွင်အခကြေးငွေနှုန်းထားများသည်တိကျမှုရှိနိုင်သည်,",
"disable_fiat": "Fiat ကိုပိတ်ပါ။",
"disable_sell": "ရောင်းချခြင်းလုပ်ဆောင်ချက်ကို ပိတ်ပါ။",
"disable_trade_option": "ကုန်သွယ်ရေး option ကိုပိတ်ပါ",
"disableBatteryOptimization": "ဘက်ထရီ optimization ကိုပိတ်ပါ",
"disableBatteryOptimizationDescription": "နောက်ခံထပ်တူပြုခြင်းနှင့်ချောချောမွေ့မွေ့ပြုလုပ်နိုင်ရန်ဘက်ထရီ optimization ကိုသင်ပိတ်ထားလိုပါသလား။",
"disabled": "မသန်စွမ်း",

View file

@ -123,6 +123,8 @@
"change_rep_successful": "Met succes veranderde vertegenwoordiger",
"change_wallet_alert_content": "Wilt u de huidige portemonnee wijzigen in ${wallet_name}?",
"change_wallet_alert_title": "Wijzig huidige portemonnee",
"choose_a_payment_method": "Kies een betaalmethode",
"choose_a_provider": "Kies een provider",
"choose_account": "Kies account",
"choose_address": "\n\nKies het adres:",
"choose_card_value": "Kies een kaartwaarde",
@ -217,6 +219,7 @@
"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",
"disable_fiat": "Schakel Fiat uit",
"disable_sell": "Verkoopactie uitschakelen",
"disable_trade_option": "Schakel handelsoptie uit",
"disableBatteryOptimization": "Schakel de batterijoptimalisatie uit",
"disableBatteryOptimizationDescription": "Wilt u de optimalisatie van de batterij uitschakelen om achtergrondsynchronisatie te laten werken, vrijer en soepeler?",
"disabled": "Gehandicapt",

View file

@ -123,6 +123,8 @@
"change_rep_successful": "Pomyślnie zmienił przedstawiciela",
"change_wallet_alert_content": "Czy chcesz zmienić obecny portfel na ${wallet_name}?",
"change_wallet_alert_title": "Zmień obecny portfel",
"choose_a_payment_method": "Wybierz metodę płatności",
"choose_a_provider": "Wybierz dostawcę",
"choose_account": "Wybierz konto",
"choose_address": "\n\nWybierz adres:",
"choose_card_value": "Wybierz wartość karty",
@ -217,6 +219,7 @@
"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",
"disable_fiat": "Wyłącz waluty FIAT",
"disable_sell": "Wyłącz akcję sprzedaży",
"disable_trade_option": "Wyłącz opcję handlu",
"disableBatteryOptimization": "Wyłącz optymalizację baterii",
"disableBatteryOptimizationDescription": "Czy chcesz wyłączyć optymalizację baterii, aby synchronizacja tła działała swobodniej i płynnie?",
"disabled": "Wyłączone",

View file

@ -123,6 +123,8 @@
"change_rep_successful": "Mudou com sucesso o representante",
"change_wallet_alert_content": "Quer mudar a carteira atual para ${wallet_name}?",
"change_wallet_alert_title": "Alterar carteira atual",
"choose_a_payment_method": "Escolha um método de pagamento",
"choose_a_provider": "Escolha um provedor",
"choose_account": "Escolha uma conta",
"choose_address": "\n\nEscolha o endereço:",
"choose_card_value": "Escolha um valor de cartão",
@ -217,6 +219,7 @@
"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",
"disable_fiat": "Desativar fiat",
"disable_sell": "Desativar ação de venda",
"disable_trade_option": "Desativar a opção comercial",
"disableBatteryOptimization": "Desative a otimização da bateria",
"disableBatteryOptimizationDescription": "Deseja desativar a otimização da bateria para fazer a sincronização de fundo funcionar de forma mais livre e suave?",
"disabled": "Desabilitado",

View file

@ -123,6 +123,8 @@
"change_rep_successful": "Успешно изменил представитель",
"change_wallet_alert_content": "Вы хотите изменить текущий кошелек на ${wallet_name}?",
"change_wallet_alert_title": "Изменить текущий кошелек",
"choose_a_payment_method": "Выберите способ оплаты",
"choose_a_provider": "Выберите поставщика",
"choose_account": "Выберите аккаунт",
"choose_address": "\n\nПожалуйста, выберите адрес:",
"choose_card_value": "Выберите значение карты",
@ -217,6 +219,7 @@
"disable_fee_api_warning": "Выключив это, в некоторых случаях ставки платы могут быть неточными, так что вы можете в конечном итоге переплачивать или недоплачивать сборы за ваши транзакции",
"disable_fiat": "Отключить фиат",
"disable_sell": "Отключить действие продажи",
"disable_trade_option": "Отключить возможность торговли",
"disableBatteryOptimization": "Отключить оптимизацию батареи",
"disableBatteryOptimizationDescription": "Вы хотите отключить оптимизацию батареи, чтобы сделать фона синхронизации более свободно и плавно?",
"disabled": "Отключено",

View file

@ -123,6 +123,8 @@
"change_rep_successful": "เปลี่ยนตัวแทนสำเร็จ",
"change_wallet_alert_content": "คุณต้องการเปลี่ยนกระเป๋าปัจจุบันเป็น ${wallet_name} หรือไม่?",
"change_wallet_alert_title": "เปลี่ยนกระเป๋าปัจจุบัน",
"choose_a_payment_method": "เลือกวิธีการชำระเงิน",
"choose_a_provider": "เลือกผู้ให้บริการ",
"choose_account": "เลือกบัญชี",
"choose_address": "\n\nโปรดเลือกที่อยู่:",
"choose_card_value": "เลือกค่าบัตร",
@ -217,6 +219,7 @@
"disable_fee_api_warning": "โดยการปิดสิ่งนี้อัตราค่าธรรมเนียมอาจไม่ถูกต้องในบางกรณีดังนั้นคุณอาจจบลงด้วยการจ่ายเงินมากเกินไปหรือจ่ายค่าธรรมเนียมสำหรับการทำธุรกรรมของคุณมากเกินไป",
"disable_fiat": "ปิดใช้งานสกุลเงินตรา",
"disable_sell": "ปิดการใช้งานการขาย",
"disable_trade_option": "ปิดใช้งานตัวเลือกการค้า",
"disableBatteryOptimization": "ปิดใช้งานการเพิ่มประสิทธิภาพแบตเตอรี่",
"disableBatteryOptimizationDescription": "คุณต้องการปิดใช้งานการเพิ่มประสิทธิภาพแบตเตอรี่เพื่อให้การซิงค์พื้นหลังทำงานได้อย่างอิสระและราบรื่นมากขึ้นหรือไม่?",
"disabled": "ปิดใช้งาน",

View file

@ -123,6 +123,8 @@
"change_rep_successful": "Matagumpay na nagbago ng representative",
"change_wallet_alert_content": "Gusto mo bang palitan ang kasalukuyang wallet sa ${wallet_name}?",
"change_wallet_alert_title": "Baguhin ang kasalukuyang wallet",
"choose_a_payment_method": "Pumili ng isang paraan ng pagbabayad",
"choose_a_provider": "Pumili ng isang provider",
"choose_account": "Pumili ng account",
"choose_address": "Mangyaring piliin ang address:",
"choose_card_value": "Pumili ng isang halaga ng card",
@ -217,6 +219,7 @@
"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",
"disable_fiat": "Huwag paganahin ang fiat",
"disable_sell": "Huwag paganahin ang pagkilos ng pagbebenta",
"disable_trade_option": "Huwag paganahin ang pagpipilian sa kalakalan",
"disableBatteryOptimization": "Huwag Paganahin ang Pag-optimize ng Baterya",
"disableBatteryOptimizationDescription": "Nais mo bang huwag paganahin ang pag-optimize ng baterya upang gawing mas malaya at maayos ang background sync?",
"disabled": "Hindi pinagana",

View file

@ -123,6 +123,8 @@
"change_rep_successful": "Temsilciyi başarıyla değiştirdi",
"change_wallet_alert_content": "Şimdiki cüzdanı ${wallet_name} cüzdanı ile değiştirmek istediğinden emin misin?",
"change_wallet_alert_title": "Şimdiki cüzdanı değiştir",
"choose_a_payment_method": "Bir Ödeme Yöntemi Seçin",
"choose_a_provider": "Bir Sağlayıcı Seçin",
"choose_account": "Hesabı seç",
"choose_address": "\n\nLütfen adresi seçin:",
"choose_card_value": "Bir kart değeri seçin",
@ -217,6 +219,7 @@
"disable_fee_api_warning": "Bunu kapatarak, ücret oranları bazı durumlarda yanlış olabilir, bu nedenle işlemleriniz için ücretleri fazla ödeyebilir veya az ödeyebilirsiniz.",
"disable_fiat": "İtibari paraları devre dışı bırak",
"disable_sell": "Satış işlemini devre dışı bırak",
"disable_trade_option": "Ticaret seçeneğini devre dışı bırakın",
"disableBatteryOptimization": "Pil optimizasyonunu devre dışı bırakın",
"disableBatteryOptimizationDescription": "Arka plan senkronizasyonunu daha özgür ve sorunsuz bir şekilde çalıştırmak için pil optimizasyonunu devre dışı bırakmak istiyor musunuz?",
"disabled": "Devre dışı",

View file

@ -123,6 +123,8 @@
"change_rep_successful": "Успішно змінив представник",
"change_wallet_alert_content": "Ви хочете змінити поточний гаманець на ${wallet_name}?",
"change_wallet_alert_title": "Змінити поточний гаманець",
"choose_a_payment_method": "Виберіть метод оплати",
"choose_a_provider": "Виберіть постачальника",
"choose_account": "Оберіть акаунт",
"choose_address": "\n\nБудь ласка, оберіть адресу:",
"choose_card_value": "Виберіть значення картки",
@ -185,7 +187,7 @@
"creating_new_wallet_error": "Помилка: ${description}",
"creation_date": "Дата створення",
"custom": "на замовлення",
"custom_drag": "На замовлення (утримуйте та перетягується)",
"custom_drag": "На замовлення (утримуйте та перетягуйте)",
"custom_redeem_amount": "Власна сума викупу",
"custom_value": "Спеціальне значення",
"dark_theme": "Темна",
@ -206,17 +208,18 @@
"description": "опис",
"destination_tag": "Тег призначення:",
"dfx_option_description": "Купуйте криптовалюту з EUR & CHF. Для роздрібних та корпоративних клієнтів у Європі",
"didnt_get_code": "Не отримуєте код?",
"didnt_get_code": "Не отримали код?",
"digit_pin": "-значний PIN",
"digital_and_physical_card": " цифрова та фізична передплачена дебетова картка",
"disable": "Вимкнути",
"disable_bulletin": "Вимкнути статус послуги",
"disable_buy": "Вимкнути дію покупки",
"disable_cake_2fa": "Вимкнути Cake 2FA",
"disable_exchange": "Вимкнути exchange",
"disable_exchange": "Вимкнути можливість обміну",
"disable_fee_api_warning": "Вимкнувши це, ставки плати в деяких випадках можуть бути неточними, тому ви можете переплатити або недооплатити плату за свої транзакції",
"disable_fiat": "Вимкнути фиат",
"disable_sell": "Вимкнути дію продажу",
"disable_trade_option": "Вимкнути можливість торгівлі",
"disableBatteryOptimization": "Вимкнути оптимізацію акумулятора",
"disableBatteryOptimizationDescription": "Ви хочете відключити оптимізацію акумулятора, щоб зробити фонову синхронізацію більш вільно та плавно?",
"disabled": "Вимкнено",
@ -226,14 +229,14 @@
"do_not_have_enough_gas_asset": "У вас недостатньо ${currency}, щоб здійснити трансакцію з поточними умовами мережі блокчейн. Вам потрібно більше ${currency}, щоб сплатити комісію мережі блокчейн, навіть якщо ви надсилаєте інший актив.",
"do_not_send": "Не надсилайте",
"do_not_share_warning_text": "Не діліться цим нікому, включно зі службою підтримки.\n\nВаші кошти можуть і будуть вкрадені!",
"do_not_show_me": "Не показуй мені це знову",
"do_not_show_me": "Не показувати це знову",
"domain_looks_up": "Пошук доменів",
"donation_link_details": "Деталі посилання для пожертв",
"e_sign_consent": "Згода електронного підпису",
"edit": "Редагувати",
"edit_backup_password": "Змінити пароль резервної копії",
"edit_node": "Редагувати вузол",
"edit_token": "Редагувати маркер",
"edit_token": "Редагувати токен",
"electrum_address_disclaimer": "Ми створюємо нові адреси щоразу, коли ви використовуєте їх, але попередні адреси продовжують працювати",
"email_address": "Адреса електронної пошти",
"enable": "Ввімкнути",

View file

@ -123,6 +123,8 @@
"change_rep_successful": "نمائندہ کو کامیابی کے ساتھ تبدیل کیا",
"change_wallet_alert_content": "کیا آپ موجودہ والیٹ کو ${wallet_name} میں تبدیل کرنا چاہتے ہیں؟",
"change_wallet_alert_title": "موجودہ پرس تبدیل کریں۔",
"choose_a_payment_method": "ادائیگی کا طریقہ منتخب کریں",
"choose_a_provider": "فراہم کنندہ کا انتخاب کریں",
"choose_account": "اکاؤنٹ کا انتخاب کریں۔",
"choose_address": "\\n\\nبراہ کرم پتہ منتخب کریں:",
"choose_card_value": "کارڈ کی قیمت کا انتخاب کریں",
@ -217,6 +219,7 @@
"disable_fee_api_warning": "اس کو بند کرنے سے ، کچھ معاملات میں فیس کی شرح غلط ہوسکتی ہے ، لہذا آپ اپنے لین دین کے لئے فیسوں کو زیادہ ادائیگی یا ادائیگی ختم کرسکتے ہیں۔",
"disable_fiat": "فیاٹ کو غیر فعال کریں۔",
"disable_sell": "فروخت کی کارروائی کو غیر فعال کریں۔",
"disable_trade_option": "تجارت کے آپشن کو غیر فعال کریں",
"disableBatteryOptimization": "بیٹری کی اصلاح کو غیر فعال کریں",
"disableBatteryOptimizationDescription": "کیا آپ پس منظر کی مطابقت پذیری کو زیادہ آزادانہ اور آسانی سے چلانے کے لئے بیٹری کی اصلاح کو غیر فعال کرنا چاہتے ہیں؟",
"disabled": "معذور",

View file

@ -218,6 +218,7 @@
"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.",
"disable_fiat": "Vô hiệu hóa tiền tệ fiat",
"disable_sell": "Vô hiệu hóa chức năng bán",
"disable_trade_option": "Tắt tùy chọn thương mại",
"disableBatteryOptimization": "Vô hiệu hóa Tối ưu hóa Pin",
"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?",
"disabled": "Đã vô hiệu hóa",

View file

@ -123,6 +123,8 @@
"change_rep_successful": "Ni ifijišẹ yipada aṣoju",
"change_wallet_alert_content": "Ṣe ẹ fẹ́ pààrọ̀ àpamọ́wọ́ yìí sí ${wallet_name}?",
"change_wallet_alert_title": "Ẹ pààrọ̀ àpamọ́wọ́ yìí",
"choose_a_payment_method": "Yan ọna isanwo kan",
"choose_a_provider": "Yan olupese",
"choose_account": "Yan àkáǹtì",
"choose_address": "\n\nẸ jọ̀wọ́ yan àdírẹ́sì:",
"choose_card_value": "Yan iye kaadi",
@ -217,6 +219,7 @@
"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ẹ",
"disable_fiat": "Pa owó tí ìjọba pàṣẹ wa lò",
"disable_sell": "Ko iṣọrọ iṣọrọ",
"disable_trade_option": "Mu aṣayan iṣowo ṣiṣẹ",
"disableBatteryOptimization": "Mu Ifasi batiri",
"disableBatteryOptimizationDescription": "Ṣe o fẹ lati mu iṣapelo batiri si lati le ṣiṣe ayẹwo ẹhin ati laisiyonu?",
"disabled": "Wọ́n tí a ti pa",

View file

@ -123,6 +123,8 @@
"change_rep_successful": "成功改变了代表",
"change_wallet_alert_content": "您是否想将当前钱包改为 ${wallet_name}?",
"change_wallet_alert_title": "更换当前钱包",
"choose_a_payment_method": "选择付款方式",
"choose_a_provider": "选择一个提供商",
"choose_account": "选择账户",
"choose_address": "\n\n請選擇地址",
"choose_card_value": "选择卡值",
@ -217,6 +219,7 @@
"disable_fee_api_warning": "通过将其关闭,在某些情况下,收费率可能不准确,因此您最终可能会超额付款或支付交易费用",
"disable_fiat": "禁用法令",
"disable_sell": "禁用卖出操作",
"disable_trade_option": "禁用贸易选项",
"disableBatteryOptimization": "禁用电池优化",
"disableBatteryOptimizationDescription": "您是否要禁用电池优化以使背景同步更加自由,平稳地运行?",
"disabled": "禁用",

View file

@ -44,6 +44,8 @@ class SecretKey {
SecretKey('cakePayApiKey', () => ''),
SecretKey('CSRFToken', () => ''),
SecretKey('authorization', () => ''),
SecretKey('meldTestApiKey', () => ''),
SecretKey('meldTestPublicKey', () => ''),
SecretKey('moneroTestWalletSeeds', () => ''),
SecretKey('moneroLegacyTestWalletSeeds ', () => ''),
SecretKey('bitcoinTestWalletSeeds', () => ''),