diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index e096545c2..3927f04ce 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -173,6 +173,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 diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 7713cc95d..fc2c0ed8f 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -156,6 +156,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 diff --git a/assets/images/apple_pay_logo.png b/assets/images/apple_pay_logo.png new file mode 100644 index 000000000..346007e3b Binary files /dev/null and b/assets/images/apple_pay_logo.png differ diff --git a/assets/images/apple_pay_round_dark.svg b/assets/images/apple_pay_round_dark.svg new file mode 100644 index 000000000..82443bfb4 --- /dev/null +++ b/assets/images/apple_pay_round_dark.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/assets/images/apple_pay_round_light.svg b/assets/images/apple_pay_round_light.svg new file mode 100644 index 000000000..2beb1248f --- /dev/null +++ b/assets/images/apple_pay_round_light.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/assets/images/bank.png b/assets/images/bank.png new file mode 100644 index 000000000..9dc68147a Binary files /dev/null and b/assets/images/bank.png differ diff --git a/assets/images/bank_dark.svg b/assets/images/bank_dark.svg new file mode 100644 index 000000000..670120796 --- /dev/null +++ b/assets/images/bank_dark.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/assets/images/bank_light.svg b/assets/images/bank_light.svg new file mode 100644 index 000000000..804716289 --- /dev/null +++ b/assets/images/bank_light.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/assets/images/buy_sell.png b/assets/images/buy_sell.png new file mode 100644 index 000000000..0fbffe56f Binary files /dev/null and b/assets/images/buy_sell.png differ diff --git a/assets/images/card.svg b/assets/images/card.svg new file mode 100644 index 000000000..95530cdc9 --- /dev/null +++ b/assets/images/card.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/card_dark.svg b/assets/images/card_dark.svg new file mode 100644 index 000000000..2e5bcf986 --- /dev/null +++ b/assets/images/card_dark.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/assets/images/dollar_coin.svg b/assets/images/dollar_coin.svg new file mode 100644 index 000000000..22218f332 --- /dev/null +++ b/assets/images/dollar_coin.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/google_pay_icon.png b/assets/images/google_pay_icon.png new file mode 100644 index 000000000..a3ca38311 Binary files /dev/null and b/assets/images/google_pay_icon.png differ diff --git a/assets/images/meld_logo.svg b/assets/images/meld_logo.svg new file mode 100644 index 000000000..1d9211d64 --- /dev/null +++ b/assets/images/meld_logo.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/revolut.png b/assets/images/revolut.png new file mode 100644 index 000000000..bbe342592 Binary files /dev/null and b/assets/images/revolut.png differ diff --git a/assets/images/skrill.svg b/assets/images/skrill.svg new file mode 100644 index 000000000..b264b57eb --- /dev/null +++ b/assets/images/skrill.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/usd_round_dark.svg b/assets/images/usd_round_dark.svg new file mode 100644 index 000000000..f329dd617 --- /dev/null +++ b/assets/images/usd_round_dark.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/images/usd_round_light.svg b/assets/images/usd_round_light.svg new file mode 100644 index 000000000..f5965c597 --- /dev/null +++ b/assets/images/usd_round_light.svg @@ -0,0 +1,2 @@ + + diff --git a/assets/images/wallet_new.png b/assets/images/wallet_new.png new file mode 100644 index 000000000..47c43bfca Binary files /dev/null and b/assets/images/wallet_new.png differ diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart index 630078949..af6037a3b 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -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; + } +} diff --git a/lib/buy/buy_provider.dart b/lib/buy/buy_provider.dart index 1a37e09b3..8e79e16b8 100644 --- a/lib/buy/buy_provider.dart +++ b/lib/buy/buy_provider.dart @@ -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 launchProvider(BuildContext context, bool? isBuyAction); + Future? launchProvider( + {required BuildContext context, + required Quote quote, + required double amount, + required bool isBuyAction, + required String cryptoCurrencyAddress, + String? countryCode}) => + null; Future requestUrl(String amount, String sourceCurrency) => throw UnimplementedError(); Future findOrderById(String id) => throw UnimplementedError(); - Future calculateAmount(String amount, String sourceCurrency) => throw UnimplementedError(); + Future calculateAmount(String amount, String sourceCurrency) => + throw UnimplementedError(); + + Future> getAvailablePaymentTypes( + String fiatCurrency, String cryptoCurrency, bool isBuyAction) async => + []; + + Future?> fetchQuote( + {required CryptoCurrency cryptoCurrency, + required FiatCurrency fiatCurrency, + required double amount, + required bool isBuyAction, + required String walletAddress, + PaymentType? paymentType, + String? countryCode}) async => + null; } diff --git a/lib/buy/buy_quote.dart b/lib/buy/buy_quote.dart new file mode 100644 index 000000000..99e22775f --- /dev/null +++ b/lib/buy/buy_quote.dart @@ -0,0 +1,236 @@ +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/provider_types.dart'; +import 'package:cw_core/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, + }) : 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 recommendations; + String? rampId; + String? rampName; + String? rampIconPath; + bool isSelected = false; + bool isBestRate = false; + bool isBuyAction; + + late Currency sourceCurrency; + late Currency destinationCurrency; + + @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 get badges => recommendations.map((e) => e.title).toList(); + + @override + String get leftSubTitle => this.rate > 0 + ? '1 ${isBuyAction ? destinationCurrency : sourceCurrency} = ${rate.toStringAsFixed(2)} ${isBuyAction ? sourceCurrency : destinationCurrency}' + : ''; + + @override + String? get rightSubTitle => ''; + + @override + String get rightSubTitleLightIconPath => provider.isAggregator ? provider.lightIcon : ''; + + @override + String get rightSubTitleDarkIconPath => provider.isAggregator ? provider.darkIcon : ''; + + String get quoteTitle => '${provider.title} - ${paymentType.name}'; + + String get formatedFee => '$feeAmount ${isBuyAction ? sourceCurrency : destinationCurrency}'; + + void set setIsSelected(bool isSelected) => this.isSelected = isSelected; + + void set setIsBestRate(bool isBestRate) => this.isBestRate = isBestRate; + + void set setSourceCurrency(Currency sourceCurrency) => this.sourceCurrency = sourceCurrency; + + void set setDestinationCurrency(Currency destinationCurrency) => + this.destinationCurrency = destinationCurrency; + + factory Quote.fromOnramperJson(Map json, + bool isBuyAction, Map 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 rampName = metaData[rampId]['displayName'] as String? ?? ''; + final rampIconPath = metaData[rampId]['svg'] as String? ?? ''; + + final recommendations = json['recommendations'] != null + ? List.from(json['recommendations'] as List) + : []; + + final enumRecommendations = recommendations + .map((e) => getRecommendationFromString(e)) + .whereType() + .toList(); + + 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, + ); + } + + factory Quote.fromMoonPayJson( + Map 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)); + 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, + ); + } + + factory Quote.fromDFXJson( + Map json, bool isBuyAction, PaymentType paymentType) { + final rate = _toDouble(json['exchangeRate']) ?? 0.0; + final fees = json['fees'] as Map; + return Quote( + rate: isBuyAction ? rate : 1 / rate, + feeAmount: _toDouble(json['feeAmount']) ?? 0.0, + networkFee: _toDouble(fees['networkFee']) ?? 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, + ); + } + + factory Quote.fromRobinhoodJson( + Map json, bool isBuyAction, PaymentType paymentType) { + final networkFee = json['networkFee'] as Map; + final processingFee = json['processingFee'] as Map; + 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, + ); + } + + factory Quote.fromMeldJson( + Map json, bool isBuyAction, PaymentType paymentType) { + final quotes = json['quotes'][0] as Map; + 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, + ); + } + + 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; + } +} diff --git a/lib/buy/dfx/dfx_buy_provider.dart b/lib/buy/dfx/dfx_buy_provider.dart index b3ed72498..8bb0648e2 100644 --- a/lib/buy/dfx/dfx_buy_provider.dart +++ b/lib/buy/dfx/dfx_buy_provider.dart @@ -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,14 +50,8 @@ 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); } } @@ -93,7 +77,8 @@ class DFXBuyProvider extends BuyProvider { // } Future auth() async { - final signMessage = await getSignature(await getSignMessage()); + final signMessage = await getSignature( + await getSignMessage()); //TODO: Sign message does not work for LTC and BCH final requestBody = jsonEncode({ 'wallet': walletName, @@ -135,8 +120,178 @@ class DFXBuyProvider extends BuyProvider { } } + Future> 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; + for (final item in data) { + if (item['name'] == fiatCurrency) return item as Map; + } + 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> 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; + } else if (responseData is Map) { + 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> getAvailablePaymentTypes( + String fiatCurrency, String cryptoCurrency, bool isBuyAction) async { + final List paymentMethods = []; + + if (isBuyAction) { + final fiatBuyCredentials = await fetchFiatCredentials(fiatCurrency); + if (fiatBuyCredentials.isNotEmpty) { + fiatBuyCredentials.forEach((key, value) { + if (key == 'limits') { + final limits = value as Map; + 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 launchProvider(BuildContext context, bool? isBuyAction) async { + Future?> 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 = 'Card'; + } + + 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) { + final paymentType = _getPaymentTypeByString(responseData['paymentMethod'] as String?); + final quote = Quote.fromDFXJson(responseData, isBuyAction, paymentType); + quote.setSourceCurrency = isBuyAction ? cryptoCurrency : fiatCurrency; + quote.setDestinationCurrency = isBuyAction ? fiatCurrency : cryptoCurrency; + return [quote]; + } else { + print('DFX: Unexpected data type: ${responseData.runtimeType}'); + return null; + } + } else { + if (responseData is Map && 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? 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 +307,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 uri = Uri.https('services.dfx.swiss', actionType, { 'session': accessToken, 'lang': 'en', - 'asset-out': assetOut, + 'asset-out': quote.destinationCurrency.toString(), 'blockchain': blockchain, - 'asset-in': 'EUR', + 'asset-in': quote.sourceCurrency.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 +337,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; + } } diff --git a/lib/buy/meld/meld_buy_provider.dart b/lib/buy/meld/meld_buy_provider.dart new file mode 100644 index 000000000..bb2b75e97 --- /dev/null +++ b/lib/buy/meld/meld_buy_provider.dart @@ -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> 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; + final paymentMethods = + data.map((e) => PaymentMethod.fromMeldJson(e as Map)).toList(); + return paymentMethods; + } else { + print('Meld: Failed to fetch payment types'); + return List.empty(); + } + } catch (e) { + print('Meld: Failed to fetch payment types: $e'); + return List.empty(); + } + } + + @override + Future?> 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; + final paymentType = _getPaymentTypeByString(data['paymentMethodType'] as String?); + final quote = Quote.fromMeldJson(data, isBuyAction, paymentType); + + quote.setSourceCurrency = isBuyAction ? cryptoCurrency : fiatCurrency; + quote.setDestinationCurrency = isBuyAction ? fiatCurrency : cryptoCurrency; + + return [quote]; + } else { + return null; + } + } catch (e) { + print('Error fetching buy quote: $e'); + return null; + } + } + + Future? 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.sourceCurrency, + 'destinationCurrencyCode': quote.destinationCurrency, + '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( + 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; + } + } +} diff --git a/lib/buy/moonpay/moonpay_provider.dart b/lib/buy/moonpay/moonpay_provider.dart index 67ee75d7c..b51cfce06 100644 --- a/lib/buy/moonpay/moonpay_provider.dart +++ b/lib/buy/moonpay/moonpay_provider.dart @@ -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 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)['signature'] as String; @@ -94,85 +99,198 @@ class MoonPayProvider extends BuyProvider { } } - Future 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> 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 originalUri = Uri.https( - baseSellUrl, - '', - params, - ); + final path = '$_currenciesPath/${cryptocurrency.toLowerCase()}/limits'; + final url = Uri.https(_baseUrl, path, params); - if (isTestEnvironment) { - return originalUri; + try { + final response = await get(url, headers: {'accept': 'application/json'}); + if (response.statusCode == 200) { + return jsonDecode(response.body) as Map; + } else { + print('MoonPay does not support fiat: $fiatCurrency'); + return {}; + } + } catch (e) { + print('MoonPay Error fetching fiat currencies: $e'); + return {}; } + } - final signature = await getMoonpaySignature('?${originalUri.query}'); + Future> getAvailablePaymentTypes( + String fiatCurrency, String cryptoCurrency, bool isBuyAction) async { + final List 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; + } + } - final query = Map.from(originalUri.queryParameters); - query['signature'] = signature; - final signedUri = originalUri.replace(queryParameters: query); - return signedUri; + return paymentMethods; } - // BUY: - static const _currenciesSuffix = '/v3/currencies'; - static const _quoteSuffix = '/buy_quote'; - static const _transactionsSuffix = '/v1/transactions'; - static const _ipAddressSuffix = '/v4/ip_address'; + @override + Future?> 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 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; + + // 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?; + 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.setSourceCurrency = isBuyAction ? cryptoCurrency : fiatCurrency; + quote.setDestinationCurrency = isBuyAction ? fiatCurrency : 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? launchProvider( + {required BuildContext context, + required Quote quote, + required double amount, + required bool isBuyAction, + required String cryptoCurrencyAddress, + String? countryCode}) async { + final currency = (isBuyAction ? quote.destinationCurrency : quote.sourceCurrency).name; + + final Map 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.sourceCurrency.name : quote.sourceCurrency.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.destinationCurrency.name; + if (!isBuyAction) params['quoteCurrencyCode'] = quote.destinationCurrency.name; + + try { + { + final uri = await requestMoonPayUrl( + currency: currency, + walletAddress: cryptoCurrencyAddress, + settingsStore: _settingsStore, + isBuyAction: isBuyAction, + amount: amount.toString(), + params: params); + + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } else { + throw Exception('Could not launch URL'); + } + } + } catch (e) { + if (context.mounted) { + await showDialog( + 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(), + ); + }, + ); + } } + } - final originalUri = Uri.https( - baseBuyUrl, - '', - params, - ); + Future requestMoonPayUrl({ + required String currency, + required String walletAddress, + required SettingsStore settingsStore, + required bool isBuyAction, + required Map params, + String? amount, + }) async { + if (_apiKey.isNotEmpty) params['apiKey'] = _apiKey; - if (isTestEnvironment) { - return originalUri; - } + final baseUrl = isBuyAction ? baseBuyUrl : baseSellUrl; + final originalUri = Uri.https(baseUrl, '', params); + + if (isTestEnvironment) return originalUri; final signature = await getMoonpaySignature('?${originalUri.query}'); final query = Map.from(originalUri.queryParameters); @@ -181,33 +299,6 @@ class MoonPayProvider extends BuyProvider { return signedUri; } - Future 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; - 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 findOrderById(String id) async { final url = _apiUrl + _transactionsSuffix + '/$id' + '?apiKey=' + _apiKey; final uri = Uri.parse(url); @@ -235,74 +326,83 @@ class MoonPayProvider extends BuyProvider { walletId: wallet.id); } - static Future onEnabled() async { - final url = _apiUrl + _ipAddressSuffix + '?apiKey=' + _apiKey; - var isBuyEnable = false; - final uri = Uri.parse(url); - final response = await get(uri); + String _normalizeCurrency(CryptoCurrency currency) { + if (currency.tag == 'POLY') { + return '${currency.title.toLowerCase()}_polygon'; + } - try { - final responseJSON = json.decode(response.body) as Map; - isBuyEnable = responseJSON['isBuyAllowed'] as bool; - } catch (e) { - isBuyEnable = false; - print(e.toString()); + if (currency.tag == 'TRX') { + return '${currency.title.toLowerCase()}_trx'; } - return isBuyEnable; + return currency.toString().toLowerCase(); } - @override - Future 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( - 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? 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; } } - String _normalizeCurrency(CryptoCurrency currency) { - if (currency == CryptoCurrency.maticpoly) { - return "POL_POLYGON"; - } else if (currency == CryptoCurrency.matic) { - return "POL"; + 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; } - - return currency.toString().toLowerCase(); } } diff --git a/lib/buy/onramper/onramper_buy_provider.dart b/lib/buy/onramper/onramper_buy_provider.dart index 1f1c86962..98cc8498f 100644 --- a/lib/buy/onramper/onramper_buy_provider.dart +++ b/lib/buy/onramper/onramper_buy_provider.dart @@ -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,332 @@ 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> 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 data = jsonDecode(response.body) as Map; + final List message = data['message'] as List; + return message + .map((item) => PaymentMethod.fromOnramperJson(item as Map)) + .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> 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 data = jsonDecode(response.body) as Map; + + final List onramps = data['message'] as List; + + final Map 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; - - primaryColor = getColorStr(Theme.of(context).primaryColor); - secondaryColor = getColorStr(Theme.of(context).colorScheme.background); - primaryTextColor = - getColorStr(Theme.of(context).extension()!.titleColor); - secondaryTextColor = getColorStr( - Theme.of(context).extension()!.secondaryTextColor); - containerColor = getColorStr(Theme.of(context).colorScheme.background); - cardColor = getColorStr(Theme.of(context).cardColor); + @override + Future?> 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 actionType = isBuyAction ? 'buy' : 'sell'; + + final normalizedCryptoCurrency = _getNormalizeCryptoCurrency(cryptoCurrency); + + final params = { + 'amount': amount.toString(), + if (paymentMethod != null) 'paymentMethod': paymentMethod, + 'uuid': 'acad3928-556f-48a1-a478-4e2ec76700cd', + '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; + if (data.isEmpty) return null; + + List validQuotes = []; + + final onrampMetadata = await getOnrampMetadata(); + + for (var item in data) { + if (item['errors'] != null) break; + + final paymentMethod = (item as Map)['paymentMethod'] as String; + + final rampId = item['ramp'] as String?; + final rampMetaData = onrampMetadata[rampId] as Map?; + + if (rampMetaData == null) continue; + + final quote = Quote.fromOnramperJson( + item, isBuyAction, onrampMetadata, _getPaymentTypeByString(paymentMethod)); + quote.setSourceCurrency = isBuyAction ? fiatCurrency : cryptoCurrency; + quote.setDestinationCurrency = isBuyAction ? cryptoCurrency : fiatCurrency; + 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? 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()!.titleColor); + final secondaryTextColor = + getColorStr(Theme.of(context).extension()!.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 networkName = wallet.currency.fullName?.toUpperCase().replaceAll(" ", ""); - return Uri.https(_baseUrl, '', { + final defaultFiat = isBuyAction ? quote.sourceCurrency.name : quote.destinationCurrency.name; + final defaultCrypto = isBuyAction + ? _getNormalizeCryptoCurrency(quote.destinationCurrency) + : _getNormalizeCryptoCurrency(quote.sourceCurrency); + + final paymentMethod = normalizePaymentMethod(quote.paymentType); + + final uri = Uri.https(_baseUrl, '', { 'apiKey': _apiKey, - 'defaultCrypto': _normalizeCryptoCurrency, - 'sell_defaultCrypto': _normalizeCryptoCurrency, + 'mode': actionType, + '${prefix}defaultFiat': defaultFiat, + '${prefix}defaultCrypto': defaultCrypto, + '${prefix}defaultAmount': amount.toString(), + if (paymentMethod != null) '${prefix}defaultPaymentMethod': paymentMethod, + 'onlyOnramps': quote.rampId, 'networkWallets': '${networkName}:${wallet.walletAddresses.address}', + if (cryptoCurrencyAddress.isNotEmpty) 'walletAddress': cryptoCurrencyAddress, 'supportSwap': "false", 'primaryColor': primaryColor, 'secondaryColor': secondaryColor, + 'containerColor': containerColor, 'primaryTextColor': primaryTextColor, 'secondaryTextColor': secondaryTextColor, - 'containerColor': containerColor, 'cardColor': cardColor, - 'mode': isBuyAction == true ? 'buy' : 'sell', }); - } - Future 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 mainCurrency = [ + CryptoCurrency.btc, + CryptoCurrency.eth, + CryptoCurrency.sol, + ]; + + String _tagToNetwork(String tag) { + switch (tag) { + case 'OMNI': + return tag; + case 'POLY': + 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'), ""); } diff --git a/lib/buy/payment_method.dart b/lib/buy/payment_method.dart new file mode 100644 index 000000000..cf85c441b --- /dev/null +++ b/lib/buy/payment_method.dart @@ -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 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 json, PaymentType paymentType) { + return PaymentMethod( + paymentMethodType: paymentType, + customTitle: json['paymentMethod'] as String, + customIconPath: 'assets/images/card.png'); + } + + factory PaymentMethod.fromMeldJson(Map json) { + final type = PaymentMethod.getPaymentTypeId(json['paymentMethod'] as String?); + final logos = json['logos'] as Map; + 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; + } + } +} diff --git a/lib/buy/robinhood/robinhood_buy_provider.dart b/lib/buy/robinhood/robinhood_buy_provider.dart index 2d809772e..1ea3bb3f1 100644 --- a/lib/buy/robinhood/robinhood_buy_provider.dart +++ b/lib/buy/robinhood/robinhood_buy_provider.dart @@ -1,21 +1,28 @@ 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/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'; import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; +import '../../entities/fiat_currency.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 +40,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 +96,13 @@ class RobinhoodBuyProvider extends BuyProvider { }); } - Future launchProvider(BuildContext context, bool? isBuyAction) async { + Future? 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 +132,87 @@ class RobinhoodBuyProvider extends BuyProvider { }); } } + + @override + Future?> 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': isBuyAction ? fiatCurrency.name : cryptoCurrency.title, + 'assetCode': isBuyAction ? cryptoCurrency.title : fiatCurrency.name, + '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; + + if (response.statusCode == 200) { + final paymentType = _getPaymentTypeByString(responseData['paymentMethod'] as String?); + final quote = Quote.fromRobinhoodJson(responseData, isBuyAction, paymentType); + quote.setSourceCurrency = isBuyAction ? cryptoCurrency : fiatCurrency; + quote.setDestinationCurrency = isBuyAction ? fiatCurrency : 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; + } + } } diff --git a/lib/buy/sell_buy_states.dart b/lib/buy/sell_buy_states.dart new file mode 100644 index 000000000..26ea20205 --- /dev/null +++ b/lib/buy/sell_buy_states.dart @@ -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 {} \ No newline at end of file diff --git a/lib/buy/wyre/wyre_buy_provider.dart b/lib/buy/wyre/wyre_buy_provider.dart index e09186ad5..78b109ac0 100644 --- a/lib/buy/wyre/wyre_buy_provider.dart +++ b/lib/buy/wyre/wyre_buy_provider.dart @@ -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 launchProvider(BuildContext context, bool? isBuyAction) { - // TODO: implement launchProvider - throw UnimplementedError(); - } } diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 42e24d3c7..b7cbb1d36 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -209,9 +209,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 = @@ -264,14 +262,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( @@ -457,10 +449,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), diff --git a/lib/core/selectable_option.dart b/lib/core/selectable_option.dart new file mode 100644 index 000000000..a39a1f44a --- /dev/null +++ b/lib/core/selectable_option.dart @@ -0,0 +1,37 @@ +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 leftSubTitle => null; + + String? get leftSubTitleIconPath => null; + + String? get rightSubTitle => null; + + String? get rightSubTitleLightIconPath => null; + + String? get rightSubTitleDarkIconPath => null; + + List get badges => []; + + bool get isOptionSelected => false; + + set isOptionSelected(bool isSelected) => false; +} + + diff --git a/lib/di.dart b/lib/di.dart index 0b98244e6..214105a7c 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -19,6 +19,7 @@ import 'package:cake_wallet/core/backup_service.dart'; import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/core/new_wallet_type_arguments.dart'; import 'package:cake_wallet/core/secure_storage.dart'; +import 'package:cake_wallet/core/selectable_option.dart'; import 'package:cake_wallet/core/totp_request_details.dart'; import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart'; import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart'; @@ -34,6 +35,8 @@ import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart'; import 'package:cake_wallet/entities/wallet_manager.dart'; +import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart'; +import 'package:cake_wallet/src/screens/buy/payment_method_options_page.dart'; import 'package:cake_wallet/src/screens/receive/address_list_page.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cake_wallet/tron/tron.dart'; @@ -59,7 +62,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'; @@ -131,6 +133,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/dashboard/desktop_sidebar_view_model.dart'; import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart'; import 'package:cake_wallet/view_model/anonpay_details_view_model.dart'; @@ -241,6 +244,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; @@ -975,6 +980,10 @@ Future setup({ wallet: getIt.get().wallet!, )); + getIt.registerFactory(() => MeldBuyProvider( + wallet: getIt.get().wallet!, + )); + getIt.registerFactoryParam((title, uri) => WebViewPage(title, uri)); getIt.registerFactory(() => PayfuraBuyProvider( @@ -1159,8 +1168,25 @@ Future setup({ getIt.registerFactory(() => BuyAmountViewModel()); - getIt.registerFactoryParam( - (isBuyOption, _) => BuySellOptionsPage(getIt.get(), isBuyOption)); + getIt.registerFactory(() => BuySellViewModel(getIt.get())); + + getIt.registerFactory(() => BuySellPage(getIt.get())); + + getIt.registerFactoryParam, void>((List args, _) { + final items = args.first as List; + final pickAnOption = args[1] as void Function(SelectableOption option)?; + + return BuyOptionsPage( + items: items, pickAnOption: pickAnOption); + }); + + getIt.registerFactoryParam, void>((List args, _) { + final items = args.first as List; + final pickAnOption = args[1] as void Function(SelectableOption option)?; + + return PaymentMethodOptionsPage( + items: items, pickAnOption: pickAnOption); + }); getIt.registerFactory(() { final wallet = getIt.get().wallet; diff --git a/lib/entities/country.dart b/lib/entities/country.dart new file mode 100644 index 000000000..6925e1b99 --- /dev/null +++ b/lib/entities/country.dart @@ -0,0 +1,123 @@ +import 'package:cw_core/enumerable_item.dart'; + +class Country extends EnumerableItem with Serializable { + const Country({required String code, required this.fullName, required this.countryCode}) + : super(title: fullName, raw: code); + + final String fullName; + final String countryCode; + + static List get all => _all.values.toList(); + + static const arg = Country(code: 'arg', countryCode: 'AR', fullName: "Argentina"); + static const aus = Country(code: 'aus', countryCode: 'AU', fullName: "Australia"); + static const bgd = Country(code: 'bgd', countryCode: 'BD', fullName: "Bangladesh"); + static const bgr = Country(code: 'bgr', countryCode: 'BG', fullName: "Bulgaria"); + static const bra = Country(code: 'bra', countryCode: 'BR', fullName: "Brazil"); + static const cad = Country(code: 'cad', countryCode: 'CA', fullName: "Canada"); + static const che = Country(code: 'che', countryCode: 'CH', fullName: "Switzerland"); + static const chl = Country(code: 'chl', countryCode: 'CL', fullName: "Chile"); + static const chn = Country(code: 'chn', countryCode: 'CN', fullName: "China"); + static const col = Country(code: 'col', countryCode: 'CO', fullName: "Colombia"); + static const czk = Country(code: 'czk', countryCode: 'CZ', fullName: "Czech Republic"); + static const dnk = Country(code: 'dnk', countryCode: 'DK', fullName: "Denmark"); + static const egy = Country(code: 'egy', countryCode: 'EG', fullName: "Egypt"); + static const eur = Country(code: 'eur', countryCode: 'EU', fullName: "European Union"); + static const gbr = Country(code: 'gbr', countryCode: 'GB', fullName: "United Kingdom"); + static const gha = Country(code: 'gha', countryCode: 'GH', fullName: "Ghana"); + static const gtm = Country(code: 'gtm', countryCode: 'GT', fullName: "Guatemala"); + static const hkg = Country(code: 'hkg', countryCode: 'HK', fullName: "Hong Kong"); + static const hrv = Country(code: 'hrv', countryCode: 'HR', fullName: "Croatia"); + static const hun = Country(code: 'hun', countryCode: 'HU', fullName: "Hungary"); + static const idn = Country(code: 'idn', countryCode: 'ID', fullName: "Indonesia"); + static const isr = Country(code: 'isr', countryCode: 'IL', fullName: "Israel"); + static const ind = Country(code: 'ind', countryCode: 'IN', fullName: "India"); + static const irn = Country(code: 'irn', countryCode: 'IR', fullName: "Iran"); + static const isl = Country(code: 'isl', countryCode: 'IS', fullName: "Iceland"); + static const jpn = Country(code: 'jpn', countryCode: 'JP', fullName: "Japan"); + static const kor = Country(code: 'kor', countryCode: 'KR', fullName: "South Korea"); + static const mar = Country(code: 'mar', countryCode: 'MA', fullName: "Morocco"); + static const mex = Country(code: 'mex', countryCode: 'MX', fullName: "Mexico"); + static const mys = Country(code: 'mys', countryCode: 'MY', fullName: "Malaysia"); + static const nga = Country(code: 'nga', countryCode: 'NG', fullName: "Nigeria"); + static const nor = Country(code: 'nor', countryCode: 'NO', fullName: "Norway"); + static const nzl = Country(code: 'nzl', countryCode: 'NZ', fullName: "New Zealand"); + static const phl = Country(code: 'phl', countryCode: 'PH', fullName: "Philippines"); + static const pak = Country(code: 'pak', countryCode: 'PK', fullName: "Pakistan"); + static const pol = Country(code: 'pol', countryCode: 'PL', fullName: "Poland"); + static const rou = Country(code: 'rou', countryCode: 'RO', fullName: "Romania"); + static const rus = Country(code: 'rus', countryCode: 'RU', fullName: "Russia"); + static const sau = Country(code: 'sau', countryCode: 'SA', fullName: "Saudi Arabia"); + static const swe = Country(code: 'swe', countryCode: 'SE', fullName: "Sweden"); + static const sgp = Country(code: 'sgp', countryCode: 'SG', fullName: "Singapore"); + static const tha = Country(code: 'tha', countryCode: 'TH', fullName: "Thailand"); + static const twn = Country(code: 'twn', countryCode: 'TW', fullName: "Taiwan"); + static const ukr = Country(code: 'ukr', countryCode: 'UA', fullName: "Ukraine"); + static const usa = Country(code: 'usa', countryCode: 'US', fullName: "United States"); + static const ven = Country(code: 'ven', countryCode: 'VE', fullName: "Venezuela"); + static const vnm = Country(code: 'vnm', countryCode: 'VN', fullName: "Vietnam"); + static const saf = Country(code: 'saf', countryCode: 'ZA', fullName: "South Africa"); + static const tur = Country(code: 'tur', countryCode: 'TR', fullName: "Turkey"); + + static final _all = { + Country.arg.raw: Country.arg, + Country.aus.raw: Country.aus, + Country.bgd.raw: Country.bgd, + Country.bgr.raw: Country.bgr, + Country.bra.raw: Country.bra, + Country.cad.raw: Country.cad, + Country.che.raw: Country.che, + Country.chl.raw: Country.chl, + Country.chn.raw: Country.chn, + Country.col.raw: Country.col, + Country.czk.raw: Country.czk, + Country.dnk.raw: Country.dnk, + Country.egy.raw: Country.egy, + Country.eur.raw: Country.eur, + Country.gbr.raw: Country.gbr, + Country.gha.raw: Country.gha, + Country.gtm.raw: Country.gtm, + Country.hkg.raw: Country.hkg, + Country.hrv.raw: Country.hrv, + Country.hun.raw: Country.hun, + Country.idn.raw: Country.idn, + Country.isr.raw: Country.isr, + Country.ind.raw: Country.ind, + Country.irn.raw: Country.irn, + Country.isl.raw: Country.isl, + Country.jpn.raw: Country.jpn, + Country.kor.raw: Country.kor, + Country.mar.raw: Country.mar, + Country.mex.raw: Country.mex, + Country.mys.raw: Country.mys, + Country.nga.raw: Country.nga, + Country.nor.raw: Country.nor, + Country.nzl.raw: Country.nzl, + Country.phl.raw: Country.phl, + Country.pak.raw: Country.pak, + Country.pol.raw: Country.pol, + Country.rou.raw: Country.rou, + Country.rus.raw: Country.rus, + Country.sau.raw: Country.sau, + Country.swe.raw: Country.swe, + Country.sgp.raw: Country.sgp, + Country.tha.raw: Country.tha, + Country.twn.raw: Country.twn, + Country.ukr.raw: Country.ukr, + Country.usa.raw: Country.usa, + Country.ven.raw: Country.ven, + Country.vnm.raw: Country.vnm, + Country.saf.raw: Country.saf, + Country.tur.raw: Country.tur, + }; + + static Country deserialize({required String raw}) => _all[raw]!; + + @override + bool operator ==(Object other) => other is Country && other.raw == raw; + + @override + int get hashCode => raw.hashCode ^ title.hashCode; + + String get iconPath => "assets/images/flags/$raw.png"; +} diff --git a/lib/entities/main_actions.dart b/lib/entities/main_actions.dart index c1dd71cc9..94be0d2b7 100644 --- a/lib/entities/main_actions.dart +++ b/lib/entities/main_actions.dart @@ -23,31 +23,18 @@ class MainActions { }); static List 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 _showErrorDialog( - BuildContext context, String title, String errorMessage) async { - await showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: title, - alertContent: errorMessage, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop(), - ); - }, - ); - } } \ No newline at end of file diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 0c032a736..f2c9746c4 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -20,10 +20,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 walletListAscending = 'wallet_list_ascending'; static const currentFiatApiModeKey = 'current_fiat_api_mode'; diff --git a/lib/entities/provider_types.dart b/lib/entities/provider_types.dart index b9dd4ef2a..f085cc815 100644 --- a/lib/entities/provider_types.dart +++ b/lib/entities/provider_types.dart @@ -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,24 +86,22 @@ class ProvidersHelper { case WalletType.ethereum: case WalletType.polygon: return [ - ProviderType.askEachTime, ProviderType.onramper, ProviderType.moonpay, ProviderType.dfx, + ProviderType.robinhood, ]; case WalletType.litecoin: case WalletType.bitcoinCash: - return [ProviderType.askEachTime, ProviderType.moonpay]; + return [ProviderType.moonpay, ProviderType.robinhood]; case WalletType.solana: return [ - ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood, ProviderType.moonpay, ]; case WalletType.tron: return [ - ProviderType.askEachTime, ProviderType.robinhood, ProviderType.moonpay, ]; @@ -129,7 +125,9 @@ class ProvidersHelper { return getIt.get(); case ProviderType.moonpay: return getIt.get(); - case ProviderType.askEachTime: + case ProviderType.meld: + return getIt.get(); + default: return null; } } diff --git a/lib/router.dart b/lib/router.dart index 16eeefeb1..f3b269fbe 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -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'; @@ -124,7 +125,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; @@ -549,7 +551,15 @@ Route createRoute(RouteSettings settings) { case Routes.buySellPage: final args = settings.arguments as bool; - return MaterialPageRoute(builder: (_) => getIt.get(param1: args)); + return MaterialPageRoute(builder: (_) => getIt.get(param1: args)); + + case Routes.buyOptionsPage: + final args = settings.arguments as List; + return MaterialPageRoute(builder: (_) => getIt.get(param1: args)); + + case Routes.paymentMethodOptionsPage: + final args = settings.arguments as List; + return MaterialPageRoute(builder: (_) => getIt.get(param1: args)); case Routes.buyWebView: final args = settings.arguments as List; diff --git a/lib/routes.dart b/lib/routes.dart index 83d90248f..6ae270cf3 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -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'; diff --git a/lib/src/screens/InfoPage.dart b/lib/src/screens/Info_page.dart similarity index 100% rename from lib/src/screens/InfoPage.dart rename to lib/src/screens/Info_page.dart diff --git a/lib/src/screens/buy/buy_options_page.dart b/lib/src/screens/buy/buy_options_page.dart deleted file mode 100644 index 38f3ed968..000000000 --- a/lib/src/screens/buy/buy_options_page.dart +++ /dev/null @@ -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()?.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()!.detailsTitlesColor, - ), - ), - ), - ); - } -} diff --git a/lib/src/screens/buy/buy_sell_options_page.dart b/lib/src/screens/buy/buy_sell_options_page.dart new file mode 100644 index 000000000..c3081a127 --- /dev/null +++ b/lib/src/screens/buy/buy_sell_options_page.dart @@ -0,0 +1,44 @@ +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}); + + final List items; + final Function(SelectableOption option)? pickAnOption; + + @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 + TextStyle? get subTitleTextStyle => null; + + @override + Color? get selectedBackgroundColor => null; + + @override + double? get tileBorderRadius => 30; + + @override + String get bottomSectionText => ''; + + @override + void Function(SelectableOption option)? get onOptionTap => pickAnOption; +} diff --git a/lib/src/screens/buy/buy_sell_page.dart b/lib/src/screens/buy/buy_sell_page.dart new file mode 100644 index 000000000..ae099b6a5 --- /dev/null +++ b/lib/src/screens/buy/buy_sell_page.dart @@ -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/entities/fiat_currency.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/utils/debounce.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/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(); + final fiatCurrencyKey = GlobalKey(); + final _formKey = GlobalKey(); + final _fiatAmountFocus = FocusNode(); + final _depositAddressFocus = FocusNode(); + final _cryptoAmountFocus = FocusNode(); + final _receiveAddressFocus = FocusNode(); + final _cryptoAmountDebounce = Debounce(Duration(milliseconds: 500)); + final _fiatAmountDebounce = Debounce(Duration(milliseconds: 500)); + 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()!.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), + SizedBox(height: 12), + _buildQuoteTile(context) + ], + ), + ), + ])), + bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), + bottomSection: Column(children: [ + Observer( + builder: (_) => LoadingPrimaryButton( + text: 'Next', + onPressed: () async { + if(!_formKey.currentState!.validate()) return; + await buySellViewModel.launchTrade(context); + }, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + isDisabled: !buySellViewModel.isReadyToTrade, + isLoading: false)), + ]), + )), + )); + } + + 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()!.titleColor), + ); + }); + } + return OptionTilePlaceholder(errorText: 'No payment methods available', borderRadius: 30); + } + + Widget _buildQuoteTile(BuildContext context) { + if (buySellViewModel.buySellQuotState is BuySellQuotLoading || + buySellViewModel.buySellQuotState is InitialBuySellQuotState) { + return OptionTilePlaceholder( + leadingIcon: Icons.arrow_forward_ios, + borderRadius: 30, + imageWidth: 50, + imageHeight: 50, + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8), + isDarkTheme: buySellViewModel.isDarkTheme); + } + if (buySellViewModel.buySellQuotState is BuySellQuotLoaded && + buySellViewModel.selectedQuote != null) { + return Observer(builder: (_) { + final selectedQuote = buySellViewModel.selectedQuote!; + return ProviderOptionTile( + lightImagePath: selectedQuote.lightIconPath, + darkImagePath: selectedQuote.darkIconPath, + title: selectedQuote.title, + badges: selectedQuote.badges, + imageWidth: 50, + imageHeight: 50, + leftSubTitle: selectedQuote.leftSubTitle, + rightSubTitleLightIconPath: selectedQuote.rightSubTitleLightIconPath, + rightSubTitleDarkIconPath: selectedQuote.rightSubTitleDarkIconPath, + onPressed: () => _pickQuote(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()!.titleColor)); + }); + } + return OptionTilePlaceholder(errorText: 'No quotes 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 _pickQuote(BuildContext context) async { + await Navigator.of(context).pushNamed( + Routes.buyOptionsPage, + arguments: [ + buySellViewModel.quoteOptions, + buySellViewModel.changeOption + ], + ); + } + + void _setReactions(BuildContext context, BuySellViewModel buySellViewModel) { + if (_isReactionsSet) { + return; + } + + final fiatAmountController = fiatCurrencyKey.currentState!.amountController; + final cryptoAmountController = cryptoCurrencyKey.currentState!.amountController; + final cryptoAddressController = cryptoCurrencyKey.currentState!.addressController; + + _onCryptoCurrencyChange(buySellViewModel.cryptoCurrency, buySellViewModel, cryptoCurrencyKey); + _onFiatCurrencyChange(buySellViewModel.fiatCurrency, buySellViewModel, fiatCurrencyKey); + + reaction( + (_) => buySellViewModel.cryptoCurrency, + (CryptoCurrency currency) => + _onCryptoCurrencyChange(currency, buySellViewModel, cryptoCurrencyKey)); + + reaction( + (_) => buySellViewModel.fiatCurrency, + (FiatCurrency currency) => + _onFiatCurrencyChange(currency, buySellViewModel, fiatCurrencyKey)); + + reaction((_) => buySellViewModel.fiatAmount, (String amount) { + if (fiatCurrencyKey.currentState!.amountController.text != amount) { + fiatCurrencyKey.currentState!.amountController.text = amount; + } + }); + + reaction((_) => buySellViewModel.cryptoAmount, (String amount) { + if (cryptoCurrencyKey.currentState!.amountController.text != amount) { + cryptoCurrencyKey.currentState!.amountController.text = amount; + } + }); + + fiatAmountController.addListener(() { + if (fiatAmountController.text != buySellViewModel.fiatAmount) { + _fiatAmountDebounce.run(() { + buySellViewModel.changeFiatAmount(amount: fiatAmountController.text); + }); + } + }); + + cryptoAmountController.addListener(() { + if (cryptoAmountController.text != buySellViewModel.cryptoAmount) { + _cryptoAmountDebounce.run(() { + buySellViewModel.changeCryptoAmount(amount: cryptoAmountController.text); + }); + } + }); + + cryptoAddressController.addListener(() { + buySellViewModel.changeCryptoCurrencyAddress(cryptoAddressController.text); + }); + + + _isReactionsSet = true; + } + + void _onCryptoCurrencyChange(CryptoCurrency currency, BuySellViewModel buySellViewModel, + GlobalKey key) { + final isCurrentTypeWallet = currency == buySellViewModel.wallet.currency; + + key.currentState!.changeSelectedCurrency(currency); + + key.currentState!.changeAddress( + address: isCurrentTypeWallet ? buySellViewModel.wallet.walletAddresses.address : ''); + + key.currentState!.changeAmount(amount: ''); + } + + void _onFiatCurrencyChange( + FiatCurrency currency, BuySellViewModel buySellViewModel, GlobalKey key) { + key.currentState!.changeSelectedCurrency(currency); + key.currentState!.changeAmount(amount: ''); + } + + void disposeBestRateSync() => {}; + + Widget _exchangeCardsSection(BuildContext context) { + final fiatExchangeCard = Observer( + builder: (_) => ExchangeCard( + cardInstanceName: 'fiat_currency_trade_card', + onDispose: disposeBestRateSync, + amountFocusNode: _fiatAmountFocus, + addressFocusNode: _depositAddressFocus, + 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()!.textFieldButtonColor, + borderColor: + Theme.of(context).extension()!.textFieldBorderTopPanelColor, + onPushPasteButton: (context) async {}, + onPushAddressBookButton: (context) async {}, + )); + + final cryptoExchangeCard = Observer( + builder: (_) => ExchangeCard( + cardInstanceName: 'crypto_currency_trade_card', + onDispose: disposeBestRateSync, + amountFocusNode: _cryptoAmountFocus, + addressFocusNode: _receiveAddressFocus, + key: cryptoCurrencyKey, + title: 'Crypto ${S.of(context).amount}', + initialCurrency: buySellViewModel.cryptoCurrency, + initialWalletName: '', + initialAddress: buySellViewModel.cryptoCurrency == buySellViewModel.wallet.currency + ? buySellViewModel.wallet.walletAddresses.address + : '', + 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()!.textFieldButtonColor, + borderColor: + Theme.of(context).extension()!.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: () => + !buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null, + onSellTap: () => + buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null, + isBuySellOption: true, + ); + } else { + return MobileExchangeCardsSection( + firstExchangeCard: cryptoExchangeCard, + secondExchangeCard: fiatExchangeCard, + onBuyTap: () => + !buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null, + onSellTap: () => + buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null, + isBuySellOption: true, + ); + } + }, + ); + } + + return Observer( + builder: (_) { + if (buySellViewModel.isBuyAction) { + return DesktopExchangeCardsSection( + firstExchangeCard: fiatExchangeCard, + secondExchangeCard: cryptoExchangeCard, + ); + } else { + return DesktopExchangeCardsSection( + firstExchangeCard: cryptoExchangeCard, + secondExchangeCard: fiatExchangeCard, + ); + } + }, + ); + } +} diff --git a/lib/src/screens/buy/payment_method_options_page.dart b/lib/src/screens/buy/payment_method_options_page.dart new file mode 100644 index 000000000..5f1c9e241 --- /dev/null +++ b/lib/src/screens/buy/payment_method_options_page.dart @@ -0,0 +1,44 @@ +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 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 + TextStyle? get subTitleTextStyle => null; + + @override + Color? get selectedBackgroundColor => null; + + @override + double? get tileBorderRadius => 30; + + @override + String get bottomSectionText => ''; + + @override + void Function(SelectableOption option)? get onOptionTap => pickAnOption; +} diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart index d36c06013..7bb5f77f8 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart @@ -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), ), ), ], diff --git a/lib/src/screens/exchange/widgets/exchange_card.dart b/lib/src/screens/exchange/widgets/exchange_card.dart index 75a2eadd7..ea51eb313 100644 --- a/lib/src/screens/exchange/widgets/exchange_card.dart +++ b/lib/src/screens/exchange/widgets/exchange_card.dart @@ -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 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 currencies; - final Function(CryptoCurrency) onCurrencySelected; + final List 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 createState() => ExchangeCardState(); } -class ExchangeCardState extends State { +class ExchangeCardState extends State> { ExchangeCardState() : _title = '', _min = '', @@ -89,7 +97,6 @@ class ExchangeCardState extends State { _isAmountEditable = false, _isAddressEditable = false, _walletName = '', - _selectedCurrency = CryptoCurrency.btc, _isAmountEstimated = false, _isMoneroWallet = false, _cardInstanceName = ''; @@ -101,7 +108,7 @@ class ExchangeCardState extends State { String _title; String? _min; String? _max; - CryptoCurrency _selectedCurrency; + late T _selectedCurrency; String _walletName; bool _isAmountEditable; bool _isAddressEditable; @@ -136,7 +143,7 @@ class ExchangeCardState extends State { }); } - void changeSelectedCurrency(CryptoCurrency currency) { + void changeSelectedCurrency(T currency) { setState(() => _selectedCurrency = currency); } @@ -222,7 +229,7 @@ class ExchangeCardState extends State { Divider(height: 1, color: Theme.of(context).extension()!.textFieldHintColor), Padding( padding: EdgeInsets.only(top: 5), - child: Container( + child: widget.showLimitsField ? Container( height: 15, child: Row(mainAxisAlignment: MainAxisAlignment.start, children: [ _min != null @@ -247,7 +254,7 @@ class ExchangeCardState extends State { ), ) : Offstage(), - ])), + ])) : Offstage(), ), !_isAddressEditable && widget.hasRefundAddress ? Padding( @@ -261,10 +268,11 @@ class ExchangeCardState extends State { )) : 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 +288,29 @@ class ExchangeCardState extends State { 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()!.hintTextColor), + color: + Theme.of(context).extension()!.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 +413,7 @@ class ExchangeCardState extends State { 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), ), ); } @@ -425,3 +436,4 @@ class ExchangeCardState extends State { }); } } + diff --git a/lib/src/screens/exchange/widgets/mobile_exchange_cards_section.dart b/lib/src/screens/exchange/widgets/mobile_exchange_cards_section.dart index 126bca835..d53f16339 100644 --- a/lib/src/screens/exchange/widgets/mobile_exchange_cards_section.dart +++ b/lib/src/screens/exchange/widgets/mobile_exchange_cards_section.dart @@ -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 { + 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()!.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()!.textFieldButtonColor, + onTap: () { + setState(() => isBuySelected = false); + if (widget.onSellTap != null) widget.onSellTap!(); + }, + ), + ), + Expanded(flex: 2, child: SizedBox()), + ], + ), + ); + } +} diff --git a/lib/src/screens/seed/pre_seed_page.dart b/lib/src/screens/seed/pre_seed_page.dart index 730dfa5f8..7b9e9a707 100644 --- a/lib/src/screens/seed/pre_seed_page.dart +++ b/lib/src/screens/seed/pre_seed_page.dart @@ -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 { diff --git a/lib/src/screens/select_options_page.dart b/lib/src/screens/select_options_page.dart new file mode 100644 index 000000000..7a4ea0943 --- /dev/null +++ b/lib/src/screens/select_options_page.dart @@ -0,0 +1,197 @@ +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; + + TextStyle? get subTitleTextStyle; + + Color? get selectedBackgroundColor; + + double? get tileBorderRadius; + + String get bottomSectionText; + + bool get confirmButtonEnabled => true; + + List get items; + + void Function(SelectableOption option)? get onOptionTap; + + @override + String get title => pageTitle; + + @override + Widget body(BuildContext context) { + return ScrollableWithBottomSection( + content: BodySelectOptionsPage( + items: items, + // Updated to pass items list + onOptionTap: onOptionTap, + tilePadding: tilePadding, + tileBorderRadius: tileBorderRadius, + subTitleTextStyle: subTitleTextStyle, + 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()!.detailsTitlesColor, + ), + ), + if (confirmButtonEnabled) + LoadingPrimaryButton( + text: 'Confirm', + onPressed: () => 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.subTitleTextStyle, + this.imageHeight, + this.imageWidth, + this.innerPadding, + }); + + final List items; + final void Function(SelectableOption option)? onOptionTap; + final EdgeInsets? tilePadding; + final double? tileBorderRadius; + final TextStyle? subTitleTextStyle; + final double? imageHeight; + final double? imageWidth; + final EdgeInsets? innerPadding; + + @override + _BodySelectOptionsPageState createState() => _BodySelectOptionsPageState(); +} + +class _BodySelectOptionsPageState extends State { + late List _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()?.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, + leftSubTitle: item.leftSubTitle, + rightSubTitle: item.rightSubTitle, + rightSubTitleLightIconPath: item.rightSubTitleLightIconPath, + rightSubTitleDarkIconPath: item.rightSubTitleDarkIconPath, + badges: item.badges, + isSelected: item.isOptionSelected, + subTitleTextStyle: widget.subTitleTextStyle, + borderRadius: widget.tileBorderRadius, + isLightMode: isLightMode, + onPressed: () => _handleOptionTap(item), + ), + ); + } + return const SizedBox.shrink(); // Fallback for any unsupported items + }).toList(), + ), + ), + ); + } +} diff --git a/lib/src/screens/settings/other_settings_page.dart b/lib/src/screens/settings/other_settings_page.dart index 137f699f5..f6a6288f5 100644 --- a/lib/src/screens/settings/other_settings_page.dart +++ b/lib/src/screens/settings/other_settings_page.dart @@ -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) => diff --git a/lib/src/screens/settings/privacy_page.dart b/lib/src/screens/settings/privacy_page.dart index 53e7686e8..8652c4af6 100644 --- a/lib/src/screens/settings/privacy_page.dart +++ b/lib/src/screens/settings/privacy_page.dart @@ -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, diff --git a/lib/src/screens/setup_2fa/setup_2fa_info_page.dart b/lib/src/screens/setup_2fa/setup_2fa_info_page.dart index ff6187665..be57634ee 100644 --- a/lib/src/screens/setup_2fa/setup_2fa_info_page.dart +++ b/lib/src/screens/setup_2fa/setup_2fa_info_page.dart @@ -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 { diff --git a/lib/src/widgets/address_text_field.dart b/lib/src/widgets/address_text_field.dart index 0b1ef4796..8e4e09b42 100644 --- a/lib/src/widgets/address_text_field.dart +++ b/lib/src/widgets/address_text_field.dart @@ -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 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 diff --git a/lib/src/widgets/provider_optoin_tile.dart b/lib/src/widgets/provider_optoin_tile.dart new file mode 100644 index 000000000..810329679 --- /dev/null +++ b/lib/src/widgets/provider_optoin_tile.dart @@ -0,0 +1,496 @@ +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.leftSubTitle, + this.rightSubTitle, + this.leftSubTitleIconPath, + this.rightSubTitleLightIconPath, + this.rightSubTitleDarkIconPath, + this.description, + this.badges, + this.borderRadius, + this.imageHeight, + this.imageWidth, + this.padding, + this.titleTextStyle, + this.subTitleTextStyle, + this.leadingIcon, + this.selectedBackgroundColor, + this.isSelected = false, + required this.isLightMode, + }); + + final VoidCallback onPressed; + final String lightImagePath; + final String darkImagePath; + final String title; + final String? leftSubTitle; + final String? rightSubTitle; + final String? leftSubTitleIconPath; + final String? rightSubTitleLightIconPath; + final String? rightSubTitleDarkIconPath; + final String? description; + final List? badges; + final double? borderRadius; + final double? imageHeight; + final double? imageWidth; + final EdgeInsets? padding; + final TextStyle? titleTextStyle; + final TextStyle? subTitleTextStyle; + 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()!.currentTileBackgroundColor + : Theme.of(context).extension()!.titleColor + : Theme.of(context).cardColor; + + final textColor = isSelected + ? isLightMode + ? Colors.white + : Theme.of(context).cardColor + : Theme.of(context).extension()!.titleColor; + + final badgeColor = isSelected + ? Theme.of(context).cardColor + : Theme.of(context).extension()!.titleColor; + + final badgeTextColor = isSelected + ? Theme.of(context).extension()!.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 (leftSubTitle != null || rightSubTitle != null) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + leftSubTitle != null || leftSubTitleIconPath != null + ? Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + if (leftSubTitleIconPath != null && + leftSubTitleIconPath!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(right: 6), + child: getImage(leftSubTitleIconPath!), + ), + Text( + leftSubTitle ?? '', + style: subTitleTextStyle ?? + TextStyle( + fontSize: 16, + 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: subTitleTextStyle ?? + TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: textColor), + ), + ], + ) + : Offstage(), + ], + ), + 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 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 + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _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()!.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), + ], + ), + ), + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index debaeb07a..74fef9286 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -60,8 +60,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 bool initialDisableBulletin, required WalletListOrderType initialWalletListOrder, required bool initialWalletListAscending, @@ -141,8 +140,7 @@ abstract class SettingsStoreBase with Store { useTOTP2FA = initialUseTOTP2FA, numberOfFailedTokenTrials = initialFailedTokenTrial, isAppSecure = initialAppSecure, - disableBuy = initialDisableBuy, - disableSell = initialDisableSell, + disableTradeOption = initialDisableTrade, disableBulletin = initialDisableBulletin, walletListOrder = initialWalletListOrder, walletListAscending = initialWalletListAscending, @@ -167,9 +165,7 @@ abstract class SettingsStoreBase with Store { initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings, currentSyncMode = initialSyncMode, currentSyncAll = initialSyncAll, - priority = ObservableMap(), - defaultBuyProviders = ObservableMap(), - defaultSellProviders = ObservableMap() { + priority = ObservableMap() { //this.nodes = ObservableMap.of(nodes); if (initialMoneroTransactionPriority != null) { @@ -206,30 +202,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( @@ -243,20 +215,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) { @@ -305,14 +263,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) => @@ -618,10 +571,7 @@ abstract class SettingsStoreBase with Store { bool isAppSecure; @observable - bool disableBuy; - - @observable - bool disableSell; + bool disableTradeOption; @observable bool disableBulletin; @@ -701,12 +651,6 @@ abstract class SettingsStoreBase with Store { @observable ObservableMap trocadorProviderStates = ObservableMap(); - @observable - ObservableMap defaultBuyProviders; - - @observable - ObservableMap defaultSellProviders; - @observable SortBalanceBy sortBalanceBy; @@ -869,8 +813,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 = WalletListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0]; @@ -1156,8 +1099,7 @@ abstract class SettingsStoreBase with Store { initialBitcoinSeedType: bitcoinSeedType, initialNanoSeedType: nanoSeedType, initialAppSecure: isAppSecure, - initialDisableBuy: disableBuy, - initialDisableSell: disableSell, + initialDisableTrade: disableTradeOption, initialDisableBulletin: disableBulletin, initialWalletListOrder: walletListOrder, initialWalletListAscending: walletListAscending, @@ -1300,8 +1242,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 = diff --git a/lib/typography.dart b/lib/typography.dart index 8ae2cb4e5..816f116b4 100644 --- a/lib/typography.dart +++ b/lib/typography.dart @@ -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); diff --git a/lib/view_model/buy/buy_sell_view_model.dart b/lib/view_model/buy/buy_sell_view_model.dart new file mode 100644 index 000000000..be40726f2 --- /dev/null +++ b/lib/view_model/buy/buy_sell_view_model.dart @@ -0,0 +1,376 @@ +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/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 = '', + cryptoCurrencies = [], + fiatCurrencies = [], + paymentMethodState = InitialPaymentMethod(), + buySellQuotState = InitialBuySellQuotState(), + cryptoCurrency = appStore.wallet!.currency, + fiatCurrency = appStore.settingsStore.fiatCurrency, + providerList = [], + sortedRecommendedQuotes = ObservableList(), + sortedQuotes = ObservableList(), + paymentMethods = ObservableList(), + 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(); + } + + @observable + List cryptoCurrencies; + + @observable + List fiatCurrencies; + + final NumberFormat _cryptoNumberFormat; + late Timer bestRateSync; + + List get availableBuyProviders { + final providerTypes = ProvidersHelper.getAvailableBuyProviderTypes( + walletTypeForCurrency(cryptoCurrency) ?? wallet.type); + return providerTypes + .map((type) => ProvidersHelper.getProviderByType(type)) + .where((provider) => provider != null) + .cast() + .toList(); + } + + List get availableSellProviders { + final providerTypes = ProvidersHelper.getAvailableSellProviderTypes( + walletTypeForCurrency(cryptoCurrency) ?? wallet.type); + return providerTypes + .map((type) => ProvidersHelper.getProviderByType(type)) + .where((provider) => provider != null) + .cast() + .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; + + List get quoteOptions => [ + OptionTitle(title: 'Recommended'), + ...sortedRecommendedQuotes, + OptionTitle(title: 'All Providers'), + ...sortedQuotes + ]; + + @observable + bool isBuyAction = true; + + @observable + List providerList; + + @observable + ObservableList sortedRecommendedQuotes; + + @observable + ObservableList sortedQuotes; + + @observable + ObservableList paymentMethods; + + @observable + FiatCurrency fiatCurrency; + + @observable + CryptoCurrency cryptoCurrency; + + @observable + String cryptoAmount; + + @observable + String fiatAmount; + + @observable + String cryptoCurrencyAddress; + + @observable + Quote? bestRateQuote; + + @observable + Quote? selectedQuote; + + @observable + PaymentMethod? selectedPaymentMethod; + + @observable + PaymentMethodLoadingState paymentMethodState; + + @observable + BuySellQuotLoadingState buySellQuotState; + + @computed + bool get isReadyToTrade => + selectedQuote != null && + selectedPaymentMethod != null && + paymentMethodState is PaymentMethodLoaded && + buySellQuotState is BuySellQuotLoaded; + + @action + void reset() { + cryptoCurrency = wallet.currency; + fiatCurrency = settingsStore.fiatCurrency; + _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(); + } + + @action + void changeCryptoCurrencyAddress(String address) => cryptoCurrencyAddress = address; + + @action + Future changeFiatAmount({required String amount}) async { + fiatAmount = amount; + + if (amount.isEmpty) { + fiatAmount = ''; + cryptoAmount = ''; + return; + } + + final enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0; + + if (bestRateQuote == null) { + cryptoAmount = S.current.fetching; + } + + if (bestRateQuote != null) { + _cryptoNumberFormat.maximumFractionDigits = cryptoCurrency.decimals; + cryptoAmount = _cryptoNumberFormat + .format(enteredAmount / bestRateQuote!.rate) + .toString() + .replaceAll(RegExp('\\,'), ''); + } + + await calculateBestRate(); + } + + @action + Future changeCryptoAmount({required String amount}) async { + cryptoAmount = amount; + + if (amount.isEmpty) { + fiatAmount = ''; + cryptoAmount = ''; + return; + } + + final enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0; + + if (bestRateQuote == null) { + fiatAmount = S.current.fetching; + } + + if (bestRateQuote != null) { + fiatAmount = _cryptoNumberFormat + .format(enteredAmount * bestRateQuote!.rate) + .toString() + .replaceAll(RegExp('\\,'), ''); + } + await calculateBestRate(); + } + + @action + void changeOption(SelectableOption option) { + if (option is Quote) { + sortedRecommendedQuotes.forEach((element) => element.isSelected = false); + sortedQuotes.forEach((element) => element.isSelected = false); + option.isSelected = 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 _onPairChange() { + _initialize(); + } + + void _setProviders() => + providerList = isBuyAction ? availableBuyProviders : availableSellProviders; + + Future _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 _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 uniquePaymentMethods = {}; + for (var methods in result) { + for (var method in methods) { + uniquePaymentMethods[method.paymentMethodType] = method; + } + } + + paymentMethods = ObservableList.of(uniquePaymentMethods.values); + if (paymentMethods.isNotEmpty) { + paymentMethods.insert(0, PaymentMethod.all()); + selectedPaymentMethod = paymentMethods.first; + selectedPaymentMethod!.isSelected = true; + paymentMethodState = PaymentMethodLoaded(); + } else { + paymentMethodState = PaymentMethodFailed(); + } + } + + @action + Future calculateBestRate() async { + buySellQuotState = BuySellQuotLoading(); + + final result = await Future.wait?>(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 addedProviders = {}; + final List 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 + ..isBestRate = true + ..isSelected = true + ..recommendations.insert(0, ProviderRecommendation.bestRate); + bestRateQuote = sortedRecommendedQuotes.first; + selectedQuote = sortedRecommendedQuotes.first; + } + + buySellQuotState = BuySellQuotLoaded(); + } + + @action + Future launchTrade(BuildContext context) async { + final provider = selectedQuote!.provider; + provider.launchProvider( + context: context, + quote: selectedQuote!, + amount: amount, + isBuyAction: isBuyAction, + cryptoCurrencyAddress: cryptoCurrencyAddress, + ); + } +} diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 4c3a9e1ea..4e2b1619e 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -68,8 +68,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, @@ -436,37 +435,8 @@ abstract class DashboardViewModelBase with Store { Map> 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 get availableBuyProviders { - final providerTypes = ProvidersHelper.getAvailableBuyProviderTypes(wallet.type); - return providerTypes - .map((type) => ProvidersHelper.getProviderByType(type)) - .where((provider) => provider != null) - .cast() - .toList(); - } - - bool get hasBuyProviders => ProvidersHelper.getAvailableBuyProviderTypes(wallet.type).isNotEmpty; - - List get availableSellProviders { - final providerTypes = ProvidersHelper.getAvailableSellProviderTypes(wallet.type); - return providerTypes - .map((type) => ProvidersHelper.getProviderByType(type)) - .where((provider) => provider != null) - .cast() - .toList(); - } - - bool get hasSellProviders => - ProvidersHelper.getAvailableSellProviderTypes(wallet.type).isNotEmpty; - bool get shouldShowYatPopup => settingsStore.shouldShowYatPopup; @action @@ -479,16 +449,10 @@ abstract class DashboardViewModelBase with Store { bool hasExchangeAction; @computed - bool get isEnabledBuyAction => !settingsStore.disableBuy && hasBuyProviders; - - @observable - bool hasBuyAction; - - @computed - bool get isEnabledSellAction => !settingsStore.disableSell && hasSellProviders; + bool get isEnabledTradeAction => !settingsStore.disableTradeOption; @observable - bool hasSellAction; + bool hasTradeAction; @computed bool get isEnabledBulletinAction => !settingsStore.disableBulletin; @@ -670,8 +634,7 @@ abstract class DashboardViewModelBase with Store { void updateActions() { hasExchangeAction = !isHaven; - hasBuyAction = !isHaven; - hasSellAction = !isHaven; + hasTradeAction = !isHaven; } @computed diff --git a/lib/view_model/settings/other_settings_view_model.dart b/lib/view_model/settings/other_settings_view_model.dart index 9af8c67cf..3036e8ae9 100644 --- a/lib/view_model/settings/other_settings_view_model.dart +++ b/lib/view_model/settings/other_settings_view_model.dart @@ -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 get availableBuyProvidersTypes { - return ProvidersHelper.getAvailableBuyProviderTypes(walletType); - } - - List 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; } diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart index c1e0fb1ce..eaa9f9e84 100644 --- a/lib/view_model/settings/privacy_settings_view_model.dart +++ b/lib/view_model/settings/privacy_settings_view_model.dart @@ -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; diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 12ecceed9..045443f5f 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -122,6 +122,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": "اختر قيمة بطاقة", @@ -214,6 +216,7 @@ "disable_fee_api_warning": "من خلال إيقاف تشغيل هذا ، قد تكون معدلات الرسوم غير دقيقة في بعض الحالات ، لذلك قد ينتهي بك الأمر إلى دفع مبالغ زائدة أو دفع رسوم المعاملات الخاصة بك", "disable_fiat": "تعطيل fiat", "disable_sell": "قم بتعطيل إجراء البيع", + "disable_trade_option": "تعطيل خيار التجارة", "disableBatteryOptimization": "تعطيل تحسين البطارية", "disableBatteryOptimizationDescription": "هل تريد تعطيل تحسين البطارية من أجل جعل الخلفية مزامنة تعمل بحرية وسلاسة؟", "disabled": "معطلة", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index b1a1096e6..b69ed94b9 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -122,6 +122,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": "Изберете стойност на картата", @@ -214,6 +216,7 @@ "disable_fee_api_warning": "Като изключите това, таксите могат да бъдат неточни в някои случаи, така че може да се препланите или да не плащате таксите за вашите транзакции", "disable_fiat": "Деактивиране на fiat", "disable_sell": "Деактивирайте действието за продажба", + "disable_trade_option": "Деактивирайте опцията за търговия", "disableBatteryOptimization": "Деактивирайте оптимизацията на батерията", "disableBatteryOptimizationDescription": "Искате ли да деактивирате оптимизацията на батерията, за да направите синхронизирането на фона да работи по -свободно и гладко?", "disabled": "Деактивирано", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 7ce797845..1fc5b3d07 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -122,6 +122,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", @@ -214,6 +216,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", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index e599da769..9f81d98b0 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -122,6 +122,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", @@ -214,6 +216,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", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 72e05b8b5..aa3c9f55f 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -122,6 +122,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", @@ -214,6 +216,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", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 2cdc2318a..d94685da9 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -122,6 +122,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": "Elija un valor de tarjeta", @@ -214,6 +216,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", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 27109c0ae..3d0a6ed8f 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -122,6 +122,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", @@ -214,6 +216,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é", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 1c293bd54..ccdab8b3f 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -122,6 +122,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", @@ -214,6 +216,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", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index a93e20ea0..146451fab 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -122,6 +122,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": "एक कार्ड मूल्य चुनें", @@ -214,6 +216,7 @@ "disable_fee_api_warning": "इसे बंद करने से, कुछ मामलों में शुल्क दरें गलत हो सकती हैं, इसलिए आप अपने लेनदेन के लिए फीस को कम कर सकते हैं या कम कर सकते हैं", "disable_fiat": "िएट को अक्षम करें", "disable_sell": "बेचने की कार्रवाई अक्षम करें", + "disable_trade_option": "व्यापार विकल्प अक्षम करें", "disableBatteryOptimization": "बैटरी अनुकूलन अक्षम करें", "disableBatteryOptimizationDescription": "क्या आप बैकग्राउंड सिंक को अधिक स्वतंत्र और सुचारू रूप से चलाने के लिए बैटरी ऑप्टिमाइज़ेशन को अक्षम करना चाहते हैं?", "disabled": "अक्षम", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index b60a056fd..bcc2d0841 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -122,6 +122,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", @@ -214,6 +216,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", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index eeb1d3f99..aaaa3fddc 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -122,6 +122,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": "Ընտրեք քարտի արժեք", @@ -214,6 +216,7 @@ "disable_fee_api_warning": "Դրանից անջատելով, վճարների տեմպերը որոշ դեպքերում կարող են անճիշտ լինել, այնպես որ դուք կարող եք վերջ տալ ձեր գործարքների համար վճարների գերավճարների կամ գերավճարների վրա", "disable_fiat": "Անջատել ֆիատ", "disable_sell": "Անջատել վաճառք գործողությունը", + "disable_trade_option": "Անջատեք առեւտրի տարբերակը", "disableBatteryOptimization": "Անջատել մարտկոցի օպտիմիզացիան", "disableBatteryOptimizationDescription": "Դուք ցանկանում եք անջատել մարտկոցի օպտիմիզացիան ֆոնային համաժամացման ավելի ազատ և հարթ ընթացքի համար?", "disabled": "Անջատված", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 5e48ff1d3..3245cf2aa 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -122,6 +122,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", @@ -214,6 +216,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", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index d509cb256..2628d8fdd 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -122,6 +122,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", @@ -215,6 +217,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", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index f08437336..2843033b2 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -122,6 +122,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": "カード値を選択します", @@ -214,6 +216,7 @@ "disable_fee_api_warning": "これをオフにすることで、料金金利は場合によっては不正確になる可能性があるため、取引の費用が過払いまたは不足している可能性があります", "disable_fiat": "フィアットを無効にする", "disable_sell": "販売アクションを無効にする", + "disable_trade_option": "取引オプションを無効にします", "disableBatteryOptimization": "バッテリーの最適化を無効にします", "disableBatteryOptimizationDescription": "バックグラウンドシンクをより自由かつスムーズに実行するために、バッテリーの最適化を無効にしたいですか?", "disabled": "無効", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 60ee05da2..e52bd9f7f 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -122,6 +122,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": "카드 값을 선택하십시오", @@ -214,6 +216,7 @@ "disable_fee_api_warning": "이것을 끄면 경우에 따라 수수료가 부정확 할 수 있으므로 거래 수수료를 초과 지불하거나 지불 할 수 있습니다.", "disable_fiat": "법정화폐 비활성화", "disable_sell": "판매 조치 비활성화", + "disable_trade_option": "거래 옵션 비활성화", "disableBatteryOptimization": "배터리 최적화를 비활성화합니다", "disableBatteryOptimizationDescription": "백그라운드 동기화를보다 자유롭고 매끄럽게 실행하기 위해 배터리 최적화를 비활성화하고 싶습니까?", "disabled": "장애가 있는", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index d0aae18ea..8de8581db 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -122,6 +122,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": "ကဒ်တန်ဖိုးတစ်ခုရွေးပါ", @@ -214,6 +216,7 @@ "disable_fee_api_warning": "ဤအရာကိုဖွင့်ခြင်းအားဖြင့်အချို့သောကိစ္စရပ်များတွင်အခကြေးငွေနှုန်းထားများသည်တိကျမှုရှိနိုင်သည်,", "disable_fiat": "Fiat ကိုပိတ်ပါ။", "disable_sell": "ရောင်းချခြင်းလုပ်ဆောင်ချက်ကို ပိတ်ပါ။", + "disable_trade_option": "ကုန်သွယ်ရေး option ကိုပိတ်ပါ", "disableBatteryOptimization": "ဘက်ထရီ optimization ကိုပိတ်ပါ", "disableBatteryOptimizationDescription": "နောက်ခံထပ်တူပြုခြင်းနှင့်ချောချောမွေ့မွေ့ပြုလုပ်နိုင်ရန်ဘက်ထရီ optimization ကိုသင်ပိတ်ထားလိုပါသလား။", "disabled": "မသန်စွမ်း", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 273a65ae5..8d43e0738 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -122,6 +122,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", @@ -214,6 +216,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", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 046db2187..8243e9c02 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -122,6 +122,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", @@ -214,6 +216,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", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 164cb9530..5dd478893 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -122,6 +122,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", @@ -214,6 +216,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", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 28d856191..8e46d90df 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -122,6 +122,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": "Выберите значение карты", @@ -214,6 +216,7 @@ "disable_fee_api_warning": "Выключив это, в некоторых случаях ставки платы могут быть неточными, так что вы можете в конечном итоге переплачивать или недоплачивать сборы за ваши транзакции", "disable_fiat": "Отключить фиат", "disable_sell": "Отключить действие продажи", + "disable_trade_option": "Отключить возможность торговли", "disableBatteryOptimization": "Отключить оптимизацию батареи", "disableBatteryOptimizationDescription": "Вы хотите отключить оптимизацию батареи, чтобы сделать фона синхронизации более свободно и плавно?", "disabled": "Отключено", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index ee6d5b7a2..af08ac660 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -122,6 +122,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": "เลือกค่าบัตร", @@ -214,6 +216,7 @@ "disable_fee_api_warning": "โดยการปิดสิ่งนี้อัตราค่าธรรมเนียมอาจไม่ถูกต้องในบางกรณีดังนั้นคุณอาจจบลงด้วยการจ่ายเงินมากเกินไปหรือจ่ายค่าธรรมเนียมสำหรับการทำธุรกรรมของคุณมากเกินไป", "disable_fiat": "ปิดใช้งานสกุลเงินตรา", "disable_sell": "ปิดการใช้งานการขาย", + "disable_trade_option": "ปิดใช้งานตัวเลือกการค้า", "disableBatteryOptimization": "ปิดใช้งานการเพิ่มประสิทธิภาพแบตเตอรี่", "disableBatteryOptimizationDescription": "คุณต้องการปิดใช้งานการเพิ่มประสิทธิภาพแบตเตอรี่เพื่อให้การซิงค์พื้นหลังทำงานได้อย่างอิสระและราบรื่นมากขึ้นหรือไม่?", "disabled": "ปิดใช้งาน", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 2e7b4f4db..2b837daa6 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -122,6 +122,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", @@ -214,6 +216,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", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index fba4a796e..c135f2ab0 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -122,6 +122,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", @@ -214,6 +216,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ışı", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index ff1f2905f..375d71bc2 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -122,6 +122,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": "Виберіть значення картки", @@ -182,7 +184,7 @@ "creating_new_wallet_error": "Помилка: ${description}", "creation_date": "Дата створення", "custom": "на замовлення", - "custom_drag": "На замовлення (утримуйте та перетягується)", + "custom_drag": "На замовлення (утримуйте та перетягуйте)", "custom_redeem_amount": "Власна сума викупу", "custom_value": "Спеціальне значення", "dark_theme": "Темна", @@ -203,17 +205,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": "Вимкнено", @@ -224,14 +227,14 @@ "do_not_send": "Не надсилайте", "do_not_send_funds_to_contract_address_warning": "Не надсилайте кошти на цю адресу \n\n Це лише ідентифікатор для маркера, будь -які кошти, надіслані на цю адресу", "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_mempool_api": "API Mempool для точних зборів та дат", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 8cfe2ec9f..b42124e70 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -122,6 +122,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": "کارڈ کی قیمت کا انتخاب کریں", @@ -214,6 +216,7 @@ "disable_fee_api_warning": "اس کو بند کرنے سے ، کچھ معاملات میں فیس کی شرح غلط ہوسکتی ہے ، لہذا آپ اپنے لین دین کے لئے فیسوں کو زیادہ ادائیگی یا ادائیگی ختم کرسکتے ہیں۔", "disable_fiat": "فیاٹ کو غیر فعال کریں۔", "disable_sell": "فروخت کی کارروائی کو غیر فعال کریں۔", + "disable_trade_option": "تجارت کے آپشن کو غیر فعال کریں", "disableBatteryOptimization": "بیٹری کی اصلاح کو غیر فعال کریں", "disableBatteryOptimizationDescription": "کیا آپ پس منظر کی مطابقت پذیری کو زیادہ آزادانہ اور آسانی سے چلانے کے لئے بیٹری کی اصلاح کو غیر فعال کرنا چاہتے ہیں؟", "disabled": "معذور", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 444c1d6d8..bf044ff0b 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -215,6 +215,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", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 15b90b973..0b28076d6 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -122,6 +122,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", @@ -214,6 +216,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", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index ae7e15132..7b973ab9d 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -122,6 +122,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": "选择卡值", @@ -214,6 +216,7 @@ "disable_fee_api_warning": "通过将其关闭,在某些情况下,收费率可能不准确,因此您最终可能会超额付款或支付交易费用", "disable_fiat": "禁用法令", "disable_sell": "禁用卖出操作", + "disable_trade_option": "禁用贸易选项", "disableBatteryOptimization": "禁用电池优化", "disableBatteryOptimizationDescription": "您是否要禁用电池优化以使背景同步更加自由,平稳地运行?", "disabled": "禁用", diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index d3b652935..e6ae72f51 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -44,6 +44,8 @@ class SecretKey { SecretKey('cakePayApiKey', () => ''), SecretKey('CSRFToken', () => ''), SecretKey('authorization', () => ''), + SecretKey('meldTestApiKey', () => ''), + SecretKey('meldTestPublicKey', () => ''), SecretKey('etherScanApiKey', () => ''), SecretKey('polygonScanApiKey', () => ''), SecretKey('letsExchangeBearerToken', () => ''),