diff --git a/lib/buy/buy_amount.dart b/lib/buy/buy_amount.dart index d049c1e03..3cf3543d8 100644 --- a/lib/buy/buy_amount.dart +++ b/lib/buy/buy_amount.dart @@ -1,8 +1,12 @@ import 'package:flutter/foundation.dart'; class BuyAmount { - BuyAmount({@required this.sourceAmount, @required this.destAmount}); + BuyAmount({ + @required this.sourceAmount, + @required this.destAmount, + this.minAmount = 0}); final double sourceAmount; final double destAmount; + final int minAmount; } \ No newline at end of file diff --git a/lib/buy/moonpay/moonpay_buy_provider.dart b/lib/buy/moonpay/moonpay_buy_provider.dart index eef0e941f..22b349c79 100644 --- a/lib/buy/moonpay/moonpay_buy_provider.dart +++ b/lib/buy/moonpay/moonpay_buy_provider.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:crypto/crypto.dart'; import 'package:cake_wallet/buy/buy_exception.dart'; import 'package:http/http.dart'; import 'package:cake_wallet/buy/buy_amount.dart'; @@ -13,17 +14,18 @@ import 'package:cake_wallet/.secrets.g.dart' as secrets; class MoonPayBuyProvider extends BuyProvider { MoonPayBuyProvider({WalletBase wallet, bool isTestEnvironment = false}) : super(wallet: wallet, isTestEnvironment: isTestEnvironment) { - baseApiUrl = isTestEnvironment - ? _baseTestApiUrl - : _baseProductApiUrl; + baseUrl = isTestEnvironment ? _baseTestUrl : _baseProductUrl; } - static const _baseTestApiUrl = 'https://buy-staging.moonpay.com'; - static const _baseProductApiUrl = 'https://api.moonpay.com'; + static const _baseTestUrl = 'https://buy-staging.moonpay.com'; + static const _baseProductUrl = 'https://buy.moonpay.com'; + static const _apiUrl = 'https://api.moonpay.com'; static const _currenciesSuffix = '/v3/currencies'; static const _quoteSuffix = '/buy_quote'; static const _transactionsSuffix = '/v1/transactions'; + static const _countrySuffix = '/v3/countries'; static const _apiKey = secrets.moonPayApiKey; + static const _secretKey = secrets.moonPaySecretKey; @override String get title => 'MoonPay'; @@ -35,9 +37,9 @@ class MoonPayBuyProvider extends BuyProvider { walletTypeToCryptoCurrency(walletType).title.toLowerCase(); @override - String get trackUrl => baseApiUrl + '/transaction_receipt?transactionId='; + String get trackUrl => baseUrl + '/transaction_receipt?transactionId='; - String baseApiUrl; + String baseUrl; @override Future requestUrl(String amount, String sourceCurrency) async { @@ -45,22 +47,32 @@ class MoonPayBuyProvider extends BuyProvider { 'credit_debit_card%2Capple_pay%2Cgoogle_pay%2Csamsung_pay' '%2Csepa_bank_transfer%2Cgbp_bank_transfer%2Cgbp_open_banking_payment'; - final originalUrl = baseApiUrl + '?apiKey=' + _apiKey + '¤cyCode=' + + final suffix = '?apiKey=' + _apiKey + '¤cyCode=' + currencyCode + '&enabledPaymentMethods=' + enabledPaymentMethods + '&walletAddress=' + walletAddress + '&baseCurrencyCode=' + sourceCurrency.toLowerCase() + '&baseCurrencyAmount=' + amount + '&lockAmount=true' + '&showAllCurrencies=false' + '&showWalletAddressForm=false'; - return originalUrl; + final originalUrl = baseUrl + suffix; + + final messageBytes = utf8.encode(suffix); + final key = utf8.encode(_secretKey); + final hmac = Hmac(sha256, key); + final digest = hmac.convert(messageBytes); + final signature = base64.encode(digest.bytes); + final urlWithSignature = originalUrl + + '&signature=${Uri.encodeComponent(signature)}'; + + return isTestEnvironment ? originalUrl : urlWithSignature; } @override Future calculateAmount(String amount, String sourceCurrency) async { - final url = _baseProductApiUrl + _currenciesSuffix + '/$currencyCode' + + final url = _apiUrl + _currenciesSuffix + '/$currencyCode' + _quoteSuffix + '/?apiKey=' + _apiKey + '&baseCurrencyAmount=' + amount + - '&baseCurrencyCode' + sourceCurrency.toLowerCase(); + '&baseCurrencyCode=' + sourceCurrency.toLowerCase(); final response = await get(url); @@ -73,13 +85,17 @@ class MoonPayBuyProvider extends BuyProvider { 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); + return BuyAmount( + sourceAmount: sourceAmount, + destAmount: destAmount, + minAmount: minSourceAmount); } @override Future findOrderById(String id) async { - final url = _baseProductApiUrl + _transactionsSuffix + '/$id' + + final url = _apiUrl + _transactionsSuffix + '/$id' + '?apiKey=' + _apiKey; final response = await get(url); @@ -108,4 +124,29 @@ class MoonPayBuyProvider extends BuyProvider { walletId: walletId ); } + + static Future onEnabled(String deviceCountryCode) async { + final url = _apiUrl + _countrySuffix; + var isBuyEnable = false; + + final response = await get(url); + + try { + final responseJSON = json.decode(response.body) as List; + + for (var element in responseJSON) { + final countryMap = element as Map; + final countryCode = countryMap['alpha2'] as String; + if (countryCode == deviceCountryCode) { + isBuyEnable = countryMap['isBuyAllowed'] as bool; + break; + } + } + } catch (e) { + isBuyEnable = false; + print(e.toString()); + } + + return isBuyEnable; + } } \ No newline at end of file diff --git a/lib/di.dart b/lib/di.dart index 675366209..c00cb129e 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/bitcoin/bitcoin_wallet_service.dart'; import 'package:cake_wallet/bitcoin/litecoin_wallet_service.dart'; +import 'package:cake_wallet/buy/moonpay/moonpay_buy_provider.dart'; import 'package:cake_wallet/core/backup_service.dart'; import 'package:cake_wallet/core/wallet_service.dart'; import 'package:cake_wallet/entities/biometric_auth.dart'; @@ -91,6 +92,7 @@ import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:cake_wallet/view_model/wallet_restore_view_model.dart'; import 'package:cake_wallet/view_model/wallet_seed_view_model.dart'; import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart'; +import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/widgets.dart'; import 'package:get_it/get_it.dart'; import 'package:hive/hive.dart'; @@ -152,8 +154,14 @@ Future setup( final isBitcoinBuyEnabled = (secrets.wyreSecretKey?.isNotEmpty ?? false) && (secrets.wyreApiKey?.isNotEmpty ?? false) && (secrets.wyreAccountId?.isNotEmpty ?? false); + + final locale = await Devicelocale.currentLocale; + final deviceCountryCode = locale.split('_').last; + final isMoonPayEnabled = await MoonPayBuyProvider.onEnabled(deviceCountryCode); + final settingsStore = await SettingsStoreBase.load( - nodeSource: _nodeSource, isBitcoinBuyEnabled: isBitcoinBuyEnabled); + nodeSource: _nodeSource, isBitcoinBuyEnabled: isBitcoinBuyEnabled, + isMoonPayEnabled: isMoonPayEnabled); if (_isSetupFinished) { return; @@ -541,7 +549,8 @@ Future setup( final wallet = getIt.get().wallet; return BuyViewModel(_ordersSource, getIt.get(), - getIt.get(), wallet: wallet); + getIt.get(), getIt.get(), + wallet: wallet); }); getIt.registerFactory(() { diff --git a/lib/src/screens/buy/pre_order_page.dart b/lib/src/screens/buy/pre_order_page.dart index dbe03b07e..dd4832cc1 100644 --- a/lib/src/screens/buy/pre_order_page.dart +++ b/lib/src/screens/buy/pre_order_page.dart @@ -1,7 +1,11 @@ import 'dart:ui'; import 'package:cake_wallet/buy/buy_amount.dart'; +import 'package:cake_wallet/buy/buy_provider.dart'; +import 'package:cake_wallet/buy/moonpay/moonpay_buy_provider.dart'; import 'package:cake_wallet/src/screens/buy/widgets/buy_list_item.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/buy/buy_view_model.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -172,13 +176,16 @@ class PreOrderPage extends BasePage { builder: (context, AsyncSnapshot snapshot) { double sourceAmount; double destAmount; + int minAmount; if (snapshot.hasData) { sourceAmount = snapshot.data.sourceAmount; destAmount = snapshot.data.destAmount; + minAmount = snapshot.data.minAmount; } else { sourceAmount = 0.0; destAmount = 0.0; + minAmount = 0; } return Padding( @@ -192,14 +199,15 @@ class PreOrderPage extends BasePage { sourceCurrency: buyViewModel.fiatCurrency, destAmount: destAmount, destCurrency: buyViewModel.cryptoCurrency, - onTap: - buyViewModel.buyAmountViewModel - .doubleAmount == 0.0 ? null : () { - buyViewModel.selectedProvider = item.provider; - sourceAmount > 0 - ? buyViewModel.isDisabled = false - : buyViewModel.isDisabled = true; - } + onTap: ((buyViewModel.buyAmountViewModel + .doubleAmount != 0.0) && + (snapshot.hasData)) ? () => + onSelectBuyProvider( + context: context, + provider: item.provider, + sourceAmount: sourceAmount, + minAmount: minAmount + ) : null ); }) ); @@ -242,4 +250,26 @@ class PreOrderPage extends BasePage { ) ); } + + void onSelectBuyProvider({BuildContext context, BuyProvider provider, + double sourceAmount, int minAmount}) { + + if ((provider is MoonPayBuyProvider)&& + (buyViewModel.buyAmountViewModel.doubleAmount < minAmount)) { + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: 'MoonPay', + alertContent: 'Value of the amount must be more than $minAmount ${buyViewModel.fiatCurrency.toString()}', + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + return; + } + buyViewModel.selectedProvider = provider; + sourceAmount > 0 + ? buyViewModel.isDisabled = false + : buyViewModel.isDisabled = true; + } } \ No newline at end of file diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index da2410378..2b26b2dd8 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart'; +import 'package:cake_wallet/buy/moonpay/moonpay_buy_provider.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/transaction_priority.dart'; import 'package:cake_wallet/themes/theme_base.dart'; @@ -18,6 +19,7 @@ import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/entities/node.dart'; import 'package:cake_wallet/entities/monero_transaction_priority.dart'; import 'package:cake_wallet/entities/action_list_display_mode.dart'; +import 'package:cake_wallet/.secrets.g.dart' as secrets; part 'settings_store.g.dart'; @@ -39,6 +41,7 @@ abstract class SettingsStoreBase with Store { @required TransactionPriority initialBitcoinTransactionPriority, @required TransactionPriority initialMoneroTransactionPriority, @required this.isBitcoinBuyEnabled, + @required this.isMoonPayEnabled, this.actionlistDisplayMode}) { fiatCurrency = initialFiatCurrency; balanceDisplayMode = initialBalanceDisplayMode; @@ -147,9 +150,12 @@ abstract class SettingsStoreBase with Store { bool isBitcoinBuyEnabled; + bool isMoonPayEnabled; + static Future load( {@required Box nodeSource, @required bool isBitcoinBuyEnabled, + @required bool isMoonPayEnabled, FiatCurrency initialFiatCurrency = FiatCurrency.usd, MoneroTransactionPriority initialMoneroTransactionPriority = MoneroTransactionPriority.slow, @@ -221,6 +227,7 @@ abstract class SettingsStoreBase with Store { }, appVersion: packageInfo.version, isBitcoinBuyEnabled: isBitcoinBuyEnabled, + isMoonPayEnabled: isMoonPayEnabled, initialFiatCurrency: currentFiatCurrency, initialBalanceDisplayMode: currentBalanceDisplayMode, initialSaveRecipientAddress: shouldSaveRecipientAddress, @@ -242,8 +249,18 @@ abstract class SettingsStoreBase with Store { BitcoinTransactionPriority.medium, BalanceDisplayMode initialBalanceDisplayMode = BalanceDisplayMode.availableBalance}) async { + final isBitcoinBuyEnabled = (secrets.wyreSecretKey?.isNotEmpty ?? false) && + (secrets.wyreApiKey?.isNotEmpty ?? false) && + (secrets.wyreAccountId?.isNotEmpty ?? false); + + final locale = await Devicelocale.currentLocale; + final deviceCountryCode = locale.split('_').last; + final isMoonPayEnabled = await MoonPayBuyProvider.onEnabled(deviceCountryCode); + final settings = await SettingsStoreBase.load( nodeSource: nodeSource, + isBitcoinBuyEnabled: isBitcoinBuyEnabled, + isMoonPayEnabled: isMoonPayEnabled, initialBalanceDisplayMode: initialBalanceDisplayMode, initialFiatCurrency: initialFiatCurrency, initialMoneroTransactionPriority: initialMoneroTransactionPriority, diff --git a/lib/view_model/buy/buy_view_model.dart b/lib/view_model/buy/buy_view_model.dart index 90dbd6cfa..68afb9577 100644 --- a/lib/view_model/buy/buy_view_model.dart +++ b/lib/view_model/buy/buy_view_model.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/buy/wyre/wyre_buy_provider.dart'; import 'package:cake_wallet/entities/crypto_currency.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/entities/wallet_type.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/view_model/buy/buy_item.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; @@ -18,12 +19,14 @@ part 'buy_view_model.g.dart'; class BuyViewModel = BuyViewModelBase with _$BuyViewModel; abstract class BuyViewModelBase with Store { - BuyViewModelBase(this.ordersSource, this.ordersStore, this.buyAmountViewModel, - {@required this.wallet}) { - providerList = [ - WyreBuyProvider(wallet: wallet), - MoonPayBuyProvider(wallet: wallet) - ]; + BuyViewModelBase(this.ordersSource, this.ordersStore, this.settingsStore, + this.buyAmountViewModel, {@required this.wallet}) { + providerList = [WyreBuyProvider(wallet: wallet)]; + + if (settingsStore.isMoonPayEnabled ?? false) { + providerList.add(MoonPayBuyProvider(wallet: wallet)); + } + items = providerList.map((provider) => BuyItem(provider: provider, buyAmountViewModel: buyAmountViewModel)) .toList(); @@ -34,6 +37,7 @@ abstract class BuyViewModelBase with Store { final Box ordersSource; final OrdersStore ordersStore; + final SettingsStore settingsStore; final BuyAmountViewModel buyAmountViewModel; final WalletBase wallet;