CAKE-306 | added MoonPay option for BTC buying; applied PreOrderPage to the app; created Wyre and MoonPay buy providers

This commit is contained in:
OleksandrSobol 2021-04-12 21:22:22 +03:00
parent 7e6de105b8
commit 346a034d0a
25 changed files with 903 additions and 51 deletions

8
lib/buy/buy_amount.dart Normal file
View file

@ -0,0 +1,8 @@
import 'package:flutter/foundation.dart';
class BuyAmount {
BuyAmount({@required this.sourceAmount, @required this.destAmount});
final double sourceAmount;
final double destAmount;
}

View file

@ -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';
}

26
lib/buy/buy_provider.dart Normal file
View file

@ -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<String> requestUrl(String amount, String sourceCurrency);
Future<Order> findOrderById(String id);
Future<BuyAmount> calculateAmount(String amount, String sourceCurrency);
}

View file

@ -0,0 +1,21 @@
import 'package:cake_wallet/entities/enumerable_item.dart';
class BuyProviderDescription extends EnumerableItem<int>
with Serializable<int> {
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;
}
}
}

View file

@ -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<String> 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 + '&currencyCode=' +
currencyCode + '&enabledPaymentMethods=' + enabledPaymentMethods +
'&walletAddress=' + walletAddress +
'&baseCurrencyCode=' + sourceCurrency.toLowerCase() +
'&baseCurrencyAmount=' + amount + '&lockAmount=true' +
'&showAllCurrencies=false' + '&showWalletAddressForm=false';
return originalUrl;
}
@override
Future<BuyAmount> 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<String, dynamic>;
final sourceAmount = responseJSON['totalAmount'] as double;
final destAmount = responseJSON['quoteCurrencyAmount'] as double;
return BuyAmount(sourceAmount: sourceAmount, destAmount: destAmount);
}
@override
Future<Order> 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<String, dynamic>;
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
);
}
}

View file

@ -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);
}

View file

@ -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<String> 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<String, dynamic>;
final urlFromResponse = responseJSON['url'] as String;
return urlFromResponse;
}
@override
Future<BuyAmount> 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<String, dynamic>;
final sourceAmount = responseJSON['sourceAmount'] as double;
final destAmount = responseJSON['destAmount'] as double;
return BuyAmount(sourceAmount: sourceAmount, destAmount: destAmount);
}
@override
Future<Order> 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<String, dynamic>;
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<String, dynamic>;
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
);
}
}

View file

@ -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<WyreViewModel>(),
ordersStore: getIt.get<OrdersStore>(), url: url));
getIt.registerFactory(() => BuyAmountViewModel());
getIt.registerFactory(() {
final wallet = getIt.get<AppStore>().wallet;
return BuyViewModel(ordersSource, getIt.get<OrdersStore>(),
getIt.get<BuyAmountViewModel>(), wallet: wallet);
});
getIt.registerFactory(() {
return PreOrderPage(buyViewModel: getIt.get<BuyViewModel>());
});
getIt.registerFactoryParam<OrderDetailsViewModel, Order, void>(
(order, _) => OrderDetailsViewModel(
wyreViewModel: getIt.get<WyreViewModel>(),

View file

@ -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<double> 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<String, dynamic>;
final amount = responseJSON['destAmount'] as double;
return amount;
}
}

View file

@ -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';

View file

@ -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);

View file

@ -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<dynamic> createRoute(RouteSettings settings) {
builder: (_) =>
getIt.get<OrderDetailsPage>(param1: settings.arguments as Order));
case Routes.preOrder:
return MaterialPageRoute<void>(
builder: (_) =>
getIt.get<PreOrderPage>());
case Routes.wyre:
return MaterialPageRoute<void>(
builder: (_) =>

View file

@ -54,4 +54,5 @@ class Routes {
static const support = '/support';
static const orderDetails = '/order_details';
static const wyre = '/wyre';
static const preOrder = '/pre_order';
}

View file

@ -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<BuyAmount>(
future: item.buyAmount,
builder: (context, AsyncSnapshot<BuyAmount> 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
);
})
)
)
);
}
}

View file

@ -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;
}
}
}

View file

@ -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);
},
),
],
)),
)

View file

@ -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;

View file

@ -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';

View file

@ -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;
}
}

View file

@ -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<BuyAmount> 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;
}
}

View file

@ -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<Order> ordersSource;
final OrdersStore ordersStore;
final BuyAmountViewModel buyAmountViewModel;
final WalletBase wallet;
@observable
List<BuyProvider> providerList;
@observable
BuyProvider selectedProvider;
@observable
List<BuyItem> 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<void> saveOrder(String orderId) async {
try {
final order = await selectedProvider?.findOrderById(orderId);
await ordersSource.add(order);
ordersStore.setOrder(order);
} catch (e) {
print(e.toString());
}
}
}

View file

@ -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;

View file

@ -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';

View file

@ -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';

View file

@ -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';