diff --git a/assets/web_pages/checkout_status_redirect.html b/assets/web_pages/checkout_status_redirect.html
index d9aa2631c1..fdb69020fc 100644
--- a/assets/web_pages/checkout_status_redirect.html
+++ b/assets/web_pages/checkout_status_redirect.html
@@ -1,48 +1,45 @@
+
Komodo Payment Redirect
+
+
\ No newline at end of file
diff --git a/assets/web_pages/fiat_widget.html b/assets/web_pages/fiat_widget.html
index a42d14db13..67427c2307 100644
--- a/assets/web_pages/fiat_widget.html
+++ b/assets/web_pages/fiat_widget.html
@@ -2,109 +2,129 @@
- Fiat OnRamp
-
-
+ Fiat OnRamp
+
+
+
-
-
+ }
+
+ if (targetUrl) {
+ document.getElementById('fiat-onramp-iframe').src = targetUrl;
+ } else {
+ console.error('No URL parameter provided');
+ }
+ }
+
+ /**
+ * Get URL parameter by name
+ *
+ * @param {string} name - The name of the URL parameter to retrieve
+ * @returns {string|null} - The value of the URL parameter or null if not found
+ */
+ function _komodoGetUrlParameter(name) {
+ const params = new URLSearchParams(window.location.search);
+ return params.get(name);
+ }
+
+ /**
+ * Handle messages from the iframe
+ *
+ * @param {MessageEvent} messageEvent
+ */
+ function _komodoOnMessageHandler(messageEvent) {
+ let messageData;
+ try {
+ messageData = typeof messageEvent.data === 'string' ? JSON.parse(messageEvent.data) : messageEvent.data;
+ } catch (parseError) {
+ messageData = messageEvent.data;
+ }
+
+ try {
+ _komodoPostMessageToParent(messageData);
+ } catch (postError) {
+ console.error('Error posting message', postError);
+ }
+ }
+
+ /**
+ * Post a message to the parent window
+ *
+ * @param {string|object} messageData
+ */
+ function _komodoPostMessageToParent(messageData) {
+ const messageString = (typeof messageData === 'object') ? JSON.stringify(messageData) : String(messageData);
+
+ // flutter_inappwebview
+ console.log(messageString);
+
+ // universal_url opener
+ if (window.opener) {
+ return window.opener.postMessage(messageString, "*");
+ }
+
+ if (window.parent && window.parent !== window) {
+ return window.parent.postMessage(messageString, "*");
+ }
+
+ // Windows WebView2 (desktop_webview_window)
+ // https://learn.microsoft.com/en-us/microsoft-edge/webview2/how-to/communicate-btwn-web-native
+ if (window.chrome && window.chrome.webview) {
+ return window.chrome.webview.postMessage(messageString);
+ }
+
+ console.error('No valid postMessage target found');
+ }
+
\ No newline at end of file
diff --git a/lib/app_config/app_config.dart b/lib/app_config/app_config.dart
index 7e2aa67527..e7bcbbf7a9 100644
--- a/lib/app_config/app_config.dart
+++ b/lib/app_config/app_config.dart
@@ -99,6 +99,24 @@ const List excludedAssetListTrezor = [
'VAL',
];
+/// Some coins returned by the Banxa API are returning errors when attempting
+/// to create an order. This is a temporary workaround to filter out those coins
+/// until the issue is resolved.
+const banxaUnsupportedCoinsList = [
+ 'APE', // chain not configured for APE
+ 'AVAX', // avax & bep20 - invalid wallet address error
+ 'DOT', // bep20 - invalid wallet address error
+ 'FIL', // bep20 - invalid wallet address error
+ 'ONE', // invalid wallet address error (one**** (native) format expected)
+ 'TON', // erc20 - invalid wallet address error
+ 'TRX', // bep20 - invalid wallet address error
+ 'XML', // invalid wallet address error
+];
+
+const rampUnsupportedCoinsList = [
+ 'ONE', // invalid wallet address error (one**** format expected)
+];
+
// Assets in wallet-only mode on app level,
// global wallet-only assets are defined in coins config files.
const List appWalletOnlyAssetList = [
diff --git a/lib/bloc/fiat/banxa_fiat_provider.dart b/lib/bloc/fiat/banxa_fiat_provider.dart
index bd6e8cc018..27c79fd05e 100644
--- a/lib/bloc/fiat/banxa_fiat_provider.dart
+++ b/lib/bloc/fiat/banxa_fiat_provider.dart
@@ -1,5 +1,6 @@
import 'dart:convert';
+import 'package:decimal/decimal.dart';
import 'package:logging/logging.dart';
import 'package:web_dex/app_config/app_config.dart';
import 'package:web_dex/bloc/fiat/base_fiat_provider.dart';
@@ -24,7 +25,7 @@ class BanxaFiatProvider extends BaseFiatProvider {
FiatOrderStatus _parseStatusFromResponse(Map response) {
final statusString = response['data']?['order']?['status'] as String?;
- return _parseOrderStatus(statusString ?? '');
+ return FiatOrderStatus.fromString(statusString ?? '');
}
Future _getPaymentMethods(
@@ -96,34 +97,6 @@ class BanxaFiatProvider extends BaseFiatProvider {
},
);
- FiatOrderStatus _parseOrderStatus(String status) {
- // The case statements are references to Banxa's order statuses. See the
- // docs link here for more info: https://docs.banxa.com/docs/order-status
- switch (status) {
- case 'complete':
- return FiatOrderStatus.success;
-
- case 'cancelled':
- case 'declined':
- case 'expired':
- case 'refunded':
- return FiatOrderStatus.failed;
-
- case 'extraVerification':
- case 'pendingPayment':
- case 'waitingPayment':
- return FiatOrderStatus.pending;
-
- case 'paymentReceived':
- case 'inProgress':
- case 'coinTransferred':
- return FiatOrderStatus.inProgress;
-
- default:
- throw Exception('Unknown status: $status');
- }
- }
-
// These will be in BLOC:
@override
Stream watchOrderStatus(String orderId) async* {
@@ -162,8 +135,9 @@ class BanxaFiatProvider extends BaseFiatProvider {
return data
.map(
(item) => FiatCurrency(
- item['fiat_code'] as String,
- item['fiat_name'] as String,
+ symbol: item['fiat_code'] as String,
+ name: item['fiat_name'] as String,
+ minPurchaseAmount: Decimal.zero,
),
)
.toList();
@@ -171,12 +145,19 @@ class BanxaFiatProvider extends BaseFiatProvider {
@override
Future> getCoinList() async {
+ // TODO: add model classes to parse responses like these when migrating to
+ // the SDK
final response = await _getCoins();
final data = response['data']['coins'] as List;
final List currencyList = [];
for (final item in data) {
final coinCode = item['coin_code'] as String;
+ if (banxaUnsupportedCoinsList.contains(coinCode)) {
+ _log.warning('Banxa does not support $coinCode');
+ continue;
+ }
+
final coinName = item['coin_name'] as String;
final blockchains = item['blockchains'] as List;
@@ -186,11 +167,32 @@ class BanxaFiatProvider extends BaseFiatProvider {
continue;
}
+ // Parse min_value which can be a string, int, or double
+ final dynamic minValue = blockchain['min_value'];
+ Decimal minPurchaseAmount;
+
+ if (minValue == null) {
+ minPurchaseAmount = Decimal.fromInt(0);
+ } else if (minValue is String) {
+ minPurchaseAmount = Decimal.fromJson(minValue);
+ } else if (minValue is int) {
+ minPurchaseAmount = Decimal.fromInt(minValue);
+ } else if (minValue is double) {
+ minPurchaseAmount = Decimal.parse(minValue.toString());
+ } else {
+ // Default to zero for any other unexpected types
+ minPurchaseAmount = Decimal.fromInt(0);
+ _log.warning(
+ 'Unexpected type for min_value: ${minValue.runtimeType}',
+ );
+ }
+
currencyList.add(
CryptoCurrency(
- coinCode,
- coinName,
- coinType,
+ symbol: coinCode,
+ name: coinName,
+ chainType: coinType,
+ minPurchaseAmount: minPurchaseAmount,
),
);
}
@@ -206,6 +208,11 @@ class BanxaFiatProvider extends BaseFiatProvider {
String sourceAmount,
) async {
try {
+ if (banxaUnsupportedCoinsList.contains(target.configSymbol)) {
+ _log.warning('Banxa does not support ${target.configSymbol}');
+ return [];
+ }
+
final response =
await _getPaymentMethods(source, target, sourceAmount: sourceAmount);
final List paymentMethods = (response['data']
diff --git a/lib/bloc/fiat/base_fiat_provider.dart b/lib/bloc/fiat/base_fiat_provider.dart
index 40c1cc745d..f9c34968ab 100644
--- a/lib/bloc/fiat/base_fiat_provider.dart
+++ b/lib/bloc/fiat/base_fiat_provider.dart
@@ -58,11 +58,6 @@ abstract class BaseFiatProvider {
final domainUri = Uri.parse(domain);
Uri url;
- // Remove the leading '/' if it exists in /api/fiats kind of an endpoint
- if (endpoint.startsWith('/')) {
- endpoint = endpoint.substring(1);
- }
-
// Add `is_test_mode` query param to all requests if we are in debug mode
final passedQueryParams = {}
..addAll(queryParams ?? {})
@@ -73,7 +68,8 @@ abstract class BaseFiatProvider {
url = Uri(
scheme: domainUri.scheme,
host: domainUri.host,
- path: endpoint,
+ // Remove the leading '/' if it exists in /api/fiats kind of an endpoint
+ path: endpoint.startsWith('/') ? endpoint.substring(1) : endpoint,
query: Uri(queryParameters: passedQueryParams).query,
);
@@ -98,7 +94,13 @@ abstract class BaseFiatProvider {
return json.decode(response.body);
} else {
_log.warning('Request failed with status: ${response.statusCode}');
- return Future.error(json.decode(response.body) as Object);
+ dynamic decoded;
+ try {
+ decoded = json.decode(response.body);
+ } catch (_) {
+ decoded = response.body;
+ }
+ return Future.error(decoded as Object);
}
} catch (e, s) {
_log.severe('Network error', e, s);
@@ -132,7 +134,6 @@ abstract class BaseFiatProvider {
return 'MATIC';
case CoinType.mvr20:
return 'MOVR';
- // ignore: no_default_cases
default:
return null;
}
@@ -234,6 +235,7 @@ abstract class BaseFiatProvider {
return CoinType.etc;
case 'FTM':
return CoinType.ftm20;
+ case 'ARBITRUM':
case 'ARB':
return CoinType.arb20;
case 'HARMONY':
diff --git a/lib/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart b/lib/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart
index b0d8ba3bf8..1712098fc2 100644
--- a/lib/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart
+++ b/lib/bloc/fiat/fiat_onramp_form/fiat_form_bloc.dart
@@ -1,10 +1,12 @@
import 'dart:convert';
+import 'dart:io' show Platform;
import 'package:bloc/bloc.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:collection/collection.dart';
import 'package:decimal/decimal.dart';
import 'package:equatable/equatable.dart';
+import 'package:flutter/foundation.dart';
import 'package:formz/formz.dart';
import 'package:komodo_defi_sdk/komodo_defi_sdk.dart';
import 'package:komodo_defi_types/komodo_defi_types.dart';
@@ -15,10 +17,10 @@ import 'package:web_dex/bloc/fiat/fiat_order_status.dart';
import 'package:web_dex/bloc/fiat/fiat_repository.dart';
import 'package:web_dex/bloc/fiat/models/models.dart';
import 'package:web_dex/bloc/fiat/payment_status_type.dart';
-import 'package:web_dex/model/coin_type.dart';
import 'package:web_dex/model/forms/fiat/currency_input.dart';
import 'package:web_dex/model/forms/fiat/fiat_amount_input.dart';
import 'package:web_dex/shared/utils/extensions/string_extensions.dart';
+import 'package:web_dex/views/fiat/webview_dialog.dart' show WebViewDialogMode;
part 'fiat_form_event.dart';
part 'fiat_form_state.dart';
@@ -43,8 +45,8 @@ class FiatFormBloc extends Bloc {
on(_onWebViewClosed);
on(_onAssetAddressUpdated);
- // debounce used here instead of restartable, since multiple user actions
- // can trigger this event, and restartable results in hitching
+ // transformer used here to restart the stream when a new event is added
+ // (i.e. from user input).
on(_onRefreshForm, transformer: restartable());
on(
_onLoadCurrencyLists,
@@ -90,7 +92,7 @@ class FiatFormBloc extends Bloc {
try {
if (!await _sdk.auth.isSignedIn()) {
- return emit(state.copyWith(selectedAssetAddress: null));
+ return emit(state.copyWith(selectedAssetAddress: () => null));
}
final asset = event.selectedCoin.toAsset(_sdk);
@@ -99,13 +101,13 @@ class FiatFormBloc extends Bloc {
emit(
state.copyWith(
- selectedAssetAddress: address,
- selectedCoinPubkeys: assetPubkeys,
+ selectedAssetAddress: address != null ? () => address : null,
+ selectedCoinPubkeys: () => assetPubkeys,
),
);
} catch (e, s) {
_log.shout('Error getting pubkeys for selected coin', e, s);
- emit(state.copyWith(selectedAssetAddress: null));
+ emit(state.copyWith(selectedAssetAddress: () => null));
}
}
@@ -129,7 +131,7 @@ class FiatFormBloc extends Bloc {
state.fiatAmount.value,
selectedPaymentMethod: event.paymentMethod,
),
- fiatOrderStatus: FiatOrderStatus.pending,
+ fiatOrderStatus: FiatOrderStatus.initial,
status: FiatFormStatus.initial,
),
);
@@ -151,13 +153,14 @@ class FiatFormBloc extends Bloc {
try {
final newOrder = await _fiatRepository.buyCoin(
- state.selectedAssetAddress!.address,
- state.selectedFiat.value!.symbol,
- state.selectedAsset.value!,
- state.selectedAssetAddress!.address,
- state.selectedPaymentMethod,
- state.fiatAmount.value,
- BaseFiatProvider.successUrl(state.selectedAssetAddress!.address),
+ accountReference: state.selectedAssetAddress!.address,
+ source: state.selectedFiat.value!.getAbbr(),
+ target: state.selectedAsset.value!,
+ walletAddress: state.selectedAssetAddress!.address,
+ paymentMethod: state.selectedPaymentMethod,
+ sourceAmount: state.fiatAmount.value,
+ returnUrlOnSuccess:
+ BaseFiatProvider.successUrl(state.selectedAssetAddress!.address),
);
if (!newOrder.error.isNone) {
@@ -165,7 +168,7 @@ class FiatFormBloc extends Bloc {
return emit(_parseOrderError(newOrder.error));
}
- final checkoutUrl = newOrder.checkoutUrl as String? ?? '';
+ var checkoutUrl = newOrder.checkoutUrl as String? ?? '';
if (checkoutUrl.isEmpty) {
_log.severe('Invalid checkout URL received.');
return emit(
@@ -175,12 +178,19 @@ class FiatFormBloc extends Bloc {
);
}
+ // Only Ramp on web requires the intermediate html page to satisfy cors
+ // rules and allow for console.log and postMessage events to be handled.
+ // Banxa does not use `postMessage` and does not require this.
+ checkoutUrl = BaseFiatProvider.fiatWrapperPageUrl(checkoutUrl);
+ final webViewMode = _determineWebViewMode();
+
emit(
state.copyWith(
checkoutUrl: checkoutUrl,
orderId: newOrder.id,
status: FiatFormStatus.success,
fiatOrderStatus: FiatOrderStatus.submitted,
+ webViewMode: webViewMode,
),
);
} catch (e, s) {
@@ -194,6 +204,25 @@ class FiatFormBloc extends Bloc {
}
}
+ /// Determines the appropriate WebViewDialogMode based on platform and
+ /// environment
+ WebViewDialogMode _determineWebViewMode() {
+ final bool isLinux = !kIsWeb && !kIsWasm && Platform.isLinux;
+ const bool isWeb = kIsWeb || kIsWasm;
+ final bool isBanxa = state.selectedPaymentMethod.providerId == 'Banxa';
+
+ // Banxa "Return to Komodo" button attempts to navigate the top window to
+ // the return URL, which is not supported in a dialog. So we need to open
+ // it in a new tab.
+ if (isLinux || (isWeb && isBanxa)) {
+ return WebViewDialogMode.newTab;
+ } else if (isWeb) {
+ return WebViewDialogMode.dialog;
+ } else {
+ return WebViewDialogMode.fullscreen;
+ }
+ }
+
Future _onRefreshForm(
FiatFormRefreshed event,
Emitter emit,
@@ -219,14 +248,14 @@ class FiatFormBloc extends Bloc {
return state;
}
- final asset =
- _sdk.getSdkAsset(state.selectedAsset.value?.symbol ?? 'BTC-segwit');
+ final asset = _sdk
+ .getSdkAsset(state.selectedAsset.value?.getAbbr() ?? 'BTC-segwit');
final pubkeys = await _sdk.pubkeys.getPubkeys(asset);
final address = pubkeys.keys.firstOrNull;
return state.copyWith(
- selectedAssetAddress: address,
- selectedCoinPubkeys: pubkeys,
+ selectedAssetAddress: address != null ? () => address : null,
+ selectedCoinPubkeys: () => pubkeys,
);
} catch (e, s) {
if (attempts >= maxRetries) {
@@ -235,7 +264,9 @@ class FiatFormBloc extends Bloc {
e,
s,
);
- return state.copyWith(selectedAssetAddress: null);
+ if (state.selectedAssetAddress == null) {
+ return state.copyWith(selectedAssetAddress: () => null);
+ }
}
_log.warning(
@@ -244,11 +275,11 @@ class FiatFormBloc extends Bloc {
s,
);
- await Future.delayed(Duration(milliseconds: 500 * attempts));
+ await Future.delayed(Duration(milliseconds: 500 * attempts));
}
}
- return state.copyWith(selectedAssetAddress: null);
+ return state.copyWith(selectedAssetAddress: () => null);
}
void _onPaymentStatusMessage(
@@ -274,15 +305,15 @@ class FiatFormBloc extends Bloc {
case PaymentStatusType.widgetClose:
case PaymentStatusType.widgetCloseRequest:
updatedStatus = FiatOrderStatus.windowCloseRequested;
- break;
case PaymentStatusType.purchaseCreated:
updatedStatus = FiatOrderStatus.inProgress;
- break;
case PaymentStatusType.paymentStatus:
final status = data['status'] as String? ?? 'declined';
updatedStatus = FiatOrderStatus.fromString(status);
- break;
- default:
+ case PaymentStatusType.widgetConfigFailed:
+ case PaymentStatusType.widgetConfigDone:
+ case PaymentStatusType.widgetCloseRequestCancelled:
+ case PaymentStatusType.offrampSaleCreated:
break;
}
@@ -300,7 +331,7 @@ class FiatFormBloc extends Bloc {
) {
emit(
state.copyWith(
- selectedAssetAddress: event.address,
+ selectedAssetAddress: () => event.address,
),
);
}
@@ -322,11 +353,13 @@ class FiatFormBloc extends Bloc {
emit(state.copyWith(fiatList: fiatList, coinList: coinList));
} catch (e, s) {
_log.shout('Error loading currency list', e, s);
- emit(state.copyWith(
- fiatList: [],
- coinList: [],
- status: FiatFormStatus.failure,
- ));
+ emit(
+ state.copyWith(
+ fiatList: [],
+ coinList: [],
+ status: FiatFormStatus.failure,
+ ),
+ );
}
}
@@ -368,13 +401,16 @@ class FiatFormBloc extends Bloc {
// to allow the user to submit another order
if (state.fiatOrderStatus != FiatOrderStatus.inProgress) {
_log.info('WebView closed, resetting order status to pending');
- emit(state.copyWith(
- fiatOrderStatus: FiatOrderStatus.pending,
- checkoutUrl: '',
- ));
+ emit(
+ state.copyWith(
+ fiatOrderStatus: FiatOrderStatus.initial,
+ checkoutUrl: '',
+ ),
+ );
} else {
_log.info(
- 'WebView closed, but order is in progress. Keeping current status.');
+ 'WebView closed, but order is in progress. Keeping current status.',
+ );
}
}
@@ -382,7 +418,9 @@ class FiatFormBloc extends Bloc {
FiatFormAssetAddressUpdated event,
Emitter emit,
) {
- emit(state.copyWith(selectedAssetAddress: event.selectedAssetAddress));
+ emit(
+ state.copyWith(selectedAssetAddress: () => event.selectedAssetAddress),
+ );
}
FiatFormState _parseOrderError(FiatBuyOrderError error) {
@@ -394,6 +432,7 @@ class FiatFormBloc extends Bloc {
checkoutUrl: '',
status: FiatFormStatus.failure,
fiatOrderStatus: FiatOrderStatus.failed,
+ providerError: () => error.title,
);
}
@@ -423,6 +462,22 @@ class FiatFormBloc extends Bloc {
maxAmount = Decimal.tryParse(firstLimit.max.toString());
}
+ // Use the minimum transaction amount provided by Ramp and Banxa per coin
+ // to determine the minimum amount that can be purchased. The payment
+ // method list provides a minimum amount for the fiat currency, but this is
+ // not always the same as the minimum amount for the coin.
+ final coinAmount = paymentMethod.priceInfo.coinAmount;
+ final fiatAmount = paymentMethod.priceInfo.fiatAmount;
+ final minPurchaseAmount =
+ state.selectedAsset.value?.minPurchaseAmount ?? Decimal.zero;
+ if (coinAmount < minPurchaseAmount && coinAmount > Decimal.zero) {
+ final minFiatAmount = ((minPurchaseAmount * fiatAmount) / coinAmount)
+ .toDecimal(scaleOnInfinitePrecision: 18);
+ minAmount = minAmount != null && minAmount > minFiatAmount
+ ? minAmount
+ : minFiatAmount;
+ }
+
return FiatAmountInput.dirty(
amount,
minValue: minAmount,
@@ -435,6 +490,7 @@ class FiatFormBloc extends Bloc {
yield state.copyWith(
fiatAmount: _getAmountInputWithBounds(state.fiatAmount.value),
+ providerError: () => null,
);
try {
@@ -445,6 +501,7 @@ class FiatFormBloc extends Bloc {
yield state.copyWith(
paymentMethods: [],
status: FiatFormStatus.failure,
+ providerError: () => null,
);
}
}
@@ -453,7 +510,8 @@ class FiatFormBloc extends Bloc {
if (_hasValidFiatAmount()) {
yield state.copyWith(
status: FiatFormStatus.loading,
- fiatOrderStatus: FiatOrderStatus.pending,
+ fiatOrderStatus: FiatOrderStatus.initial,
+ providerError: () => null,
);
} else {
yield _defaultPaymentMethods();
@@ -462,7 +520,7 @@ class FiatFormBloc extends Bloc {
Stream _fetchAndUpdatePaymentMethods() async* {
final methodsStream = _fiatRepository.getPaymentMethodsList(
- state.selectedFiat.value!.symbol,
+ state.selectedFiat.value!.getAbbr(),
state.selectedAsset.value!,
_getSourceAmount(),
);
@@ -476,6 +534,7 @@ class FiatFormBloc extends Bloc {
yield state.copyWith(
paymentMethods: [],
status: FiatFormStatus.failure,
+ providerError: () => null,
);
}
}
@@ -507,6 +566,7 @@ class FiatFormBloc extends Bloc {
paymentMethods: methods,
selectedPaymentMethod: method,
status: FiatFormStatus.success,
+ providerError: () => null,
fiatAmount: _getAmountInputWithBounds(
state.fiatAmount.value,
selectedPaymentMethod: method,
@@ -514,10 +574,16 @@ class FiatFormBloc extends Bloc {
);
}
- return state.copyWith(status: FiatFormStatus.success);
+ return state.copyWith(
+ status: FiatFormStatus.success,
+ providerError: () => null,
+ );
} catch (e, s) {
_log.shout('Error updating payment methods', e, s);
- return state.copyWith(paymentMethods: []);
+ return state.copyWith(
+ paymentMethods: [],
+ providerError: () => null,
+ );
}
}
@@ -526,7 +592,8 @@ class FiatFormBloc extends Bloc {
paymentMethods: defaultFiatPaymentMethods,
selectedPaymentMethod: defaultFiatPaymentMethods.first,
status: FiatFormStatus.initial,
- fiatOrderStatus: FiatOrderStatus.pending,
+ fiatOrderStatus: FiatOrderStatus.initial,
+ providerError: () => null,
);
}
}
diff --git a/lib/bloc/fiat/fiat_onramp_form/fiat_form_state.dart b/lib/bloc/fiat/fiat_onramp_form/fiat_form_state.dart
index a736470ee2..2c266731e1 100644
--- a/lib/bloc/fiat/fiat_onramp_form/fiat_form_state.dart
+++ b/lib/bloc/fiat/fiat_onramp_form/fiat_form_state.dart
@@ -18,20 +18,18 @@ final class FiatFormState extends Equatable with FormzMixin {
required this.fiatList,
required this.coinList,
this.status = FiatFormStatus.initial,
- this.fiatOrderStatus = FiatOrderStatus.pending,
+ this.fiatOrderStatus = FiatOrderStatus.initial,
this.fiatMode = FiatMode.onramp,
this.selectedAssetAddress,
this.selectedCoinPubkeys,
+ this.webViewMode = WebViewDialogMode.fullscreen,
+ this.providerError,
});
/// Creates an initial state with default values.
FiatFormState.initial()
- : selectedFiat = const CurrencyInput.dirty(
- FiatCurrency('USD', 'United States Dollar'),
- ),
- selectedAsset = const CurrencyInput.dirty(
- CryptoCurrency('BTC-segwit', 'Bitcoin', CoinType.utxo),
- ),
+ : selectedFiat = CurrencyInput.dirty(FiatCurrency.usd()),
+ selectedAsset = CurrencyInput.dirty(CryptoCurrency.bitcoin()),
fiatAmount = const FiatAmountInput.pure(),
selectedAssetAddress = null,
selectedPaymentMethod = FiatPaymentMethod.none,
@@ -41,9 +39,11 @@ final class FiatFormState extends Equatable with FormzMixin {
paymentMethods = const [],
fiatList = const [],
coinList = const [],
- fiatOrderStatus = FiatOrderStatus.pending,
+ fiatOrderStatus = FiatOrderStatus.initial,
fiatMode = FiatMode.onramp,
- selectedCoinPubkeys = null;
+ selectedCoinPubkeys = null,
+ webViewMode = WebViewDialogMode.fullscreen,
+ providerError = null;
/// The selected fiat currency to use to purchase [selectedAsset].
final CurrencyInput selectedFiat;
@@ -90,15 +90,21 @@ final class FiatFormState extends Equatable with FormzMixin {
/// once the order history tab is implemented
final FiatMode fiatMode;
+ /// The mode to use for displaying the WebView dialog
+ final WebViewDialogMode webViewMode;
+
+ /// Raw error message from the provider when there is an order error
+ final String? providerError;
+
/// Gets the transaction limit from the selected payment method
FiatTransactionLimit? get transactionLimit =>
selectedPaymentMethod.transactionLimits.firstOrNull;
/// The minimum fiat amount that is allowed for the selected payment method
- Decimal? get minFiatAmount => transactionLimit?.min;
+ Decimal? get minFiatAmount => fiatAmount.minValue ?? transactionLimit?.min;
/// The maximum fiat amount that is allowed for the selected payment method
- Decimal? get maxFiatAmount => transactionLimit?.max;
+ Decimal? get maxFiatAmount => fiatAmount.maxValue ?? transactionLimit?.max;
/// Whether currencies are still being loaded
bool get isLoadingCurrencies => fiatList.length < 2 || coinList.length < 2;
@@ -119,7 +125,7 @@ final class FiatFormState extends Equatable with FormzMixin {
CurrencyInput? selectedAsset,
FiatAmountInput? fiatAmount,
FiatPaymentMethod? selectedPaymentMethod,
- PubkeyInfo? selectedAssetAddress,
+ ValueGetter? selectedAssetAddress,
String? checkoutUrl,
String? orderId,
FiatFormStatus? status,
@@ -128,14 +134,18 @@ final class FiatFormState extends Equatable with FormzMixin {
Iterable? coinList,
FiatOrderStatus? fiatOrderStatus,
FiatMode? fiatMode,
- AssetPubkeys? selectedCoinPubkeys,
+ ValueGetter? selectedCoinPubkeys,
+ WebViewDialogMode? webViewMode,
+ ValueGetter? providerError,
}) {
return FiatFormState(
selectedFiat: selectedFiat ?? this.selectedFiat,
selectedAsset: selectedAsset ?? this.selectedAsset,
selectedPaymentMethod:
selectedPaymentMethod ?? this.selectedPaymentMethod,
- selectedAssetAddress: selectedAssetAddress ?? this.selectedAssetAddress,
+ selectedAssetAddress: selectedAssetAddress != null
+ ? selectedAssetAddress()
+ : this.selectedAssetAddress,
checkoutUrl: checkoutUrl ?? this.checkoutUrl,
orderId: orderId ?? this.orderId,
fiatAmount: fiatAmount ?? this.fiatAmount,
@@ -145,7 +155,12 @@ final class FiatFormState extends Equatable with FormzMixin {
coinList: coinList ?? this.coinList,
fiatOrderStatus: fiatOrderStatus ?? this.fiatOrderStatus,
fiatMode: fiatMode ?? this.fiatMode,
- selectedCoinPubkeys: selectedCoinPubkeys ?? this.selectedCoinPubkeys,
+ selectedCoinPubkeys: selectedCoinPubkeys != null
+ ? selectedCoinPubkeys()
+ : this.selectedCoinPubkeys,
+ webViewMode: webViewMode ?? this.webViewMode,
+ providerError:
+ providerError != null ? providerError() : this.providerError,
);
}
@@ -172,5 +187,7 @@ final class FiatFormState extends Equatable with FormzMixin {
fiatOrderStatus,
fiatMode,
selectedCoinPubkeys,
+ webViewMode,
+ providerError,
];
}
diff --git a/lib/bloc/fiat/fiat_order_status.dart b/lib/bloc/fiat/fiat_order_status.dart
index 2a39a337cc..72bcf77a31 100644
--- a/lib/bloc/fiat/fiat_order_status.dart
+++ b/lib/bloc/fiat/fiat_order_status.dart
@@ -1,12 +1,17 @@
// TODO: Differentiate between different error and in-progress statuses
+import 'package:logging/logging.dart';
+
enum FiatOrderStatus {
- /// User has not yet started the payment process
- pending,
+ /// Initial status: User has not yet started the payment process
+ initial,
/// User has started the process, and the payment method has been opened.
/// E.g. Ramp or Banxa websites have been opened
submitted,
+ /// Payment is awaiting user action (e.g., user needs to complete payment)
+ pendingPayment,
+
/// Payment has been submitted with the provider, and is being processed
inProgress,
@@ -14,16 +19,18 @@ enum FiatOrderStatus {
success,
/// Payment has been cancelled, declined, expired or refunded
- failed,
-
- /// The user closed the payment window using the provider close button
+ failed,
+
+ /// The user closed the payment window using the provider close button
/// or "return to Komodo Wallet" button
windowCloseRequested;
bool get isTerminal =>
this == FiatOrderStatus.success || this == FiatOrderStatus.failed;
bool get isSubmitting =>
- this == FiatOrderStatus.inProgress || this == FiatOrderStatus.submitted;
+ this == FiatOrderStatus.inProgress ||
+ this == FiatOrderStatus.submitted ||
+ this == FiatOrderStatus.pendingPayment;
bool get isFailed => this == FiatOrderStatus.failed;
bool get isSuccess => this == FiatOrderStatus.success;
@@ -32,7 +39,8 @@ enum FiatOrderStatus {
static FiatOrderStatus fromString(String status) {
// The case statements are references to Banxa's order statuses. See the
// docs link here for more info: https://docs.banxa.com/docs/order-status
- switch (status) {
+ final normalized = status.toLowerCase();
+ switch (normalized) {
case 'complete':
return FiatOrderStatus.success;
@@ -42,18 +50,25 @@ enum FiatOrderStatus {
case 'refunded':
return FiatOrderStatus.failed;
- case 'extraVerification':
- case 'pendingPayment':
- case 'waitingPayment':
- return FiatOrderStatus.pending;
+ case 'extraverification':
+ case 'pendingpayment':
+ case 'waitingpayment':
+ return FiatOrderStatus.pendingPayment;
- case 'paymentReceived':
- case 'inProgress':
- case 'coinTransferred':
+ case 'paymentreceived':
+ case 'inprogress':
+ case 'cointransferred':
+ case 'cryptotransferred':
return FiatOrderStatus.inProgress;
default:
- throw Exception('Unknown status: $status');
+ // Default to in progress if the status is not recognized
+ // to avoid alarming users with "Payment failed" popup messages
+ // unless we are sure that the payment has failed.
+ // Ideally, this section should not be reached.
+ Logger('FiatOrderStatus')
+ .warning('Unknown status: $status, defaulting to in progress');
+ return FiatOrderStatus.inProgress;
}
}
}
diff --git a/lib/bloc/fiat/fiat_repository.dart b/lib/bloc/fiat/fiat_repository.dart
index f006032102..a6444b5267 100644
--- a/lib/bloc/fiat/fiat_repository.dart
+++ b/lib/bloc/fiat/fiat_repository.dart
@@ -1,5 +1,4 @@
import 'package:decimal/decimal.dart';
-import 'package:universal_html/html.dart';
import 'package:web_dex/app_config/app_config.dart';
import 'package:web_dex/bloc/coins_bloc/coins_repo.dart';
import 'package:web_dex/bloc/fiat/base_fiat_provider.dart';
@@ -51,8 +50,12 @@ class FiatRepository {
for (final currencyList in results) {
for (final currency in currencyList) {
- bool isCoinUnknown() => !knownCoins.containsKey(currency.getAbbr());
- if (isCoin && (currency.isFiat || isCoinUnknown())) {
+ final isCoinSupported = knownCoins.containsKey(currency.getAbbr());
+ if (isCoin && (currency.isFiat || !isCoinSupported)) {
+ _log.fine(
+ 'Skipping ${currency.getAbbr()} because it is not a coin or '
+ 'not supported (${currency.configSymbol})',
+ );
continue;
}
@@ -252,15 +255,15 @@ class FiatRepository {
);
}
- Future buyCoin(
- String accountReference,
- String source,
- ICurrency target,
- String walletAddress,
- FiatPaymentMethod paymentMethod,
- String sourceAmount,
- String returnUrlOnSuccess,
- ) async {
+ Future buyCoin({
+ required String accountReference,
+ required String source,
+ required ICurrency target,
+ required String walletAddress,
+ required FiatPaymentMethod paymentMethod,
+ required String sourceAmount,
+ required String returnUrlOnSuccess,
+ }) async {
final provider = _getPaymentMethodProvider(paymentMethod);
if (provider == null) return Future.error('Provider not found');
diff --git a/lib/bloc/fiat/models/fiat_buy_order_error.dart b/lib/bloc/fiat/models/fiat_buy_order_error.dart
index d4d436e8d4..ddfe524cbd 100644
--- a/lib/bloc/fiat/models/fiat_buy_order_error.dart
+++ b/lib/bloc/fiat/models/fiat_buy_order_error.dart
@@ -18,6 +18,11 @@ class FiatBuyOrderError extends Equatable {
const FiatBuyOrderError.none() : this(code: 0, status: 0, title: '');
+ /// Error indicating a parsing issue with the response data
+ const FiatBuyOrderError.parsing({
+ String message = 'Failed to parse response data',
+ }) : this(code: -1, status: 400, title: message);
+
bool get isNone => this == const FiatBuyOrderError.none();
final int code;
diff --git a/lib/bloc/fiat/models/fiat_buy_order_info.dart b/lib/bloc/fiat/models/fiat_buy_order_info.dart
index 3c7a3e78cc..cdf29a0ee9 100644
--- a/lib/bloc/fiat/models/fiat_buy_order_info.dart
+++ b/lib/bloc/fiat/models/fiat_buy_order_info.dart
@@ -21,7 +21,7 @@ class FiatBuyOrderInfo extends Equatable {
required this.error,
});
- FiatBuyOrderInfo.info()
+ FiatBuyOrderInfo.fromCheckoutUrl(String url)
: this(
id: '',
accountId: '',
@@ -34,12 +34,12 @@ class FiatBuyOrderInfo extends Equatable {
extAccountId: '',
network: '',
paymentCode: '',
- checkoutUrl: '',
+ checkoutUrl: url,
createdAt: '',
error: const FiatBuyOrderError.none(),
);
- FiatBuyOrderInfo.fromCheckoutUrl(String url)
+ FiatBuyOrderInfo.empty()
: this(
id: '',
accountId: '',
@@ -52,18 +52,30 @@ class FiatBuyOrderInfo extends Equatable {
extAccountId: '',
network: '',
paymentCode: '',
- checkoutUrl: url,
+ checkoutUrl: '',
createdAt: '',
error: const FiatBuyOrderError.none(),
);
factory FiatBuyOrderInfo.fromJson(Map json) {
- Map data = json;
- if (json['data'] != null) {
- final orderData = json['data'] as Map? ?? {};
- data = orderData['order'] as Map? ?? {};
+ final jsonData = json['data'] as Map?;
+ final errors = json['errors'] as Map?;
+
+ if (json['data'] == null && errors == null) {
+ return FiatBuyOrderInfo.empty().copyWith(
+ error:
+ const FiatBuyOrderError.parsing(message: 'Missing order payload'),
+ );
}
+ if (jsonData == null && errors != null) {
+ return FiatBuyOrderInfo.empty().copyWith(
+ error: FiatBuyOrderError.fromJson(errors),
+ );
+ }
+
+ final data = jsonData!['order'] as Map;
+
return FiatBuyOrderInfo(
id: data['id'] as String? ?? '',
accountId: data['account_id'] as String? ?? '',
@@ -78,8 +90,8 @@ class FiatBuyOrderInfo extends Equatable {
paymentCode: data['payment_code'] as String? ?? '',
checkoutUrl: data['checkout_url'] as String? ?? '',
createdAt: assertString(data['created_at']) ?? '',
- error: data['errors'] != null
- ? FiatBuyOrderError.fromJson(data['errors'] as Map)
+ error: errors != null
+ ? FiatBuyOrderError.fromJson(errors)
: const FiatBuyOrderError.none(),
);
}
diff --git a/lib/bloc/fiat/models/fiat_price_info.dart b/lib/bloc/fiat/models/fiat_price_info.dart
index eed0081407..774a6e19d7 100644
--- a/lib/bloc/fiat/models/fiat_price_info.dart
+++ b/lib/bloc/fiat/models/fiat_price_info.dart
@@ -10,25 +10,25 @@ class FiatPriceInfo extends Equatable {
required this.spotPriceIncludingFee,
});
- static final zero = FiatPriceInfo(
- fiatAmount: Decimal.zero,
- coinAmount: Decimal.zero,
- fiatCode: '',
- coinCode: '',
- spotPriceIncludingFee: Decimal.zero,
- );
-
factory FiatPriceInfo.fromJson(Map json) {
return FiatPriceInfo(
- fiatAmount: Decimal.parse(json['fiat_amount']?.toString() ?? '0'),
- coinAmount: Decimal.parse(json['coin_amount']?.toString() ?? '0'),
+ fiatAmount: _safeParseDecimal(json['fiat_amount']),
+ coinAmount: _safeParseDecimal(json['coin_amount']),
fiatCode: json['fiat_code'] as String? ?? '',
coinCode: json['coin_code'] as String? ?? '',
spotPriceIncludingFee:
- Decimal.parse(json['spot_price_including_fee']?.toString() ?? '0'),
+ _safeParseDecimal(json['spot_price_including_fee']),
);
}
+ static final zero = FiatPriceInfo(
+ fiatAmount: Decimal.zero,
+ coinAmount: Decimal.zero,
+ fiatCode: '',
+ coinCode: '',
+ spotPriceIncludingFee: Decimal.zero,
+ );
+
final Decimal fiatAmount;
final Decimal coinAmount;
final String fiatCode;
@@ -62,6 +62,14 @@ class FiatPriceInfo extends Equatable {
};
}
+ static Decimal _safeParseDecimal(dynamic value) {
+ try {
+ return Decimal.parse(value?.toString() ?? '0');
+ } on FormatException {
+ return Decimal.zero;
+ }
+ }
+
@override
List