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
This commit is contained in:
Serhii 2025-05-15 19:48:06 +03:00 committed by GitHub
parent 66efce4d70
commit 557e1c9839
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 89 additions and 21 deletions

View file

@ -64,6 +64,7 @@ abstract class BuyProvider {
required bool isBuyAction, required bool isBuyAction,
required String walletAddress, required String walletAddress,
PaymentType? paymentType, PaymentType? paymentType,
String? customPaymentMethodType,
String? countryCode}) async => String? countryCode}) async =>
null; null;
} }

View file

@ -50,6 +50,7 @@ class Quote extends SelectableOption {
this.rampName, this.rampName,
this.rampIconPath, this.rampIconPath,
this.limits, this.limits,
this.customPaymentMethodType,
}) : super(title: provider.isAggregator ? rampName ?? '' : provider.title); }) : super(title: provider.isAggregator ? rampName ?? '' : provider.title);
final double rate; final double rate;
@ -68,6 +69,7 @@ class Quote extends SelectableOption {
bool _isBestRate = false; bool _isBestRate = false;
bool isBuyAction; bool isBuyAction;
Limits? limits; Limits? limits;
String? customPaymentMethodType;
late FiatCurrency _fiatCurrency; late FiatCurrency _fiatCurrency;
late CryptoCurrency _cryptoCurrency; late CryptoCurrency _cryptoCurrency;
@ -130,7 +132,7 @@ class Quote extends SelectableOption {
set setLimits(Limits limits) => this.limits = limits; set setLimits(Limits limits) => this.limits = limits;
factory Quote.fromOnramperJson(Map<String, dynamic> json, bool isBuyAction, factory Quote.fromOnramperJson(Map<String, dynamic> json, bool isBuyAction,
Map<String, dynamic> metaData, PaymentType paymentType) { Map<String, dynamic> metaData, PaymentType paymentType, String? customPaymentMethodType) {
final rate = _toDouble(json['rate']) ?? 0.0; final rate = _toDouble(json['rate']) ?? 0.0;
final networkFee = _toDouble(json['networkFee']) ?? 0.0; final networkFee = _toDouble(json['networkFee']) ?? 0.0;
final transactionFee = _toDouble(json['transactionFee']) ?? 0.0; final transactionFee = _toDouble(json['transactionFee']) ?? 0.0;
@ -183,6 +185,7 @@ class Quote extends SelectableOption {
rampName: rampName, rampName: rampName,
rampIconPath: rampIconPath, rampIconPath: rampIconPath,
paymentType: paymentType, paymentType: paymentType,
customPaymentMethodType: customPaymentMethodType,
quoteId: json['quoteId'] as String? ?? '', quoteId: json['quoteId'] as String? ?? '',
recommendations: enumRecommendations, recommendations: enumRecommendations,
provider: ProvidersHelper.getProviderByType(ProviderType.onramper)!, provider: ProvidersHelper.getProviderByType(ProviderType.onramper)!,

View file

@ -231,6 +231,7 @@ class DFXBuyProvider extends BuyProvider {
required bool isBuyAction, required bool isBuyAction,
required String walletAddress, required String walletAddress,
PaymentType? paymentType, PaymentType? paymentType,
String? customPaymentMethodType,
String? countryCode}) async { String? countryCode}) async {
/// if buying with any currency other than eur or chf then DFX is not supported /// if buying with any currency other than eur or chf then DFX is not supported
@ -373,7 +374,7 @@ class DFXBuyProvider extends BuyProvider {
case 'Instant': case 'Instant':
return PaymentType.sepa; return PaymentType.sepa;
default: default:
return PaymentType.all; return PaymentType.unknown;
} }
} }

View file

@ -113,6 +113,7 @@ class KryptonimBuyProvider extends BuyProvider {
required bool isBuyAction, required bool isBuyAction,
required String walletAddress, required String walletAddress,
PaymentType? paymentType, PaymentType? paymentType,
String? customPaymentMethodType,
String? countryCode, String? countryCode,
}) async { }) async {
log('Kryptonim: Fetching quote: ${isBuyAction ? cryptoCurrency : fiatCurrency} -> ${isBuyAction ? fiatCurrency : cryptoCurrency}, amount: $amount'); log('Kryptonim: Fetching quote: ${isBuyAction ? cryptoCurrency : fiatCurrency} -> ${isBuyAction ? fiatCurrency : cryptoCurrency}, amount: $amount');
@ -149,7 +150,7 @@ class KryptonimBuyProvider extends BuyProvider {
final selectedPaymentType = final selectedPaymentType =
PaymentMethod.getPaymentTypeId(selectedPaymentMethod['payment_method'] as String?); PaymentMethod.getPaymentTypeId(selectedPaymentMethod['payment_method'] as String?);
final quote = Quote.fromKryptonimJson(selectedPaymentMethod, isBuyAction, selectedPaymentType); final quote = Quote.fromKryptonimJson(selectedPaymentMethod, isBuyAction, selectedPaymentType ?? PaymentType.unknown);
quote.setFiatCurrency = fiatCurrency; quote.setFiatCurrency = fiatCurrency;
quote.setCryptoCurrency = cryptoCurrency; quote.setCryptoCurrency = cryptoCurrency;

View file

@ -104,6 +104,7 @@ class MeldBuyProvider extends BuyProvider {
required bool isBuyAction, required bool isBuyAction,
required String walletAddress, required String walletAddress,
PaymentType? paymentType, PaymentType? paymentType,
String? customPaymentMethodType,
String? countryCode}) async { String? countryCode}) async {
String? paymentMethod; String? paymentMethod;
if (paymentType != null && paymentType != PaymentType.all) { if (paymentType != null && paymentType != PaymentType.all) {

View file

@ -162,6 +162,7 @@ class MoonPayProvider extends BuyProvider {
required bool isBuyAction, required bool isBuyAction,
required String walletAddress, required String walletAddress,
PaymentType? paymentType, PaymentType? paymentType,
String? customPaymentMethodType,
String? countryCode}) async { String? countryCode}) async {
String? paymentMethod; String? paymentMethod;
@ -410,7 +411,7 @@ class MoonPayProvider extends BuyProvider {
case 'yellow_card_bank_transfer': case 'yellow_card_bank_transfer':
return PaymentType.yellowCardBankTransfer; return PaymentType.yellowCardBankTransfer;
default: default:
return PaymentType.all; return PaymentType.unknown;
} }
} }
} }

