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 + '¤cyCode=' +
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