mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-12-30 15:29:41 +00:00
1025 lines
33 KiB
Dart
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",
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|