Cw 521 moonpay buy (#1335)
* save * save * moonpay fixes * fix for debug mode * code cleanup * another fix for debug mode * [skip ci] fixes * test build * code cleanup * fix buy page
Matthew Fosse committed
Mar 28, 2024 at 06:30 UTC
78685b74f0c4040da4f5298b31ec4bcef2edcf7c
7 files changed
+180
-184
lib/buy/moonpay/moonpay_provider.dart
+116
-115
@@ -22,20 +22,24 @@ import 'package:flutter/material.dart';
22
import 'package:http/http.dart';
23
import 'package:url_launcher/url_launcher.dart';
24
25
-class MoonPaySellProvider extends BuyProvider {
26
- MoonPaySellProvider({
25
+class MoonPayProvider extends BuyProvider {
26
+ MoonPayProvider({
27
required SettingsStore settingsStore,
28
required WalletBase wallet,
29
bool isTestEnvironment = false,
30
- }) : baseUrl = isTestEnvironment ? _baseTestUrl : _baseProductUrl,
30
+ }) : baseSellUrl = isTestEnvironment ? _baseSellTestUrl : _baseSellProductUrl,
31
+ baseBuyUrl = isTestEnvironment ? _baseBuyTestUrl : _baseBuyProductUrl,
32
this._settingsStore = settingsStore,
33
super(wallet: wallet, isTestEnvironment: isTestEnvironment);
34
35
final SettingsStore _settingsStore;
36
36
- static const _baseTestUrl = 'sell-sandbox.moonpay.com';
37
- static const _baseProductUrl = 'sell.moonpay.com';
37
+ static const _baseSellTestUrl = 'sell-sandbox.moonpay.com';
38
+ static const _baseSellProductUrl = 'sell.moonpay.com';
39
+ static const _baseBuyTestUrl = 'buy-staging.moonpay.com';
40
+ static const _baseBuyProductUrl = 'buy.moonpay.com';
41
static const _cIdBaseUrl = 'exchange-helper.cakewallet.com';
42
+ static const _apiUrl = 'https://api.moonpay.com';
43
44
@override
45
String get providerDescription =>
@@ -62,8 +66,14 @@ class MoonPaySellProvider extends BuyProvider {
66
67
static String get _apiKey => secrets.moonPayApiKey;
68
69
+ final String baseBuyUrl;
70
+ final String baseSellUrl;
71
+
72
+ String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase();
73
+
74
+ String get trackUrl => baseBuyUrl + '/transaction_receipt?transactionId=';
75
+
76
static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey;
66
- final String baseUrl;
77
78
Future<String> getMoonpaySignature(String query) async {
79
final uri = Uri.https(_cIdBaseUrl, "/api/moonpay");
@@ -85,147 +95,92 @@ class MoonPaySellProvider extends BuyProvider {
95
}
96
}
97
88
- Future<Uri> requestMoonPayUrl({
98
+ Future<Uri> requestSellMoonPayUrl({
99
required CryptoCurrency currency,
100
required String refundWalletAddress,
101
required SettingsStore settingsStore,
102
}) async {
93
- final customParams = {
103
+ final params = {
104
'theme': themeToMoonPayTheme(settingsStore.currentTheme),
105
'language': settingsStore.languageCode,
106
'colorCode': settingsStore.currentTheme.type == ThemeType.dark
107
? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}'
108
: '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}',
109
+ 'defaultCurrencyCode': _normalizeCurrency(currency),
110
+ 'refundWalletAddress': refundWalletAddress,
111
};
112
113
+ if (_apiKey.isNotEmpty) {
114
+ params['apiKey'] = _apiKey;
115
+ }
116
+
117
final originalUri = Uri.https(
102
- baseUrl,
118
+ baseSellUrl,
119
'',
104
- <String, dynamic>{
105
- 'apiKey': _apiKey,
106
- 'defaultBaseCurrencyCode': _normalizeCurrency(currency),
107
- 'refundWalletAddress': refundWalletAddress,
108
- }..addAll(customParams),
120
+ params,
121
);
122
111
- final signature = await getMoonpaySignature('?${originalUri.query}');
112
-
123
if (isTestEnvironment) {
124
return originalUri;
125
}
126
127
+ final signature = await getMoonpaySignature('?${originalUri.query}');
128
+
129
final query = Map<String, dynamic>.from(originalUri.queryParameters);
130
query['signature'] = signature;
131
final signedUri = originalUri.replace(queryParameters: query);
132
return signedUri;
133
}
134
123
- @override
124
- Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
125
- try {
126
- final uri = await requestMoonPayUrl(
127
- currency: wallet.currency,
128
- refundWalletAddress: wallet.walletAddresses.address,
129
- settingsStore: _settingsStore,
130
- );
131
-
132
- if (await canLaunchUrl(uri)) {
133
- if (DeviceInfo.instance.isMobile) {
134
- Navigator.of(context).pushNamed(Routes.webViewPage, arguments: ['MoonPay', uri]);
135
- } else {
136
- await launchUrl(uri, mode: LaunchMode.externalApplication);
137
- }
138
- } else {
139
- throw Exception('Could not launch URL');
140
- }
141
- } catch (e) {
142
- await showDialog<void>(
143
- context: context,
144
- builder: (BuildContext context) {
145
- return AlertWithOneAction(
146
- alertTitle: 'MoonPay',
147
- alertContent: 'The MoonPay service is currently unavailable: $e',
148
- buttonText: S.of(context).ok,
149
- buttonAction: () => Navigator.of(context).pop(),
150
- );
151
- },
152
- );
153
- }
154
- }
155
-
156
- String _normalizeCurrency(CryptoCurrency currency) {
157
- if (currency == CryptoCurrency.maticpoly) {
158
- return "MATIC_POLYGON";
159
- }
160
-
161
- return currency.toString().toLowerCase();
162
- }
163
-}
164
-
165
-class MoonPayBuyProvider extends BuyProvider {
166
- MoonPayBuyProvider({required WalletBase wallet, bool isTestEnvironment = false})
167
- : baseUrl = isTestEnvironment ? _baseTestUrl : _baseProductUrl,
168
- super(wallet: wallet, isTestEnvironment: isTestEnvironment);
169
-
170
- static const _baseTestUrl = 'https://buy-staging.moonpay.com';
171
- static const _baseProductUrl = 'https://buy.moonpay.com';
172
- static const _apiUrl = 'https://api.moonpay.com';
135
+ // BUY:
136
static const _currenciesSuffix = '/v3/currencies';
137
static const _quoteSuffix = '/buy_quote';
138
static const _transactionsSuffix = '/v1/transactions';
139
static const _ipAddressSuffix = '/v4/ip_address';
177
- static const _apiKey = secrets.moonPayApiKey;
178
- static const _secretKey = secrets.moonPaySecretKey;
179
-
180
- @override
181
- String get title => 'MoonPay';
182
-
183
- @override
184
- String get providerDescription =>
185
- 'MoonPay offers a fast and simple way to buy and sell cryptocurrencies';
186
-
187
- @override
188
- String get lightIcon => 'assets/images/moonpay_light.png';
189
-
190
- @override
191
- String get darkIcon => 'assets/images/moonpay_dark.png';
192
-
193
- String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase();
194
-
195
- String get trackUrl => baseUrl + '/transaction_receipt?transactionId=';
140
197
- String baseUrl;
198
-
199
- Future<String> requestUrl(String amount, String sourceCurrency) async {
200
- final enabledPaymentMethods = 'credit_debit_card%2Capple_pay%2Cgoogle_pay%2Csamsung_pay'
201
- '%2Csepa_bank_transfer%2Cgbp_bank_transfer%2Cgbp_open_banking_payment';
141
+ Future<Uri> requestBuyMoonPayUrl({
142
+ required CryptoCurrency currency,
143
+ required SettingsStore settingsStore,
144
+ required String walletAddress,
145
+ String? amount,
146
+ }) async {
147
+ final params = {
148
+ 'theme': themeToMoonPayTheme(settingsStore.currentTheme),
149
+ 'language': settingsStore.languageCode,
150
+ 'colorCode': settingsStore.currentTheme.type == ThemeType.dark
151
+ ? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}'
152
+ : '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}',
153
+ 'defaultCurrencyCode': _normalizeCurrency(currency),
154
+ 'baseCurrencyCode': _normalizeCurrency(currency),
155
+ 'baseCurrencyAmount': amount ?? '0',
156
+ 'currencyCode': currencyCode,
157
+ 'walletAddress': walletAddress,
158
+ 'lockAmount': 'true',
159
+ 'showAllCurrencies': 'false',
160
+ 'showWalletAddressForm': 'false',
161
+ 'enabledPaymentMethods':
162
+ 'credit_debit_card,apple_pay,google_pay,samsung_pay,sepa_bank_transfer,gbp_bank_transfer,gbp_open_banking_payment',
163
+ };
164
203
- final suffix = '?apiKey=' +
204
- _apiKey +
205
- '¤cyCode=' +
206
- currencyCode +
207
- '&enabledPaymentMethods=' +
208
- enabledPaymentMethods +
209
- '&walletAddress=' +
210
- wallet.walletAddresses.address +
211
- '&baseCurrencyCode=' +
212
- sourceCurrency.toLowerCase() +
213
- '&baseCurrencyAmount=' +
214
- amount +
215
- '&lockAmount=true' +
216
- '&showAllCurrencies=false' +
217
- '&showWalletAddressForm=false';
165
+ if (_apiKey.isNotEmpty) {
166
+ params['apiKey'] = _apiKey;
167
+ }
168
219
- final originalUrl = baseUrl + suffix;
169
+ final originalUri = Uri.https(
170
+ baseBuyUrl,
171
+ '',
172
+ params,
173
+ );
174
221
- final messageBytes = utf8.encode(suffix);
222
- final key = utf8.encode(_secretKey);
223
- final hmac = Hmac(sha256, key);
224
- final digest = hmac.convert(messageBytes);
225
- final signature = base64.encode(digest.bytes);
226
- final urlWithSignature = originalUrl + '&signature=${Uri.encodeComponent(signature)}';
175
+ if (isTestEnvironment) {
176
+ return originalUri;
177
+ }
178
228
- return isTestEnvironment ? originalUrl : urlWithSignature;
179
+ final signature = await getMoonpaySignature('?${originalUri.query}');
180
+ final query = Map<String, dynamic>.from(originalUri.queryParameters);
181
+ query['signature'] = signature;
182
+ final signedUri = originalUri.replace(queryParameters: query);
183
+ return signedUri;
184
}
185
186
Future<BuyAmount> calculateAmount(String amount, String sourceCurrency) async {
@@ -300,6 +255,52 @@ class MoonPayBuyProvider extends BuyProvider {
255
}
256
257
@override
303
- Future<void> launchProvider(BuildContext context, bool? isBuyAction) =>
304
- throw UnimplementedError();
258
+ Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
259
+ // try {
260
+ late final Uri uri;
261
+ if (isBuyAction ?? true) {
262
+ uri = await requestBuyMoonPayUrl(
263
+ currency: wallet.currency,
264
+ walletAddress: wallet.walletAddresses.address,
265
+ settingsStore: _settingsStore,
266
+ );
267
+ } else {
268
+ uri = await requestSellMoonPayUrl(
269
+ currency: wallet.currency,
270
+ refundWalletAddress: wallet.walletAddresses.address,
271
+ settingsStore: _settingsStore,
272
+ );
273
+ }
274
+
275
+ if (await canLaunchUrl(uri)) {
276
+ if (DeviceInfo.instance.isMobile) {
277
+ Navigator.of(context).pushNamed(Routes.webViewPage, arguments: ['MoonPay', uri]);
278
+ } else {
279
+ await launchUrl(uri, mode: LaunchMode.externalApplication);
280
+ }
281
+ } else {
282
+ throw Exception('Could not launch URL');
283
+ }
284
+ // } catch (e) {
285
+ // await showDialog<void>(
286
+ // context: context,
287
+ // builder: (BuildContext context) {
288
+ // return AlertWithOneAction(
289
+ // alertTitle: 'MoonPay',
290
+ // alertContent: 'The MoonPay service is currently unavailable: $e',
291
+ // buttonText: S.of(context).ok,
292
+ // buttonAction: () => Navigator.of(context).pop(),
293
+ // );
294
+ // },
295
+ // );
296
+ // }
297
+ }
298
+
299
+ String _normalizeCurrency(CryptoCurrency currency) {
300
+ if (currency == CryptoCurrency.maticpoly) {
301
+ return "MATIC_POLYGON";
302
+ }
303
+
304
+ return currency.toString().toLowerCase();
305
+ }
306
}
lib/di.dart
+6
-2
@@ -198,6 +198,7 @@ import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart';
198
import 'package:cake_wallet/view_model/wallet_restore_view_model.dart';
199
import 'package:cake_wallet/view_model/wallet_seed_view_model.dart';
200
import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart';
201
+import 'package:flutter/foundation.dart';
202
import 'package:flutter/widgets.dart';
203
import 'package:get_it/get_it.dart';
204
import 'package:hive/hive.dart';
@@ -806,8 +807,11 @@ Future<void> setup({
807
getIt
808
.registerFactory<DFXBuyProvider>(() => DFXBuyProvider(wallet: getIt.get<AppStore>().wallet!));
809
809
- getIt.registerFactory<MoonPaySellProvider>(() => MoonPaySellProvider(
810
- settingsStore: getIt.get<AppStore>().settingsStore, wallet: getIt.get<AppStore>().wallet!));
810
+ getIt.registerFactory<MoonPayProvider>(() => MoonPayProvider(
811
+ settingsStore: getIt.get<AppStore>().settingsStore,
812
+ wallet: getIt.get<AppStore>().wallet!,
813
+ isTestEnvironment: kDebugMode,
814
+ ));
815
816
getIt.registerFactory<OnRamperBuyProvider>(() => OnRamperBuyProvider(
817
getIt.get<AppStore>().settingsStore,
lib/entities/provider_types.dart
+10
-9
@@ -11,7 +11,7 @@ enum ProviderType {
11
robinhood,
12
dfx,
13
onramper,
14
- moonpaySell,
14
+ moonpay,
15
}
16
17
extension ProviderTypeName on ProviderType {
@@ -25,7 +25,7 @@ extension ProviderTypeName on ProviderType {
25
return 'DFX Connect';
26
case ProviderType.onramper:
27
return 'Onramper';
28
- case ProviderType.moonpaySell:
28
+ case ProviderType.moonpay:
29
return 'MoonPay';
30
}
31
}
@@ -40,7 +40,7 @@ extension ProviderTypeName on ProviderType {
40
return 'dfx_connect_provider';
41
case ProviderType.onramper:
42
return 'onramper_provider';
43
- case ProviderType.moonpaySell:
43
+ case ProviderType.moonpay:
44
return 'moonpay_provider';
45
}
46
}
@@ -62,10 +62,11 @@ class ProvidersHelper {
62
ProviderType.onramper,
63
ProviderType.dfx,
64
ProviderType.robinhood,
65
+ ProviderType.moonpay,
66
];
67
case WalletType.litecoin:
68
case WalletType.bitcoinCash:
68
- return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood];
69
+ return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood, ProviderType.moonpay];
70
case WalletType.solana:
71
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood];
72
case WalletType.none:
@@ -82,18 +83,18 @@ class ProvidersHelper {
83
return [
84
ProviderType.askEachTime,
85
ProviderType.onramper,
85
- ProviderType.moonpaySell,
86
+ ProviderType.moonpay,
87
ProviderType.dfx,
88
];
89
case WalletType.litecoin:
90
case WalletType.bitcoinCash:
90
- return [ProviderType.askEachTime, ProviderType.moonpaySell];
91
+ return [ProviderType.askEachTime, ProviderType.moonpay];
92
case WalletType.solana:
93
return [
94
ProviderType.askEachTime,
95
ProviderType.onramper,
96
ProviderType.robinhood,
96
- ProviderType.moonpaySell,
97
+ ProviderType.moonpay,
98
];
99
case WalletType.monero:
100
case WalletType.nano:
@@ -112,10 +113,10 @@ class ProvidersHelper {
113
return getIt.get<DFXBuyProvider>();
114
case ProviderType.onramper:
115
return getIt.get<OnRamperBuyProvider>();
116
+ case ProviderType.moonpay:
117
+ return getIt.get<MoonPayProvider>();
118
case ProviderType.askEachTime:
119
return null;
117
- case ProviderType.moonpaySell:
118
- return getIt.get<MoonPaySellProvider>();
120
}
121
}
122
}
lib/src/screens/buy/buy_options_page.dart
+40
-38
@@ -1,6 +1,7 @@
1
import 'package:cake_wallet/generated/i18n.dart';
2
import 'package:cake_wallet/src/screens/base_page.dart';
3
import 'package:cake_wallet/src/widgets/option_tile.dart';
4
+import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
5
import 'package:cake_wallet/themes/extensions/option_tile_theme.dart';
6
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
7
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
@@ -25,45 +26,46 @@ class BuySellOptionsPage extends BasePage {
26
? dashboardViewModel.availableBuyProviders
27
: dashboardViewModel.availableSellProviders;
28
28
- return Container(
29
- child: Center(
30
- child: ConstrainedBox(
31
- constraints: BoxConstraints(maxWidth: 330),
32
- child: Column(
33
- children: [
34
- ...availableProviders.map((provider) {
35
- final icon = Image.asset(
36
- isLightMode ? provider.lightIcon : provider.darkIcon,
37
- height: 40,
38
- width: 40,
39
- );
29
+ return ScrollableWithBottomSection(
30
+ content: Container(
31
+ child: Center(
32
+ child: ConstrainedBox(
33
+ constraints: BoxConstraints(maxWidth: 330),
34
+ child: Column(
35
+ children: [
36
+ ...availableProviders.map((provider) {
37
+ final icon = Image.asset(
38
+ isLightMode ? provider.lightIcon : provider.darkIcon,
39
+ height: 40,
40
+ width: 40,
41
+ );
42
41
- return Padding(
42
- padding: EdgeInsets.only(top: 24),
43
- child: OptionTile(
44
- image: icon,
45
- title: provider.toString(),
46
- description: provider.providerDescription,
47
- onPressed: () => provider.launchProvider(context, isBuyAction),
48
- ),
49
- );
50
- }).toList(),
51
- Spacer(),
52
- Padding(
53
- padding: EdgeInsets.fromLTRB(24, 24, 24, 32),
54
- child: Text(
55
- isBuyAction
56
- ? S.of(context).select_buy_provider_notice
57
- : S.of(context).select_sell_provider_notice,
58
- textAlign: TextAlign.center,
59
- style: TextStyle(
60
- fontSize: 14,
61
- fontWeight: FontWeight.normal,
62
- color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
63
- ),
64
- ),
65
- ),
66
- ],
43
+ return Padding(
44
+ padding: EdgeInsets.only(top: 24),
45
+ child: OptionTile(
46
+ image: icon,
47
+ title: provider.toString(),
48
+ description: provider.providerDescription,
49
+ onPressed: () => provider.launchProvider(context, isBuyAction),
50
+ ),
51
+ );
52
+ }).toList(),
53
+ ],
54
+ ),
55
+ ),
56
+ ),
57
+ ),
58
+ bottomSection: Padding(
59
+ padding: EdgeInsets.fromLTRB(24, 24, 24, 32),
60
+ child: Text(
61
+ isBuyAction
62
+ ? S.of(context).select_buy_provider_notice
63
+ : S.of(context).select_sell_provider_notice,
64
+ textAlign: TextAlign.center,
65
+ style: TextStyle(
66
+ fontSize: 14,
67
+ fontWeight: FontWeight.normal,
68
+ color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
69
),
70
),
71
),
lib/src/screens/buy/buy_webview_page.dart
+1
-1
@@ -60,7 +60,7 @@ class BuyWebViewPageBodyState extends State<BuyWebViewPageBody> {
60
_saveOrder(keyword: 'completed', splitSymbol: '/');
61
}
62
63
- if (widget.buyViewModel.selectedProvider is MoonPayBuyProvider) {
63
+ if (widget.buyViewModel.selectedProvider is MoonPayProvider) {
64
_saveOrder(keyword: 'transactionId', splitSymbol: '=');
65
}
66
}
lib/view_model/buy/buy_view_model.dart
-12
@@ -93,18 +93,6 @@ abstract class BuyViewModelBase with Store {
93
_providerList.add(WyreBuyProvider(wallet: wallet));
94
}
95
96
- var isMoonPayEnabled = false;
97
- try {
98
- isMoonPayEnabled = await MoonPayBuyProvider.onEnabled();
99
- } catch (e) {
100
- isMoonPayEnabled = false;
101
- print(e.toString());
102
- }
103
-
104
- if (isMoonPayEnabled) {
105
- _providerList.add(MoonPayBuyProvider(wallet: wallet));
106
- }
107
-
96
items = _providerList.map((provider) =>
97
BuyItem(provider: provider, buyAmountViewModel: buyAmountViewModel))
98
.toList();
lib/view_model/order_details_view_model.dart
+7
-7
@@ -27,7 +27,7 @@ abstract class OrderDetailsViewModelBase with Store {
27
_provider = WyreBuyProvider(wallet: wallet);
28
break;
29
case BuyProviderDescription.moonPay:
30
- _provider = MoonPayBuyProvider(wallet: wallet);
30
+ // _provider = MoonPayProvider(wallet: wallet);// TODO: CW-521
31
break;
32
}
33
}
@@ -50,9 +50,9 @@ abstract class OrderDetailsViewModelBase with Store {
50
@action
51
Future<void> _updateOrder() async {
52
try {
53
- if (_provider != null && (_provider is MoonPayBuyProvider || _provider is WyreBuyProvider)) {
54
- final updatedOrder = _provider is MoonPayBuyProvider
55
- ? await (_provider as MoonPayBuyProvider).findOrderById(order.id)
53
+ if (_provider != null && (_provider is MoonPayProvider || _provider is WyreBuyProvider)) {
54
+ final updatedOrder = _provider is MoonPayProvider
55
+ ? await (_provider as MoonPayProvider).findOrderById(order.id)
56
: await (_provider as WyreBuyProvider).findOrderById(order.id);
57
updatedOrder.from = order.from;
58
updatedOrder.to = order.to;
@@ -89,10 +89,10 @@ abstract class OrderDetailsViewModelBase with Store {
89
value: order.provider.title)
90
);
91
92
- if (_provider != null && (_provider is MoonPayBuyProvider || _provider is WyreBuyProvider)) {
92
+ if (_provider != null && (_provider is MoonPayProvider || _provider is WyreBuyProvider)) {
93
94
- final trackUrl = _provider is MoonPayBuyProvider
95
- ? (_provider as MoonPayBuyProvider).trackUrl
94
+ final trackUrl = _provider is MoonPayProvider
95
+ ? (_provider as MoonPayProvider).trackUrl
96
: (_provider as WyreBuyProvider).trackUrl;
97
98
if (trackUrl.isNotEmpty ?? false) {