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 - '&currencyCode=' +
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) {