From 3564426248c952674dcc98d801c76e34194865f8 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 2 May 2023 10:22:35 -0600 Subject: [PATCH] show trocador provider options --- lib/models/exchange/exchange_form_state.dart | 21 +- .../sub_widgets/exchange_provider_option.dart | 377 ++++++++++-------- .../exchange_provider_options.dart | 9 +- .../exchange/trocador/trocador_exchange.dart | 20 +- 4 files changed, 240 insertions(+), 187 deletions(-) diff --git a/lib/models/exchange/exchange_form_state.dart b/lib/models/exchange/exchange_form_state.dart index 3678f01fd..52fe0bffd 100644 --- a/lib/models/exchange/exchange_form_state.dart +++ b/lib/models/exchange/exchange_form_state.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:stackwallet/models/exchange/aggregate_currency.dart'; @@ -158,16 +160,23 @@ class ExchangeFormState extends ChangeNotifier { required String providerName, required bool shouldUpdateData, required bool shouldNotifyListeners, + bool shouldAwait = true, }) async { _exchange = exchange; _providerName = providerName; if (shouldUpdateData) { - await _updateRangesAndEstimate( - shouldNotifyListeners: false, - ); - } - - if (shouldNotifyListeners) { + if (shouldAwait) { + await _updateRangesAndEstimate( + shouldNotifyListeners: shouldNotifyListeners, + ); + } else { + unawaited( + _updateRangesAndEstimate( + shouldNotifyListeners: shouldNotifyListeners, + ), + ); + } + } else if (shouldNotifyListeners) { _notify(); } } diff --git a/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart b/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart index 7c6d625ed..0d0f453ea 100644 --- a/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart +++ b/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart @@ -8,7 +8,6 @@ import 'package:stackwallet/providers/exchange/exchange_form_state_provider.dart import 'package:stackwallet/providers/global/locale_provider.dart'; import 'package:stackwallet/services/exchange/exchange.dart'; import 'package:stackwallet/services/exchange/exchange_response.dart'; -import 'package:stackwallet/services/exchange/trocador/trocador_exchange.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -21,35 +20,25 @@ import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/exchange/trocador/trocador_kyc_info_button.dart'; import 'package:stackwallet/widgets/exchange/trocador/trocador_rating_type_enum.dart'; -class ExchangeProviderOption extends ConsumerStatefulWidget { - const ExchangeProviderOption({ +class ExchangeOption extends ConsumerStatefulWidget { + const ExchangeOption({ Key? key, required this.exchange, - required this.exchangeProvider, required this.fixedRate, required this.reversed, }) : super(key: key); final Exchange exchange; - final String exchangeProvider; final bool fixedRate; final bool reversed; @override - ConsumerState createState() => - _ExchangeProviderOptionState(); + ConsumerState createState() => + _ExchangeMultiProviderOptionState(); } -class _ExchangeProviderOptionState - extends ConsumerState { +class _ExchangeMultiProviderOptionState extends ConsumerState { final isDesktop = Util.isDesktop; - late final String _id; - - @override - void initState() { - _id = "${widget.exchange.name} (${widget.exchangeProvider})"; - super.initState(); - } @override Widget build(BuildContext context) { @@ -62,9 +51,179 @@ class _ExchangeProviderOptionState final toAmount = ref.watch( exchangeFormStateProvider.select((value) => value.receiveAmount)); - final selected = ref.watch(exchangeFormStateProvider + return AnimatedSize( + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOutCubicEmphasized, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (sendCurrency != null && + receivingCurrency != null && + toAmount != null && + toAmount > Decimal.zero && + fromAmount != null && + fromAmount > Decimal.zero) + FutureBuilder( + future: widget.exchange.getEstimates( + sendCurrency.ticker, + receivingCurrency.ticker, + widget.reversed ? toAmount : fromAmount, + widget.fixedRate, + widget.reversed, + ), + builder: (context, + AsyncSnapshot>> snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + final estimates = snapshot.data?.value; + + if (estimates != null && estimates.isNotEmpty) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (int i = 0; i < estimates.length; i++) + Builder( + builder: (context) { + final e = estimates[i]; + int decimals; + try { + decimals = coinFromTickerCaseInsensitive( + receivingCurrency.ticker) + .decimals; + } catch (_) { + decimals = 8; // some reasonable alternative + } + Amount rate; + if (e.reversed) { + rate = (toAmount / e.estimatedAmount) + .toDecimal(scaleOnInfinitePrecision: 18) + .toAmount(fractionDigits: decimals); + } else { + rate = (e.estimatedAmount / fromAmount) + .toDecimal(scaleOnInfinitePrecision: 18) + .toAmount(fractionDigits: decimals); + } + + final rateString = + "1 ${sendCurrency.ticker.toUpperCase()} ~ ${rate.localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + )} ${receivingCurrency.ticker.toUpperCase()}"; + + return ConditionalParent( + condition: i > 0, + builder: (child) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + isDesktop + ? Container( + height: 1, + color: Theme.of(context) + .extension()! + .background, + ) + : const SizedBox( + height: 16, + ), + child, + ], + ), + child: _ProviderOption( + key: Key(widget.exchange.name + + e.exchangeProvider), + exchange: widget.exchange, + providerName: e.exchangeProvider, + rateString: rateString, + kycRating: e.kycRating, + ), + ); + }, + ), + ], + ); + } else if (snapshot.data?.exception + is PairUnavailableException) { + return _ProviderOption( + exchange: widget.exchange, + providerName: widget.exchange.name, + rateString: "Unsupported pair", + ); + } else { + Logging.instance.log( + "$runtimeType failed to fetch rate for ${widget.exchange.name}: ${snapshot.data}", + level: LogLevel.Warning, + ); + + return _ProviderOption( + exchange: widget.exchange, + providerName: widget.exchange.name, + rateString: "Failed to fetch rate", + ); + } + } else { + // show loading + return _ProviderOption( + exchange: widget.exchange, + providerName: widget.exchange.name, + rateString: "", + loadingString: true, + ); + } + }, + ), + ], + ), + ); + } +} + +class _ProviderOption extends ConsumerStatefulWidget { + const _ProviderOption({ + Key? key, + required this.exchange, + required this.providerName, + required this.rateString, + this.kycRating, + this.loadingString = false, + }) : super(key: key); + + final Exchange exchange; + final String providerName; + final String rateString; + final String? kycRating; + final bool loadingString; + + @override + ConsumerState<_ProviderOption> createState() => _ProviderOptionState(); +} + +class _ProviderOptionState extends ConsumerState<_ProviderOption> { + final isDesktop = Util.isDesktop; + + late final String _id; + + @override + void initState() { + _id = "${widget.exchange.name} (${widget.providerName})"; + super.initState(); + } + + @override + Widget build(BuildContext context) { + bool selected = ref.watch(exchangeFormStateProvider .select((value) => value.combinedExchangeId)) == _id; + String groupValue = ref.watch( + exchangeFormStateProvider.select((value) => value.combinedExchangeId)); + + if (ref.watch( + exchangeFormStateProvider.select((value) => value.exchange.name)) == + widget.providerName) { + selected = true; + groupValue = _id; + } return ConditionalParent( condition: isDesktop, @@ -75,17 +234,12 @@ class _ExchangeProviderOptionState child: GestureDetector( onTap: () { if (!selected) { - // showLoading( - // whileFuture: ref.read(exchangeFormStateProvider).updateExchange( exchange: widget.exchange, shouldUpdateData: true, shouldNotifyListeners: true, - providerName: widget.exchangeProvider, - // ), - // context: context, - // message: "Updating rates", - // isDesktop: isDesktop, + providerName: widget.providerName, + shouldAwait: false, ); } }, @@ -107,15 +261,15 @@ class _ExchangeProviderOptionState .extension()! .radioButtonIconEnabled, value: _id, - groupValue: ref.watch(exchangeFormStateProvider - .select((value) => value.combinedExchangeId)), + groupValue: groupValue, onChanged: (_) { if (!selected) { ref.read(exchangeFormStateProvider).updateExchange( exchange: widget.exchange, shouldUpdateData: false, shouldNotifyListeners: true, - providerName: widget.exchangeProvider, + providerName: widget.providerName, + shouldAwait: false, ); } }, @@ -149,153 +303,46 @@ class _ExchangeProviderOptionState crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - widget.exchangeProvider, + widget.providerName, style: STextStyles.titleBold12(context).copyWith( color: Theme.of(context) .extension()! .textDark2, ), ), - if (sendCurrency != null && - receivingCurrency != null && - toAmount != null && - toAmount > Decimal.zero && - fromAmount != null && - fromAmount > Decimal.zero) - FutureBuilder( - future: widget.exchange.getEstimates( - sendCurrency.ticker, - receivingCurrency.ticker, - widget.reversed ? toAmount : fromAmount, - widget.fixedRate, - widget.reversed, - ), - builder: (context, - AsyncSnapshot>> - snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - final estimates = snapshot.data?.value; - if (estimates != null && - estimates - .where((e) => - e.exchangeProvider == - widget.exchangeProvider) - .isNotEmpty) { - final estimate = estimates.firstWhere((e) => - e.exchangeProvider == - widget.exchangeProvider); - int decimals; - try { - decimals = coinFromTickerCaseInsensitive( - receivingCurrency.ticker) - .decimals; - } catch (_) { - decimals = 8; // some reasonable alternative - } - Amount rate; - if (estimate.reversed) { - rate = (toAmount / estimate.estimatedAmount) - .toDecimal(scaleOnInfinitePrecision: 18) - .toAmount(fractionDigits: decimals); - } else { - rate = (estimate.estimatedAmount / fromAmount) - .toDecimal(scaleOnInfinitePrecision: 18) - .toAmount(fractionDigits: decimals); - } - - return ConditionalParent( - condition: widget.exchange.name == - TrocadorExchange.exchangeName, - builder: (child) { - return Row( - children: [ - child, - TrocadorKYCInfoButton( - kycType: TrocadorKYCType.fromString( - estimate.kycRating!, - ), - ), - ], - ); - }, - child: Text( - "1 ${sendCurrency.ticker.toUpperCase()} ~ ${rate.localizedStringAsFixed( - locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - )} ${receivingCurrency.ticker.toUpperCase()}", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ), - ); - } else if (snapshot.data?.exception - is PairUnavailableException) { - return Text( - "Unsupported pair", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ); - } else { - Logging.instance.log( - "$runtimeType failed to fetch rate for $_id}: ${snapshot.data}", - level: LogLevel.Warning, - ); - return Text( - "Failed to fetch rate", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ); - } - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Loading", - "Loading.", - "Loading..", - "Loading...", - ], - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ); - } - }, - ), - if (!(sendCurrency != null && - receivingCurrency != null && - toAmount != null && - toAmount > Decimal.zero && - fromAmount != null && - fromAmount > Decimal.zero)) - Text( - "n/a", - style: STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ), + widget.loadingString + ? AnimatedText( + stringsToLoopThrough: const [ + "Loading", + "Loading.", + "Loading..", + "Loading...", + ], + style: + STextStyles.itemSubtitle12(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ) + : Text( + widget.rateString, + style: + STextStyles.itemSubtitle12(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), ], ), ), + if (widget.kycRating != null) + TrocadorKYCInfoButton( + kycType: TrocadorKYCType.fromString( + widget.kycRating!, + ), + ), ], ), ), diff --git a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart index feeb6b661..4d3b96f90 100644 --- a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart +++ b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart @@ -84,9 +84,8 @@ class _ExchangeProviderOptionsState child: Column( children: [ if (showChangeNow) - ExchangeProviderOption( + ExchangeOption( exchange: ChangeNowExchange.instance, - exchangeProvider: ChangeNowExchange.exchangeName, fixedRate: widget.fixedRate, reversed: widget.reversed, ), @@ -101,9 +100,8 @@ class _ExchangeProviderOptionsState height: 16, ), if (showMajesticBank) - ExchangeProviderOption( + ExchangeOption( exchange: MajesticBankExchange.instance, - exchangeProvider: MajesticBankExchange.exchangeName, fixedRate: widget.fixedRate, reversed: widget.reversed, ), @@ -118,11 +116,10 @@ class _ExchangeProviderOptionsState height: 16, ), if (showTrocador) - ExchangeProviderOption( + ExchangeOption( fixedRate: widget.fixedRate, reversed: widget.reversed, exchange: TrocadorExchange.instance, - exchangeProvider: 'LetsExchange', ), ], ), diff --git a/lib/services/exchange/trocador/trocador_exchange.dart b/lib/services/exchange/trocador/trocador_exchange.dart index 8085cfca5..e0becb11c 100644 --- a/lib/services/exchange/trocador/trocador_exchange.dart +++ b/lib/services/exchange/trocador/trocador_exchange.dart @@ -187,9 +187,7 @@ class TrocadorExchange extends Exchange { bool fixedRate, bool reversed, ) async { - final isPayment = reversed || fixedRate; - - final response = isPayment + final response = reversed ? await TrocadorAPI.getNewPaymentRate( isOnion: false, fromTicker: from, @@ -215,14 +213,15 @@ class TrocadorExchange extends Exchange { final List cOrLowerQuotes = []; for (final quote in response.value!.quotes) { - if (quote.fixed == isPayment) { + if (quote.fixed == fixedRate && + quote.provider.toLowerCase() != "changenow") { final rating = quote.kycRating.toLowerCase(); if (rating == "a" || rating == "b") { estimates.add( Estimate( - estimatedAmount: isPayment ? quote.amountFrom! : quote.amountTo!, + estimatedAmount: reversed ? quote.amountFrom! : quote.amountTo!, fixedRate: quote.fixed, - reversed: isPayment, + reversed: reversed, exchangeProvider: quote.provider, rateId: response.value!.tradeId, kycRating: quote.kycRating, @@ -236,13 +235,13 @@ class TrocadorExchange extends Exchange { cOrLowerQuotes.sort((a, b) => b.waste.compareTo(a.waste)); - for (int i = 0; i < max(3, cOrLowerQuotes.length); i++) { + for (int i = 0; i < min(3, cOrLowerQuotes.length); i++) { final quote = cOrLowerQuotes[i]; estimates.add( Estimate( - estimatedAmount: isPayment ? quote.amountFrom! : quote.amountTo!, + estimatedAmount: reversed ? quote.amountFrom! : quote.amountTo!, fixedRate: quote.fixed, - reversed: isPayment, + reversed: reversed, exchangeProvider: quote.provider, rateId: response.value!.tradeId, kycRating: quote.kycRating, @@ -251,7 +250,8 @@ class TrocadorExchange extends Exchange { } return ExchangeResponse( - value: estimates, + value: estimates + ..sort((a, b) => b.estimatedAmount.compareTo(a.estimatedAmount)), ); }