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 String walletAddress,
PaymentType? paymentType,
String? customPaymentMethodType,
String? countryCode}) async =>
null;
}

View file

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

View file

@ -231,6 +231,7 @@ class DFXBuyProvider extends BuyProvider {
required bool isBuyAction,
required String walletAddress,
PaymentType? paymentType,
String? customPaymentMethodType,
String? countryCode}) async {
/// 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':
return PaymentType.sepa;
default:
return PaymentType.all;
return PaymentType.unknown;
}
}

View file

@ -113,6 +113,7 @@ class KryptonimBuyProvider extends BuyProvider {
required bool isBuyAction,
required String walletAddress,
PaymentType? paymentType,
String? customPaymentMethodType,
String? countryCode,
}) async {
log('Kryptonim: Fetching quote: ${isBuyAction ? cryptoCurrency : fiatCurrency} -> ${isBuyAction ? fiatCurrency : cryptoCurrency}, amount: $amount');
@ -149,7 +150,7 @@ class KryptonimBuyProvider extends BuyProvider {
final selectedPaymentType =
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.setCryptoCurrency = cryptoCurrency;

View file

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

View file

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

View file

@ -33,6 +33,7 @@ class OnRamperBuyProvider extends BuyProvider {
static const quotes = '/quotes';
static const paymentTypes = '/payment-types';
static const supported = '/supported';
static const defaultsAll = '/defaults/all';
static const List<CryptoCurrency> _notSupportedCrypto = [];
static const List<FiatCurrency> _notSupportedFiat = [];
@ -40,6 +41,8 @@ class OnRamperBuyProvider extends BuyProvider {
final SettingsStore _settingsStore;
String? recommendedPaymentType;
String get _apiKey => secrets.onramperApiKey;
@override
@ -57,6 +60,34 @@ class OnRamperBuyProvider extends BuyProvider {
@override
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(
String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async {
@ -77,9 +108,14 @@ class OnRamperBuyProvider extends BuyProvider {
if (response.statusCode == 200) {
final Map<String, dynamic> data = jsonDecode(response.body) as Map<String, dynamic>;
final List<dynamic> message = data['message'] as List<dynamic>;
return message
final allAvailablePaymentMethods = message
.map((item) => PaymentMethod.fromOnramperJson(item as Map<String, dynamic>))
.toList();
recommendedPaymentType = await getRecommendedPaymentType(isBuyAction);
return allAvailablePaymentMethods;
} else {
final responseBody =
jsonDecode(response.body) as Map<String, dynamic>;
@ -131,13 +167,13 @@ class OnRamperBuyProvider extends BuyProvider {
required bool isBuyAction,
required String walletAddress,
PaymentType? paymentType,
String? customPaymentMethodType,
String? countryCode}) async {
String? paymentMethod;
if (paymentType != null && paymentType != PaymentType.all) {
paymentMethod = normalizePaymentMethod(paymentType);
if (paymentMethod == null) paymentMethod = paymentType.name;
}
if (paymentType == PaymentType.all && recommendedPaymentType != null) paymentMethod = recommendedPaymentType!;
else if (paymentType == PaymentType.unknown) paymentMethod = customPaymentMethodType;
else if (paymentType != null) paymentMethod = normalizePaymentMethod(paymentType);
final actionType = isBuyAction ? 'buy' : 'sell';
@ -182,7 +218,7 @@ class OnRamperBuyProvider extends BuyProvider {
if (rampMetaData == null) continue;
final quote = Quote.fromOnramperJson(
item, isBuyAction, _onrampMetadata, _getPaymentTypeByString(paymentMethod));
item, isBuyAction, _onrampMetadata, _getPaymentTypeByString(paymentMethod), customPaymentMethodType);
quote.setFiatCurrency = fiatCurrency;
quote.setCryptoCurrency = cryptoCurrency;
validQuotes.add(quote);
@ -225,7 +261,7 @@ class OnRamperBuyProvider extends BuyProvider {
final defaultCrypto =
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, '', {
'apiKey': _apiKey,
@ -330,6 +366,8 @@ class OnRamperBuyProvider extends BuyProvider {
return 'dana';
case PaymentType.ideal:
return 'ideal';
case PaymentType.pixPay:
return 'pix';
default:
return null;
}
@ -379,8 +417,10 @@ class OnRamperBuyProvider extends BuyProvider {
return PaymentType.dana;
case 'ideal':
return PaymentType.ideal;
case 'pix':
return PaymentType.pixPay;
default:
return PaymentType.all;
return PaymentType.unknown;
}
}

View file

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

View file

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

View file

@ -359,14 +359,23 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
onTimeout: () => [],
)));
final Map<PaymentType, PaymentMethod> uniquePaymentMethods = {};
final List<PaymentMethod> tempPaymentMethods = [];
for (var methods in result) {
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) {
paymentMethods.insert(0, PaymentMethod.all());
selectedPaymentMethod = paymentMethods.first;
@ -404,6 +413,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
paymentType: selectedPaymentMethod?.paymentMethodType,
isBuyAction: isBuyAction,
walletAddress: wallet.walletAddresses.address,
customPaymentMethodType: selectedPaymentMethod?.customPaymentMethodType,
)
.timeout(
Duration(seconds: 10),