import 'dart:async'; import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/models/exchange/response_objects/currency.dart'; import 'package:stackwallet/models/exchange/response_objects/fixed_rate_market.dart'; import 'package:stackwallet/models/exchange/response_objects/pair.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/exchange_view/exchange_coin_selection/fixed_rate_pair_coin_selection_view.dart'; import 'package:stackwallet/pages/exchange_view/exchange_coin_selection/floating_rate_currency_selection_view.dart'; import 'package:stackwallet/pages/exchange_view/exchange_step_views/step_2_view.dart'; import 'package:stackwallet/pages/exchange_view/exchange_view.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/step_row.dart'; import 'package:stackwallet/providers/exchange/available_currencies_state_provider.dart'; import 'package:stackwallet/providers/exchange/available_floating_rate_pairs_state_provider.dart'; import 'package:stackwallet/providers/exchange/change_now_provider.dart'; import 'package:stackwallet/providers/exchange/estimate_rate_exchange_form_provider.dart'; import 'package:stackwallet/providers/exchange/exchange_send_from_wallet_id_provider.dart'; import 'package:stackwallet/providers/exchange/fixed_rate_exchange_form_provider.dart'; import 'package:stackwallet/providers/exchange/fixed_rate_market_pairs_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:tuple/tuple.dart'; class WalletInitiatedExchangeView extends ConsumerStatefulWidget { const WalletInitiatedExchangeView({ Key? key, required this.walletId, required this.coin, }) : super(key: key); static const String routeName = "/walletInitiatedExchange"; final String walletId; final Coin coin; @override ConsumerState createState() => _WalletInitiatedExchangeViewState(); } class _WalletInitiatedExchangeViewState extends ConsumerState { late final String walletId; late final Coin coin; late final TextEditingController _sendController; late final TextEditingController _receiveController; final FocusNode _sendFocusNode = FocusNode(); final FocusNode _receiveFocusNode = FocusNode(); bool _swapLock = false; Future _swap() async { _swapLock = true; _sendFocusNode.unfocus(); _receiveFocusNode.unfocus(); unawaited( showDialog( context: context, barrierDismissible: false, builder: (_) => WillPopScope( onWillPop: () async => false, child: Container( color: Theme.of(context) .extension()! .overlay .withOpacity(0.6), child: const CustomLoadingOverlay( message: "Updating exchange rate", eventBus: null, ), ), ), ), ); if (ref.watch(prefsChangeNotifierProvider .select((pref) => pref.exchangeRateType)) == ExchangeRateType.estimated) { await ref.read(estimatedRateExchangeFormProvider).swap(); } else { final from = ref.read(fixedRateExchangeFormProvider).market?.from; final to = ref.read(fixedRateExchangeFormProvider).market?.to; if (to != null && from != null) { final markets = ref .read(fixedRateMarketPairsStateProvider.state) .state .where((e) => e.from == to && e.to == from); if (markets.isNotEmpty) { await ref.read(fixedRateExchangeFormProvider).swap(markets.first); } } } if (mounted) { Navigator.of(context).pop(); } _swapLock = false; } Future _showFloatingRateSelectionSheet({ required List currencies, required String excludedTicker, required String fromTicker, required void Function(Currency) onSelected, }) async { _sendFocusNode.unfocus(); _receiveFocusNode.unfocus(); List availablePairs = []; if (fromTicker.isEmpty || fromTicker == "-" || excludedTicker.isEmpty || excludedTicker == "-") { availablePairs = ref.read(availableFloatingRatePairsStateProvider.state).state; } else if (excludedTicker == fromTicker) { availablePairs = ref .read(availableFloatingRatePairsStateProvider.state) .state .where((e) => e.from == excludedTicker) .toList(growable: false); } else { availablePairs = ref .read(availableFloatingRatePairsStateProvider.state) .state .where((e) => e.to == excludedTicker) .toList(growable: false); } final List tickers = currencies.where((e) { if (excludedTicker == fromTicker) { return e.ticker != excludedTicker && availablePairs.where((e2) => e2.to == e.ticker).isNotEmpty; } else { return e.ticker != excludedTicker && availablePairs.where((e2) => e2.from == e.ticker).isNotEmpty; } }).toList(growable: false); final result = await Navigator.of(context).push( MaterialPageRoute( builder: (_) => FloatingRateCurrencySelectionView( currencies: tickers, ), ), ); if (mounted && result is Currency) { onSelected(result); } } String? _fetchIconUrlFromTickerForFixedRateFlow(String? ticker) { if (ticker == null) return null; final possibleCurrencies = ref .read(availableChangeNowCurrenciesStateProvider.state) .state .where((e) => e.ticker.toUpperCase() == ticker.toUpperCase()); for (final currency in possibleCurrencies) { if (currency.image.isNotEmpty) { return currency.image; } } return null; } Future _showFixedRateSelectionSheet({ required String excludedTicker, required String fromTicker, required void Function(String) onSelected, }) async { _sendFocusNode.unfocus(); _receiveFocusNode.unfocus(); List marketsThatPairWithExcludedTicker = []; if (excludedTicker == "" || excludedTicker == "-" || fromTicker == "" || fromTicker == "-") { marketsThatPairWithExcludedTicker = ref.read(fixedRateMarketPairsStateProvider.state).state; } else if (excludedTicker == fromTicker) { marketsThatPairWithExcludedTicker = ref .read(fixedRateMarketPairsStateProvider.state) .state .where((e) => e.from == excludedTicker && e.to != excludedTicker) .toList(growable: false); } else { marketsThatPairWithExcludedTicker = ref .read(fixedRateMarketPairsStateProvider.state) .state .where((e) => e.to == excludedTicker && e.from != excludedTicker) .toList(growable: false); } final result = await Navigator.of(context).push( MaterialPageRoute( builder: (_) => FixedRateMarketPairCoinSelectionView( markets: marketsThatPairWithExcludedTicker, currencies: ref.read(availableChangeNowCurrenciesStateProvider.state).state, isFrom: excludedTicker != fromTicker, ), ), ); if (mounted && result is String) { onSelected(result); } } @override void initState() { walletId = widget.walletId; coin = widget.coin; _sendController = TextEditingController(); _receiveController = TextEditingController(); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { ref.read(estimatedRateExchangeFormProvider).clearAmounts(true); // ref.read(fixedRateExchangeFormProvider); }); _sendFocusNode.addListener(() async { if (!_sendFocusNode.hasFocus) { final newFromAmount = Decimal.tryParse(_sendController.text); if (newFromAmount != null) { if (ref.read(prefsChangeNotifierProvider).exchangeRateType == ExchangeRateType.estimated) { await ref .read(estimatedRateExchangeFormProvider) .setFromAmountAndCalculateToAmount(newFromAmount, true); } else { await ref .read(fixedRateExchangeFormProvider) .setFromAmountAndCalculateToAmount(newFromAmount, true); } } else { if (ref.read(prefsChangeNotifierProvider).exchangeRateType == ExchangeRateType.estimated) { await ref .read(estimatedRateExchangeFormProvider) .setFromAmountAndCalculateToAmount(Decimal.zero, true); } else { await ref .read(fixedRateExchangeFormProvider) .setFromAmountAndCalculateToAmount(Decimal.zero, true); } _receiveController.text = ref.read(prefsChangeNotifierProvider).exchangeRateType == ExchangeRateType.estimated ? "-" : ""; } } }); _receiveFocusNode.addListener(() async { if (!_receiveFocusNode.hasFocus) { final newToAmount = Decimal.tryParse(_receiveController.text); if (newToAmount != null) { if (ref.read(prefsChangeNotifierProvider).exchangeRateType == ExchangeRateType.estimated) { // await ref // .read(estimatedRateExchangeFormProvider) // .setToAmountAndCalculateFromAmount(newToAmount, true); } else { await ref .read(fixedRateExchangeFormProvider) .setToAmountAndCalculateFromAmount(newToAmount, true); } } else { if (ref.read(prefsChangeNotifierProvider).exchangeRateType == ExchangeRateType.estimated) { // await ref // .read(estimatedRateExchangeFormProvider) // .setToAmountAndCalculateFromAmount(Decimal.zero, true); } else { await ref .read(fixedRateExchangeFormProvider) .setToAmountAndCalculateFromAmount(Decimal.zero, true); } _sendController.text = ""; } } }); super.initState(); } @override void dispose() { _receiveController.dispose(); _sendController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); final isEstimated = ref.watch(prefsChangeNotifierProvider .select((pref) => pref.exchangeRateType)) == ExchangeRateType.estimated; ref.listen( isEstimated ? estimatedRateExchangeFormProvider .select((value) => value.toAmountString) : fixedRateExchangeFormProvider.select( (value) => value.toAmountString), (previous, String next) { if (!_receiveFocusNode.hasFocus) { _receiveController.text = isEstimated && next.isEmpty ? "-" : next; debugPrint("RECEIVE AMOUNT LISTENER ACTIVATED"); if (_swapLock) { _sendController.text = isEstimated ? ref.read(estimatedRateExchangeFormProvider).fromAmountString : ref.read(fixedRateExchangeFormProvider).fromAmountString; } } }); ref.listen( isEstimated ? estimatedRateExchangeFormProvider .select((value) => value.fromAmountString) : fixedRateExchangeFormProvider.select( (value) => value.fromAmountString), (previous, String next) { if (!_sendFocusNode.hasFocus) { _sendController.text = next; debugPrint("SEND AMOUNT LISTENER ACTIVATED"); if (_swapLock) { _receiveController.text = isEstimated ? ref .read(estimatedRateExchangeFormProvider) .toAmountString .isEmpty ? "-" : ref.read(estimatedRateExchangeFormProvider).toAmountString : ref.read(fixedRateExchangeFormProvider).toAmountString; } } }); return Scaffold( backgroundColor: Theme.of(context).extension()!.background, appBar: AppBar( leading: AppBarBackButton( onPressed: () async { if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); await Future.delayed(const Duration(milliseconds: 75)); } if (mounted) { Navigator.of(context).pop(); } }, ), title: Text( "Exchange", style: STextStyles.navBarTitle(context), ), ), body: LayoutBuilder( builder: (context, constraints) { final width = MediaQuery.of(context).size.width - 32; return Padding( padding: const EdgeInsets.all(12), child: SingleChildScrollView( child: ConstrainedBox( constraints: BoxConstraints( minHeight: constraints.maxHeight - 24, ), child: IntrinsicHeight( child: Padding( padding: const EdgeInsets.all(4), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ StepRow( count: 4, current: 0, width: width, ), const SizedBox( height: 14, ), Text( "Exchange amount", style: STextStyles.pageTitleH1(context), ), const SizedBox( height: 8, ), Text( "Network fees and other exchange charges are included in the rate.", style: STextStyles.itemSubtitle(context), ), const SizedBox( height: 24, ), Text( "You will send", style: STextStyles.itemSubtitle(context).copyWith( color: Theme.of(context) .extension()! .textDark3, ), ), const SizedBox( height: 4, ), TextFormField( style: STextStyles.smallMed14(context).copyWith( color: Theme.of(context) .extension()! .textDark, ), focusNode: _sendFocusNode, controller: _sendController, textAlign: TextAlign.right, onTap: () { if (_sendController.text == "-") { _sendController.text = ""; } }, onChanged: (value) async { final newFromAmount = Decimal.tryParse(value); if (newFromAmount != null) { if (ref .read(prefsChangeNotifierProvider) .exchangeRateType == ExchangeRateType.estimated) { await ref .read(estimatedRateExchangeFormProvider) .setFromAmountAndCalculateToAmount( newFromAmount, false); } else { await ref .read(fixedRateExchangeFormProvider) .setFromAmountAndCalculateToAmount( newFromAmount, false); } } else { if (ref .read(prefsChangeNotifierProvider) .exchangeRateType == ExchangeRateType.estimated) { await ref .read(estimatedRateExchangeFormProvider) .setFromAmountAndCalculateToAmount( Decimal.zero, false); } else { await ref .read(fixedRateExchangeFormProvider) .setFromAmountAndCalculateToAmount( Decimal.zero, false); } _receiveController.text = isEstimated ? "-" : ""; } }, keyboardType: const TextInputType.numberWithOptions( signed: false, decimal: true, ), inputFormatters: [ // regex to validate a crypto amount with 8 decimal places TextInputFormatter.withFunction((oldValue, newValue) => RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') .hasMatch(newValue.text) ? newValue : oldValue), ], decoration: InputDecoration( contentPadding: const EdgeInsets.only( top: 12, right: 12, ), hintText: "0", hintStyle: STextStyles.fieldLabel(context).copyWith( fontSize: 14, ), prefixIcon: FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: GestureDetector( onTap: () async { if (ref .read(prefsChangeNotifierProvider) .exchangeRateType == ExchangeRateType.estimated) { final String fromTicker = ref .read( estimatedRateExchangeFormProvider) .from ?.ticker ?? "-"; if (fromTicker.toLowerCase() == coin.ticker.toLowerCase()) { // do not allow changing away from wallet coin return; } await _showFloatingRateSelectionSheet( currencies: ref .read( availableChangeNowCurrenciesStateProvider .state) .state, excludedTicker: ref .read( estimatedRateExchangeFormProvider) .to ?.ticker ?? "-", fromTicker: fromTicker, onSelected: (from) => ref .read( estimatedRateExchangeFormProvider) .updateFrom(from, true)); } else { final toTicker = ref .read(fixedRateExchangeFormProvider) .market ?.to ?? ""; final fromTicker = ref .read(fixedRateExchangeFormProvider) .market ?.from ?? ""; if (fromTicker.toLowerCase() == coin.ticker.toLowerCase()) { // do not allow changing away from wallet coin return; } await _showFixedRateSelectionSheet( excludedTicker: toTicker, fromTicker: fromTicker, onSelected: (selectedFromTicker) async { try { final market = ref .read( fixedRateMarketPairsStateProvider .state) .state .firstWhere( (e) => e.to == toTicker && e.from == selectedFromTicker, ); await ref .read( fixedRateExchangeFormProvider) .updateMarket(market, true); } catch (e) { unawaited(showDialog( context: context, builder: (_) => const StackDialog( title: "Fixed rate market error", message: "Could not find the specified fixed rate trade pair", ), )); return; } }, ); } }, child: Container( color: Colors.transparent, child: Padding( padding: const EdgeInsets.all(12), child: Row( children: [ Container( width: 18, height: 18, decoration: BoxDecoration( borderRadius: BorderRadius.circular(18), ), child: Builder( builder: (context) { String? image; if (ref.watch(prefsChangeNotifierProvider .select((value) => value .exchangeRateType)) == ExchangeRateType.estimated) { image = ref .watch( estimatedRateExchangeFormProvider .select((value) => value.from)) ?.image; } else { image = _fetchIconUrlFromTickerForFixedRateFlow( ref.watch( fixedRateExchangeFormProvider .select((value) => value.market ?.from))); } if (image != null && image.isNotEmpty) { return Center( child: SvgPicture.network( image, height: 18, placeholderBuilder: (_) => Container( width: 18, height: 18, decoration: BoxDecoration( color: Theme.of(context) .extension< StackColors>()! .textSubtitle2, borderRadius: BorderRadius .circular( 18, ), ), child: ClipRRect( borderRadius: BorderRadius .circular( 18, ), child: const LoadingIndicator(), ), ), ), ); } else { return Container( width: 18, height: 18, decoration: BoxDecoration( // color: Theme.of(context).extension()!.accentColorDark borderRadius: BorderRadius.circular( 18), ), child: SvgPicture.asset( Assets.svg.circleQuestion, width: 18, height: 18, color: Theme.of(context) .extension< StackColors>()! .textSubtitle2, ), ); } }, ), ), const SizedBox( width: 6, ), Text( isEstimated ? ref.watch( estimatedRateExchangeFormProvider .select((value) => value .from?.ticker .toUpperCase())) ?? "-" : ref.watch( fixedRateExchangeFormProvider .select((value) => value .market?.from .toUpperCase())) ?? "-", style: STextStyles.smallMed14(context) .copyWith( color: Theme.of(context) .extension()! .accentColorDark), ), const SizedBox( width: 6, ), Builder(builder: (context) { final ticker = isEstimated ? ref.watch( estimatedRateExchangeFormProvider .select((value) => value.from ?.ticker)) ?? "-" : ref.watch( fixedRateExchangeFormProvider .select((value) => value.market ?.from)) ?? "-"; if (ticker.toLowerCase() == coin.ticker.toLowerCase()) { return Container(); } return SvgPicture.asset( Assets.svg.chevronDown, width: 5, height: 2.5, color: Theme.of(context) .extension()! .accentColorDark); }), ], ), ), ), ), ), ), ), const SizedBox( height: 4, ), Stack( children: [ Positioned.fill( child: Align( alignment: Alignment.bottomLeft, child: Text( "You will receive", style: STextStyles.itemSubtitle(context) .copyWith( color: Theme.of(context) .extension()! .textDark3, ), ), ), ), Center( child: GestureDetector( onTap: () async { await _swap(); }, child: Padding( padding: const EdgeInsets.all(4), child: SvgPicture.asset( Assets.svg.swap, width: 20, height: 20, color: Theme.of(context) .extension()! .accentColorDark, ), ), ), ), Positioned.fill( child: Align( alignment: Alignment.topRight, child: Text( isEstimated ? ref.watch( estimatedRateExchangeFormProvider .select((value) => value.minimumSendWarning)) : ref.watch(fixedRateExchangeFormProvider .select((value) => value.sendAmountWarning)), style: STextStyles.errorSmall(context), ), ), ), ], ), const SizedBox( height: 4, ), TextFormField( style: STextStyles.smallMed14(context).copyWith( color: Theme.of(context) .extension()! .textDark, ), focusNode: _receiveFocusNode, controller: _receiveController, readOnly: ref .read(prefsChangeNotifierProvider) .exchangeRateType == ExchangeRateType.estimated, onTap: () { if (!isEstimated && _receiveController.text == "-") { _receiveController.text = ""; } }, onChanged: (value) async { final newToAmount = Decimal.tryParse(value); if (newToAmount != null) { if (ref .read(prefsChangeNotifierProvider) .exchangeRateType == ExchangeRateType.estimated) { // await ref // .read(estimatedRateExchangeFormProvider) // .setToAmountAndCalculateFromAmount( // newToAmount, false); } else { await ref .read(fixedRateExchangeFormProvider) .setToAmountAndCalculateFromAmount( newToAmount, false); } } else { if (ref .read(prefsChangeNotifierProvider) .exchangeRateType == ExchangeRateType.estimated) { // await ref // .read(estimatedRateExchangeFormProvider) // .setToAmountAndCalculateFromAmount( // Decimal.zero, false); } else { await ref .read(fixedRateExchangeFormProvider) .setToAmountAndCalculateFromAmount( Decimal.zero, false); } _sendController.text = ""; } }, textAlign: TextAlign.right, keyboardType: const TextInputType.numberWithOptions( signed: false, decimal: true, ), inputFormatters: [ // regex to validate a crypto amount with 8 decimal places TextInputFormatter.withFunction((oldValue, newValue) => RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') .hasMatch(newValue.text) ? newValue : oldValue), ], decoration: InputDecoration( contentPadding: const EdgeInsets.only( top: 12, right: 12, ), hintText: "0", hintStyle: STextStyles.fieldLabel(context).copyWith( fontSize: 14, ), prefixIcon: FittedBox( fit: BoxFit.scaleDown, child: GestureDetector( onTap: () async { if (ref .read(prefsChangeNotifierProvider) .exchangeRateType == ExchangeRateType.estimated) { final toTicker = ref .read( estimatedRateExchangeFormProvider) .to ?.ticker ?? ""; if (toTicker.toLowerCase() == coin.ticker.toLowerCase()) { // do not allow changing away from wallet coin return; } await _showFloatingRateSelectionSheet( currencies: ref .read( availableChangeNowCurrenciesStateProvider .state) .state, excludedTicker: ref .read( estimatedRateExchangeFormProvider) .from ?.ticker ?? "", fromTicker: ref .read( estimatedRateExchangeFormProvider) .from ?.ticker ?? "", onSelected: (to) => ref .read( estimatedRateExchangeFormProvider) .updateTo(to, true)); } else { final fromTicker = ref .read(fixedRateExchangeFormProvider) .market ?.from ?? ""; final toTicker = ref .read(fixedRateExchangeFormProvider) .market ?.to ?? ""; if (toTicker.toLowerCase() == coin.ticker.toLowerCase()) { // do not allow changing away from wallet coin return; } await _showFixedRateSelectionSheet( excludedTicker: fromTicker, fromTicker: fromTicker, onSelected: (selectedToTicker) async { try { final market = ref .read( fixedRateMarketPairsStateProvider .state) .state .firstWhere( (e) => e.to == selectedToTicker && e.from == fromTicker, ); await ref .read( fixedRateExchangeFormProvider) .updateMarket(market, true); } catch (e) { unawaited(showDialog( context: context, builder: (_) => const StackDialog( title: "Fixed rate market error", message: "Could not find the specified fixed rate trade pair", ), )); return; } }, ); } }, child: Container( color: Colors.transparent, child: Padding( padding: const EdgeInsets.all(12), child: Row( children: [ Container( width: 18, height: 18, decoration: BoxDecoration( borderRadius: BorderRadius.circular(18), ), child: Builder( builder: (context) { String? image; if (ref.watch(prefsChangeNotifierProvider .select((value) => value .exchangeRateType)) == ExchangeRateType.estimated) { image = ref .watch( estimatedRateExchangeFormProvider .select((value) => value.to)) ?.image; } else { image = _fetchIconUrlFromTickerForFixedRateFlow( ref.watch( fixedRateExchangeFormProvider .select((value) => value.market ?.to))); } if (image != null && image.isNotEmpty) { return Center( child: SvgPicture.network( image, height: 18, placeholderBuilder: (_) => Container( width: 18, height: 18, decoration: BoxDecoration( color: Theme.of(context) .extension< StackColors>()! .textSubtitle2, borderRadius: BorderRadius .circular(18), ), child: ClipRRect( borderRadius: BorderRadius .circular( 18, ), child: const LoadingIndicator(), ), ), ), ); } else { return Container( width: 18, height: 18, decoration: BoxDecoration( // color: Theme.of(context).extension()!.accentColorDark borderRadius: BorderRadius.circular( 18), ), child: SvgPicture.asset( Assets.svg.circleQuestion, width: 18, height: 18, color: Theme.of(context) .extension< StackColors>()! .textSubtitle2, ), ); } }, ), ), const SizedBox( width: 6, ), Text( isEstimated ? ref.watch( estimatedRateExchangeFormProvider .select((value) => value .to?.ticker .toUpperCase())) ?? "-" : ref.watch( fixedRateExchangeFormProvider .select((value) => value .market?.to .toUpperCase())) ?? "-", style: STextStyles.smallMed14(context) .copyWith( color: Theme.of(context) .extension()! .accentColorDark), ), const SizedBox( width: 6, ), Builder(builder: (context) { final ticker = isEstimated ? ref.watch( estimatedRateExchangeFormProvider .select((value) => value.to ?.ticker)) ?? "-" : ref.watch( fixedRateExchangeFormProvider .select((value) => value.market ?.to)) ?? "-"; if (ticker.toLowerCase() == coin.ticker.toLowerCase()) { return Container(); } return SvgPicture.asset( Assets.svg.chevronDown, width: 5, height: 2.5, color: Theme.of(context) .extension()! .accentColorDark); }), ], ), ), ), ), ), ), ), // if (ref // .watch(exchangeFormSateProvider // .select((value) => value.minimumReceiveWarning)) // .isNotEmpty) // SizedBox( // height: 4, // ), // // if (ref // .watch(exchangeFormSateProvider // .select((value) => value.minimumReceiveWarning)) // .isNotEmpty) // Row( // children: [ // Spacer(), // Text( // ref.watch(exchangeFormSateProvider.select( // (value) => value.minimumReceiveWarning)), // style: STextStyles.errorSmall(context), // ), // ], // ), const SizedBox( height: 12, ), RateInfo( onChanged: (rateType) async { _receiveFocusNode.unfocus(); _sendFocusNode.unfocus(); switch (rateType) { case ExchangeRateType.estimated: final market = ref .read(fixedRateExchangeFormProvider) .market; final fromTicker = market?.from ?? ""; final toTicker = market?.to ?? ""; if (!(fromTicker.isEmpty || toTicker.isEmpty || toTicker == "-" || fromTicker == "-")) { final available = ref .read( availableFloatingRatePairsStateProvider .state) .state .where((e) => e.to == toTicker && e.from == fromTicker); if (available.isNotEmpty) { final availableCurrencies = ref .read( availableChangeNowCurrenciesStateProvider .state) .state .where((e) => e.ticker == fromTicker || e.ticker == toTicker); if (availableCurrencies.length > 1) { final from = availableCurrencies.firstWhere( (e) => e.ticker == fromTicker); final to = availableCurrencies.firstWhere( (e) => e.ticker == toTicker); await ref .read( estimatedRateExchangeFormProvider) .updateTo(to, false); await ref .read( estimatedRateExchangeFormProvider) .updateFrom(from, true); return; } } } unawaited(showFloatingFlushBar( type: FlushBarType.warning, message: "Estimated rate trade pair \"$fromTicker-$toTicker\" unavailable. Reverting to last estimated rate pair.", context: context, )); break; case ExchangeRateType.fixed: final fromTicker = ref .read(estimatedRateExchangeFormProvider) .from ?.ticker ?? ""; final toTicker = ref .read(estimatedRateExchangeFormProvider) .to ?.ticker ?? ""; if (!(fromTicker.isEmpty || toTicker.isEmpty || toTicker == "-" || fromTicker == "-")) { FixedRateMarket? market; try { market = ref .read(fixedRateMarketPairsStateProvider .state) .state .firstWhere((e) => e.from == fromTicker && e.to == toTicker); } catch (_) { market = null; } await ref .read(fixedRateExchangeFormProvider) .updateMarket(market, true); await ref .read(fixedRateExchangeFormProvider) .setFromAmountAndCalculateToAmount( Decimal.tryParse( _sendController.text) ?? Decimal.zero, true, ); return; } unawaited(showFloatingFlushBar( type: FlushBarType.warning, message: "Fixed rate trade pair \"$fromTicker-$toTicker\" unavailable. Reverting to last fixed rate pair.", context: context, )); break; } }, ), const SizedBox( height: 12, ), const Spacer(), TextButton( style: ((ref .read(prefsChangeNotifierProvider) .exchangeRateType == ExchangeRateType.estimated) ? ref.watch(estimatedRateExchangeFormProvider .select((value) => value.canExchange)) : ref.watch(fixedRateExchangeFormProvider .select((value) => value.canExchange))) ? Theme.of(context) .extension()! .getPrimaryEnabledButtonColor(context) : Theme.of(context) .extension()! .getSecondaryEnabledButtonColor(context), onPressed: ((ref .read(prefsChangeNotifierProvider) .exchangeRateType == ExchangeRateType.estimated) ? ref.watch(estimatedRateExchangeFormProvider .select((value) => value.canExchange)) : ref.watch(fixedRateExchangeFormProvider .select((value) => value.canExchange))) ? () async { final isEstimated = ref .read(prefsChangeNotifierProvider) .exchangeRateType == ExchangeRateType.estimated; // final ft = isEstimated // ? ref // .read( // estimatedRateExchangeFormProvider) // .from // ?.ticker ?? // "" // : ref // .read( // fixedRateExchangeFormProvider) // .market // ?.from ?? // ""; // // final manager = ref // .read(walletsChangeNotifierProvider) // .getManager(walletId); final sendAmount = Decimal.parse(ref .read(estimatedRateExchangeFormProvider) .fromAmountString); // if (ft.toLowerCase() == // coin.ticker.toLowerCase()) { // bool shouldPop = false; // bool wasPopped = false; // unawaited(showDialog( // context: context, // builder: (_) => WillPopScope( // onWillPop: () async { // if (shouldPop) { // wasPopped = true; // } // return shouldPop; // }, // child: const CustomLoadingOverlay( // message: "Checking available balance", // eventBus: null, // ), // ), // )); // // final availableBalance = // await manager.availableBalance; // // final feeObject = await manager.fees; // // final fee = await manager.estimateFeeFor( // Format.decimalAmountToSatoshis( // sendAmount), // feeObject.medium); // // shouldPop = true; // if (!wasPopped && mounted) { // Navigator.of(context).pop(); // } // // if (availableBalance < // sendAmount + // Format.satoshisToAmount(fee)) { // unawaited(showDialog( // context: context, // builder: (_) => StackOkDialog( // title: "Insufficient balance", // message: // "Current ${coin.prettyName} wallet does not have enough ${coin.ticker} for this trade", // ), // )); // return; // } // } if (isEstimated) { final fromTicker = ref .read( estimatedRateExchangeFormProvider) .from ?.ticker ?? ""; final toTicker = ref .read( estimatedRateExchangeFormProvider) .to ?.ticker ?? ""; bool isAvailable = false; final availableFloatingPairs = ref .read( availableFloatingRatePairsStateProvider .state) .state; for (final pair in availableFloatingPairs) { if (pair.from == fromTicker && pair.to == toTicker) { isAvailable = true; break; } } if (!isAvailable) { unawaited(showDialog( context: context, barrierDismissible: true, builder: (_) => StackDialog( title: "Selected trade pair unavailable", message: "The $fromTicker - $toTicker market is currently disabled for estimated/floating rate trades", ), )); return; } final rateType = ref .read(prefsChangeNotifierProvider) .exchangeRateType; final response = await ref .read(changeNowProvider) .getEstimate( fromTicker, toTicker, sendAmount, false, false, ); if (response.value == null) { unawaited(showDialog( context: context, barrierDismissible: true, builder: (_) => StackDialog( title: "Failed to update trade estimate", message: response.exception?.toString(), ), )); return; } String rate = "1 ${fromTicker.toUpperCase()} ~${(response.value!.estimatedAmount / sendAmount).toDecimal(scaleOnInfinitePrecision: 8).toStringAsFixed(8)} ${toTicker.toUpperCase()}"; final model = IncompleteExchangeModel( sendTicker: fromTicker.toUpperCase(), receiveTicker: toTicker.toUpperCase(), rateInfo: rate, sendAmount: sendAmount, receiveAmount: response.value!.estimatedAmount, rateType: rateType, rateId: response.value!.rateId, ); if (mounted) { ref .read( exchangeSendFromWalletIdStateProvider .state) .state = Tuple2(walletId, coin); unawaited(Navigator.of(context).pushNamed( Step2View.routeName, arguments: model, )); } } else { final fromTicker = ref .read(fixedRateExchangeFormProvider) .market ?.from ?? ""; final toTicker = ref .read(fixedRateExchangeFormProvider) .market ?.to ?? ""; final sendAmount = Decimal.parse(ref .read(fixedRateExchangeFormProvider) .fromAmountString); final rateType = ref .read(prefsChangeNotifierProvider) .exchangeRateType; final response = await ref .read(changeNowProvider) .getEstimate( fromTicker, toTicker, sendAmount, true, false, ); bool? shouldCancel; if (response.value == null) { unawaited(showDialog( context: context, barrierDismissible: true, builder: (_) => StackDialog( title: "Failed to update trade estimate", message: response.exception?.toString(), ), )); return; } else if (response.value!.warningMessage != null && response.value!.warningMessage! .isNotEmpty) { shouldCancel = await showDialog( context: context, barrierDismissible: true, builder: (_) => StackDialog( title: "Failed to update trade estimate", message: "${response.value!.warningMessage!}\n\nDo you want to attempt trade anyways?", leftButton: TextButton( style: Theme.of(context) .extension()! .getSecondaryEnabledButtonColor( context), child: Text( "Cancel", style: STextStyles.itemSubtitle12( context), ), onPressed: () { // notify return to cancel Navigator.of(context).pop(true); }, ), rightButton: TextButton( style: Theme.of(context) .extension()! .getPrimaryEnabledButtonColor( context), child: Text( "Attempt", style: STextStyles.button(context), ), onPressed: () { // continue and try to attempt trade Navigator.of(context).pop(false); }, ), ), ); } if (shouldCancel is bool && shouldCancel) { return; } String rate = "1 $fromTicker ~${ref.read(fixedRateExchangeFormProvider).rate!.toStringAsFixed(8)} $toTicker"; final model = IncompleteExchangeModel( sendTicker: fromTicker, receiveTicker: toTicker, rateInfo: rate, sendAmount: sendAmount, receiveAmount: response.value!.estimatedAmount, rateType: rateType, rateId: response.value!.rateId, ); if (mounted) { ref .read( exchangeSendFromWalletIdStateProvider .state) .state = Tuple2(walletId, coin); unawaited(Navigator.of(context).pushNamed( Step2View.routeName, arguments: model, )); } } } : null, child: Text( "Next", style: STextStyles.button(context), ), ), ], ), ), ), ), ), ); }, ), ); } }