diff --git a/lib/buy/buy_amount.dart b/lib/buy/buy_amount.dart new file mode 100644 index 000000000..d049c1e03 --- /dev/null +++ b/lib/buy/buy_amount.dart @@ -0,0 +1,8 @@ +import 'package:flutter/foundation.dart'; + +class BuyAmount { + BuyAmount({@required this.sourceAmount, @required this.destAmount}); + + final double sourceAmount; + final double destAmount; +} \ No newline at end of file diff --git a/lib/buy/buy_exception.dart b/lib/buy/buy_exception.dart new file mode 100644 index 000000000..28064fdfc --- /dev/null +++ b/lib/buy/buy_exception.dart @@ -0,0 +1,12 @@ +import 'package:flutter/foundation.dart'; +import 'package:cake_wallet/buy/buy_provider_description.dart'; + +class BuyException implements Exception { + BuyException({@required this.description, @required this.text}); + + final BuyProviderDescription description; + final String text; + + @override + String toString() => '${description.title}: $text'; +} \ No newline at end of file diff --git a/lib/buy/buy_provider.dart b/lib/buy/buy_provider.dart new file mode 100644 index 000000000..78ad54451 --- /dev/null +++ b/lib/buy/buy_provider.dart @@ -0,0 +1,26 @@ +import 'package:cake_wallet/buy/buy_amount.dart'; +import 'package:cake_wallet/buy/buy_provider_description.dart'; +import 'package:cake_wallet/buy/order.dart'; +import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/entities/wallet_type.dart'; + +abstract class BuyProvider { + BuyProvider({this.wallet, this.isTestEnvironment}); + + final WalletBase wallet; + final bool isTestEnvironment; + + String get title; + BuyProviderDescription get description; + + WalletType get walletType => wallet.type; + String get walletAddress => wallet.address; + String get walletId => wallet.id; + + @override + String toString() => title; + + Future requestUrl(String amount, String sourceCurrency); + Future findOrderById(String id); + Future calculateAmount(String amount, String sourceCurrency); +} \ No newline at end of file diff --git a/lib/buy/buy_provider_description.dart b/lib/buy/buy_provider_description.dart new file mode 100644 index 000000000..46b3bcdc1 --- /dev/null +++ b/lib/buy/buy_provider_description.dart @@ -0,0 +1,21 @@ +import 'package:cake_wallet/entities/enumerable_item.dart'; + +class BuyProviderDescription extends EnumerableItem + with Serializable { + const BuyProviderDescription({String title, int raw}) + : super(title: title, raw: raw); + + static const wyre = BuyProviderDescription(title: 'Wyre', raw: 0); + static const moonPay = BuyProviderDescription(title: 'MoonPay', raw: 1); + + static BuyProviderDescription deserialize({int raw}) { + switch (raw) { + case 0: + return wyre; + case 1: + return moonPay; + default: + return null; + } + } +} \ No newline at end of file diff --git a/lib/buy/moonpay/moonpay_buy_provider.dart b/lib/buy/moonpay/moonpay_buy_provider.dart new file mode 100644 index 000000000..972b2d2a4 --- /dev/null +++ b/lib/buy/moonpay/moonpay_buy_provider.dart @@ -0,0 +1,109 @@ +import 'dart:convert'; +import 'package:cake_wallet/buy/buy_exception.dart'; +import 'package:http/http.dart'; +import 'package:cake_wallet/buy/buy_amount.dart'; +import 'package:cake_wallet/buy/buy_provider.dart'; +import 'package:cake_wallet/buy/buy_provider_description.dart'; +import 'package:cake_wallet/buy/order.dart'; +import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/entities/wallet_type.dart'; +import 'package:cake_wallet/exchange/trade_state.dart'; +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; + } + + static const _baseTestApiUrl = 'https://buy-staging.moonpay.com'; + static const _baseProductApiUrl = 'https://api.moonpay.com'; + static const _currenciesSuffix = '/v3/currencies'; + static const _quoteSuffix = '/buy_quote'; + static const _transactionsSuffix = '/v1/transactions'; + static const _apiKey = secrets.moonPayApiKey; + + @override + String get title => 'MoonPay'; + + @override + BuyProviderDescription get description => BuyProviderDescription.moonPay; + + String get currencyCode => + walletTypeToCryptoCurrency(walletType).title.toLowerCase(); + + String baseApiUrl; + + @override + Future requestUrl(String amount, String sourceCurrency) async { + final enabledPaymentMethods = + '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=' + + currencyCode + '&enabledPaymentMethods=' + enabledPaymentMethods + + '&walletAddress=' + walletAddress + + '&baseCurrencyCode=' + sourceCurrency.toLowerCase() + + '&baseCurrencyAmount=' + amount + '&lockAmount=true' + + '&showAllCurrencies=false' + '&showWalletAddressForm=false'; + + return originalUrl; + } + + @override + Future calculateAmount(String amount, String sourceCurrency) async { + final url = baseApiUrl + _currenciesSuffix + '/$currencyCode' + + _quoteSuffix + '/?apiKey=' + _apiKey + + '&baseCurrencyAmount=' + amount + + '&baseCurrencyCode' + sourceCurrency.toLowerCase(); + + final response = await get(url); + + if (response.statusCode != 200) { + throw BuyException( + description: description, + text: 'Quote is not found!'); + } + + final responseJSON = json.decode(response.body) as Map; + final sourceAmount = responseJSON['totalAmount'] as double; + final destAmount = responseJSON['quoteCurrencyAmount'] as double; + + return BuyAmount(sourceAmount: sourceAmount, destAmount: destAmount); + } + + @override + Future findOrderById(String id) async { + final url = baseApiUrl + _transactionsSuffix + '/$id' + + '?apiKey=' + _apiKey; + + final response = await get(url); + + if (response.statusCode != 200) { + throw BuyException( + description: description, + text: 'Transaction $id is not found!'); + } + + final responseJSON = json.decode(response.body) as Map; + final status = responseJSON['status'] as String; + final state = TradeState.deserialize(raw: status.toLowerCase()); + final createdAt = responseJSON['createdAt'] as DateTime; + final amount = responseJSON['quoteCurrencyAmount'] as double; + + return Order( + id: id, + provider: description, + transferId: id, + from: 'USD', //FIXME + to: 'BTC', //FIXME + state: state, + createdAt: createdAt, + amount: amount.toString(), + receiveAddress: walletAddress, + walletId: walletId + ); + } +} \ No newline at end of file diff --git a/lib/entities/order.dart b/lib/buy/order.dart similarity index 76% rename from lib/entities/order.dart rename to lib/buy/order.dart index 1676edd99..54c13bd37 100644 --- a/lib/entities/order.dart +++ b/lib/buy/order.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/buy/buy_provider_description.dart'; import 'package:hive/hive.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/entities/format_amount.dart'; @@ -8,6 +9,7 @@ part 'order.g.dart'; class Order extends HiveObject { Order( {this.id, + BuyProviderDescription provider, this.transferId, this.from, this.to, @@ -16,7 +18,8 @@ class Order extends HiveObject { this.amount, this.receiveAddress, this.walletId}) - : stateRaw = state?.raw; + : providerRaw = provider?.raw, + stateRaw = state?.raw; static const typeId = 8; static const boxName = 'Orders'; @@ -51,5 +54,11 @@ class Order extends HiveObject { @HiveField(8) String walletId; + @HiveField(9) + int providerRaw; + + BuyProviderDescription get provider => + BuyProviderDescription.deserialize(raw: providerRaw); + String amountFormatted() => formatAmount(amount); } \ No newline at end of file diff --git a/lib/buy/wyre/wyre_buy_provider.dart b/lib/buy/wyre/wyre_buy_provider.dart new file mode 100644 index 000000000..0b94816b2 --- /dev/null +++ b/lib/buy/wyre/wyre_buy_provider.dart @@ -0,0 +1,161 @@ +import 'dart:convert'; +import 'package:cake_wallet/buy/buy_exception.dart'; +import 'package:http/http.dart'; +import 'package:cake_wallet/buy/buy_amount.dart'; +import 'package:cake_wallet/buy/buy_provider.dart'; +import 'package:cake_wallet/buy/buy_provider_description.dart'; +import 'package:cake_wallet/buy/order.dart'; +import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/entities/wallet_type.dart'; +import 'package:cake_wallet/exchange/trade_state.dart'; +import 'package:cake_wallet/.secrets.g.dart' as secrets; + +class WyreBuyProvider extends BuyProvider { + WyreBuyProvider({WalletBase wallet, bool isTestEnvironment = false}) + : super(wallet: wallet, isTestEnvironment: isTestEnvironment) { + baseApiUrl = isTestEnvironment + ? _baseTestApiUrl + : _baseProductApiUrl; + trackUrl = isTestEnvironment + ? _trackTestUrl + : _trackProductUrl; + } + + static const _baseTestApiUrl = 'https://api.testwyre.com'; + static const _baseProductApiUrl = 'https://api.sendwyre.com'; + static const _trackTestUrl = 'https://dash.testwyre.com/track/'; + static const _trackProductUrl = 'https://dash.sendwyre.com/track/'; + static const _ordersSuffix = '/v3/orders'; + static const _reserveSuffix = '/reserve'; + static const _quoteSuffix = '/quote/partner'; + static const _timeStampSuffix = '?timestamp='; + static const _transferSuffix = '/v2/transfer/'; + static const _trackSuffix = '/track'; + static const _secretKey = secrets.wyreSecretKey; + static const _accountId = secrets.wyreAccountId; + + @override + String get title => 'Wyre'; + + @override + BuyProviderDescription get description => BuyProviderDescription.wyre; + + String baseApiUrl; + String trackUrl; + + @override + Future requestUrl(String amount, String sourceCurrency) async { + final timestamp = DateTime.now().millisecondsSinceEpoch.toString(); + final url = baseApiUrl + _ordersSuffix + _reserveSuffix + + _timeStampSuffix + timestamp; + final body = { + 'amount': amount, + 'sourceCurrency': sourceCurrency, + 'destCurrency': walletTypeToCryptoCurrency(walletType).title, + 'dest': walletTypeToString(walletType).toLowerCase() + ':' + walletAddress, + 'referrerAccountId': _accountId, + 'lockFields': ['amount', 'sourceCurrency', 'destCurrency', 'dest'] + }; + + final response = await post(url, + headers: { + 'Authorization': 'Bearer $_secretKey', + 'Content-Type': 'application/json', + 'cache-control': 'no-cache' + }, + body: json.encode(body)); + + if (response.statusCode != 200) { + throw BuyException( + description: description, + text: 'Url $url is not found!'); + } + + final responseJSON = json.decode(response.body) as Map; + final urlFromResponse = responseJSON['url'] as String; + return urlFromResponse; + } + + @override + Future calculateAmount(String amount, String sourceCurrency) async { + final quoteUrl = baseApiUrl + _ordersSuffix + _quoteSuffix; + final body = { + 'amount': amount, + 'sourceCurrency': sourceCurrency, + 'destCurrency': walletTypeToCryptoCurrency(walletType).title, + 'dest': walletTypeToString(walletType).toLowerCase() + ':' + walletAddress, + 'accountId': _accountId, + 'country': 'US' //FIXME + }; + + final response = await post(quoteUrl, + headers: { + 'Authorization': 'Bearer $_secretKey', + 'Content-Type': 'application/json', + 'cache-control': 'no-cache' + }, + body: json.encode(body)); + + if (response.statusCode != 200) { + throw BuyException( + description: description, + text: 'Quote is not found!'); + } + + final responseJSON = json.decode(response.body) as Map; + final sourceAmount = responseJSON['sourceAmount'] as double; + final destAmount = responseJSON['destAmount'] as double; + + return BuyAmount(sourceAmount: sourceAmount, destAmount: destAmount); + } + + @override + Future findOrderById(String id) async { + final orderUrl = baseApiUrl + _ordersSuffix + '/$id'; + final orderResponse = await get(orderUrl); + + if (orderResponse.statusCode != 200) { + throw BuyException( + description: description, + text: 'Order $id is not found!'); + } + + final orderResponseJSON = + json.decode(orderResponse.body) as Map; + final transferId = orderResponseJSON['transferId'] as String; + final from = orderResponseJSON['sourceCurrency'] as String; + final to = orderResponseJSON['destCurrency'] as String; + final status = orderResponseJSON['status'] as String; + final state = TradeState.deserialize(raw: status.toLowerCase()); + final createdAtRaw = orderResponseJSON['createdAt'] as int; + final createdAt = + DateTime.fromMillisecondsSinceEpoch(createdAtRaw).toLocal(); + + final transferUrl = + baseApiUrl + _transferSuffix + transferId + _trackSuffix; + final transferResponse = await get(transferUrl); + + if (transferResponse.statusCode != 200) { + throw BuyException( + description: description, + text: 'Transfer $transferId is not found!'); + } + + final transferResponseJSON = + json.decode(transferResponse.body) as Map; + final amount = transferResponseJSON['destAmount'] as double; + + return Order( + id: id, + provider: description, + transferId: transferId, + from: from, + to: to, + state: state, + createdAt: createdAt, + amount: amount.toString(), + receiveAddress: walletAddress, + walletId: walletId + ); + } +} \ No newline at end of file diff --git a/lib/di.dart b/lib/di.dart index 66ac22231..8663b31de 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -4,7 +4,7 @@ import 'package:cake_wallet/core/wallet_service.dart'; import 'package:cake_wallet/entities/biometric_auth.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/load_current_wallet.dart'; -import 'package:cake_wallet/entities/order.dart'; +import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/entities/transaction_description.dart'; import 'package:cake_wallet/entities/transaction_info.dart'; import 'package:cake_wallet/entities/wyre_service.dart'; @@ -15,7 +15,7 @@ import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/reactions/on_authentication_state_change.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/pre_order_page.dart'; import 'package:cake_wallet/src/screens/contact/contact_list_page.dart'; import 'package:cake_wallet/src/screens/contact/contact_page.dart'; import 'package:cake_wallet/src/screens/exchange_trade/exchange_confirm_page.dart'; @@ -61,6 +61,8 @@ import 'package:cake_wallet/src/screens/subaddress/address_edit_or_create_page.d import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart'; import 'package:cake_wallet/store/wallet_list_store.dart'; import 'package:cake_wallet/view_model/backup_view_model.dart'; +import 'package:cake_wallet/view_model/buy/buy_amount_view_model.dart'; +import 'package:cake_wallet/view_model/buy/buy_view_model.dart'; import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart'; import 'package:cake_wallet/view_model/edit_backup_password_view_model.dart'; @@ -546,6 +548,19 @@ Future setup( WyrePage(getIt.get(), ordersStore: getIt.get(), url: url)); + getIt.registerFactory(() => BuyAmountViewModel()); + + getIt.registerFactory(() { + final wallet = getIt.get().wallet; + + return BuyViewModel(ordersSource, getIt.get(), + getIt.get(), wallet: wallet); + }); + + getIt.registerFactory(() { + return PreOrderPage(buyViewModel: getIt.get()); + }); + getIt.registerFactoryParam( (order, _) => OrderDetailsViewModel( wyreViewModel: getIt.get(), diff --git a/lib/entities/wyre_service.dart b/lib/entities/wyre_service.dart index b83bfce68..f45f33d44 100644 --- a/lib/entities/wyre_service.dart +++ b/lib/entities/wyre_service.dart @@ -5,7 +5,7 @@ import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; -import 'package:cake_wallet/entities/order.dart'; +import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/entities/wallet_type.dart'; class WyreService { @@ -26,6 +26,7 @@ class WyreService { static const _trackProductUrl = 'https://dash.sendwyre.com/track/'; static const _ordersSuffix = '/v3/orders'; static const _reserveSuffix = '/reserve'; + static const _quoteSuffix = '/quote/partner'; static const _timeStampSuffix = '?timestamp='; static const _transferSuffix = '/v2/transfer/'; static const _trackSuffix = '/track'; @@ -47,10 +48,12 @@ class WyreService { final secretKey = secrets.wyreSecretKey; final accountId = secrets.wyreAccountId; final body = { + 'amount': '1', + 'sourceCurrency': 'USD', 'destCurrency': walletTypeToCryptoCurrency(walletType).title, 'dest': walletTypeToString(walletType).toLowerCase() + ':' + walletAddress, 'referrerAccountId': accountId, - 'lockFields': ['destCurrency', 'dest'] + 'lockFields': ['amount', 'sourceCurrency', 'destCurrency', 'dest'] }; final response = await post(url, @@ -113,4 +116,36 @@ class WyreService { walletId: walletId ); } + + Future getAmount() async { + final quoteUrl = baseApiUrl + _ordersSuffix + _quoteSuffix; + + final secretKey = secrets.wyreSecretKey; + final accountId = secrets.wyreAccountId; + final body = { + 'amount': '1', + 'sourceCurrency': 'USD', + 'destCurrency': walletTypeToCryptoCurrency(walletType).title, + 'dest': walletTypeToString(walletType).toLowerCase() + ':' + walletAddress, + 'accountId': accountId, + 'country': 'US' + }; + + final response = await post(quoteUrl, + headers: { + 'Authorization': 'Bearer $secretKey', + 'Content-Type': 'application/json', + 'cache-control': 'no-cache' + }, + body: json.encode(body)); + + if (response.statusCode != 200) { + throw WyreException('Quote is not found! '); + } + + final responseJSON = json.decode(response.body) as Map; + final amount = responseJSON['destAmount'] as double; + + return amount; + } } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 48b54c416..80b48f50b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,5 @@ import 'package:cake_wallet/entities/language_service.dart'; -import 'package:cake_wallet/entities/order.dart'; +import 'package:cake_wallet/buy/order.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hive/hive.dart'; diff --git a/lib/palette.dart b/lib/palette.dart index 336f7b7f9..a26a27163 100644 --- a/lib/palette.dart +++ b/lib/palette.dart @@ -23,6 +23,7 @@ class Palette { static const Color persianRed = Color.fromRGBO(206, 55, 55, 1.0); static const Color blueCraiola = Color.fromRGBO(69, 110, 255, 1.0); static const Color blueGreyCraiola = Color.fromRGBO(106, 177, 207, 1.0); + static const Color greyBlueCraiola = Color.fromRGBO(116, 139, 219, 1.0); static const Color darkBlueCraiola = Color.fromRGBO(53, 86, 136, 1.0); static const Color pinkFlamingo = Color.fromRGBO(240, 60, 243, 1.0); static const Color redHat = Color.fromRGBO(209, 68, 37, 1.0); diff --git a/lib/router.dart b/lib/router.dart index 0474758e3..f8a3d454e 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -1,8 +1,9 @@ import 'package:cake_wallet/entities/contact_record.dart'; -import 'package:cake_wallet/entities/order.dart'; +import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/entities/transaction_description.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/pre_order_page.dart'; import 'package:cake_wallet/src/screens/order_details/order_details_page.dart'; import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; import 'package:cake_wallet/src/screens/restore/restore_from_backup_page.dart'; @@ -297,6 +298,11 @@ Route createRoute(RouteSettings settings) { builder: (_) => getIt.get(param1: settings.arguments as Order)); + case Routes.preOrder: + return MaterialPageRoute( + builder: (_) => + getIt.get()); + case Routes.wyre: return MaterialPageRoute( builder: (_) => diff --git a/lib/routes.dart b/lib/routes.dart index 2bf8ee7b7..88b9b8440 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -54,4 +54,5 @@ class Routes { static const support = '/support'; static const orderDetails = '/order_details'; static const wyre = '/wyre'; + static const preOrder = '/pre_order'; } \ No newline at end of file diff --git a/lib/src/screens/buy/pre_order_page.dart b/lib/src/screens/buy/pre_order_page.dart new file mode 100644 index 000000000..e42b64790 --- /dev/null +++ b/lib/src/screens/buy/pre_order_page.dart @@ -0,0 +1,210 @@ +import 'dart:ui'; +import 'package:cake_wallet/buy/buy_amount.dart'; +import 'package:cake_wallet/src/screens/buy/widgets/buy_list_item.dart'; +import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; +import 'package:cake_wallet/view_model/buy/buy_view_model.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:keyboard_actions/keyboard_actions.dart'; +import 'package:cake_wallet/routes.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/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; + +class PreOrderPage extends BasePage { + PreOrderPage({@required this.buyViewModel}) + : _amountFocus = FocusNode(), + _amountController = TextEditingController() { + + _amountController.addListener(() { + final amount = _amountController.text; + + if (amount != buyViewModel.buyAmountViewModel.amount) { + buyViewModel.buyAmountViewModel.amount = amount; + } + + if (buyViewModel.buyAmountViewModel.doubleAmount == 0.0) { + buyViewModel.selectedProvider = null; + } + }); + } + + final BuyViewModel buyViewModel; + final FocusNode _amountFocus; + final TextEditingController _amountController; + + @override + String get title => 'Buy Bitcoin'; + + @override + Color get titleColor => Colors.white; + + @override + bool get resizeToAvoidBottomInset => false; + + @override + bool get extendBodyBehindAppBar => true; + + @override + AppBarStyle get appBarStyle => AppBarStyle.transparent; + + @override + Widget body(BuildContext context) { + return KeyboardActions( + config: KeyboardActionsConfig( + keyboardActionsPlatform: KeyboardActionsPlatform.IOS, + keyboardBarColor: Theme.of(context).accentTextTheme.body2 + .backgroundColor, + nextFocus: false, + actions: [ + KeyboardActionsItem( + focusNode: _amountFocus, + toolbarButtons: [(_) => KeyboardDoneButton()], + ), + ]), + child: Container( + height: 0, + color: Theme.of(context).backgroundColor, + child: ScrollableWithBottomSection( + contentPadding: EdgeInsets.only(bottom: 24), + content: Column( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(24), + bottomRight: Radius.circular(24)), + gradient: LinearGradient(colors: [ + Theme.of(context).primaryTextTheme.subhead.color, + Theme.of(context) + .primaryTextTheme + .subhead + .decorationColor, + ], begin: Alignment.topLeft, end: Alignment.bottomRight), + ), + child: Padding( + padding: EdgeInsets.fromLTRB(100, 100, 100, 65), + child: BaseTextFormField( + focusNode: _amountFocus, + controller: _amountController, + keyboardType: + TextInputType.numberWithOptions( + signed: false, decimal: true), + inputFormatters: [ + FilteringTextInputFormatter + .allow(RegExp('^([0-9]+([.\,][0-9]{0,2})?|[.\,][0-9]{1,2})\$')) + ], + prefixIcon: Padding( + padding: EdgeInsets.only(top: 2), + child: + Text(buyViewModel.fiatCurrency.title + ': ', + style: TextStyle( + fontSize: 36, + fontWeight: FontWeight.w600, + color: Colors.white, + )), + ), + hintText: '0.00', + borderColor: Theme.of(context) + .primaryTextTheme + .headline + .color, + textStyle: TextStyle( + fontSize: 36, + fontWeight: FontWeight.w500, + color: Colors.white), + placeholderTextStyle: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .headline + .decorationColor, + fontWeight: FontWeight.w500, + fontSize: 36), + ) + ) + ), + Padding( + padding: EdgeInsets.only(top: 38, bottom: 18), + child: Text( + 'Buy with:', + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).primaryTextTheme.title.color, + fontSize: 18, + fontWeight: FontWeight.w500 + ), + ) + ), + ...buyViewModel.items.map( + (item) => Observer(builder: (_) => FutureBuilder( + future: item.buyAmount, + builder: (context, AsyncSnapshot snapshot) { + double sourceAmount; + double destAmount; + + if (snapshot.hasData) { + sourceAmount = snapshot.data.sourceAmount; + destAmount = snapshot.data.destAmount; + } else { + sourceAmount = 0.0; + destAmount = 0.0; + } + + return Padding( + padding: + EdgeInsets.only(left: 15, top: 20, right: 15), + child: Observer(builder: (_) => BuyListItem( + selectedProvider: buyViewModel.selectedProvider, + provider: item.provider, + sourceAmount: sourceAmount, + 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; + } + )) + ); + } + ),) + ) + ], + ), + bottomSectionPadding: + EdgeInsets.only(left: 24, right: 24, bottom: 24), + bottomSection: Observer(builder: (_) { + return LoadingPrimaryButton( + onPressed: buyViewModel.isRunning + ? null + : () { + buyViewModel.isRunning = true; + + // FIXME: Start WebView + + buyViewModel.isRunning = false; + }, + text: buyViewModel.selectedProvider == null + ? 'Buy' + : 'Buy with ${buyViewModel.selectedProvider + .description.title}', + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + isLoading: buyViewModel.isRunning, + isDisabled: (buyViewModel.selectedProvider == null) || + buyViewModel.isDisabled + ); + }) + ) + ) + ); + } +} \ No newline at end of file diff --git a/lib/src/screens/buy/widgets/buy_list_item.dart b/lib/src/screens/buy/widgets/buy_list_item.dart new file mode 100644 index 000000000..fe198a350 --- /dev/null +++ b/lib/src/screens/buy/widgets/buy_list_item.dart @@ -0,0 +1,131 @@ +import 'package:cake_wallet/buy/buy_provider_description.dart'; +import 'package:cake_wallet/buy/buy_provider.dart'; +import 'package:cake_wallet/entities/crypto_currency.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cake_wallet/palette.dart'; +import 'package:flutter/material.dart'; + +class BuyListItem extends StatelessWidget { + BuyListItem({ + @required this.selectedProvider, + @required this.provider, + @required this.sourceAmount, + @required this.sourceCurrency, + @required this.destAmount, + @required this.destCurrency, + @required this.onTap + }); + + final _wyreIcon = + Image.asset('assets/images/wyre-icon.png', width: 36, height: 36); + final _mooonPayIcon = + Image.asset('assets/images/wyre-icon.png', width: 36, height: 36); + + final BuyProvider selectedProvider; + final BuyProvider provider; + final double sourceAmount; + final FiatCurrency sourceCurrency; + final double destAmount; + final CryptoCurrency destCurrency; + final void Function() onTap; + + @override + Widget build(BuildContext context) { + final providerIcon = _getProviderIcon(provider.description); + + final backgroundColor = selectedProvider != null + ? selectedProvider.description == provider.description + ? Palette.greyBlueCraiola + : Palette.shadowWhite + : Palette.shadowWhite; + + final primaryTextColor = selectedProvider != null + ? selectedProvider.description == provider.description + ? Colors.white + : Palette.darkGray + : Palette.darkGray; + + final secondaryTextColor = selectedProvider != null + ? selectedProvider.description == provider.description + ? Colors.white + : Palette.darkBlueCraiola + : Palette.darkBlueCraiola; + + return GestureDetector( + onTap: () => onTap?.call(), + child: Container( + height: 102, + padding: EdgeInsets.only( + left: 20, + right: 20 + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(25)), + color: backgroundColor + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (providerIcon != null) Padding( + padding: EdgeInsets.only(right: 10), + child: providerIcon + ), + Text( + provider.description.title, + style: TextStyle( + color: primaryTextColor, + fontSize: 24, + fontWeight: FontWeight.w500 + ), + ) + ], + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '${destAmount?.toString()} ${destCurrency.title}', + style: TextStyle( + color: secondaryTextColor, + fontSize: 18, + fontWeight: FontWeight.w500 + ), + ), + Padding( + padding: EdgeInsets.only(top: 5), + child: Text( + '${sourceAmount?.toString()} ${sourceCurrency.title}', + style: TextStyle( + color: primaryTextColor, + fontSize: 18, + fontWeight: FontWeight.w500 + ), + ), + ) + ], + ) + ], + ), + ) + ); + } + + Image _getProviderIcon(BuyProviderDescription providerDescription) { + switch (providerDescription) { + case BuyProviderDescription.wyre: + return _wyreIcon; + case BuyProviderDescription.moonPay: + //return _mooonPayIcon; + return null; + default: + return null; + } + } +} \ No newline at end of file diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index 172483ed0..9ea094dda 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -125,39 +125,13 @@ class DashboardPage extends BasePage { image: exchangeImage, title: S.of(context).exchange, route: Routes.exchange), - if (walletViewModel.type == WalletType.bitcoin) Observer( - builder: (_) => Stack( - clipBehavior: Clip.none, - alignment: Alignment.topCenter, - children: [ - if (walletViewModel.isRunningWebView) Positioned( - top: -5, - child: SpinKitRing( - color: Theme.of(context).buttonColor, - lineWidth: 3, - size: 70.0, - ), - ), - ActionButton( - image: buyImage, - title: S.of(context).buy, - onClick: walletViewModel.isRunningWebView - ? null - : () async { - try { - walletViewModel.isRunningWebView = true; - final url = - await walletViewModel.wyreViewModel.wyreUrl; - await Navigator.of(context) - .pushNamed(Routes.wyre, arguments: url); - walletViewModel.isRunningWebView = false; - } catch(e) { - print(e.toString()); - walletViewModel.isRunningWebView = false; - } - }) - ], - )), + if (walletViewModel.type == WalletType.bitcoin) ActionButton( + image: buyImage, + title: S.of(context).buy, + onClick: () { + Navigator.of(context).pushNamed(Routes.preOrder); + }, + ), ], )), ) diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 9fae0ec23..eb4153676 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -67,7 +67,7 @@ class SendPage extends BasePage { Color get titleColor => Colors.white; @override - bool get resizeToAvoidBottomPadding => false; + bool get resizeToAvoidBottomInset => false; @override bool get extendBodyBehindAppBar => true; diff --git a/lib/store/dashboard/orders_store.dart b/lib/store/dashboard/orders_store.dart index 01c2b13a9..d3fc94bda 100644 --- a/lib/store/dashboard/orders_store.dart +++ b/lib/store/dashboard/orders_store.dart @@ -1,5 +1,5 @@ import 'dart:async'; -import 'package:cake_wallet/entities/order.dart'; +import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/view_model/dashboard/order_list_item.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; diff --git a/lib/view_model/buy/buy_amount_view_model.dart b/lib/view_model/buy/buy_amount_view_model.dart new file mode 100644 index 000000000..e027166f9 --- /dev/null +++ b/lib/view_model/buy/buy_amount_view_model.dart @@ -0,0 +1,28 @@ +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; + +part 'buy_amount_view_model.g.dart'; + +class BuyAmountViewModel = BuyAmountViewModelBase with _$BuyAmountViewModel; + +abstract class BuyAmountViewModelBase with Store { + BuyAmountViewModelBase() : amount = ''; + + @observable + String amount; + + FiatCurrency get fiatCurrency => FiatCurrency.usd; + + @computed + double get doubleAmount { + double _amount; + + try { + _amount = double.parse(amount.replaceAll(',', '.')) ?? 0.0; + } catch (e) { + _amount = 0.0; + } + + return _amount; + } +} \ No newline at end of file diff --git a/lib/view_model/buy/buy_item.dart b/lib/view_model/buy/buy_item.dart new file mode 100644 index 000000000..8d97597c4 --- /dev/null +++ b/lib/view_model/buy/buy_item.dart @@ -0,0 +1,29 @@ +import 'package:cake_wallet/buy/buy_amount.dart'; +import 'package:cake_wallet/buy/buy_provider.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cake_wallet/view_model/buy/buy_amount_view_model.dart'; + +class BuyItem { + BuyItem({this.provider, this.buyAmountViewModel}); + + final BuyProvider provider; + final BuyAmountViewModel buyAmountViewModel; + + double get amount => buyAmountViewModel.doubleAmount; + + FiatCurrency get fiatCurrency => buyAmountViewModel.fiatCurrency; + + Future get buyAmount async { + BuyAmount _buyAmount; + + try { + _buyAmount = await provider + .calculateAmount(amount?.toString(), fiatCurrency.title); + } catch (e) { + _buyAmount = BuyAmount(sourceAmount: 0.0, destAmount: 0.0); + print(e.toString()); + } + + return _buyAmount; + } +} \ No newline at end of file diff --git a/lib/view_model/buy/buy_view_model.dart b/lib/view_model/buy/buy_view_model.dart new file mode 100644 index 000000000..6b24cadfa --- /dev/null +++ b/lib/view_model/buy/buy_view_model.dart @@ -0,0 +1,81 @@ +import 'package:cake_wallet/buy/buy_provider.dart'; +import 'package:cake_wallet/buy/moonpay/moonpay_buy_provider.dart'; +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/view_model/buy/buy_item.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hive/hive.dart'; +import 'package:cake_wallet/buy/order.dart'; +import 'package:cake_wallet/store/dashboard/orders_store.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/core/wallet_base.dart'; +import 'buy_amount_view_model.dart'; + +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) + ]; + items = providerList.map((provider) => + BuyItem(provider: provider, buyAmountViewModel: buyAmountViewModel)) + .toList(); + isRunning = false; + isDisabled = true; + } + + final Box ordersSource; + final OrdersStore ordersStore; + final BuyAmountViewModel buyAmountViewModel; + final WalletBase wallet; + + @observable + List providerList; + + @observable + BuyProvider selectedProvider; + + @observable + List items; + + @observable + bool isRunning; + + @observable + bool isDisabled; + + WalletType get type => wallet.type; + + double get doubleAmount => buyAmountViewModel.doubleAmount; + + FiatCurrency get fiatCurrency => buyAmountViewModel.fiatCurrency; + + CryptoCurrency get cryptoCurrency => walletTypeToCryptoCurrency(type); + + Future createOrder() async { + try { + final url = await selectedProvider + ?.requestUrl(doubleAmount?.toString(), fiatCurrency.title); + // FIXME: Start WebView + } catch (e) { + print(e.toString()); + } + } + + Future saveOrder(String orderId) async { + try { + final order = await selectedProvider?.findOrderById(orderId); + await ordersSource.add(order); + ordersStore.setOrder(order); + } catch (e) { + print(e.toString()); + } + } +} \ No newline at end of file diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 66fc53269..d471c866a 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -4,7 +4,7 @@ import 'dart:io'; import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; import 'package:cake_wallet/entities/balance.dart'; -import 'package:cake_wallet/entities/order.dart'; +import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/entities/transaction_history.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/monero/account.dart'; @@ -95,8 +95,6 @@ abstract class DashboardViewModelBase with Store { ] }; - isRunningWebView = false; - name = appStore.wallet?.name; wallet ??= appStore.wallet; type = wallet.type; @@ -162,9 +160,6 @@ abstract class DashboardViewModelBase with Store { @observable String subname; - @observable - bool isRunningWebView; - @computed String get address => wallet.address; diff --git a/lib/view_model/dashboard/order_list_item.dart b/lib/view_model/dashboard/order_list_item.dart index 7eeff4bd1..e4bb2cbd1 100644 --- a/lib/view_model/dashboard/order_list_item.dart +++ b/lib/view_model/dashboard/order_list_item.dart @@ -1,4 +1,4 @@ -import 'package:cake_wallet/entities/order.dart'; +import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; diff --git a/lib/view_model/order_details_view_model.dart b/lib/view_model/order_details_view_model.dart index 44367a19d..275247670 100644 --- a/lib/view_model/order_details_view_model.dart +++ b/lib/view_model/order_details_view_model.dart @@ -1,5 +1,5 @@ import 'dart:async'; -import 'package:cake_wallet/entities/order.dart'; +import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/utils/date_formatter.dart'; import 'package:cake_wallet/view_model/wyre_view_model.dart'; import 'package:mobx/mobx.dart'; diff --git a/lib/view_model/wyre_view_model.dart b/lib/view_model/wyre_view_model.dart index 8624cd0e6..2d5ab3315 100644 --- a/lib/view_model/wyre_view_model.dart +++ b/lib/view_model/wyre_view_model.dart @@ -1,7 +1,7 @@ import 'package:cake_wallet/entities/wyre_service.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; -import 'package:cake_wallet/entities/order.dart'; +import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/store/dashboard/orders_store.dart'; import 'package:mobx/mobx.dart';