From 1c33dd5062b8c0150abfc96537bb3cb74f7ee55a Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 4 May 2023 12:19:50 -0600 Subject: [PATCH] swap rate refresh fixes and error propagation --- lib/pages/exchange_view/exchange_form.dart | 121 ++++++++---------- .../sub_widgets/exchange_provider_option.dart | 83 ++++++------ .../exchange_form_state_provider.dart | 19 ++- 3 files changed, 105 insertions(+), 118 deletions(-) diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index 65a83a3cd..c4d11d162 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -10,6 +10,7 @@ import 'package:isar/isar.dart'; import 'package:stackwallet/models/exchange/aggregate_currency.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/models/exchange/response_objects/estimate.dart'; +import 'package:stackwallet/models/exchange/response_objects/range.dart'; import 'package:stackwallet/models/isar/exchange_cache/currency.dart'; import 'package:stackwallet/models/isar/exchange_cache/pair.dart'; import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; @@ -28,7 +29,6 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/exchange_rate_type_enum.dart'; -import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -43,6 +43,9 @@ import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/textfields/exchange_textfield.dart'; import 'package:tuple/tuple.dart'; +import 'package:uuid/uuid.dart'; + +import '../../services/exchange/exchange_response.dart'; class ExchangeForm extends ConsumerStatefulWidget { const ExchangeForm({ @@ -604,7 +607,9 @@ class _ExchangeFormState extends ConsumerState { } Future update() async { - _addUpdate(); + final uuid = const Uuid().v1(); + _latestUuid = uuid; + _addUpdate(uuid); for (final exchange in exchanges) { ref.read(efEstimatesListProvider(exchange.name).notifier).state = null; } @@ -619,10 +624,12 @@ class _ExchangeFormState extends ConsumerState { amount <= Decimal.zero || pair.send == null || pair.receive == null) { - _removeUpdate(); + _removeUpdate(uuid); return; } final rateType = ref.read(efRateTypeProvider); + final Map>, Range?>> + results = {}; for (final exchange in exchanges) { final sendCurrency = pair.send?.forExchange(exchange.name); @@ -635,14 +642,6 @@ class _ExchangeFormState extends ConsumerState { rateType == ExchangeRateType.fixed, ); - if (rangeResponse.value == null) { - Logging.instance.log( - "Tried to $runtimeType.update Range for:" - " $exchange where response: $rangeResponse", - level: LogLevel.Info, - ); - } - final estimateResponse = await exchange.getEstimates( sendCurrency.ticker, receiveCurrency.ticker, @@ -651,43 +650,41 @@ class _ExchangeFormState extends ConsumerState { reversed, ); - if (estimateResponse.value == null) { - Logging.instance.log( - "Tried to $runtimeType._fetchEstimateAndRange Estimate for:" - " $exchange where response: $estimateResponse", - level: LogLevel.Info, - ); - } - - if (mounted) { - if (estimateResponse.value != null && rangeResponse.value != null) { - ref.read(efEstimatesListProvider(exchange.name).notifier).state = - Tuple2(estimateResponse.value!, rangeResponse.value!); - } else { - ref.read(efEstimatesListProvider(exchange.name).notifier).state = - null; - } - } + results.addAll( + { + exchange.name: Tuple2( + estimateResponse, + rangeResponse.value, + ), + }, + ); } } - // WidgetsBinding.instance.addPostFrameCallback((_) { - _removeUpdate(); - // }); + for (final exchange in exchanges) { + if (uuid == _latestUuid) { + ref.read(efEstimatesListProvider(exchange.name).notifier).state = + results[exchange.name]; + } + } + + _removeUpdate(uuid); } - int _updateCount = 0; + String? _latestUuid; + final Set _uuids = {}; - void _addUpdate() { - _updateCount++; + void _addUpdate(String uuid) { + _uuids.add(uuid); ref.read(efRefreshingProvider.notifier).state = true; } - void _removeUpdate() { - _updateCount--; - if (_updateCount <= 0) { - _updateCount = 0; - ref.read(efRefreshingProvider.notifier).state = false; + void _removeUpdate(String uuid) { + _uuids.remove(uuid); + if (_uuids.isEmpty) { + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(efRefreshingProvider.notifier).state = false; + }); } } @@ -711,16 +708,24 @@ class _ExchangeFormState extends ConsumerState { _sendFocusNode.addListener(() { if (_sendFocusNode.hasFocus) { + final reversed = ref.read(efReversedProvider); WidgetsBinding.instance.addPostFrameCallback((_) { ref.read(efReversedProvider.notifier).state = false; + if (reversed == true) { + update(); + } }); } }); _receiveFocusNode.addListener(() { if (_receiveFocusNode.hasFocus && ref.read(efExchangeProvider).name != ChangeNowExchange.exchangeName) { + final reversed = ref.read(efReversedProvider); WidgetsBinding.instance.addPostFrameCallback((_) { ref.read(efReversedProvider.notifier).state = true; + if (reversed != true) { + update(); + } }); } }); @@ -856,12 +861,6 @@ class _ExchangeFormState extends ConsumerState { SizedBox( height: isDesktop ? 10 : 4, ), - // if (ref.watch(efWarningProvider).isNotEmpty && - // !ref.watch(efReversedProvider)) - // Text( - // ref.watch(efWarningProvider), - // style: STextStyles.errorSmall(context), - // ), Row( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -926,27 +925,24 @@ class _ExchangeFormState extends ConsumerState { borderRadius: Constants.size.circularBorderRadius, background: Theme.of(context).extension()!.textFieldDefaultBG, - onTap: () { - if (!(ref.read(efRateTypeProvider) == ExchangeRateType.estimated) && - _receiveController.text == "-") { - _receiveController.text = ""; - } - }, + onTap: rateType == ExchangeRateType.estimated && + ref.watch(efExchangeProvider).name == + ChangeNowExchange.exchangeName + ? null + : () { + if (_sendController.text == "-") { + _sendController.text = ""; + } + }, onChanged: receiveFieldOnChanged, onButtonTap: selectReceiveCurrency, isWalletCoin: isWalletCoin(coin, true), currency: ref .watch(efCurrencyPairProvider.select((value) => value.receive)), - readOnly: (rateType) == ExchangeRateType.estimated && + readOnly: rateType == ExchangeRateType.estimated && ref.watch(efExchangeProvider).name == ChangeNowExchange.exchangeName, ), - // if (ref.watch(efWarningProvider).isNotEmpty && - // ref.watch(efReversedProvider)) - // Text( - // ref.watch(efWarningProvider), - // style: STextStyles.errorSmall(context), - // ), SizedBox( height: isDesktop ? 20 : 12, ), @@ -957,21 +953,12 @@ class _ExchangeFormState extends ConsumerState { onChanged: onRateTypeChanged, ), ), - // AnimatedSize( - // duration: const Duration(milliseconds: 250), - // curve: Curves.easeInOutCirc, - // child: ref.watch(efSendAmountProvider).sendAmount != null && - // ref.watch(exchangeFormStateProvider).sendAmount != Decimal.zero - // ? Padding( padding: EdgeInsets.only(top: isDesktop ? 20 : 12), child: ExchangeProviderOptions( fixedRate: rateType == ExchangeRateType.fixed, reversed: ref.watch(efReversedProvider), ), - // : const SizedBox( - // height: 0, - // ), ), SizedBox( height: isDesktop ? 20 : 12, 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 83c22b0b3..1212caf49 100644 --- a/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart +++ b/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart @@ -9,6 +9,7 @@ import 'package:stackwallet/services/exchange/exchange.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/exchange_rate_type_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -31,11 +32,10 @@ class ExchangeOption extends ConsumerStatefulWidget { final bool reversed; @override - ConsumerState createState() => - _ExchangeMultiProviderOptionState(); + ConsumerState createState() => _ExchangeOptionState(); } -class _ExchangeMultiProviderOptionState extends ConsumerState { +class _ExchangeOptionState extends ConsumerState { final isDesktop = Util.isDesktop; @override @@ -49,7 +49,8 @@ class _ExchangeMultiProviderOptionState extends ConsumerState { ? ref.watch(efReceiveAmountProvider) : ref.watch(efSendAmountProvider); - final estimates = ref.watch(efEstimatesListProvider(widget.exchange.name)); + final data = ref.watch(efEstimatesListProvider(widget.exchange.name)); + final estimates = data?.item1.value; return AnimatedSize( duration: const Duration(milliseconds: 500), @@ -68,14 +69,14 @@ class _ExchangeMultiProviderOptionState extends ConsumerState { receivingCurrency != null && amount != null && amount > Decimal.zero) { - if (estimates != null && estimates.item1.isNotEmpty) { + if (estimates != null && estimates.isNotEmpty) { return Column( mainAxisSize: MainAxisSize.min, children: [ - for (int i = 0; i < estimates.item1.length; i++) + for (int i = 0; i < estimates.length; i++) Builder( builder: (context) { - final e = estimates.item1[i]; + final e = estimates[i]; int decimals; try { @@ -136,14 +137,37 @@ class _ExchangeMultiProviderOptionState extends ConsumerState { ); } else { Logging.instance.log( - "$runtimeType failed to fetch rate for ${widget.exchange.name}: $estimates", + "$runtimeType rate unavailable for ${widget.exchange.name}: $data", level: LogLevel.Warning, ); - return _ProviderOption( - exchange: widget.exchange, - estimate: null, - rateString: "Failed to fetch rate", + return Consumer( + builder: (_, ref, __) { + String? message; + + final range = data?.item2; + if (range != null) { + if (range.min != null && amount < range.min!) { + message ??= "Amount too small"; + } else if (range.max != null && amount > range.max!) { + message ??= "Amount too large"; + } + } else if (data?.item1.value == null) { + final rateType = ref.watch(efRateTypeProvider) == + ExchangeRateType.estimated + ? "estimated" + : "fixed"; + message ??= "Pair unavailable on $rateType rate flow"; + } + + return _ProviderOption( + exchange: widget.exchange, + estimate: null, + rateString: message ?? "Failed to fetch rate", + rateColor: + Theme.of(context).extension()!.textError, + ); + }, ); } } else { @@ -168,6 +192,7 @@ class _ProviderOption extends ConsumerStatefulWidget { required this.rateString, this.kycRating, this.loadingString = false, + this.rateColor, }) : super(key: key); final Exchange exchange; @@ -175,6 +200,7 @@ class _ProviderOption extends ConsumerStatefulWidget { final String rateString; final String? kycRating; final bool loadingString; + final Color? rateColor; @override ConsumerState<_ProviderOption> createState() => _ProviderOptionState(); @@ -201,8 +227,6 @@ class _ProviderOptionState extends ConsumerState<_ProviderOption> { groupValue = _id; } - // bool selected = groupValue == _id; - return ConditionalParent( condition: isDesktop, builder: (child) => MouseRegion( @@ -214,17 +238,6 @@ class _ProviderOptionState extends ConsumerState<_ProviderOption> { ref.read(efExchangeProvider.notifier).state = widget.exchange; ref.read(efExchangeProviderNameProvider.notifier).state = widget.estimate?.exchangeProvider ?? widget.exchange.name; - - // if (!selected) { - // ref.read(exchangeFormStateProvider).updateExchange( - // exchange: widget.exchange, - // shouldUpdateData: false, - // shouldNotifyListeners: false, - // providerName: - // widget.estimate?.exchangeProvider ?? widget.exchange.name, - // shouldAwait: false, - // ); - // } }, child: Container( color: Colors.transparent, @@ -253,19 +266,6 @@ class _ProviderOptionState extends ConsumerState<_ProviderOption> { .state = widget.estimate?.exchangeProvider ?? widget.exchange.name; - // if (!selected) { - // - // - // ref.read(exchangeFormStateProvider).updateExchange( - // exchange: widget.exchange, - // shouldUpdateData: false, - // shouldNotifyListeners: false, - // providerName: - // widget.estimate?.exchangeProvider ?? - // widget.exchange.name, - // shouldAwait: false, - // ); - // } }, ), ), @@ -324,9 +324,10 @@ class _ProviderOptionState extends ConsumerState<_ProviderOption> { widget.rateString, style: STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: widget.rateColor ?? + Theme.of(context) + .extension()! + .textSubtitle1, ), ), ], diff --git a/lib/providers/exchange/exchange_form_state_provider.dart b/lib/providers/exchange/exchange_form_state_provider.dart index 937fcc7db..7828e2771 100644 --- a/lib/providers/exchange/exchange_form_state_provider.dart +++ b/lib/providers/exchange/exchange_form_state_provider.dart @@ -4,12 +4,13 @@ import 'package:stackwallet/models/exchange/active_pair.dart'; import 'package:stackwallet/models/exchange/response_objects/estimate.dart'; import 'package:stackwallet/models/exchange/response_objects/range.dart'; import 'package:stackwallet/services/exchange/exchange.dart'; +import 'package:stackwallet/services/exchange/exchange_response.dart'; import 'package:stackwallet/utilities/enums/exchange_rate_type_enum.dart'; import 'package:tuple/tuple.dart'; -final efEstimatesListProvider = - StateProvider.family, Range>?, String>( - (ref, exchangeName) => null); +final efEstimatesListProvider = StateProvider.family< + Tuple2>, Range?>?, + String>((ref, exchangeName) => null); final efRateTypeProvider = StateProvider((ref) => ExchangeRateType.estimated); @@ -53,19 +54,17 @@ final efCurrencyPairProvider = ChangeNotifierProvider( (ref) => ActivePair(), ); -final efRangeProvider = StateProvider((ref) { - final exchange = ref.watch(efExchangeProvider); - return ref.watch(efEstimatesListProvider(exchange.name))?.item2; -}); - final efEstimateProvider = StateProvider((ref) { final exchange = ref.watch(efExchangeProvider); final provider = ref.watch(efExchangeProviderNameProvider); final reversed = ref.watch(efReversedProvider); final fixedRate = ref.watch(efRateTypeProvider) == ExchangeRateType.fixed; - final matches = - ref.watch(efEstimatesListProvider(exchange.name))?.item1.where((e) { + final matches = ref + .watch(efEstimatesListProvider(exchange.name)) + ?.item1 + .value + ?.where((e) { return e.exchangeProvider == provider && e.fixedRate == fixedRate && e.reversed == reversed;