View file

@ -33,6 +33,7 @@ class OnRamperBuyProvider extends BuyProvider {
static const quotes = '/quotes'; static const quotes = '/quotes';
static const paymentTypes = '/payment-types'; static const paymentTypes = '/payment-types';
static const supported = '/supported'; static const supported = '/supported';
static const defaultsAll = '/defaults/all';
static const List<CryptoCurrency> _notSupportedCrypto = []; static const List<CryptoCurrency> _notSupportedCrypto = [];
static const List<FiatCurrency> _notSupportedFiat = []; static const List<FiatCurrency> _notSupportedFiat = [];
@ -40,6 +41,8 @@ class OnRamperBuyProvider extends BuyProvider {
final SettingsStore _settingsStore; final SettingsStore _settingsStore;
String? recommendedPaymentType;
String get _apiKey => secrets.onramperApiKey; String get _apiKey => secrets.onramperApiKey;
@override @override
@ -57,6 +60,34 @@ class OnRamperBuyProvider extends BuyProvider {
@override @override
bool get isAggregator => true; bool get isAggregator => true;
Future<String?> getRecommendedPaymentType(bool isBuyAction) async {
final params = {'type': isBuyAction ? 'buy' : 'sell'};
final url = Uri.https(_baseApiUrl, '$supported$defaultsAll', params);
try {
final response =
await http.get(url, headers: {'Authorization': _apiKey, 'accept': 'application/json'});
if (response.statusCode == 200) {
final Map<String, dynamic> data = jsonDecode(response.body) as Map<String, dynamic>;
final recommended = data['message']['recommended'] as Map<String, dynamic>;
final recommendedPaymentType = recommended['paymentMethod'] as String?;
return recommendedPaymentType ;
} else {
final responseBody =
jsonDecode(response.body) as Map<String, dynamic>;
printV('Failed to fetch available payment types: ${responseBody['message']}');
}
} catch (e) {
printV('Failed to fetch available payment types: $e');
}
return null;
}
Future<List<PaymentMethod>> getAvailablePaymentTypes( Future<List<PaymentMethod>> getAvailablePaymentTypes(
String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async { String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async {
@ -77,9 +108,14 @@ class OnRamperBuyProvider extends BuyProvider {
if (response.statusCode == 200) { if (response.statusCode == 200) {
final Map<String, dynamic> data = jsonDecode(response.body) as Map<String, dynamic>; final Map<String, dynamic> data = jsonDecode(response.body) as Map<String, dynamic>;
final List<dynamic> message = data['message'] as List<dynamic>; final List<dynamic> message = data['message'] as List<dynamic>;
return message
final allAvailablePaymentMethods = message
.map((item) => PaymentMethod.fromOnramperJson(item as Map<String, dynamic>)) .map((item) => PaymentMethod.fromOnramperJson(item as Map<String, dynamic>))
.toList(); .toList();
recommendedPaymentType = await getRecommendedPaymentType(isBuyAction);
return allAvailablePaymentMethods;
} else { } else {
final responseBody = final responseBody =
jsonDecode(response.body) as Map<String, dynamic>; jsonDecode(response.body) as Map<String, dynamic>;
@ -131,13 +167,13 @@ class OnRamperBuyProvider extends BuyProvider {
required bool isBuyAction, required bool isBuyAction,
required String walletAddress, required String walletAddress,
PaymentType? paymentType, PaymentType? paymentType,
String? customPaymentMethodType,
String? countryCode}) async { String? countryCode}) async {
String? paymentMethod; String? paymentMethod;
if (paymentType != null && paymentType != PaymentType.all) { if (paymentType == PaymentType.all && recommendedPaymentType != null) paymentMethod = recommendedPaymentType!;
paymentMethod = normalizePaymentMethod(paymentType); else if (paymentType == PaymentType.unknown) paymentMethod = customPaymentMethodType;
if (paymentMethod == null) paymentMethod = paymentType.name; else if (paymentType != null) paymentMethod = normalizePaymentMethod(paymentType);
}
final actionType = isBuyAction ? 'buy' : 'sell'; final actionType = isBuyAction ? 'buy' : 'sell';
@ -182,7 +218,7 @@ class OnRamperBuyProvider extends BuyProvider {
if (rampMetaData == null) continue; if (rampMetaData == null) continue;
final quote = Quote.fromOnramperJson( final quote = Quote.fromOnramperJson(
item, isBuyAction, _onrampMetadata, _getPaymentTypeByString(paymentMethod)); item, isBuyAction, _onrampMetadata, _getPaymentTypeByString(paymentMethod), customPaymentMethodType);
quote.setFiatCurrency = fiatCurrency; quote.setFiatCurrency = fiatCurrency;
quote.setCryptoCurrency = cryptoCurrency; quote.setCryptoCurrency = cryptoCurrency;
validQuotes.add(quote); validQuotes.add(quote);
@ -225,7 +261,7 @@ class OnRamperBuyProvider extends BuyProvider {
final defaultCrypto = final defaultCrypto =
quote.cryptoCurrency.title + _getNormalizeNetwork(quote.cryptoCurrency).toLowerCase(); quote.cryptoCurrency.title + _getNormalizeNetwork(quote.cryptoCurrency).toLowerCase();
final paymentMethod = normalizePaymentMethod(quote.paymentType); final paymentMethod = quote.paymentType == PaymentType.unknown ? quote.customPaymentMethodType : normalizePaymentMethod(quote.paymentType);
final uri = Uri.https(_baseUrl, '', { final uri = Uri.https(_baseUrl, '', {
'apiKey': _apiKey, 'apiKey': _apiKey,
@ -330,6 +366,8 @@ class OnRamperBuyProvider extends BuyProvider {
return 'dana'; return 'dana';
case PaymentType.ideal: case PaymentType.ideal:
return 'ideal'; return 'ideal';
case PaymentType.pixPay:
return 'pix';
default: default:
return null; return null;
} }
@ -379,8 +417,10 @@ class OnRamperBuyProvider extends BuyProvider {
return PaymentType.dana; return PaymentType.dana;
case 'ideal': case 'ideal':
return PaymentType.ideal; return PaymentType.ideal;
case 'pix':
return PaymentType.pixPay;
default: default:
return PaymentType.all; return PaymentType.unknown;
} }
} }

View file

@ -34,6 +34,8 @@ enum PaymentType {
yellowCardBankTransfer, yellowCardBankTransfer,
fiatBalance, fiatBalance,
bancontact, bancontact,
pixPay,
unknown,
} }
extension PaymentTypeTitle on PaymentType { extension PaymentTypeTitle on PaymentType {
@ -101,6 +103,8 @@ extension PaymentTypeTitle on PaymentType {
return 'Fiat Balance'; return 'Fiat Balance';
case PaymentType.bancontact: case PaymentType.bancontact:
return 'Bancontact'; return 'Bancontact';
case PaymentType.pixPay:
return 'PIX Pay';
default: default:
return null; return null;
} }
@ -158,12 +162,14 @@ class PaymentMethod extends SelectableOption {
required this.customTitle, required this.customTitle,
required this.customIconPath, required this.customIconPath,
this.customDescription, this.customDescription,
this.customPaymentMethodType,
}) : super(title: paymentMethodType.title ?? customTitle); }) : super(title: paymentMethodType.title ?? customTitle);
final PaymentType paymentMethodType; final PaymentType paymentMethodType;
final String customTitle; final String customTitle;
final String customIconPath; final String customIconPath;
final String? customDescription; final String? customDescription;
final String? customPaymentMethodType;
bool isSelected = false; bool isSelected = false;
@override @override
@ -188,7 +194,8 @@ class PaymentMethod extends SelectableOption {
factory PaymentMethod.fromOnramperJson(Map<String, dynamic> json) { factory PaymentMethod.fromOnramperJson(Map<String, dynamic> json) {
final type = PaymentMethod.getPaymentTypeId(json['paymentTypeId'] as String?); final type = PaymentMethod.getPaymentTypeId(json['paymentTypeId'] as String?);
return PaymentMethod( return PaymentMethod(
paymentMethodType: type, paymentMethodType: type ?? PaymentType.unknown,
customPaymentMethodType: json['paymentTypeId'] as String?,
customTitle: json['name'] as String? ?? 'Unknown', customTitle: json['name'] as String? ?? 'Unknown',
customIconPath: json['icon'] as String? ?? 'assets/images/card.png', customIconPath: json['icon'] as String? ?? 'assets/images/card.png',
customDescription: json['description'] as String?); customDescription: json['description'] as String?);
@ -212,7 +219,7 @@ class PaymentMethod extends SelectableOption {
final type = PaymentMethod.getPaymentTypeId(json['paymentMethod'] as String?); final type = PaymentMethod.getPaymentTypeId(json['paymentMethod'] as String?);
final logos = json['logos'] as Map<String, dynamic>; final logos = json['logos'] as Map<String, dynamic>;
return PaymentMethod( return PaymentMethod(
paymentMethodType: type, paymentMethodType: type ?? PaymentType.unknown,
customTitle: json['name'] as String? ?? 'Unknown', customTitle: json['name'] as String? ?? 'Unknown',
customIconPath: logos['dark'] as String? ?? 'assets/images/card.png', customIconPath: logos['dark'] as String? ?? 'assets/images/card.png',
customDescription: json['description'] as String?); customDescription: json['description'] as String?);
@ -221,13 +228,13 @@ class PaymentMethod extends SelectableOption {
factory PaymentMethod.fromKryptonimJson(Map<String, dynamic> json) { factory PaymentMethod.fromKryptonimJson(Map<String, dynamic> json) {
final type = PaymentMethod.getPaymentTypeId(json['payment_method'] as String?); final type = PaymentMethod.getPaymentTypeId(json['payment_method'] as String?);
return PaymentMethod( return PaymentMethod(
paymentMethodType: type, paymentMethodType: type ?? PaymentType.unknown,
customTitle: json['payment_method'] as String? ?? 'Unknown', customTitle: json['payment_method'] as String? ?? 'Unknown',
customIconPath: 'assets/images/card.png', customIconPath: 'assets/images/card.png',
); );
} }
static PaymentType getPaymentTypeId(String? type) { static PaymentType? getPaymentTypeId(String? type) {
switch (type?.toLowerCase()) { switch (type?.toLowerCase()) {
case 'banktransfer': case 'banktransfer':
case 'bank': case 'bank':
@ -289,8 +296,10 @@ class PaymentMethod extends SelectableOption {
return PaymentType.sepaOpenBankingPayment; return PaymentType.sepaOpenBankingPayment;
case 'bancontact': case 'bancontact':
return PaymentType.bancontact; return PaymentType.bancontact;
case 'pix':
return PaymentType.pixPay;
default: default:
return PaymentType.all; return null;
} }
} }
} }

View file

@ -192,6 +192,7 @@ class RobinhoodBuyProvider extends BuyProvider {
required bool isBuyAction, required bool isBuyAction,
required String walletAddress, required String walletAddress,
PaymentType? paymentType, PaymentType? paymentType,
String? customPaymentMethodType,
String? countryCode}) async { String? countryCode}) async {
String? paymentMethod; String? paymentMethod;
@ -267,7 +268,7 @@ class RobinhoodBuyProvider extends BuyProvider {
case 'bank_transfer': case 'bank_transfer':
return PaymentType.bankTransfer; return PaymentType.bankTransfer;
default: default:
return PaymentType.all; return PaymentType.unknown;
} }
} }
} }

View file

@ -359,14 +359,23 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
onTimeout: () => [], onTimeout: () => [],
))); )));
final Map<PaymentType, PaymentMethod> uniquePaymentMethods = {}; final List<PaymentMethod> tempPaymentMethods = [];
for (var methods in result) { for (var methods in result) {
for (var method in methods) { for (var method in methods) {
uniquePaymentMethods[method.paymentMethodType] = method; final alreadyExists = tempPaymentMethods.any((m) {
return m.paymentMethodType == method.paymentMethodType &&
m.customTitle == method.customTitle;
});
if (!alreadyExists) {
tempPaymentMethods.add(method);
}
} }
} }
paymentMethods = ObservableList<PaymentMethod>.of(uniquePaymentMethods.values); paymentMethods = ObservableList<PaymentMethod>.of(tempPaymentMethods);
if (paymentMethods.isNotEmpty) { if (paymentMethods.isNotEmpty) {
paymentMethods.insert(0, PaymentMethod.all()); paymentMethods.insert(0, PaymentMethod.all());
selectedPaymentMethod = paymentMethods.first; selectedPaymentMethod = paymentMethods.first;
@ -404,6 +413,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
paymentType: selectedPaymentMethod?.paymentMethodType, paymentType: selectedPaymentMethod?.paymentMethodType,
isBuyAction: isBuyAction, isBuyAction: isBuyAction,
walletAddress: wallet.walletAddresses.address, walletAddress: wallet.walletAddresses.address,
customPaymentMethodType: selectedPaymentMethod?.customPaymentMethodType,
) )
.timeout( .timeout(
Duration(seconds: 10), Duration(seconds: 10),