stack_wallet/lib/pages/exchange_view/exchange_form.dart
2024-07-04 11:40:57 -06:00

1025 lines
33 KiB
Dart

/*
* This file is part of Stack Wallet.
*
* Copyright (c) 2023 Cypher Stack
* All Rights Reserved.
* The code is distributed under GPLv3 license, see LICENSE file for details.
* Generated by Cypher Stack on 2023-05-26
*
*/
import 'dart:async';
import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_svg/svg.dart';
import 'package:isar/isar.dart';
import 'package:tuple/tuple.dart';
import 'package:uuid/uuid.dart';
import '../../models/exchange/aggregate_currency.dart';
import '../../models/exchange/incomplete_exchange.dart';
import '../../models/exchange/response_objects/estimate.dart';
import '../../models/exchange/response_objects/range.dart';
import '../../models/isar/exchange_cache/currency.dart';
import '../../models/isar/exchange_cache/pair.dart';
import '../../models/isar/models/ethereum/eth_contract.dart';
import '../../pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart';
import '../../providers/providers.dart';
import '../../services/exchange/change_now/change_now_exchange.dart';
import '../../services/exchange/exchange.dart';
import '../../services/exchange/exchange_data_loading_service.dart';
import '../../services/exchange/exchange_response.dart';
import '../../services/exchange/majestic_bank/majestic_bank_exchange.dart';
import '../../services/exchange/nanswap/nanswap_exchange.dart';
import '../../services/exchange/trocador/trocador_exchange.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/amount/amount_unit.dart';
import '../../utilities/assets.dart';
import '../../utilities/constants.dart';
import '../../utilities/enums/exchange_rate_type_enum.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
import '../../wallets/crypto_currency/crypto_currency.dart';
import '../../widgets/conditional_parent.dart';
import '../../widgets/custom_loading_overlay.dart';
import '../../widgets/desktop/desktop_dialog.dart';
import '../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../widgets/desktop/primary_button.dart';
import '../../widgets/desktop/secondary_button.dart';
import '../../widgets/rounded_container.dart';
import '../../widgets/rounded_white_container.dart';
import '../../widgets/stack_dialog.dart';
import '../../widgets/textfields/exchange_textfield.dart';
import 'exchange_coin_selection/exchange_currency_selection_view.dart';
import 'exchange_step_views/step_1_view.dart';
import 'exchange_step_views/step_2_view.dart';
import 'sub_widgets/exchange_provider_options.dart';
import 'sub_widgets/rate_type_toggle.dart';
class ExchangeForm extends ConsumerStatefulWidget {
const ExchangeForm({
super.key,
this.walletId,
this.coin,
this.contract,
});
final String? walletId;
final CryptoCurrency? coin;
final EthContract? contract;
@override
ConsumerState<ExchangeForm> createState() => _ExchangeFormState();
}
class _ExchangeFormState extends ConsumerState<ExchangeForm> {
late final String? walletId;
late final CryptoCurrency? coin;
late final bool walletInitiated;
List<Exchange> get usableExchanges {
if (ref.read(prefsChangeNotifierProvider).useTor) {
return Exchange.exchangesWithTorSupport;
} else {
return [
MajesticBankExchange.instance,
ChangeNowExchange.instance,
TrocadorExchange.instance,
NanswapExchange.instance,
];
}
}
late final TextEditingController _sendController;
late final TextEditingController _receiveController;
final isDesktop = Util.isDesktop;
final FocusNode _sendFocusNode = FocusNode();
final FocusNode _receiveFocusNode = FocusNode();
bool _swapLock = false;
// todo: check and adjust this value?
static const _valueCheckInterval = Duration(milliseconds: 1500);
Future<T> showUpdatingExchangeRate<T>({
required Future<T> whileFuture,
}) async {
unawaited(
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) => WillPopScope(
onWillPop: () async => false,
child: Container(
color: Theme.of(context)
.extension<StackColors>()!
.overlay
.withOpacity(0.6),
child: const CustomLoadingOverlay(
message: "Updating exchange rate",
eventBus: null,
),
),
),
),
);
final result = await whileFuture;
if (mounted) {
Navigator.of(context, rootNavigator: isDesktop).pop();
}
return result;
}
Timer? _sendFieldOnChangedTimer;
void sendFieldOnChanged(String value) {
if (_sendFocusNode.hasFocus) {
_sendFieldOnChangedTimer?.cancel();
_sendFieldOnChangedTimer = Timer(_valueCheckInterval, () async {
final newFromAmount = _localizedStringToNum(value);
ref.read(efSendAmountProvider.notifier).state = newFromAmount;
if (!_swapLock && !ref.read(efReversedProvider)) {
unawaited(update());
}
});
}
}
Timer? _receiveFieldOnChangedTimer;
void receiveFieldOnChanged(String value) async {
_receiveFieldOnChangedTimer?.cancel();
_receiveFieldOnChangedTimer = Timer(_valueCheckInterval, () async {
final newToAmount = _localizedStringToNum(value);
ref.read(efReceiveAmountProvider.notifier).state = newToAmount;
if (!_swapLock && ref.read(efReversedProvider)) {
unawaited(update());
}
});
}
Decimal? _localizedStringToNum(String? value) {
if (value == null) {
return null;
}
return AmountUnit.normal
.tryParse(
value,
locale: ref.read(localeServiceChangeNotifierProvider).locale,
coin: Bitcoin(
CryptoCurrencyNetwork.main,
), // dummy value (not used due to override)
overrideWithDecimalPlacesFromString: true,
)
?.decimal;
}
Future<AggregateCurrency> _getAggregateCurrency(Currency currency) async {
final rateType = ref.read(efRateTypeProvider);
final currencies = await ExchangeDataLoadingService.instance.isar.currencies
.filter()
.group(
(q) => rateType == ExchangeRateType.fixed
? q
.rateTypeEqualTo(SupportedRateType.both)
.or()
.rateTypeEqualTo(SupportedRateType.fixed)
: q
.rateTypeEqualTo(SupportedRateType.both)
.or()
.rateTypeEqualTo(SupportedRateType.estimated),
)
.and()
.tickerEqualTo(
currency.ticker,
caseSensitive: false,
)
.and()
.tokenContractEqualTo(currency.tokenContract)
.findAll();
final items = [Tuple2(currency.exchangeName, currency)];
for (final currency in currencies) {
items.add(Tuple2(currency.exchangeName, currency));
}
return AggregateCurrency(exchangeCurrencyPairs: items);
}
void selectSendCurrency() async {
final type = ref.read(efRateTypeProvider);
final fromTicker = ref.read(efCurrencyPairProvider).send?.ticker ?? "";
if (walletInitiated) {
if (widget.contract != null &&
fromTicker.toLowerCase() == widget.contract!.symbol.toLowerCase()) {
return;
}
if (fromTicker.toLowerCase() == coin!.ticker.toLowerCase()) {
// do not allow changing away from wallet coin
return;
}
}
final selectedCurrency = await _showCurrencySelectionSheet(
willChange: ref.read(efCurrencyPairProvider).send?.ticker,
willChangeIsSend: true,
paired: ref.read(efCurrencyPairProvider).receive?.ticker,
isFixedRate: type == ExchangeRateType.fixed,
);
if (selectedCurrency != null) {
await showUpdatingExchangeRate(
whileFuture: _getAggregateCurrency(selectedCurrency).then(
(aggregateSelected) => ref.read(efCurrencyPairProvider).setSend(
aggregateSelected,
notifyListeners: true,
),
),
);
}
}
void selectReceiveCurrency() async {
final toTicker = ref.read(efCurrencyPairProvider).receive?.ticker ?? "";
if (walletInitiated &&
toTicker.toLowerCase() == coin!.ticker.toLowerCase()) {
// do not allow changing away from wallet coin
return;
}
final selectedCurrency = await _showCurrencySelectionSheet(
willChange: ref.read(efCurrencyPairProvider).receive?.ticker,
willChangeIsSend: false,
paired: ref.read(efCurrencyPairProvider).send?.ticker,
isFixedRate: ref.read(efRateTypeProvider) == ExchangeRateType.fixed,
);
if (selectedCurrency != null) {
await showUpdatingExchangeRate(
whileFuture: _getAggregateCurrency(selectedCurrency).then(
(aggregateSelected) => ref.read(efCurrencyPairProvider).setReceive(
aggregateSelected,
notifyListeners: true,
),
),
);
}
}
Future<void> _swap() async {
_swapLock = true;
_sendFocusNode.unfocus();
_receiveFocusNode.unfocus();
final temp = ref.read(efCurrencyPairProvider).send;
ref.read(efCurrencyPairProvider).setSend(
ref.read(efCurrencyPairProvider).receive,
notifyListeners: true,
);
ref.read(efCurrencyPairProvider).setReceive(
temp,
notifyListeners: true,
);
// final reversed = ref.read(efReversedProvider);
final amount = ref.read(efSendAmountProvider);
ref.read(efSendAmountProvider.notifier).state =
ref.read(efReceiveAmountProvider);
ref.read(efReceiveAmountProvider.notifier).state = amount;
unawaited(update());
_swapLock = false;
}
Future<Currency?> _showCurrencySelectionSheet({
required String? willChange,
required String? paired,
required bool isFixedRate,
required bool willChangeIsSend,
}) async {
_sendFocusNode.unfocus();
_receiveFocusNode.unfocus();
final result = isDesktop
? await showDialog<Currency?>(
context: context,
builder: (context) {
return DesktopDialog(
maxHeight: 700,
maxWidth: 580,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(
left: 32,
),
child: Text(
"Choose a coin to exchange",
style: STextStyles.desktopH3(context),
),
),
const DesktopDialogCloseButton(),
],
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
bottom: 32,
),
child: Row(
children: [
Expanded(
child: RoundedWhiteContainer(
padding: const EdgeInsets.all(16),
borderColor: Theme.of(context)
.extension<StackColors>()!
.background,
child: ExchangeCurrencySelectionView(
willChangeTicker: willChange,
pairedTicker: paired,
isFixedRate: isFixedRate,
willChangeIsSend: willChangeIsSend,
),
),
),
],
),
),
),
],
),
);
},
)
: await Navigator.of(context).push(
MaterialPageRoute<dynamic>(
builder: (_) => ExchangeCurrencySelectionView(
willChangeTicker: willChange,
pairedTicker: paired,
isFixedRate: isFixedRate,
willChangeIsSend: willChangeIsSend,
),
),
);
if (mounted && result is Currency) {
return result;
} else {
return null;
}
}
void onRateTypeChanged(ExchangeRateType newType) {
_receiveFocusNode.unfocus();
_sendFocusNode.unfocus();
ref.read(efRateTypeProvider.notifier).state = newType;
update();
}
void onExchangePressed() async {
final rateType = ref.read(efRateTypeProvider);
final fromTicker = ref.read(efCurrencyPairProvider).send?.ticker ?? "";
final toTicker = ref.read(efCurrencyPairProvider).receive?.ticker ?? "";
final estimate = ref.read(efEstimateProvider)!;
final sendAmount = ref.read(efSendAmountProvider)!;
if (rateType == ExchangeRateType.fixed && toTicker.toUpperCase() == "WOW") {
await showDialog<void>(
context: context,
builder: (context) => const StackOkDialog(
title: "WOW error",
message:
"Wownero is temporarily disabled as a receiving currency for fixed rate trades due to network issues",
),
);
return;
}
String rate;
final amountToSend =
estimate.reversed ? estimate.estimatedAmount : sendAmount;
final amountToReceive = estimate.reversed
? ref.read(efReceiveAmountProvider)!
: estimate.estimatedAmount;
switch (rateType) {
case ExchangeRateType.estimated:
rate =
"1 ${fromTicker.toUpperCase()} ~${(amountToReceive / sendAmount).toDecimal(scaleOnInfinitePrecision: 8).toStringAsFixed(8)} ${toTicker.toUpperCase()}";
break;
case ExchangeRateType.fixed:
bool? shouldCancel;
if (estimate.warningMessage != null &&
estimate.warningMessage!.isNotEmpty) {
shouldCancel = await showDialog<bool?>(
context: context,
barrierDismissible: true,
builder: (_) {
if (isDesktop) {
return DesktopDialog(
maxWidth: 500,
maxHeight: 300,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Failed to update trade estimate",
style: STextStyles.desktopH3(context),
),
const DesktopDialogCloseButton(),
],
),
const Spacer(),
Text(
estimate.warningMessage!,
style: STextStyles.desktopTextSmall(context),
),
const Spacer(),
Text(
"Do you want to attempt trade anyways?",
style: STextStyles.desktopTextSmall(context),
),
const Spacer(
flex: 2,
),
Row(
children: [
Expanded(
child: SecondaryButton(
label: "Cancel",
buttonHeight: ButtonHeight.l,
onPressed: () => Navigator.of(
context,
rootNavigator: true,
).pop(true),
),
),
const SizedBox(
width: 16,
),
Expanded(
child: PrimaryButton(
label: "Attempt",
buttonHeight: ButtonHeight.l,
onPressed: () => Navigator.of(
context,
rootNavigator: true,
).pop(false),
),
),
],
),
],
),
);
} else {
return StackDialog(
title: "Failed to update trade estimate",
message:
"${estimate.warningMessage!}\n\nDo you want to attempt trade anyways?",
leftButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(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<StackColors>()!
.getPrimaryEnabledButtonStyle(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;
}
rate =
"1 ${fromTicker.toUpperCase()} ~${(amountToReceive / amountToSend).toDecimal(
scaleOnInfinitePrecision: 12,
).toStringAsFixed(8)} ${toTicker.toUpperCase()}";
break;
}
final model = IncompleteExchangeModel(
sendTicker: fromTicker.toUpperCase(),
receiveTicker: toTicker.toUpperCase(),
rateInfo: rate,
sendAmount: amountToSend,
receiveAmount: amountToReceive,
rateType: rateType,
estimate: estimate,
reversed: estimate.reversed,
walletInitiated: walletInitiated,
);
if (mounted) {
if (walletInitiated) {
ref.read(exchangeSendFromWalletIdStateProvider.state).state =
Tuple2(walletId!, coin!);
if (isDesktop) {
ref.read(ssss.state).state = model;
await showDialog<void>(
context: context,
barrierDismissible: false,
builder: (context) {
return const DesktopDialog(
maxWidth: 720,
maxHeight: double.infinity,
child: StepScaffold(
initialStep: 2,
),
);
},
);
} else {
unawaited(
Navigator.of(context).pushNamed(
Step2View.routeName,
arguments: model,
),
);
}
} else {
ref.read(exchangeSendFromWalletIdStateProvider.state).state = null;
if (isDesktop) {
ref.read(ssss.state).state = model;
await showDialog<void>(
context: context,
barrierDismissible: false,
builder: (context) {
return const DesktopDialog(
maxWidth: 720,
maxHeight: double.infinity,
child: StepScaffold(
initialStep: 1,
),
);
},
);
} else {
unawaited(
Navigator.of(context).pushNamed(
Step1View.routeName,
arguments: model,
),
);
}
}
}
}
bool isWalletCoin(CryptoCurrency? coin, bool isSend) {
if (coin == null) {
return false;
}
final String? ticker = isSend
? ref.read(efCurrencyPairProvider).send?.ticker
: ref.read(efCurrencyPairProvider).receive?.ticker;
if (ticker == null) {
return false;
}
return coin.ticker.toUpperCase() == ticker.toUpperCase();
}
Future<void> update() async {
final uuid = const Uuid().v1();
_latestUuid = uuid;
_addUpdate(uuid);
for (final exchange in usableExchanges) {
ref.read(efEstimatesListProvider(exchange.name).notifier).state = null;
}
final reversed = ref.read(efReversedProvider);
final amount = reversed
? ref.read(efReceiveAmountProvider)
: ref.read(efSendAmountProvider);
final pair = ref.read(efCurrencyPairProvider);
if (amount == null ||
amount <= Decimal.zero ||
pair.send == null ||
pair.receive == null) {
_removeUpdate(uuid);
return;
}
final rateType = ref.read(efRateTypeProvider);
final Map<String, Tuple2<ExchangeResponse<List<Estimate>>, Range?>>
results = {};
for (final exchange in usableExchanges) {
final sendCurrency = pair.send?.forExchange(exchange.name);
final receiveCurrency = pair.receive?.forExchange(exchange.name);
if (sendCurrency != null && receiveCurrency != null) {
final rangeResponse = await exchange.getRange(
reversed ? receiveCurrency.ticker : sendCurrency.ticker,
reversed ? sendCurrency.ticker : receiveCurrency.ticker,
rateType == ExchangeRateType.fixed,
);
final estimateResponse = await exchange.getEstimates(
sendCurrency.ticker,
receiveCurrency.ticker,
amount,
rateType == ExchangeRateType.fixed,
reversed,
);
results.addAll(
{
exchange.name: Tuple2(
estimateResponse,
rangeResponse.value,
),
},
);
}
}
for (final exchange in usableExchanges) {
if (uuid == _latestUuid) {
ref.read(efEstimatesListProvider(exchange.name).notifier).state =
results[exchange.name];
}
}
_removeUpdate(uuid);
}
String? _latestUuid;
final Set<String> _uuids = {};
void _addUpdate(String uuid) {
_uuids.add(uuid);
ref.read(efRefreshingProvider.notifier).state = true;
}
void _removeUpdate(String uuid) {
_uuids.remove(uuid);
if (_uuids.isEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(efRefreshingProvider.notifier).state = false;
});
}
}
void updateSend(Estimate? estimate) {
ref.read(efSendAmountProvider.notifier).state = estimate?.estimatedAmount;
}
void updateReceive(Estimate? estimate) {
ref.read(efReceiveAmountProvider.notifier).state =
estimate?.estimatedAmount;
}
@override
void initState() {
_sendController = TextEditingController();
_receiveController = TextEditingController();
walletId = widget.walletId;
coin = widget.coin;
walletInitiated = walletId != null && coin != null;
_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();
}
});
}
});
if (walletInitiated) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
ref.read(efSendAmountProvider.notifier).state = null;
ref.read(efReceiveAmountProvider.notifier).state = null;
ref.read(efReversedProvider.notifier).state = false;
ref.read(efRefreshingProvider.notifier).state = false;
ref.read(efCurrencyPairProvider).setSend(null, notifyListeners: true);
ref
.read(efCurrencyPairProvider)
.setReceive(null, notifyListeners: true);
ExchangeDataLoadingService.instance
.getAggregateCurrency(
widget.contract == null ? coin!.ticker : widget.contract!.symbol,
ExchangeRateType.estimated,
widget.contract == null ? null : widget.contract!.address,
)
.then((value) {
if (value != null) {
ref.read(efCurrencyPairProvider).setSend(
value,
notifyListeners: true,
);
}
});
});
} else {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_sendController.text = ref.read(efSendAmountStringProvider);
_receiveController.text = ref.read(efReceiveAmountStringProvider);
});
}
super.initState();
}
@override
void dispose() {
_receiveController.dispose();
_sendController.dispose();
_receiveFocusNode.dispose();
_sendFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
final rateType = ref.watch(efRateTypeProvider);
final isEstimated = rateType == ExchangeRateType.estimated;
ref.listen(efReceiveAmountStringProvider, (previous, String next) {
if (!_receiveFocusNode.hasFocus) {
_receiveController.text = isEstimated && next.isEmpty ? "-" : next;
// if (_swapLock) {
_sendController.text = ref.read(efSendAmountStringProvider);
// }
if (_sendFocusNode.hasFocus) {
_sendController.selection = TextSelection.fromPosition(
TextPosition(
offset: _sendController.text.length,
),
);
}
}
});
ref.listen(efSendAmountStringProvider, (previous, String next) {
if (!_sendFocusNode.hasFocus) {
_sendController.text = next;
// if (_swapLock) {
_receiveController.text =
isEstimated && ref.read(efReceiveAmountStringProvider).isEmpty
? "-"
: ref.read(efReceiveAmountStringProvider);
// }
if (_receiveFocusNode.hasFocus) {
_receiveController.selection = TextSelection.fromPosition(
TextPosition(
offset: _receiveController.text.length,
),
);
}
}
});
ref.listen(efEstimateProvider.notifier, (previous, next) {
final estimate = (next).state;
if (ref.read(efReversedProvider)) {
updateSend(estimate);
} else {
updateReceive(estimate);
}
});
ref.listen(efCurrencyPairProvider, (previous, next) {
if (!_swapLock) {
update();
}
});
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"You will send",
style: STextStyles.itemSubtitle(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark3,
),
),
SizedBox(
height: isDesktop ? 10 : 4,
),
ExchangeTextField(
key: Key("exchangeTextFieldKeyFor_"
"${Theme.of(context).extension<StackColors>()!.themeId}"
"${ref.watch(efCurrencyPairProvider.select((value) => value.send?.ticker))}"),
controller: _sendController,
focusNode: _sendFocusNode,
textStyle: STextStyles.smallMed14(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark,
),
buttonColor:
Theme.of(context).extension<StackColors>()!.buttonBackSecondary,
borderRadius: Constants.size.circularBorderRadius,
background:
Theme.of(context).extension<StackColors>()!.textFieldDefaultBG,
onTap: () {
if (_sendController.text == "-") {
_sendController.text = "";
}
},
onChanged: sendFieldOnChanged,
onButtonTap: selectSendCurrency,
isWalletCoin: isWalletCoin(coin, true),
currency:
ref.watch(efCurrencyPairProvider.select((value) => value.send)),
),
SizedBox(
height: isDesktop ? 10 : 4,
),
SizedBox(
height: isDesktop ? 10 : 4,
),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"You will receive",
style: STextStyles.itemSubtitle(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark3,
),
),
ConditionalParent(
condition: isDesktop,
builder: (child) => MouseRegion(
cursor: SystemMouseCursors.click,
child: child,
),
child: Semantics(
label: "Swap Button. Reverse The Exchange Currencies.",
excludeSemantics: true,
child: RoundedContainer(
padding: isDesktop
? const EdgeInsets.all(6)
: const EdgeInsets.all(2),
color: Theme.of(context)
.extension<StackColors>()!
.buttonBackSecondary,
radiusMultiplier: 0.75,
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<StackColors>()!
.accentColorDark,
),
),
),
),
),
),
],
),
SizedBox(
height: isDesktop ? 10 : 7,
),
ExchangeTextField(
key: Key(
"exchangeTextFieldKeyFor1_${Theme.of(context).extension<StackColors>()!.themeId}",
),
focusNode: _receiveFocusNode,
controller: _receiveController,
textStyle: STextStyles.smallMed14(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark,
),
buttonColor:
Theme.of(context).extension<StackColors>()!.buttonBackSecondary,
borderRadius: Constants.size.circularBorderRadius,
background:
Theme.of(context).extension<StackColors>()!.textFieldDefaultBG,
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 &&
ref.watch(efExchangeProvider).name ==
ChangeNowExchange.exchangeName,
),
SizedBox(
height: isDesktop ? 20 : 12,
),
SizedBox(
height: isDesktop ? 60 : 40,
child: RateTypeToggle(
key: UniqueKey(),
onChanged: onRateTypeChanged,
),
),
AnimatedSize(
duration: const Duration(milliseconds: 300),
child: ref.watch(efSendAmountProvider) == null &&
ref.watch(efReceiveAmountProvider) == null
? const SizedBox(
height: 0,
)
: Padding(
padding: EdgeInsets.only(top: isDesktop ? 20 : 12),
child: ExchangeProviderOptions(
fixedRate: rateType == ExchangeRateType.fixed,
reversed: ref.watch(efReversedProvider),
),
),
),
SizedBox(
height: isDesktop ? 20 : 12,
),
PrimaryButton(
buttonHeight: isDesktop ? ButtonHeight.l : null,
enabled: ref.watch(efCanExchangeProvider),
onPressed: onExchangePressed,
label: "Swap",
),
],
);
}
}