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

OleksandrSobol committed Apr 12, 2021 at 21:22 UTC 346a034d0a3b8cd41a508c4cd3cd7ecd0aaac5d2
25 files changed +903 -51
lib/buy/buy_amount.dart new
+8
@@ -0,0 +1,8 @@
1 +import 'package:flutter/foundation.dart';
2 +
3 +class BuyAmount {
4 + BuyAmount({@required this.sourceAmount, @required this.destAmount});
5 +
6 + final double sourceAmount;
7 + final double destAmount;
8 +}
\ No newline at end of file
lib/buy/buy_exception.dart new
+12
@@ -0,0 +1,12 @@
1 +import 'package:flutter/foundation.dart';
2 +import 'package:cake_wallet/buy/buy_provider_description.dart';
3 +
4 +class BuyException implements Exception {
5 + BuyException({@required this.description, @required this.text});
6 +
7 + final BuyProviderDescription description;
8 + final String text;
9 +
10 + @override
11 + String toString() => '${description.title}: $text';
12 +}
\ No newline at end of file
lib/buy/buy_provider.dart new
+26
@@ -0,0 +1,26 @@
1 +import 'package:cake_wallet/buy/buy_amount.dart';
2 +import 'package:cake_wallet/buy/buy_provider_description.dart';
3 +import 'package:cake_wallet/buy/order.dart';
4 +import 'package:cake_wallet/core/wallet_base.dart';
5 +import 'package:cake_wallet/entities/wallet_type.dart';
6 +
7 +abstract class BuyProvider {
8 + BuyProvider({this.wallet, this.isTestEnvironment});
9 +
10 + final WalletBase wallet;
11 + final bool isTestEnvironment;
12 +
13 + String get title;
14 + BuyProviderDescription get description;
15 +
16 + WalletType get walletType => wallet.type;
17 + String get walletAddress => wallet.address;
18 + String get walletId => wallet.id;
19 +
20 + @override
21 + String toString() => title;
22 +
23 + Future<String> requestUrl(String amount, String sourceCurrency);
24 + Future<Order> findOrderById(String id);
25 + Future<BuyAmount> calculateAmount(String amount, String sourceCurrency);
26 +}
\ No newline at end of file
lib/buy/buy_provider_description.dart new
+21
@@ -0,0 +1,21 @@
1 +import 'package:cake_wallet/entities/enumerable_item.dart';
2 +
3 +class BuyProviderDescription extends EnumerableItem<int>
4 + with Serializable<int> {
5 + const BuyProviderDescription({String title, int raw})
6 + : super(title: title, raw: raw);
7 +
8 + static const wyre = BuyProviderDescription(title: 'Wyre', raw: 0);
9 + static const moonPay = BuyProviderDescription(title: 'MoonPay', raw: 1);
10 +
11 + static BuyProviderDescription deserialize({int raw}) {
12 + switch (raw) {
13 + case 0:
14 + return wyre;
15 + case 1:
16 + return moonPay;
17 + default:
18 + return null;
19 + }
20 + }
21 +}
\ No newline at end of file
lib/buy/moonpay/moonpay_buy_provider.dart new
+109
@@ -0,0 +1,109 @@
1 +import 'dart:convert';
2 +import 'package:cake_wallet/buy/buy_exception.dart';
3 +import 'package:http/http.dart';
4 +import 'package:cake_wallet/buy/buy_amount.dart';
5 +import 'package:cake_wallet/buy/buy_provider.dart';
6 +import 'package:cake_wallet/buy/buy_provider_description.dart';
7 +import 'package:cake_wallet/buy/order.dart';
8 +import 'package:cake_wallet/core/wallet_base.dart';
9 +import 'package:cake_wallet/entities/wallet_type.dart';
10 +import 'package:cake_wallet/exchange/trade_state.dart';
11 +import 'package:cake_wallet/.secrets.g.dart' as secrets;
12 +
13 +class MoonPayBuyProvider extends BuyProvider {
14 + MoonPayBuyProvider({WalletBase wallet, bool isTestEnvironment = false})
15 + : super(wallet: wallet, isTestEnvironment: isTestEnvironment) {
16 + baseApiUrl = isTestEnvironment
17 + ? _baseTestApiUrl
18 + : _baseProductApiUrl;
19 + }
20 +
21 + static const _baseTestApiUrl = 'https://buy-staging.moonpay.com';
22 + static const _baseProductApiUrl = 'https://api.moonpay.com';
23 + static const _currenciesSuffix = '/v3/currencies';
24 + static const _quoteSuffix = '/buy_quote';
25 + static const _transactionsSuffix = '/v1/transactions';
26 + static const _apiKey = secrets.moonPayApiKey;
27 +
28 + @override
29 + String get title => 'MoonPay';
30 +
31 + @override
32 + BuyProviderDescription get description => BuyProviderDescription.moonPay;
33 +
34 + String get currencyCode =>
35 + walletTypeToCryptoCurrency(walletType).title.toLowerCase();
36 +
37 + String baseApiUrl;
38 +
39 + @override
40 + Future<String> requestUrl(String amount, String sourceCurrency) async {
41 + final enabledPaymentMethods =
42 + 'credit_debit_card%2Capple_pay%2Cgoogle_pay%2Csamsung_pay'
43 + '%2Csepa_bank_transfer%2Cgbp_bank_transfer%2Cgbp_open_banking_payment';
44 +
45 + final originalUrl = baseApiUrl + '?apiKey=' + _apiKey + '&currencyCode=' +
46 + currencyCode + '&enabledPaymentMethods=' + enabledPaymentMethods +
47 + '&walletAddress=' + walletAddress +
48 + '&baseCurrencyCode=' + sourceCurrency.toLowerCase() +
49 + '&baseCurrencyAmount=' + amount + '&lockAmount=true' +
50 + '&showAllCurrencies=false' + '&showWalletAddressForm=false';
51 +
52 + return originalUrl;
53 + }
54 +
55 + @override
56 + Future<BuyAmount> calculateAmount(String amount, String sourceCurrency) async {
57 + final url = baseApiUrl + _currenciesSuffix + '/$currencyCode' +
58 + _quoteSuffix + '/?apiKey=' + _apiKey +
59 + '&baseCurrencyAmount=' + amount +
60 + '&baseCurrencyCode' + sourceCurrency.toLowerCase();
61 +
62 + final response = await get(url);
63 +
64 + if (response.statusCode != 200) {
65 + throw BuyException(
66 + description: description,
67 + text: 'Quote is not found!');
68 + }
69 +
70 + final responseJSON = json.decode(response.body) as Map<String, dynamic>;
71 + final sourceAmount = responseJSON['totalAmount'] as double;
72 + final destAmount = responseJSON['quoteCurrencyAmount'] as double;
73 +
74 + return BuyAmount(sourceAmount: sourceAmount, destAmount: destAmount);
75 + }
76 +
77 + @override
78 + Future<Order> findOrderById(String id) async {
79 + final url = baseApiUrl + _transactionsSuffix + '/$id' +
80 + '?apiKey=' + _apiKey;
81 +
82 + final response = await get(url);
83 +
84 + if (response.statusCode != 200) {
85 + throw BuyException(
86 + description: description,
87 + text: 'Transaction $id is not found!');
88 + }
89 +
90 + final responseJSON = json.decode(response.body) as Map<String, dynamic>;
91 + final status = responseJSON['status'] as String;
92 + final state = TradeState.deserialize(raw: status.toLowerCase());
93 + final createdAt = responseJSON['createdAt'] as DateTime;
94 + final amount = responseJSON['quoteCurrencyAmount'] as double;
95 +
96 + return Order(
97 + id: id,
98 + provider: description,
99 + transferId: id,
100 + from: 'USD', //FIXME
101 + to: 'BTC', //FIXME
102 + state: state,
103 + createdAt: createdAt,
104 + amount: amount.toString(),
105 + receiveAddress: walletAddress,
106 + walletId: walletId
107 + );
108 + }
109 +}
\ No newline at end of file
lib/buy/order.dart renamed
+10 -1
@@ -1,3 +1,4 @@
1 +import 'package:cake_wallet/buy/buy_provider_description.dart';
2 import 'package:hive/hive.dart';
3 import 'package:cake_wallet/exchange/trade_state.dart';
4 import 'package:cake_wallet/entities/format_amount.dart';
@@ -8,6 +9,7 @@ part 'order.g.dart';
9 class Order extends HiveObject {
10 Order(
11 {this.id,
12 + BuyProviderDescription provider,
13 this.transferId,
14 this.from,
15 this.to,
@@ -16,7 +18,8 @@ class Order extends HiveObject {
18 this.amount,
19 this.receiveAddress,
20 this.walletId})
19 - : stateRaw = state?.raw;
21 + : providerRaw = provider?.raw,
22 + stateRaw = state?.raw;
23
24 static const typeId = 8;
25 static const boxName = 'Orders';
@@ -51,5 +54,11 @@ class Order extends HiveObject {
54 @HiveField(8)
55 String walletId;
56
57 + @HiveField(9)
58 + int providerRaw;
59 +
60 + BuyProviderDescription get provider =>
61 + BuyProviderDescription.deserialize(raw: providerRaw);
62 +
63 String amountFormatted() => formatAmount(amount);
64 }
\ No newline at end of file
lib/buy/wyre/wyre_buy_provider.dart new
+161
@@ -0,0 +1,161 @@
1 +import 'dart:convert';
2 +import 'package:cake_wallet/buy/buy_exception.dart';
3 +import 'package:http/http.dart';
4 +import 'package:cake_wallet/buy/buy_amount.dart';
5 +import 'package:cake_wallet/buy/buy_provider.dart';
6 +import 'package:cake_wallet/buy/buy_provider_description.dart';
7 +import 'package:cake_wallet/buy/order.dart';
8 +import 'package:cake_wallet/core/wallet_base.dart';
9 +import 'package:cake_wallet/entities/wallet_type.dart';
10 +import 'package:cake_wallet/exchange/trade_state.dart';
11 +import 'package:cake_wallet/.secrets.g.dart' as secrets;
12 +
13 +class WyreBuyProvider extends BuyProvider {
14 + WyreBuyProvider({WalletBase wallet, bool isTestEnvironment = false})
15 + : super(wallet: wallet, isTestEnvironment: isTestEnvironment) {
16 + baseApiUrl = isTestEnvironment
17 + ? _baseTestApiUrl
18 + : _baseProductApiUrl;
19 + trackUrl = isTestEnvironment
20 + ? _trackTestUrl
21 + : _trackProductUrl;
22 + }
23 +
24 + static const _baseTestApiUrl = 'https://api.testwyre.com';
25 + static const _baseProductApiUrl = 'https://api.sendwyre.com';
26 + static const _trackTestUrl = 'https://dash.testwyre.com/track/';
27 + static const _trackProductUrl = 'https://dash.sendwyre.com/track/';
28 + static const _ordersSuffix = '/v3/orders';
29 + static const _reserveSuffix = '/reserve';
30 + static const _quoteSuffix = '/quote/partner';
31 + static const _timeStampSuffix = '?timestamp=';
32 + static const _transferSuffix = '/v2/transfer/';
33 + static const _trackSuffix = '/track';
34 + static const _secretKey = secrets.wyreSecretKey;
35 + static const _accountId = secrets.wyreAccountId;
36 +
37 + @override
38 + String get title => 'Wyre';
39 +
40 + @override
41 + BuyProviderDescription get description => BuyProviderDescription.wyre;
42 +
43 + String baseApiUrl;
44 + String trackUrl;
45 +
46 + @override
47 + Future<String> requestUrl(String amount, String sourceCurrency) async {
48 + final timestamp = DateTime.now().millisecondsSinceEpoch.toString();
49 + final url = baseApiUrl + _ordersSuffix + _reserveSuffix +
50 + _timeStampSuffix + timestamp;
51 + final body = {
52 + 'amount': amount,
53 + 'sourceCurrency': sourceCurrency,
54 + 'destCurrency': walletTypeToCryptoCurrency(walletType).title,
55 + 'dest': walletTypeToString(walletType).toLowerCase() + ':' + walletAddress,
56 + 'referrerAccountId': _accountId,
57 + 'lockFields': ['amount', 'sourceCurrency', 'destCurrency', 'dest']
58 + };
59 +
60 + final response = await post(url,
61 + headers: {
62 + 'Authorization': 'Bearer $_secretKey',
63 + 'Content-Type': 'application/json',
64 + 'cache-control': 'no-cache'
65 + },
66 + body: json.encode(body));
67 +
68 + if (response.statusCode != 200) {
69 + throw BuyException(
70 + description: description,
71 + text: 'Url $url is not found!');
72 + }
73 +
74 + final responseJSON = json.decode(response.body) as Map<String, dynamic>;
75 + final urlFromResponse = responseJSON['url'] as String;
76 + return urlFromResponse;
77 + }
78 +
79 + @override
80 + Future<BuyAmount> calculateAmount(String amount, String sourceCurrency) async {
81 + final quoteUrl = baseApiUrl + _ordersSuffix + _quoteSuffix;
82 + final body = {
83 + 'amount': amount,
84 + 'sourceCurrency': sourceCurrency,
85 + 'destCurrency': walletTypeToCryptoCurrency(walletType).title,
86 + 'dest': walletTypeToString(walletType).toLowerCase() + ':' + walletAddress,
87 + 'accountId': _accountId,
88 + 'country': 'US' //FIXME
89 + };
90 +
91 + final response = await post(quoteUrl,
92 + headers: {
93 + 'Authorization': 'Bearer $_secretKey',
94 + 'Content-Type': 'application/json',
95 + 'cache-control': 'no-cache'
96 + },
97 + body: json.encode(body));
98 +
99 + if (response.statusCode != 200) {
100 + throw BuyException(
101 + description: description,
102 + text: 'Quote is not found!');
103 + }
104 +
105 + final responseJSON = json.decode(response.body) as Map<String, dynamic>;
106 + final sourceAmount = responseJSON['sourceAmount'] as double;
107 + final destAmount = responseJSON['destAmount'] as double;
108 +
109 + return BuyAmount(sourceAmount: sourceAmount, destAmount: destAmount);
110 + }
111 +
112 + @override
113 + Future<Order> findOrderById(String id) async {
114 + final orderUrl = baseApiUrl + _ordersSuffix + '/$id';
115 + final orderResponse = await get(orderUrl);
116 +
117 + if (orderResponse.statusCode != 200) {
118 + throw BuyException(
119 + description: description,
120 + text: 'Order $id is not found!');
121 + }
122 +
123 + final orderResponseJSON =
124 + json.decode(orderResponse.body) as Map<String, dynamic>;
125 + final transferId = orderResponseJSON['transferId'] as String;
126 + final from = orderResponseJSON['sourceCurrency'] as String;
127 + final to = orderResponseJSON['destCurrency'] as String;
128 + final status = orderResponseJSON['status'] as String;
129 + final state = TradeState.deserialize(raw: status.toLowerCase());
130 + final createdAtRaw = orderResponseJSON['createdAt'] as int;
131 + final createdAt =
132 + DateTime.fromMillisecondsSinceEpoch(createdAtRaw).toLocal();
133 +
134 + final transferUrl =
135 + baseApiUrl + _transferSuffix + transferId + _trackSuffix;
136 + final transferResponse = await get(transferUrl);
137 +
138 + if (transferResponse.statusCode != 200) {
139 + throw BuyException(
140 + description: description,
141 + text: 'Transfer $transferId is not found!');
142 + }
143 +
144 + final transferResponseJSON =
145 + json.decode(transferResponse.body) as Map<String, dynamic>;
146 + final amount = transferResponseJSON['destAmount'] as double;
147 +
148 + return Order(
149 + id: id,
150 + provider: description,
151 + transferId: transferId,
152 + from: from,
153 + to: to,
154 + state: state,
155 + createdAt: createdAt,
156 + amount: amount.toString(),
157 + receiveAddress: walletAddress,
158 + walletId: walletId
159 + );
160 + }
161 +}
\ No newline at end of file
lib/di.dart
+17 -2
@@ -4,7 +4,7 @@ import 'package:cake_wallet/core/wallet_service.dart';
4 import 'package:cake_wallet/entities/biometric_auth.dart';
5 import 'package:cake_wallet/entities/contact_record.dart';
6 import 'package:cake_wallet/entities/load_current_wallet.dart';
7 -import 'package:cake_wallet/entities/order.dart';
7 +import 'package:cake_wallet/buy/order.dart';
8 import 'package:cake_wallet/entities/transaction_description.dart';
9 import 'package:cake_wallet/entities/transaction_info.dart';
10 import 'package:cake_wallet/entities/wyre_service.dart';
@@ -15,7 +15,7 @@ import 'package:cake_wallet/exchange/trade.dart';
15 import 'package:cake_wallet/reactions/on_authentication_state_change.dart';
16 import 'package:cake_wallet/src/screens/backup/backup_page.dart';
17 import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart';
18 -
18 +import 'package:cake_wallet/src/screens/buy/pre_order_page.dart';
19 import 'package:cake_wallet/src/screens/contact/contact_list_page.dart';
20 import 'package:cake_wallet/src/screens/contact/contact_page.dart';
21 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
61 import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart';
62 import 'package:cake_wallet/store/wallet_list_store.dart';
63 import 'package:cake_wallet/view_model/backup_view_model.dart';
64 +import 'package:cake_wallet/view_model/buy/buy_amount_view_model.dart';
65 +import 'package:cake_wallet/view_model/buy/buy_view_model.dart';
66 import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
67 import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart';
68 import 'package:cake_wallet/view_model/edit_backup_password_view_model.dart';
@@ -546,6 +548,19 @@ Future setup(
548 WyrePage(getIt.get<WyreViewModel>(),
549 ordersStore: getIt.get<OrdersStore>(), url: url));
550
551 + getIt.registerFactory(() => BuyAmountViewModel());
552 +
553 + getIt.registerFactory(() {
554 + final wallet = getIt.get<AppStore>().wallet;
555 +
556 + return BuyViewModel(ordersSource, getIt.get<OrdersStore>(),
557 + getIt.get<BuyAmountViewModel>(), wallet: wallet);
558 + });
559 +
560 + getIt.registerFactory(() {
561 + return PreOrderPage(buyViewModel: getIt.get<BuyViewModel>());
562 + });
563 +
564 getIt.registerFactoryParam<OrderDetailsViewModel, Order, void>(
565 (order, _) => OrderDetailsViewModel(
566 wyreViewModel: getIt.get<WyreViewModel>(),
lib/entities/wyre_service.dart
+37 -2
@@ -5,7 +5,7 @@ import 'package:cake_wallet/exchange/trade_state.dart';
5 import 'package:flutter/foundation.dart';
6 import 'package:http/http.dart';
7 import 'package:cake_wallet/.secrets.g.dart' as secrets;
8 -import 'package:cake_wallet/entities/order.dart';
8 +import 'package:cake_wallet/buy/order.dart';
9 import 'package:cake_wallet/entities/wallet_type.dart';
10
11 class WyreService {
@@ -26,6 +26,7 @@ class WyreService {
26 static const _trackProductUrl = 'https://dash.sendwyre.com/track/';
27 static const _ordersSuffix = '/v3/orders';
28 static const _reserveSuffix = '/reserve';
29 + static const _quoteSuffix = '/quote/partner';
30 static const _timeStampSuffix = '?timestamp=';
31 static const _transferSuffix = '/v2/transfer/';
32 static const _trackSuffix = '/track';
@@ -47,10 +48,12 @@ class WyreService {
48 final secretKey = secrets.wyreSecretKey;
49 final accountId = secrets.wyreAccountId;
50 final body = {
51 + 'amount': '1',
52 + 'sourceCurrency': 'USD',
53 'destCurrency': walletTypeToCryptoCurrency(walletType).title,
54 'dest': walletTypeToString(walletType).toLowerCase() + ':' + walletAddress,
55 'referrerAccountId': accountId,
53 - 'lockFields': ['destCurrency', 'dest']
56 + 'lockFields': ['amount', 'sourceCurrency', 'destCurrency', 'dest']
57 };
58
59 final response = await post(url,
@@ -113,4 +116,36 @@ class WyreService {
116 walletId: walletId
117 );
118 }
119 +
120 + Future<double> getAmount() async {
121 + final quoteUrl = baseApiUrl + _ordersSuffix + _quoteSuffix;
122 +
123 + final secretKey = secrets.wyreSecretKey;
124 + final accountId = secrets.wyreAccountId;
125 + final body = {
126 + 'amount': '1',
127 + 'sourceCurrency': 'USD',
128 + 'destCurrency': walletTypeToCryptoCurrency(walletType).title,
129 + 'dest': walletTypeToString(walletType).toLowerCase() + ':' + walletAddress,
130 + 'accountId': accountId,
131 + 'country': 'US'
132 + };
133 +
134 + final response = await post(quoteUrl,
135 + headers: {
136 + 'Authorization': 'Bearer $secretKey',
137 + 'Content-Type': 'application/json',
138 + 'cache-control': 'no-cache'
139 + },
140 + body: json.encode(body));
141 +
142 + if (response.statusCode != 200) {
143 + throw WyreException('Quote is not found! ');
144 + }
145 +
146 + final responseJSON = json.decode(response.body) as Map<String, dynamic>;
147 + final amount = responseJSON['destAmount'] as double;
148 +
149 + return amount;
150 + }
151 }
\ No newline at end of file
lib/main.dart
+1 -1
@@ -1,5 +1,5 @@
1 import 'package:cake_wallet/entities/language_service.dart';
2 -import 'package:cake_wallet/entities/order.dart';
2 +import 'package:cake_wallet/buy/order.dart';
3 import 'package:flutter/material.dart';
4 import 'package:flutter/services.dart';
5 import 'package:hive/hive.dart';
lib/palette.dart
+1
@@ -23,6 +23,7 @@ class Palette {
23 static const Color persianRed = Color.fromRGBO(206, 55, 55, 1.0);
24 static const Color blueCraiola = Color.fromRGBO(69, 110, 255, 1.0);
25 static const Color blueGreyCraiola = Color.fromRGBO(106, 177, 207, 1.0);
26 + static const Color greyBlueCraiola = Color.fromRGBO(116, 139, 219, 1.0);
27 static const Color darkBlueCraiola = Color.fromRGBO(53, 86, 136, 1.0);
28 static const Color pinkFlamingo = Color.fromRGBO(240, 60, 243, 1.0);
29 static const Color redHat = Color.fromRGBO(209, 68, 37, 1.0);
lib/router.dart
+7 -1
@@ -1,8 +1,9 @@
1 import 'package:cake_wallet/entities/contact_record.dart';
2 -import 'package:cake_wallet/entities/order.dart';
2 +import 'package:cake_wallet/buy/order.dart';
3 import 'package:cake_wallet/entities/transaction_description.dart';
4 import 'package:cake_wallet/src/screens/backup/backup_page.dart';
5 import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart';
6 +import 'package:cake_wallet/src/screens/buy/pre_order_page.dart';
7 import 'package:cake_wallet/src/screens/order_details/order_details_page.dart';
8 import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart';
9 import 'package:cake_wallet/src/screens/restore/restore_from_backup_page.dart';
@@ -297,6 +298,11 @@ Route<dynamic> createRoute(RouteSettings settings) {
298 builder: (_) =>
299 getIt.get<OrderDetailsPage>(param1: settings.arguments as Order));
300
301 + case Routes.preOrder:
302 + return MaterialPageRoute<void>(
303 + builder: (_) =>
304 + getIt.get<PreOrderPage>());
305 +
306 case Routes.wyre:
307 return MaterialPageRoute<void>(
308 builder: (_) =>
lib/routes.dart
+1
@@ -54,4 +54,5 @@ class Routes {
54 static const support = '/support';
55 static const orderDetails = '/order_details';
56 static const wyre = '/wyre';
57 + static const preOrder = '/pre_order';
58 }
\ No newline at end of file
lib/src/screens/buy/pre_order_page.dart new
+210
@@ -0,0 +1,210 @@
1 +import 'dart:ui';
2 +import 'package:cake_wallet/buy/buy_amount.dart';
3 +import 'package:cake_wallet/src/screens/buy/widgets/buy_list_item.dart';
4 +import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
5 +import 'package:cake_wallet/view_model/buy/buy_view_model.dart';
6 +import 'package:flutter/cupertino.dart';
7 +import 'package:flutter/material.dart';
8 +import 'package:flutter/services.dart';
9 +import 'package:flutter_mobx/flutter_mobx.dart';
10 +import 'package:keyboard_actions/keyboard_actions.dart';
11 +import 'package:cake_wallet/routes.dart';
12 +import 'package:cake_wallet/src/screens/base_page.dart';
13 +import 'package:cake_wallet/src/widgets/primary_button.dart';
14 +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
15 +import 'package:cake_wallet/generated/i18n.dart';
16 +import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
17 +
18 +class PreOrderPage extends BasePage {
19 + PreOrderPage({@required this.buyViewModel})
20 + : _amountFocus = FocusNode(),
21 + _amountController = TextEditingController() {
22 +
23 + _amountController.addListener(() {
24 + final amount = _amountController.text;
25 +
26 + if (amount != buyViewModel.buyAmountViewModel.amount) {
27 + buyViewModel.buyAmountViewModel.amount = amount;
28 + }
29 +
30 + if (buyViewModel.buyAmountViewModel.doubleAmount == 0.0) {
31 + buyViewModel.selectedProvider = null;
32 + }
33 + });
34 + }
35 +
36 + final BuyViewModel buyViewModel;
37 + final FocusNode _amountFocus;
38 + final TextEditingController _amountController;
39 +
40 + @override
41 + String get title => 'Buy Bitcoin';
42 +
43 + @override
44 + Color get titleColor => Colors.white;
45 +
46 + @override
47 + bool get resizeToAvoidBottomInset => false;
48 +
49 + @override
50 + bool get extendBodyBehindAppBar => true;
51 +
52 + @override
53 + AppBarStyle get appBarStyle => AppBarStyle.transparent;
54 +
55 + @override
56 + Widget body(BuildContext context) {
57 + return KeyboardActions(
58 + config: KeyboardActionsConfig(
59 + keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
60 + keyboardBarColor: Theme.of(context).accentTextTheme.body2
61 + .backgroundColor,
62 + nextFocus: false,
63 + actions: [
64 + KeyboardActionsItem(
65 + focusNode: _amountFocus,
66 + toolbarButtons: [(_) => KeyboardDoneButton()],
67 + ),
68 + ]),
69 + child: Container(
70 + height: 0,
71 + color: Theme.of(context).backgroundColor,
72 + child: ScrollableWithBottomSection(
73 + contentPadding: EdgeInsets.only(bottom: 24),
74 + content: Column(
75 + children: [
76 + Container(
77 + decoration: BoxDecoration(
78 + borderRadius: BorderRadius.only(
79 + bottomLeft: Radius.circular(24),
80 + bottomRight: Radius.circular(24)),
81 + gradient: LinearGradient(colors: [
82 + Theme.of(context).primaryTextTheme.subhead.color,
83 + Theme.of(context)
84 + .primaryTextTheme
85 + .subhead
86 + .decorationColor,
87 + ], begin: Alignment.topLeft, end: Alignment.bottomRight),
88 + ),
89 + child: Padding(
90 + padding: EdgeInsets.fromLTRB(100, 100, 100, 65),
91 + child: BaseTextFormField(
92 + focusNode: _amountFocus,
93 + controller: _amountController,
94 + keyboardType:
95 + TextInputType.numberWithOptions(
96 + signed: false, decimal: true),
97 + inputFormatters: [
98 + FilteringTextInputFormatter
99 + .allow(RegExp('^([0-9]+([.\,][0-9]{0,2})?|[.\,][0-9]{1,2})\$'))
100 + ],
101 + prefixIcon: Padding(
102 + padding: EdgeInsets.only(top: 2),
103 + child:
104 + Text(buyViewModel.fiatCurrency.title + ': ',
105 + style: TextStyle(
106 + fontSize: 36,
107 + fontWeight: FontWeight.w600,
108 + color: Colors.white,
109 + )),
110 + ),
111 + hintText: '0.00',
112 + borderColor: Theme.of(context)
113 + .primaryTextTheme
114 + .headline
115 + .color,
116 + textStyle: TextStyle(
117 + fontSize: 36,
118 + fontWeight: FontWeight.w500,
119 + color: Colors.white),
120 + placeholderTextStyle: TextStyle(
121 + color: Theme.of(context)
122 + .primaryTextTheme
123 + .headline
124 + .decorationColor,
125 + fontWeight: FontWeight.w500,
126 + fontSize: 36),
127 + )
128 + )
129 + ),
130 + Padding(
131 + padding: EdgeInsets.only(top: 38, bottom: 18),
132 + child: Text(
133 + 'Buy with:',
134 + textAlign: TextAlign.center,
135 + style: TextStyle(
136 + color: Theme.of(context).primaryTextTheme.title.color,
137 + fontSize: 18,
138 + fontWeight: FontWeight.w500
139 + ),
140 + )
141 + ),
142 + ...buyViewModel.items.map(
143 + (item) => Observer(builder: (_) => FutureBuilder<BuyAmount>(
144 + future: item.buyAmount,
145 + builder: (context, AsyncSnapshot<BuyAmount> snapshot) {
146 + double sourceAmount;
147 + double destAmount;
148 +
149 + if (snapshot.hasData) {
150 + sourceAmount = snapshot.data.sourceAmount;
151 + destAmount = snapshot.data.destAmount;
152 + } else {
153 + sourceAmount = 0.0;
154 + destAmount = 0.0;
155 + }
156 +
157 + return Padding(
158 + padding:
159 + EdgeInsets.only(left: 15, top: 20, right: 15),
160 + child: Observer(builder: (_) => BuyListItem(
161 + selectedProvider: buyViewModel.selectedProvider,
162 + provider: item.provider,
163 + sourceAmount: sourceAmount,
164 + sourceCurrency: buyViewModel.fiatCurrency,
165 + destAmount: destAmount,
166 + destCurrency: buyViewModel.cryptoCurrency,
167 + onTap:
168 + buyViewModel.buyAmountViewModel
169 + .doubleAmount == 0.0 ? null : () {
170 + buyViewModel.selectedProvider = item.provider;
171 + sourceAmount > 0
172 + ? buyViewModel.isDisabled = false
173 + : buyViewModel.isDisabled = true;
174 + }
175 + ))
176 + );
177 + }
178 + ),)
179 + )
180 + ],
181 + ),
182 + bottomSectionPadding:
183 + EdgeInsets.only(left: 24, right: 24, bottom: 24),
184 + bottomSection: Observer(builder: (_) {
185 + return LoadingPrimaryButton(
186 + onPressed: buyViewModel.isRunning
187 + ? null
188 + : () {
189 + buyViewModel.isRunning = true;
190 +
191 + // FIXME: Start WebView
192 +
193 + buyViewModel.isRunning = false;
194 + },
195 + text: buyViewModel.selectedProvider == null
196 + ? 'Buy'
197 + : 'Buy with ${buyViewModel.selectedProvider
198 + .description.title}',
199 + color: Theme.of(context).accentTextTheme.body2.color,
200 + textColor: Colors.white,
201 + isLoading: buyViewModel.isRunning,
202 + isDisabled: (buyViewModel.selectedProvider == null) ||
203 + buyViewModel.isDisabled
204 + );
205 + })
206 + )
207 + )
208 + );
209 + }
210 +}
\ No newline at end of file
lib/src/screens/buy/widgets/buy_list_item.dart new
+131
@@ -0,0 +1,131 @@
1 +import 'package:cake_wallet/buy/buy_provider_description.dart';
2 +import 'package:cake_wallet/buy/buy_provider.dart';
3 +import 'package:cake_wallet/entities/crypto_currency.dart';
4 +import 'package:cake_wallet/entities/fiat_currency.dart';
5 +import 'package:cake_wallet/palette.dart';
6 +import 'package:flutter/material.dart';
7 +
8 +class BuyListItem extends StatelessWidget {
9 + BuyListItem({
10 + @required this.selectedProvider,
11 + @required this.provider,
12 + @required this.sourceAmount,
13 + @required this.sourceCurrency,
14 + @required this.destAmount,
15 + @required this.destCurrency,
16 + @required this.onTap
17 + });
18 +
19 + final _wyreIcon =
20 + Image.asset('assets/images/wyre-icon.png', width: 36, height: 36);
21 + final _mooonPayIcon =
22 + Image.asset('assets/images/wyre-icon.png', width: 36, height: 36);
23 +
24 + final BuyProvider selectedProvider;
25 + final BuyProvider provider;
26 + final double sourceAmount;
27 + final FiatCurrency sourceCurrency;
28 + final double destAmount;
29 + final CryptoCurrency destCurrency;
30 + final void Function() onTap;
31 +
32 + @override
33 + Widget build(BuildContext context) {
34 + final providerIcon = _getProviderIcon(provider.description);
35 +
36 + final backgroundColor = selectedProvider != null
37 + ? selectedProvider.description == provider.description
38 + ? Palette.greyBlueCraiola
39 + : Palette.shadowWhite
40 + : Palette.shadowWhite;
41 +
42 + final primaryTextColor = selectedProvider != null
43 + ? selectedProvider.description == provider.description
44 + ? Colors.white
45 + : Palette.darkGray
46 + : Palette.darkGray;
47 +
48 + final secondaryTextColor = selectedProvider != null
49 + ? selectedProvider.description == provider.description
50 + ? Colors.white
51 + : Palette.darkBlueCraiola
52 + : Palette.darkBlueCraiola;
53 +
54 + return GestureDetector(
55 + onTap: () => onTap?.call(),
56 + child: Container(
57 + height: 102,
58 + padding: EdgeInsets.only(
59 + left: 20,
60 + right: 20
61 + ),
62 + decoration: BoxDecoration(
63 + borderRadius: BorderRadius.all(Radius.circular(25)),
64 + color: backgroundColor
65 + ),
66 + child: Row(
67 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
68 + mainAxisSize: MainAxisSize.max,
69 + crossAxisAlignment: CrossAxisAlignment.center,
70 + children: [
71 + Row(
72 + mainAxisAlignment: MainAxisAlignment.start,
73 + mainAxisSize: MainAxisSize.min,
74 + children: [
75 + if (providerIcon != null) Padding(
76 + padding: EdgeInsets.only(right: 10),
77 + child: providerIcon
78 + ),
79 + Text(
80 + provider.description.title,
81 + style: TextStyle(
82 + color: primaryTextColor,
83 + fontSize: 24,
84 + fontWeight: FontWeight.w500
85 + ),
86 + )
87 + ],
88 + ),
89 + Column(
90 + mainAxisSize: MainAxisSize.min,
91 + crossAxisAlignment: CrossAxisAlignment.end,
92 + children: [
93 + Text(
94 + '${destAmount?.toString()} ${destCurrency.title}',
95 + style: TextStyle(
96 + color: secondaryTextColor,
97 + fontSize: 18,
98 + fontWeight: FontWeight.w500
99 + ),
100 + ),
101 + Padding(
102 + padding: EdgeInsets.only(top: 5),
103 + child: Text(
104 + '${sourceAmount?.toString()} ${sourceCurrency.title}',
105 + style: TextStyle(
106 + color: primaryTextColor,
107 + fontSize: 18,
108 + fontWeight: FontWeight.w500
109 + ),
110 + ),
111 + )
112 + ],
113 + )
114 + ],
115 + ),
116 + )
117 + );
118 + }
119 +
120 + Image _getProviderIcon(BuyProviderDescription providerDescription) {
121 + switch (providerDescription) {
122 + case BuyProviderDescription.wyre:
123 + return _wyreIcon;
124 + case BuyProviderDescription.moonPay:
125 + //return _mooonPayIcon;
126 + return null;
127 + default:
128 + return null;
129 + }
130 + }
131 +}
\ No newline at end of file
lib/src/screens/dashboard/dashboard_page.dart
+7 -33
@@ -125,39 +125,13 @@ class DashboardPage extends BasePage {
125 image: exchangeImage,
126 title: S.of(context).exchange,
127 route: Routes.exchange),
128 - if (walletViewModel.type == WalletType.bitcoin) Observer(
129 - builder: (_) => Stack(
130 - clipBehavior: Clip.none,
131 - alignment: Alignment.topCenter,
132 - children: [
133 - if (walletViewModel.isRunningWebView) Positioned(
134 - top: -5,
135 - child: SpinKitRing(
136 - color: Theme.of(context).buttonColor,
137 - lineWidth: 3,
138 - size: 70.0,
139 - ),
140 - ),
141 - ActionButton(
142 - image: buyImage,
143 - title: S.of(context).buy,
144 - onClick: walletViewModel.isRunningWebView
145 - ? null
146 - : () async {
147 - try {
148 - walletViewModel.isRunningWebView = true;
149 - final url =
150 - await walletViewModel.wyreViewModel.wyreUrl;
151 - await Navigator.of(context)
152 - .pushNamed(Routes.wyre, arguments: url);
153 - walletViewModel.isRunningWebView = false;
154 - } catch(e) {
155 - print(e.toString());
156 - walletViewModel.isRunningWebView = false;
157 - }
158 - })
159 - ],
160 - )),
128 + if (walletViewModel.type == WalletType.bitcoin) ActionButton(
129 + image: buyImage,
130 + title: S.of(context).buy,
131 + onClick: () {
132 + Navigator.of(context).pushNamed(Routes.preOrder);
133 + },
134 + ),
135 ],
136 )),
137 )
lib/src/screens/send/send_page.dart
+1 -1
@@ -67,7 +67,7 @@ class SendPage extends BasePage {
67 Color get titleColor => Colors.white;
68
69 @override
70 - bool get resizeToAvoidBottomPadding => false;
70 + bool get resizeToAvoidBottomInset => false;
71
72 @override
73 bool get extendBodyBehindAppBar => true;
lib/store/dashboard/orders_store.dart
+1 -1
@@ -1,5 +1,5 @@
1 import 'dart:async';
2 -import 'package:cake_wallet/entities/order.dart';
2 +import 'package:cake_wallet/buy/order.dart';
3 import 'package:cake_wallet/view_model/dashboard/order_list_item.dart';
4 import 'package:hive/hive.dart';
5 import 'package:mobx/mobx.dart';
lib/view_model/buy/buy_amount_view_model.dart new
+28
@@ -0,0 +1,28 @@
1 +import 'package:mobx/mobx.dart';
2 +import 'package:cake_wallet/entities/fiat_currency.dart';
3 +
4 +part 'buy_amount_view_model.g.dart';
5 +
6 +class BuyAmountViewModel = BuyAmountViewModelBase with _$BuyAmountViewModel;
7 +
8 +abstract class BuyAmountViewModelBase with Store {
9 + BuyAmountViewModelBase() : amount = '';
10 +
11 + @observable
12 + String amount;
13 +
14 + FiatCurrency get fiatCurrency => FiatCurrency.usd;
15 +
16 + @computed
17 + double get doubleAmount {
18 + double _amount;
19 +
20 + try {
21 + _amount = double.parse(amount.replaceAll(',', '.')) ?? 0.0;
22 + } catch (e) {
23 + _amount = 0.0;
24 + }
25 +
26 + return _amount;
27 + }
28 +}
\ No newline at end of file
lib/view_model/buy/buy_item.dart new
+29
@@ -0,0 +1,29 @@
1 +import 'package:cake_wallet/buy/buy_amount.dart';
2 +import 'package:cake_wallet/buy/buy_provider.dart';
3 +import 'package:cake_wallet/entities/fiat_currency.dart';
4 +import 'package:cake_wallet/view_model/buy/buy_amount_view_model.dart';
5 +
6 +class BuyItem {
7 + BuyItem({this.provider, this.buyAmountViewModel});
8 +
9 + final BuyProvider provider;
10 + final BuyAmountViewModel buyAmountViewModel;
11 +
12 + double get amount => buyAmountViewModel.doubleAmount;
13 +
14 + FiatCurrency get fiatCurrency => buyAmountViewModel.fiatCurrency;
15 +
16 + Future<BuyAmount> get buyAmount async {
17 + BuyAmount _buyAmount;
18 +
19 + try {
20 + _buyAmount = await provider
21 + .calculateAmount(amount?.toString(), fiatCurrency.title);
22 + } catch (e) {
23 + _buyAmount = BuyAmount(sourceAmount: 0.0, destAmount: 0.0);
24 + print(e.toString());
25 + }
26 +
27 + return _buyAmount;
28 + }
29 +}
\ No newline at end of file
lib/view_model/buy/buy_view_model.dart new
+81
@@ -0,0 +1,81 @@
1 +import 'package:cake_wallet/buy/buy_provider.dart';
2 +import 'package:cake_wallet/buy/moonpay/moonpay_buy_provider.dart';
3 +import 'package:cake_wallet/buy/wyre/wyre_buy_provider.dart';
4 +import 'package:cake_wallet/entities/crypto_currency.dart';
5 +import 'package:cake_wallet/entities/fiat_currency.dart';
6 +import 'package:cake_wallet/entities/wallet_type.dart';
7 +import 'package:cake_wallet/view_model/buy/buy_item.dart';
8 +import 'package:flutter/foundation.dart';
9 +import 'package:hive/hive.dart';
10 +import 'package:cake_wallet/buy/order.dart';
11 +import 'package:cake_wallet/store/dashboard/orders_store.dart';
12 +import 'package:mobx/mobx.dart';
13 +import 'package:cake_wallet/core/wallet_base.dart';
14 +import 'buy_amount_view_model.dart';
15 +
16 +part 'buy_view_model.g.dart';
17 +
18 +class BuyViewModel = BuyViewModelBase with _$BuyViewModel;
19 +
20 +abstract class BuyViewModelBase with Store {
21 + BuyViewModelBase(this.ordersSource, this.ordersStore, this.buyAmountViewModel,
22 + {@required this.wallet}) {
23 + providerList = [
24 + WyreBuyProvider(wallet: wallet),
25 + MoonPayBuyProvider(wallet: wallet)
26 + ];
27 + items = providerList.map((provider) =>
28 + BuyItem(provider: provider, buyAmountViewModel: buyAmountViewModel))
29 + .toList();
30 + isRunning = false;
31 + isDisabled = true;
32 + }
33 +
34 + final Box<Order> ordersSource;
35 + final OrdersStore ordersStore;
36 + final BuyAmountViewModel buyAmountViewModel;
37 + final WalletBase wallet;
38 +
39 + @observable
40 + List<BuyProvider> providerList;
41 +
42 + @observable
43 + BuyProvider selectedProvider;
44 +
45 + @observable
46 + List<BuyItem> items;
47 +
48 + @observable
49 + bool isRunning;
50 +
51 + @observable
52 + bool isDisabled;
53 +
54 + WalletType get type => wallet.type;
55 +
56 + double get doubleAmount => buyAmountViewModel.doubleAmount;
57 +
58 + FiatCurrency get fiatCurrency => buyAmountViewModel.fiatCurrency;
59 +
60 + CryptoCurrency get cryptoCurrency => walletTypeToCryptoCurrency(type);
61 +
62 + Future createOrder() async {
63 + try {
64 + final url = await selectedProvider
65 + ?.requestUrl(doubleAmount?.toString(), fiatCurrency.title);
66 + // FIXME: Start WebView
67 + } catch (e) {
68 + print(e.toString());
69 + }
70 + }
71 +
72 + Future<void> saveOrder(String orderId) async {
73 + try {
74 + final order = await selectedProvider?.findOrderById(orderId);
75 + await ordersSource.add(order);
76 + ordersStore.setOrder(order);
77 + } catch (e) {
78 + print(e.toString());
79 + }
80 + }
81 +}
\ No newline at end of file
lib/view_model/dashboard/dashboard_view_model.dart
+1 -6
@@ -4,7 +4,7 @@ import 'dart:io';
4 import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart';
5 import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
6 import 'package:cake_wallet/entities/balance.dart';
7 -import 'package:cake_wallet/entities/order.dart';
7 +import 'package:cake_wallet/buy/order.dart';
8 import 'package:cake_wallet/entities/transaction_history.dart';
9 import 'package:cake_wallet/exchange/trade_state.dart';
10 import 'package:cake_wallet/monero/account.dart';
@@ -95,8 +95,6 @@ abstract class DashboardViewModelBase with Store {
95 ]
96 };
97
98 - isRunningWebView = false;
99 -
98 name = appStore.wallet?.name;
99 wallet ??= appStore.wallet;
100 type = wallet.type;
@@ -162,9 +160,6 @@ abstract class DashboardViewModelBase with Store {
160 @observable
161 String subname;
162
165 - @observable
166 - bool isRunningWebView;
167 -
163 @computed
164 String get address => wallet.address;
165
lib/view_model/dashboard/order_list_item.dart
+1 -1
@@ -1,4 +1,4 @@
1 -import 'package:cake_wallet/entities/order.dart';
1 +import 'package:cake_wallet/buy/order.dart';
2 import 'package:cake_wallet/store/settings_store.dart';
3 import 'package:cake_wallet/view_model/dashboard/action_list_item.dart';
4 import 'package:cake_wallet/entities/balance_display_mode.dart';
lib/view_model/order_details_view_model.dart
+1 -1
@@ -1,5 +1,5 @@
1 import 'dart:async';
2 -import 'package:cake_wallet/entities/order.dart';
2 +import 'package:cake_wallet/buy/order.dart';
3 import 'package:cake_wallet/utils/date_formatter.dart';
4 import 'package:cake_wallet/view_model/wyre_view_model.dart';
5 import 'package:mobx/mobx.dart';
lib/view_model/wyre_view_model.dart
+1 -1
@@ -1,7 +1,7 @@
1 import 'package:cake_wallet/entities/wyre_service.dart';
2 import 'package:flutter/foundation.dart';
3 import 'package:hive/hive.dart';
4 -import 'package:cake_wallet/entities/order.dart';
4 +import 'package:cake_wallet/buy/order.dart';
5 import 'package:cake_wallet/store/dashboard/orders_store.dart';
6 import 'package:mobx/mobx.dart';
7