Cw 935 get exchange payment method recommendations from on ramper (#2235)

* fix: default to recommended payment method in Onramper * fix: support displaying unknown payment methods * feat: fetch recommended Onramper payment type

Serhii committed May 15, 2025 at 19:48 UTC 557e1c9839807c601626038637b737ab11705106
10 files changed +89 -21
lib/buy/buy_provider.dart
+1
@@ -64,6 +64,7 @@ abstract class BuyProvider {
64 required bool isBuyAction,
65 required String walletAddress,
66 PaymentType? paymentType,
67 + String? customPaymentMethodType,
68 String? countryCode}) async =>
69 null;
70 }
lib/buy/buy_quote.dart
+4 -1
@@ -50,6 +50,7 @@ class Quote extends SelectableOption {
50 this.rampName,
51 this.rampIconPath,
52 this.limits,
53 + this.customPaymentMethodType,
54 }) : super(title: provider.isAggregator ? rampName ?? '' : provider.title);
55
56 final double rate;
@@ -68,6 +69,7 @@ class Quote extends SelectableOption {
69 bool _isBestRate = false;
70 bool isBuyAction;
71 Limits? limits;
72 + String? customPaymentMethodType;
73
74 late FiatCurrency _fiatCurrency;
75 late CryptoCurrency _cryptoCurrency;
@@ -130,7 +132,7 @@ class Quote extends SelectableOption {
132 set setLimits(Limits limits) => this.limits = limits;
133
134 factory Quote.fromOnramperJson(Map<String, dynamic> json, bool isBuyAction,
133 - Map<String, dynamic> metaData, PaymentType paymentType) {
135 + Map<String, dynamic> metaData, PaymentType paymentType, String? customPaymentMethodType) {
136 final rate = _toDouble(json['rate']) ?? 0.0;
137 final networkFee = _toDouble(json['networkFee']) ?? 0.0;
138 final transactionFee = _toDouble(json['transactionFee']) ?? 0.0;
@@ -183,6 +185,7 @@ class Quote extends SelectableOption {
185 rampName: rampName,
186 rampIconPath: rampIconPath,
187 paymentType: paymentType,
188 + customPaymentMethodType: customPaymentMethodType,
189 quoteId: json['quoteId'] as String? ?? '',
190 recommendations: enumRecommendations,
191 provider: ProvidersHelper.getProviderByType(ProviderType.onramper)!,
lib/buy/dfx/dfx_buy_provider.dart
+2 -1
@@ -231,6 +231,7 @@ class DFXBuyProvider extends BuyProvider {
231 required bool isBuyAction,
232 required String walletAddress,
233 PaymentType? paymentType,
234 + String? customPaymentMethodType,
235 String? countryCode}) async {
236 /// if buying with any currency other than eur or chf then DFX is not supported
237
@@ -373,7 +374,7 @@ class DFXBuyProvider extends BuyProvider {
374 case 'Instant':
375 return PaymentType.sepa;
376 default:
376 - return PaymentType.all;
377 + return PaymentType.unknown;
378 }
379 }
380
lib/buy/kryptonim/kryptonim.dart
+2 -1
@@ -113,6 +113,7 @@ class KryptonimBuyProvider extends BuyProvider {
113 required bool isBuyAction,
114 required String walletAddress,
115 PaymentType? paymentType,
116 + String? customPaymentMethodType,
117 String? countryCode,
118 }) async {
119 log('Kryptonim: Fetching quote: ${isBuyAction ? cryptoCurrency : fiatCurrency} -> ${isBuyAction ? fiatCurrency : cryptoCurrency}, amount: $amount');
@@ -149,7 +150,7 @@ class KryptonimBuyProvider extends BuyProvider {
150
151 final selectedPaymentType =
152 PaymentMethod.getPaymentTypeId(selectedPaymentMethod['payment_method'] as String?);
152 - final quote = Quote.fromKryptonimJson(selectedPaymentMethod, isBuyAction, selectedPaymentType);
153 + final quote = Quote.fromKryptonimJson(selectedPaymentMethod, isBuyAction, selectedPaymentType ?? PaymentType.unknown);
154
155 quote.setFiatCurrency = fiatCurrency;
156 quote.setCryptoCurrency = cryptoCurrency;
lib/buy/meld/meld_buy_provider.dart
+1
@@ -104,6 +104,7 @@ class MeldBuyProvider extends BuyProvider {
104 required bool isBuyAction,
105 required String walletAddress,
106 PaymentType? paymentType,
107 + String? customPaymentMethodType,
108 String? countryCode}) async {
109 String? paymentMethod;
110 if (paymentType != null && paymentType != PaymentType.all) {
lib/buy/moonpay/moonpay_provider.dart
+2 -1
@@ -162,6 +162,7 @@ class MoonPayProvider extends BuyProvider {
162 required bool isBuyAction,
163 required String walletAddress,
164 PaymentType? paymentType,
165 + String? customPaymentMethodType,
166 String? countryCode}) async {
167 String? paymentMethod;
168
@@ -410,7 +411,7 @@ class MoonPayProvider extends BuyProvider {
411 case 'yellow_card_bank_transfer':
412 return PaymentType.yellowCardBankTransfer;
413 default:
413 - return PaymentType.all;
414 + return PaymentType.unknown;
415 }
416 }
417 }
lib/buy/onramper/onramper_buy_provider.dart
+48 -8
@@ -33,6 +33,7 @@ class OnRamperBuyProvider extends BuyProvider {
33 static const quotes = '/quotes';
34 static const paymentTypes = '/payment-types';
35 static const supported = '/supported';
36 + static const defaultsAll = '/defaults/all';
37
38 static const List<CryptoCurrency> _notSupportedCrypto = [];
39 static const List<FiatCurrency> _notSupportedFiat = [];
@@ -40,6 +41,8 @@ class OnRamperBuyProvider extends BuyProvider {
41
42 final SettingsStore _settingsStore;
43
44 + String? recommendedPaymentType;
45 +
46 String get _apiKey => secrets.onramperApiKey;
47
48 @override
@@ -57,6 +60,34 @@ class OnRamperBuyProvider extends BuyProvider {
60 @override
61 bool get isAggregator => true;
62
63 + Future<String?> getRecommendedPaymentType(bool isBuyAction) async {
64 +
65 + final params = {'type': isBuyAction ? 'buy' : 'sell'};
66 +
67 + final url = Uri.https(_baseApiUrl, '$supported$defaultsAll', params);
68 +
69 + try {
70 + final response =
71 + await http.get(url, headers: {'Authorization': _apiKey, 'accept': 'application/json'});
72 +
73 + if (response.statusCode == 200) {
74 + final Map<String, dynamic> data = jsonDecode(response.body) as Map<String, dynamic>;
75 + final recommended = data['message']['recommended'] as Map<String, dynamic>;
76 +
77 + final recommendedPaymentType = recommended['paymentMethod'] as String?;
78 +
79 + return recommendedPaymentType ;
80 + } else {
81 + final responseBody =
82 + jsonDecode(response.body) as Map<String, dynamic>;
83 + printV('Failed to fetch available payment types: ${responseBody['message']}');
84 + }
85 + } catch (e) {
86 + printV('Failed to fetch available payment types: $e');
87 + }
88 + return null;
89 + }
90 +
91 Future<List<PaymentMethod>> getAvailablePaymentTypes(
92 String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async {
93
@@ -77,9 +108,14 @@ class OnRamperBuyProvider extends BuyProvider {
108 if (response.statusCode == 200) {
109 final Map<String, dynamic> data = jsonDecode(response.body) as Map<String, dynamic>;
110 final List<dynamic> message = data['message'] as List<dynamic>;
80 - return message
111 +
112 + final allAvailablePaymentMethods = message
113 .map((item) => PaymentMethod.fromOnramperJson(item as Map<String, dynamic>))
114 .toList();
115 +
116 + recommendedPaymentType = await getRecommendedPaymentType(isBuyAction);
117 +
118 + return allAvailablePaymentMethods;
119 } else {
120 final responseBody =
121 jsonDecode(response.body) as Map<String, dynamic>;
@@ -131,13 +167,13 @@ class OnRamperBuyProvider extends BuyProvider {
167 required bool isBuyAction,
168 required String walletAddress,
169 PaymentType? paymentType,
170 + String? customPaymentMethodType,
171 String? countryCode}) async {
172 String? paymentMethod;
173
137 - if (paymentType != null && paymentType != PaymentType.all) {
138 - paymentMethod = normalizePaymentMethod(paymentType);
139 - if (paymentMethod == null) paymentMethod = paymentType.name;
140 - }
174 + if (paymentType == PaymentType.all && recommendedPaymentType != null) paymentMethod = recommendedPaymentType!;
175 + else if (paymentType == PaymentType.unknown) paymentMethod = customPaymentMethodType;
176 + else if (paymentType != null) paymentMethod = normalizePaymentMethod(paymentType);
177
178 final actionType = isBuyAction ? 'buy' : 'sell';
179
@@ -182,7 +218,7 @@ class OnRamperBuyProvider extends BuyProvider {
218 if (rampMetaData == null) continue;
219
220 final quote = Quote.fromOnramperJson(
185 - item, isBuyAction, _onrampMetadata, _getPaymentTypeByString(paymentMethod));
221 + item, isBuyAction, _onrampMetadata, _getPaymentTypeByString(paymentMethod), customPaymentMethodType);
222 quote.setFiatCurrency = fiatCurrency;
223 quote.setCryptoCurrency = cryptoCurrency;
224 validQuotes.add(quote);
@@ -225,7 +261,7 @@ class OnRamperBuyProvider extends BuyProvider {
261 final defaultCrypto =
262 quote.cryptoCurrency.title + _getNormalizeNetwork(quote.cryptoCurrency).toLowerCase();
263
228 - final paymentMethod = normalizePaymentMethod(quote.paymentType);
264 + final paymentMethod = quote.paymentType == PaymentType.unknown ? quote.customPaymentMethodType : normalizePaymentMethod(quote.paymentType);
265
266 final uri = Uri.https(_baseUrl, '', {
267 'apiKey': _apiKey,
@@ -330,6 +366,8 @@ class OnRamperBuyProvider extends BuyProvider {
366 return 'dana';
367 case PaymentType.ideal:
368 return 'ideal';
369 + case PaymentType.pixPay:
370 + return 'pix';
371 default:
372 return null;
373 }
@@ -379,8 +417,10 @@ class OnRamperBuyProvider extends BuyProvider {
417 return PaymentType.dana;
418 case 'ideal':
419 return PaymentType.ideal;
420 + case 'pix':
421 + return PaymentType.pixPay;
422 default:
383 - return PaymentType.all;
423 + return PaymentType.unknown;
424 }
425 }
426
lib/buy/payment_method.dart
+14 -5
@@ -34,6 +34,8 @@ enum PaymentType {
34 yellowCardBankTransfer,
35 fiatBalance,
36 bancontact,
37 + pixPay,
38 + unknown,
39 }
40
41 extension PaymentTypeTitle on PaymentType {
@@ -101,6 +103,8 @@ extension PaymentTypeTitle on PaymentType {
103 return 'Fiat Balance';
104 case PaymentType.bancontact:
105 return 'Bancontact';
106 + case PaymentType.pixPay:
107 + return 'PIX Pay';
108 default:
109 return null;
110 }
@@ -158,12 +162,14 @@ class PaymentMethod extends SelectableOption {
162 required this.customTitle,
163 required this.customIconPath,
164 this.customDescription,
165 + this.customPaymentMethodType,
166 }) : super(title: paymentMethodType.title ?? customTitle);
167
168 final PaymentType paymentMethodType;
169 final String customTitle;
170 final String customIconPath;
171 final String? customDescription;
172 + final String? customPaymentMethodType;
173 bool isSelected = false;
174
175 @override
@@ -188,7 +194,8 @@ class PaymentMethod extends SelectableOption {
194 factory PaymentMethod.fromOnramperJson(Map<String, dynamic> json) {
195 final type = PaymentMethod.getPaymentTypeId(json['paymentTypeId'] as String?);
196 return PaymentMethod(
191 - paymentMethodType: type,
197 + paymentMethodType: type ?? PaymentType.unknown,
198 + customPaymentMethodType: json['paymentTypeId'] as String?,
199 customTitle: json['name'] as String? ?? 'Unknown',
200 customIconPath: json['icon'] as String? ?? 'assets/images/card.png',
201 customDescription: json['description'] as String?);
@@ -212,7 +219,7 @@ class PaymentMethod extends SelectableOption {
219 final type = PaymentMethod.getPaymentTypeId(json['paymentMethod'] as String?);
220 final logos = json['logos'] as Map<String, dynamic>;
221 return PaymentMethod(
215 - paymentMethodType: type,
222 + paymentMethodType: type ?? PaymentType.unknown,
223 customTitle: json['name'] as String? ?? 'Unknown',
224 customIconPath: logos['dark'] as String? ?? 'assets/images/card.png',
225 customDescription: json['description'] as String?);
@@ -221,13 +228,13 @@ class PaymentMethod extends SelectableOption {
228 factory PaymentMethod.fromKryptonimJson(Map<String, dynamic> json) {
229 final type = PaymentMethod.getPaymentTypeId(json['payment_method'] as String?);
230 return PaymentMethod(
224 - paymentMethodType: type,
231 + paymentMethodType: type ?? PaymentType.unknown,
232 customTitle: json['payment_method'] as String? ?? 'Unknown',
233 customIconPath: 'assets/images/card.png',
234 );
235 }
236
230 - static PaymentType getPaymentTypeId(String? type) {
237 + static PaymentType? getPaymentTypeId(String? type) {
238 switch (type?.toLowerCase()) {
239 case 'banktransfer':
240 case 'bank':
@@ -289,8 +296,10 @@ class PaymentMethod extends SelectableOption {
296 return PaymentType.sepaOpenBankingPayment;
297 case 'bancontact':
298 return PaymentType.bancontact;
299 + case 'pix':
300 + return PaymentType.pixPay;
301 default:
293 - return PaymentType.all;
302 + return null;
303 }
304 }
305 }
lib/buy/robinhood/robinhood_buy_provider.dart
+2 -1
@@ -192,6 +192,7 @@ class RobinhoodBuyProvider extends BuyProvider {
192 required bool isBuyAction,
193 required String walletAddress,
194 PaymentType? paymentType,
195 + String? customPaymentMethodType,
196 String? countryCode}) async {
197 String? paymentMethod;
198
@@ -267,7 +268,7 @@ class RobinhoodBuyProvider extends BuyProvider {
268 case 'bank_transfer':
269 return PaymentType.bankTransfer;
270 default:
270 - return PaymentType.all;
271 + return PaymentType.unknown;
272 }
273 }
274 }
lib/view_model/buy/buy_sell_view_model.dart
+13 -3
@@ -359,14 +359,23 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
359 onTimeout: () => [],
360 )));
361
362 - final Map<PaymentType, PaymentMethod> uniquePaymentMethods = {};
362 + final List<PaymentMethod> tempPaymentMethods = [];
363 +
364 for (var methods in result) {
365 for (var method in methods) {
365 - uniquePaymentMethods[method.paymentMethodType] = method;
366 + final alreadyExists = tempPaymentMethods.any((m) {
367 + return m.paymentMethodType == method.paymentMethodType &&
368 + m.customTitle == method.customTitle;
369 + });
370 +
371 + if (!alreadyExists) {
372 + tempPaymentMethods.add(method);
373 + }
374 }
375 }
376
369 - paymentMethods = ObservableList<PaymentMethod>.of(uniquePaymentMethods.values);
377 + paymentMethods = ObservableList<PaymentMethod>.of(tempPaymentMethods);
378 +
379 if (paymentMethods.isNotEmpty) {
380 paymentMethods.insert(0, PaymentMethod.all());
381 selectedPaymentMethod = paymentMethods.first;
@@ -404,6 +413,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
413 paymentType: selectedPaymentMethod?.paymentMethodType,
414 isBuyAction: isBuyAction,
415 walletAddress: wallet.walletAddresses.address,
416 + customPaymentMethodType: selectedPaymentMethod?.customPaymentMethodType,
417 )
418 .timeout(
419 Duration(seconds: 10),