stack_wallet/lib/pages/send_view/send_view.dart

2185 lines
104 KiB
Dart
Raw Normal View History

2023-05-26 21:21:16 +00:00
/*
* 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 'dart:io';
import 'package:cw_core/monero_transaction_priority.dart';
2022-08-26 08:11:35 +00:00
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/flutter_svg.dart';
2023-03-07 21:45:22 +00:00
import 'package:stackwallet/models/isar/models/isar_models.dart';
2023-01-26 17:46:16 +00:00
import 'package:stackwallet/models/paynym/paynym_account_lite.dart';
2022-08-26 08:11:35 +00:00
import 'package:stackwallet/models/send_view_auto_fill_data.dart';
import 'package:stackwallet/pages/address_book_views/address_book_view.dart';
2023-03-07 21:45:22 +00:00
import 'package:stackwallet/pages/coin_control/coin_control_view.dart';
2022-08-26 08:11:35 +00:00
import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart';
import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart';
import 'package:stackwallet/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart';
2022-08-26 08:11:35 +00:00
import 'package:stackwallet/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/providers/ui/fee_rate_type_state_provider.dart';
import 'package:stackwallet/providers/ui/preview_tx_button_state_provider.dart';
2022-09-06 23:27:14 +00:00
import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart';
2022-08-26 08:11:35 +00:00
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/services/mixins/coin_control_interface.dart';
import 'package:stackwallet/themes/coin_icon_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
2022-08-26 08:11:35 +00:00
import 'package:stackwallet/utilities/address_utils.dart';
2023-04-06 21:24:56 +00:00
import 'package:stackwallet/utilities/amount/amount.dart';
2023-05-29 21:10:55 +00:00
import 'package:stackwallet/utilities/amount/amount_formatter.dart';
import 'package:stackwallet/utilities/amount/amount_input_formatter.dart';
2023-05-30 15:02:09 +00:00
import 'package:stackwallet/utilities/amount/amount_unit.dart';
2022-08-26 08:11:35 +00:00
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/barcode_scanner_interface.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
2022-08-26 08:11:35 +00:00
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
import 'package:stackwallet/wallets/models/tx_data.dart';
2023-11-16 16:54:57 +00:00
import 'package:stackwallet/wallets/wallet/mixins/paynym_interface.dart';
2022-08-26 08:11:35 +00:00
import 'package:stackwallet/widgets/animated_text.dart';
import 'package:stackwallet/widgets/background.dart';
2022-08-26 08:11:35 +00:00
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
import 'package:stackwallet/widgets/fee_slider.dart';
2022-08-26 08:11:35 +00:00
import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
2023-03-07 21:45:22 +00:00
import 'package:stackwallet/widgets/rounded_white_container.dart';
2022-08-26 08:11:35 +00:00
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
2023-03-07 21:45:22 +00:00
import 'package:tuple/tuple.dart';
2022-08-26 08:11:35 +00:00
class SendView extends ConsumerStatefulWidget {
const SendView({
Key? key,
required this.walletId,
required this.coin,
this.autoFillData,
this.clipboard = const ClipboardWrapper(),
this.barcodeScanner = const BarcodeScannerWrapper(),
2023-01-26 17:46:16 +00:00
this.accountLite,
2022-08-26 08:11:35 +00:00
}) : super(key: key);
static const String routeName = "/sendView";
final String walletId;
final Coin coin;
final SendViewAutoFillData? autoFillData;
final ClipboardInterface clipboard;
final BarcodeScannerInterface barcodeScanner;
2023-01-26 17:46:16 +00:00
final PaynymAccountLite? accountLite;
2022-08-26 08:11:35 +00:00
@override
ConsumerState<SendView> createState() => _SendViewState();
}
class _SendViewState extends ConsumerState<SendView> {
late final String walletId;
late final Coin coin;
late final ClipboardInterface clipboard;
late final BarcodeScannerInterface scanner;
late TextEditingController sendToController;
late TextEditingController cryptoAmountController;
late TextEditingController baseAmountController;
late TextEditingController noteController;
2023-07-05 12:57:17 +00:00
late TextEditingController onChainNoteController;
2022-08-26 08:11:35 +00:00
late TextEditingController feeController;
late TextEditingController memoController;
2022-08-26 08:11:35 +00:00
late final SendViewAutoFillData? _data;
final _addressFocusNode = FocusNode();
final _noteFocusNode = FocusNode();
2023-07-05 12:57:17 +00:00
final _onChainNoteFocusNode = FocusNode();
2022-08-26 08:11:35 +00:00
final _cryptoFocus = FocusNode();
final _baseFocus = FocusNode();
final _memoFocus = FocusNode();
late final bool isStellar;
2022-08-26 08:11:35 +00:00
2023-04-05 22:06:31 +00:00
Amount? _amountToSend;
Amount? _cachedAmountToSend;
2022-08-26 08:11:35 +00:00
String? _address;
bool _addressToggleFlag = false;
bool _cryptoAmountChangeLock = false;
late VoidCallback onCryptoAmountChanged;
2023-03-07 21:45:22 +00:00
Set<UTXO> selectedUTXOs = {};
2022-08-26 08:11:35 +00:00
void _cryptoAmountChanged() async {
if (!_cryptoAmountChangeLock) {
final cryptoAmount = ref.read(pAmountFormatter(coin)).tryParse(
cryptoAmountController.text,
);
if (cryptoAmount != null) {
_amountToSend = cryptoAmount;
2022-08-26 08:11:35 +00:00
if (_cachedAmountToSend != null &&
_cachedAmountToSend == _amountToSend) {
return;
}
_cachedAmountToSend = _amountToSend;
Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend",
level: LogLevel.Info);
final price =
ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1;
if (price > Decimal.zero) {
2023-04-05 22:06:31 +00:00
baseAmountController.text = (_amountToSend!.decimal * price)
.toAmount(
fractionDigits: 2,
)
2023-05-29 21:10:55 +00:00
.fiatString(
2023-04-05 22:06:31 +00:00
locale: ref.read(localeServiceChangeNotifierProvider).locale,
);
2022-08-26 08:11:35 +00:00
}
} else {
_amountToSend = null;
baseAmountController.text = "";
}
_updatePreviewButtonState(_address, _amountToSend);
_cryptoAmountChangedFeeUpdateTimer?.cancel();
_cryptoAmountChangedFeeUpdateTimer = Timer(updateFeesTimerDuration, () {
if (coin != Coin.epicCash && !_baseFocus.hasFocus) {
setState(() {
_calculateFeesFuture = calculateFees(
_amountToSend == null
2023-04-06 20:21:49 +00:00
? 0.toAmountAsRaw(fractionDigits: coin.decimals)
2023-04-05 22:06:31 +00:00
: _amountToSend!,
);
});
}
});
2022-08-26 08:11:35 +00:00
}
}
final updateFeesTimerDuration = const Duration(milliseconds: 500);
Timer? _cryptoAmountChangedFeeUpdateTimer;
Timer? _baseAmountChangedFeeUpdateTimer;
void _baseAmountChanged() {
_baseAmountChangedFeeUpdateTimer?.cancel();
_baseAmountChangedFeeUpdateTimer = Timer(updateFeesTimerDuration, () {
if (coin != Coin.epicCash && !_cryptoFocus.hasFocus) {
setState(() {
_calculateFeesFuture = calculateFees(
_amountToSend == null
2023-04-06 20:21:49 +00:00
? 0.toAmountAsRaw(fractionDigits: coin.decimals)
2023-04-05 22:06:31 +00:00
: _amountToSend!,
);
});
}
});
}
2023-04-05 22:06:31 +00:00
late Amount _currentFee;
void _setCurrentFee(String fee, bool shouldSetState) {
2023-05-30 15:02:09 +00:00
fee = fee.trim();
if (fee.startsWith("~")) {
fee = fee.substring(1);
}
if (fee.contains(" ")) {
fee = fee.split(" ").first;
}
2023-04-17 21:36:40 +00:00
final value = fee.contains(",")
? Decimal.parse(fee.replaceFirst(",", "."))
.toAmount(fractionDigits: coin.decimals)
: Decimal.parse(fee).toAmount(fractionDigits: coin.decimals);
2023-04-05 22:06:31 +00:00
if (shouldSetState) {
setState(() => _currentFee = value);
} else {
_currentFee = value;
}
}
String? _updateInvalidAddressText(String address) {
2022-08-26 08:11:35 +00:00
if (_data != null && _data!.contactLabel == address) {
return null;
}
if (address.isNotEmpty &&
!ref
.read(pWallets)
.getWallet(walletId)
.cryptoCurrency
.validateAddress(address)) {
2022-08-26 08:11:35 +00:00
return "Invalid address";
}
return null;
}
2023-04-05 22:06:31 +00:00
void _updatePreviewButtonState(String? address, Amount? amount) {
2023-01-26 18:16:38 +00:00
if (isPaynymSend) {
ref.read(previewTxButtonStateProvider.state).state =
2023-04-05 22:06:31 +00:00
(amount != null && amount > Amount.zero);
2023-01-26 18:16:38 +00:00
} else {
final isValidAddress = ref
.read(pWallets)
.getWallet(walletId)
.cryptoCurrency
2023-01-26 18:16:38 +00:00
.validateAddress(address ?? "");
ref.read(previewTxButtonStateProvider.state).state =
2023-04-05 22:06:31 +00:00
(isValidAddress && amount != null && amount > Amount.zero);
2023-01-26 18:16:38 +00:00
}
2022-08-26 08:11:35 +00:00
}
late Future<String> _calculateFeesFuture;
2023-04-05 22:06:31 +00:00
Map<Amount, String> cachedFees = {};
Map<Amount, String> cachedFiroPrivateFees = {};
Map<Amount, String> cachedFiroPublicFees = {};
2022-08-26 08:11:35 +00:00
2023-04-05 22:06:31 +00:00
Future<String> calculateFees(Amount amount) async {
if (amount <= Amount.zero) {
2022-08-26 08:11:35 +00:00
return "0";
}
if (coin == Coin.firo || coin == Coin.firoTestNet) {
if (ref.read(publicPrivateBalanceStateProvider.state).state ==
"Private") {
if (cachedFiroPrivateFees[amount] != null) {
return cachedFiroPrivateFees[amount]!;
}
} else {
if (cachedFiroPublicFees[amount] != null) {
return cachedFiroPublicFees[amount]!;
}
}
} else if (cachedFees[amount] != null) {
2022-08-26 08:11:35 +00:00
return cachedFees[amount]!;
}
final wallet = ref.read(pWallets).getWallet(walletId);
final feeObject = await wallet.fees;
2022-08-26 08:11:35 +00:00
late final int feeRate;
switch (ref.read(feeRateTypeStateProvider.state).state) {
case FeeRateType.fast:
feeRate = feeObject.fast;
break;
case FeeRateType.average:
feeRate = feeObject.medium;
break;
case FeeRateType.slow:
feeRate = feeObject.slow;
break;
default:
feeRate = -1;
2022-08-26 08:11:35 +00:00
}
2023-04-05 22:06:31 +00:00
Amount fee;
if (coin == Coin.monero) {
MoneroTransactionPriority specialMoneroId;
switch (ref.read(feeRateTypeStateProvider.state).state) {
case FeeRateType.fast:
specialMoneroId = MoneroTransactionPriority.fast;
break;
case FeeRateType.average:
specialMoneroId = MoneroTransactionPriority.regular;
break;
case FeeRateType.slow:
specialMoneroId = MoneroTransactionPriority.slow;
break;
default:
throw ArgumentError("custom fee not available for monero");
}
fee = await wallet.estimateFeeFor(amount, specialMoneroId.raw!);
2023-05-29 21:10:55 +00:00
cachedFees[amount] = ref.read(pAmountFormatter(coin)).format(
fee,
2023-05-30 15:02:09 +00:00
withUnitName: true,
indicatePrecisionLoss: false,
2023-05-29 21:10:55 +00:00
);
return cachedFees[amount]!;
} else if (coin == Coin.firo || coin == Coin.firoTestNet) {
if (ref.read(publicPrivateBalanceStateProvider.state).state ==
"Private") {
fee = await wallet.estimateFeeFor(amount, feeRate);
2023-05-29 21:10:55 +00:00
cachedFiroPrivateFees[amount] = ref.read(pAmountFormatter(coin)).format(
fee,
2023-05-30 15:02:09 +00:00
withUnitName: true,
indicatePrecisionLoss: false,
2023-05-29 21:10:55 +00:00
);
2022-08-26 08:11:35 +00:00
return cachedFiroPrivateFees[amount]!;
} else {
// TODO: [prio=high] firo public send fees refactor or something...
throw UnimplementedError("Firo pub fees todo");
// fee = await (manager.wallet as FiroWallet)
// .estimateFeeForPublic(amount, feeRate);
//
// cachedFiroPublicFees[amount] = ref.read(pAmountFormatter(coin)).format(
// fee,
// withUnitName: true,
// indicatePrecisionLoss: false,
// );
//
// return cachedFiroPublicFees[amount]!;
}
} else {
fee = await wallet.estimateFeeFor(amount, feeRate);
2023-05-29 21:10:55 +00:00
cachedFees[amount] = ref.read(pAmountFormatter(coin)).format(
fee,
2023-05-30 15:02:09 +00:00
withUnitName: true,
indicatePrecisionLoss: false,
2023-05-29 21:10:55 +00:00
);
return cachedFees[amount]!;
}
2022-08-26 08:11:35 +00:00
}
2023-01-26 18:16:38 +00:00
Future<void> _previewTransaction() async {
// wait for keyboard to disappear
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 100),
);
final wallet = ref.read(pWallets).getWallet(walletId);
2023-01-26 18:16:38 +00:00
2023-04-05 22:06:31 +00:00
final Amount amount = _amountToSend!;
final Amount availableBalance;
2023-01-26 18:16:38 +00:00
if ((coin == Coin.firo || coin == Coin.firoTestNet)) {
if (ref.read(publicPrivateBalanceStateProvider.state).state ==
"Private") {
availableBalance = ref.read(pWalletBalance(walletId)).spendable;
2023-01-26 18:16:38 +00:00
} else {
2023-04-05 22:06:31 +00:00
availableBalance =
ref.read(pWalletBalanceSecondary(walletId)).spendable;
2023-01-26 18:16:38 +00:00
}
} else {
availableBalance = ref.read(pWalletBalance(walletId)).spendable;
2023-01-26 18:16:38 +00:00
}
2023-03-08 21:19:43 +00:00
final coinControlEnabled =
ref.read(prefsChangeNotifierProvider).enableCoinControl;
2023-03-31 21:42:28 +00:00
if (coin != Coin.ethereum &&
!(wallet is CoinControlInterface && coinControlEnabled) ||
(wallet is CoinControlInterface &&
2023-03-08 21:19:43 +00:00
coinControlEnabled &&
selectedUTXOs.isEmpty)) {
2023-03-07 21:45:22 +00:00
// confirm send all
if (amount == availableBalance) {
bool? shouldSendAll;
if (mounted) {
shouldSendAll = await showDialog<bool>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return StackDialog(
title: "Confirm send all",
message:
"You are about to send your entire balance. Would you like to continue?",
leftButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(context),
child: Text(
"Cancel",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
onPressed: () {
Navigator.of(context).pop(false);
},
2023-03-07 21:45:22 +00:00
),
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context),
child: Text(
"Yes",
style: STextStyles.button(context),
),
onPressed: () {
Navigator.of(context).pop(true);
},
2023-03-07 21:45:22 +00:00
),
);
},
);
}
2023-01-26 18:16:38 +00:00
2023-03-07 21:45:22 +00:00
if (shouldSendAll == null || shouldSendAll == false) {
// cancel preview
return;
}
2023-01-26 18:16:38 +00:00
}
}
try {
bool wasCancelled = false;
if (mounted) {
unawaited(
showDialog<void>(
context: context,
useSafeArea: false,
barrierDismissible: false,
builder: (context) {
return BuildingTransactionDialog(
coin: wallet.info.coin,
onCancel: () {
wasCancelled = true;
2023-01-26 18:16:38 +00:00
Navigator.of(context).pop();
},
);
},
),
);
}
2023-01-26 18:16:38 +00:00
final time = Future<dynamic>.delayed(
const Duration(
2023-03-14 15:43:54 +00:00
milliseconds: 2500,
),
);
Future<TxData> txDataFuture;
2023-01-26 18:16:38 +00:00
if (isPaynymSend) {
2023-11-16 16:54:57 +00:00
final feeRate = ref.read(feeRateTypeStateProvider);
txDataFuture = (wallet as PaynymInterface).preparePaymentCodeSend(
txData: TxData(
paynymAccountLite: widget.accountLite!,
recipients: [
(
address: widget.accountLite!.code,
amount: amount,
)
],
satsPerVByte: isCustomFee ? customFeeRate : null,
feeRateType: feeRate,
utxos: (wallet is CoinControlInterface &&
coinControlEnabled &&
selectedUTXOs.isNotEmpty)
? selectedUTXOs
: null,
),
2023-01-26 18:16:38 +00:00
);
} else if ((coin == Coin.firo || coin == Coin.firoTestNet) &&
ref.read(publicPrivateBalanceStateProvider.state).state !=
"Private") {
throw UnimplementedError("FIXME");
// TODO: [prio=high] firo prepare send using TxData
// txDataFuture = (manager.wallet as FiroWallet).prepareSendPublic(
// address: _address!,
// amount: amount,
// args: {
// "feeRate": ref.read(feeRateTypeStateProvider),
// "satsPerVByte": isCustomFee ? customFeeRate : null,
// },
// );
2023-01-26 18:16:38 +00:00
} else {
final memo = coin == Coin.stellar || coin == Coin.stellarTestnet
? memoController.text
: null;
txDataFuture = wallet.prepareSend(
txData: TxData(
recipients: [(address: _address!, amount: amount)],
memo: memo,
feeRateType: ref.read(feeRateTypeStateProvider),
satsPerVByte: isCustomFee ? customFeeRate : null,
utxos: (wallet is CoinControlInterface &&
2023-03-08 21:19:43 +00:00
coinControlEnabled &&
selectedUTXOs.isNotEmpty)
2023-03-07 21:45:22 +00:00
? selectedUTXOs
: null,
),
2023-01-26 18:16:38 +00:00
);
}
final results = await Future.wait([
txDataFuture,
time,
]);
TxData txData = results.first as TxData;
2023-01-26 18:16:38 +00:00
if (!wasCancelled && mounted) {
if (isPaynymSend) {
2023-11-16 16:54:57 +00:00
txData = txData.copyWith(
paynymAccountLite: widget.accountLite!,
note: noteController.text.isNotEmpty
? noteController.text
: "PayNym send",
);
} else {
txData = txData.copyWith(note: noteController.text);
txData = txData.copyWith(noteOnChain: onChainNoteController.text);
2023-01-26 18:16:38 +00:00
}
2023-11-16 16:54:57 +00:00
// pop building dialog
Navigator.of(context).pop();
2023-01-26 18:16:38 +00:00
unawaited(Navigator.of(context).push(
RouteGenerator.getRoute(
shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute,
builder: (_) => ConfirmTransactionView(
txData: txData,
2023-01-26 18:16:38 +00:00
walletId: walletId,
isPaynymTransaction: isPaynymSend,
),
settings: const RouteSettings(
name: ConfirmTransactionView.routeName,
),
),
));
}
} catch (e) {
if (mounted) {
// pop building dialog
Navigator.of(context).pop();
unawaited(showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return StackDialog(
title: "Transaction failed",
message: e.toString(),
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(context),
child: Text(
"Ok",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
onPressed: () {
Navigator.of(context).pop();
},
),
);
},
));
}
}
}
2023-01-26 17:46:16 +00:00
bool get isPaynymSend => widget.accountLite != null;
bool isCustomFee = false;
int customFeeRate = 1;
2022-08-26 08:11:35 +00:00
@override
void initState() {
2023-04-05 22:06:31 +00:00
coin = widget.coin;
2022-08-26 08:11:35 +00:00
ref.refresh(feeSheetSessionCacheProvider);
2023-04-06 20:21:49 +00:00
_currentFee = 0.toAmountAsRaw(fractionDigits: coin.decimals);
2022-08-26 08:11:35 +00:00
2023-04-05 22:06:31 +00:00
_calculateFeesFuture =
2023-04-06 20:21:49 +00:00
calculateFees(0.toAmountAsRaw(fractionDigits: coin.decimals));
2022-08-26 08:11:35 +00:00
_data = widget.autoFillData;
walletId = widget.walletId;
clipboard = widget.clipboard;
scanner = widget.barcodeScanner;
isStellar = coin == Coin.stellar || coin == Coin.stellarTestnet;
2022-08-26 08:11:35 +00:00
sendToController = TextEditingController();
cryptoAmountController = TextEditingController();
baseAmountController = TextEditingController();
noteController = TextEditingController();
2023-07-05 12:57:17 +00:00
onChainNoteController = TextEditingController();
2022-08-26 08:11:35 +00:00
feeController = TextEditingController();
memoController = TextEditingController();
2022-08-26 08:11:35 +00:00
onCryptoAmountChanged = _cryptoAmountChanged;
cryptoAmountController.addListener(onCryptoAmountChanged);
baseAmountController.addListener(_baseAmountChanged);
2022-08-26 08:11:35 +00:00
if (_data != null) {
if (_data!.amount != null) {
2023-05-30 15:02:09 +00:00
final amount = Amount.fromDecimal(
_data!.amount!,
fractionDigits: coin.decimals,
);
cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format(
amount,
withUnitName: false,
);
2022-08-26 08:11:35 +00:00
}
sendToController.text = _data!.contactLabel;
_address = _data!.address.trim();
2022-08-26 08:11:35 +00:00
_addressToggleFlag = true;
}
2023-01-26 17:46:16 +00:00
if (isPaynymSend) {
sendToController.text = widget.accountLite!.nymName;
noteController.text = "PayNym send";
}
// if (coin != Coin.epicCash) {
// _cryptoFocus.addListener(() {
// if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) {
// if (_amountToSend == null) {
// setState(() {
// _calculateFeesFuture = calculateFees(0);
// });
// } else {
// setState(() {
// _calculateFeesFuture = calculateFees(
// Format.decimalAmountToSatoshis(_amountToSend!, coin));
// });
// }
// }
// });
// _baseFocus.addListener(() {
// if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) {
// if (_amountToSend == null) {
// setState(() {
// _calculateFeesFuture = calculateFees(0);
// });
// } else {
// setState(() {
// _calculateFeesFuture = calculateFees(
// Format.decimalAmountToSatoshis(_amountToSend!, coin));
// });
// }
// }
// });
// }
2022-08-26 08:11:35 +00:00
super.initState();
}
@override
void dispose() {
_cryptoAmountChangedFeeUpdateTimer?.cancel();
_baseAmountChangedFeeUpdateTimer?.cancel();
2022-08-26 08:11:35 +00:00
cryptoAmountController.removeListener(onCryptoAmountChanged);
baseAmountController.removeListener(_baseAmountChanged);
2022-08-26 08:11:35 +00:00
sendToController.dispose();
cryptoAmountController.dispose();
baseAmountController.dispose();
noteController.dispose();
2023-07-05 12:57:17 +00:00
onChainNoteController.dispose();
2022-08-26 08:11:35 +00:00
feeController.dispose();
memoController.dispose();
2022-08-26 08:11:35 +00:00
_noteFocusNode.dispose();
2023-07-05 12:57:17 +00:00
_onChainNoteFocusNode.dispose();
2022-08-26 08:11:35 +00:00
_addressFocusNode.dispose();
_cryptoFocus.dispose();
_baseFocus.dispose();
_memoFocus.dispose();
2022-08-26 08:11:35 +00:00
super.dispose();
}
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
final wallet = ref.watch(pWallets).getWallet(walletId);
2022-08-26 08:11:35 +00:00
final String locale = ref.watch(
localeServiceChangeNotifierProvider.select((value) => value.locale));
final showCoinControl = wallet is CoinControlInterface &&
2023-03-08 21:19:43 +00:00
ref.watch(
prefsChangeNotifierProvider.select(
(value) => value.enableCoinControl,
),
);
2023-03-07 21:45:22 +00:00
if (coin == Coin.firo || coin == Coin.firoTestNet) {
ref.listen(publicPrivateBalanceStateProvider, (previous, next) {
if (_amountToSend == null) {
setState(() {
2023-04-05 22:06:31 +00:00
_calculateFeesFuture =
2023-04-06 20:21:49 +00:00
calculateFees(0.toAmountAsRaw(fractionDigits: coin.decimals));
});
} else {
setState(() {
_calculateFeesFuture = calculateFees(
2023-04-05 22:06:31 +00:00
_amountToSend!,
);
});
}
});
}
2023-03-06 23:24:46 +00:00
// add listener for epic cash to strip http:// and https:// prefixes if the address also ocntains an @ symbol (indicating an epicbox address)
if (coin == Coin.epicCash) {
sendToController.addListener(() {
_address = sendToController.text.trim();
if (_address != null && _address!.isNotEmpty) {
_address = _address!.trim();
if (_address!.contains("\n")) {
_address = _address!.substring(0, _address!.indexOf("\n"));
}
2023-03-07 20:55:05 +00:00
sendToController.text = formatAddress(_address!);
}
});
}
return Background(
child: Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 50));
}
if (mounted) {
Navigator.of(context).pop();
}
},
),
title: Text(
"Send ${coin.ticker}",
style: STextStyles.navBarTitle(context),
),
2022-08-26 08:11:35 +00:00
),
body: LayoutBuilder(
builder: (builderContext, constraints) {
return Padding(
padding: const EdgeInsets.only(
left: 12,
top: 12,
right: 12,
),
child: SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
// subtract top and bottom padding set in parent
minHeight: constraints.maxHeight - 24,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.popupBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
2022-08-26 08:11:35 +00:00
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: [
SvgPicture.file(
File(
ref.watch(
coinIconProvider(coin),
),
),
width: 22,
height: 22,
2022-09-06 23:27:14 +00:00
),
const SizedBox(
width: 6,
),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
ref.watch(pWalletName(walletId)),
style: STextStyles.titleBold12(context)
.copyWith(fontSize: 14),
overflow: TextOverflow.ellipsis,
maxLines: 1,
2022-09-06 23:27:14 +00:00
),
// const SizedBox(
// height: 2,
// ),
if (coin == Coin.firo ||
coin == Coin.firoTestNet)
Text(
"${ref.watch(publicPrivateBalanceStateProvider.state).state} balance",
style: STextStyles.label(context)
.copyWith(fontSize: 10),
2022-08-26 08:11:35 +00:00
),
if (coin != Coin.firo &&
coin != Coin.firoTestNet)
Text(
"Available balance",
style: STextStyles.label(context)
.copyWith(fontSize: 10),
),
],
),
const Spacer(),
Builder(builder: (context) {
final Amount amount;
if (coin != Coin.firo &&
coin != Coin.firoTestNet) {
if (ref
.watch(
publicPrivateBalanceStateProvider
.state)
.state ==
"Private") {
amount = ref
.read(pWalletBalance(walletId))
.spendable;
} else {
amount = ref
.read(pWalletBalanceSecondary(
walletId))
.spendable;
}
} else {
amount = ref
.read(pWalletBalance(walletId))
.spendable;
}
return GestureDetector(
onTap: () {
cryptoAmountController.text = ref
.read(pAmountFormatter(coin))
.format(
amount,
withUnitName: false,
);
},
child: Container(
color: Colors.transparent,
child: Column(
crossAxisAlignment:
CrossAxisAlignment.end,
children: [
Text(
ref
.watch(pAmountFormatter(coin))
.format(amount),
style: STextStyles.titleBold12(
context)
.copyWith(
fontSize: 10,
),
textAlign: TextAlign.right,
2022-08-26 08:11:35 +00:00
),
Text(
"${(amount.decimal * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin).item1))).toAmount(
fractionDigits: 2,
).fiatString(
locale: locale,
)} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}",
style:
STextStyles.subtitle(context)
.copyWith(
fontSize: 8,
),
textAlign: TextAlign.right,
)
],
),
),
);
}),
],
),
2022-08-26 08:11:35 +00:00
),
),
const SizedBox(
height: 16,
2022-08-26 08:11:35 +00:00
),
2023-05-26 21:17:09 +00:00
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
2023-06-07 13:36:54 +00:00
isPaynymSend
? "Send to PayNym address"
: "Send to",
2023-05-26 21:17:09 +00:00
style: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
2023-06-07 13:36:54 +00:00
// if (coin == Coin.monero)
// CustomTextButton(
// text: "Use OpenAlias",
// onTap: () async {
// await showModalBottomSheet(
// context: context,
// builder: (context) =>
// OpenAliasBottomSheet(
// onSelected: (address) {
// sendToController.text = address;
// },
// ),
// );
// },
// ),
2023-05-26 21:17:09 +00:00
],
),
const SizedBox(
height: 8,
),
2023-01-26 17:46:16 +00:00
if (isPaynymSend)
TextField(
key: const Key("sendViewPaynymAddressFieldKey"),
controller: sendToController,
2023-01-26 17:46:16 +00:00
enabled: false,
readOnly: true,
style: STextStyles.fieldLabel(context),
),
if (!isPaynymSend)
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
2022-08-26 08:11:35 +00:00
),
2023-01-26 17:46:16 +00:00
child: TextField(
key: const Key("sendViewAddressFieldKey"),
controller: sendToController,
readOnly: false,
autocorrect: false,
enableSuggestions: false,
// inputFormatters: <TextInputFormatter>[
// FilteringTextInputFormatter.allow(
// RegExp("[a-zA-Z0-9]{34}")),
// ],
toolbarOptions: const ToolbarOptions(
copy: false,
cut: false,
paste: true,
selectAll: false,
),
2023-01-26 17:46:16 +00:00
onChanged: (newValue) {
_address = newValue.trim();
2023-01-26 17:46:16 +00:00
_updatePreviewButtonState(
_address, _amountToSend);
setState(() {
_addressToggleFlag = newValue.isNotEmpty;
});
},
focusNode: _addressFocusNode,
style: STextStyles.field(context),
decoration: standardInputDecoration(
"Enter ${coin.ticker} address",
_addressFocusNode,
context,
).copyWith(
contentPadding: const EdgeInsets.only(
left: 16,
top: 6,
bottom: 8,
right: 5,
),
suffixIcon: Padding(
padding: sendToController.text.isEmpty
? const EdgeInsets.only(right: 8)
: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceAround,
children: [
_addressToggleFlag
? TextFieldIconButton(
semanticsLabel:
"Clear Button. Clears The Address Field Input.",
2023-01-26 17:46:16 +00:00
key: const Key(
"sendViewClearAddressFieldButtonKey"),
onTap: () {
sendToController.text = "";
_address = "";
_updatePreviewButtonState(
_address,
_amountToSend);
setState(() {
_addressToggleFlag =
false;
});
},
child: const XIcon(),
)
: TextFieldIconButton(
semanticsLabel:
"Paste Button. Pastes From Clipboard To Address Field Input.",
2023-01-26 17:46:16 +00:00
key: const Key(
"sendViewPasteAddressFieldButtonKey"),
onTap: () async {
final ClipboardData? data =
await clipboard.getData(
Clipboard
.kTextPlain);
if (data?.text != null &&
data!
.text!.isNotEmpty) {
String content =
data.text!.trim();
if (content
.contains("\n")) {
content =
content.substring(
0,
content.indexOf(
"\n"));
}
if (coin ==
Coin.epicCash) {
// strip http:// and https:// if content contains @
2023-03-07 20:55:05 +00:00
content = formatAddress(
content);
}
2023-01-26 17:46:16 +00:00
sendToController.text =
content.trim();
_address = content.trim();
2023-01-26 17:46:16 +00:00
_updatePreviewButtonState(
_address,
_amountToSend);
setState(() {
_addressToggleFlag =
sendToController
.text
.isNotEmpty;
});
}
2023-01-26 17:46:16 +00:00
},
child: sendToController
.text.isEmpty
? const ClipboardIcon()
: const XIcon(),
),
if (sendToController.text.isEmpty)
TextFieldIconButton(
semanticsLabel:
"Address Book Button. Opens Address Book For Address Field.",
2023-01-26 17:46:16 +00:00
key: const Key(
"sendViewAddressBookButtonKey"),
onTap: () {
Navigator.of(context).pushNamed(
AddressBookView.routeName,
arguments: widget.coin,
);
},
child: const AddressBookIcon(),
),
if (sendToController.text.isEmpty)
TextFieldIconButton(
semanticsLabel:
"Scan QR Button. Opens Camera For Scanning QR Code.",
2023-01-26 17:46:16 +00:00
key: const Key(
"sendViewScanQrButtonKey"),
onTap: () async {
try {
// ref
// .read(
// shouldShowLockscreenOnResumeStateProvider
// .state)
// .state = false;
if (FocusScope.of(context)
.hasFocus) {
FocusScope.of(context)
.unfocus();
await Future<void>.delayed(
const Duration(
milliseconds: 75));
}
2023-01-26 17:46:16 +00:00
final qrResult =
await scanner.scan();
// Future<void>.delayed(
// const Duration(seconds: 2),
// () => ref
// .read(
// shouldShowLockscreenOnResumeStateProvider
// .state)
// .state = true,
// );
Logging.instance.log(
"qrResult content: ${qrResult.rawContent}",
level: LogLevel.Info);
final results =
AddressUtils.parseUri(
qrResult.rawContent);
Logging.instance.log(
"qrResult parsed: $results",
level: LogLevel.Info);
if (results.isNotEmpty &&
results["scheme"] ==
coin.uriScheme) {
// auto fill address
_address =
(results["address"] ??
"")
.trim();
sendToController.text =
2023-01-26 17:46:16 +00:00
_address!;
// autofill notes field
if (results["message"] !=
null) {
noteController.text =
results["message"]!;
} else if (results[
"label"] !=
null) {
noteController.text =
results["label"]!;
}
// autofill amount field
if (results["amount"] !=
null) {
2023-04-05 22:06:31 +00:00
final Amount amount =
2023-01-26 17:46:16 +00:00
Decimal.parse(results[
2023-04-05 22:06:31 +00:00
"amount"]!)
.toAmount(
fractionDigits:
coin.decimals,
);
2023-01-26 17:46:16 +00:00
cryptoAmountController
.text =
2023-05-29 21:10:55 +00:00
ref
.read(
pAmountFormatter(
coin))
.format(
amount,
withUnitName:
false,
);
2023-01-26 17:46:16 +00:00
_amountToSend = amount;
}
_updatePreviewButtonState(
_address,
_amountToSend);
setState(() {
_addressToggleFlag =
sendToController
.text.isNotEmpty;
});
2022-08-26 08:11:35 +00:00
2023-01-26 17:46:16 +00:00
// now check for non standard encoded basic address
} else if (ref
.read(pWallets)
.getWallet(walletId)
.cryptoCurrency
2023-01-26 17:46:16 +00:00
.validateAddress(qrResult
.rawContent)) {
_address = qrResult
.rawContent
.trim();
2023-01-26 17:46:16 +00:00
sendToController.text =
_address ?? "";
2023-01-26 17:46:16 +00:00
_updatePreviewButtonState(
_address,
_amountToSend);
setState(() {
_addressToggleFlag =
sendToController
.text.isNotEmpty;
});
}
2023-01-26 17:46:16 +00:00
} on PlatformException catch (e, s) {
// ref
// .read(
// shouldShowLockscreenOnResumeStateProvider
// .state)
// .state = true;
// here we ignore the exception caused by not giving permission
// to use the camera to scan a qr code
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning);
2022-08-26 08:11:35 +00:00
}
2023-01-26 17:46:16 +00:00
},
child: const QrCodeIcon(),
)
],
),
),
2022-08-26 08:11:35 +00:00
),
),
),
),
const SizedBox(
height: 10,
),
if (isStellar)
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("sendViewMemoFieldKey"),
controller: memoController,
readOnly: false,
autocorrect: false,
enableSuggestions: false,
focusNode: _memoFocus,
style: STextStyles.field(context),
onChanged: (_) {
setState(() {});
},
decoration: standardInputDecoration(
"Enter memo (optional)",
_memoFocus,
context,
).copyWith(
contentPadding: const EdgeInsets.only(
left: 16,
top: 6,
bottom: 8,
right: 5,
),
suffixIcon: Padding(
padding: memoController.text.isEmpty
? const EdgeInsets.only(right: 8)
: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceAround,
children: [
memoController.text.isNotEmpty
? TextFieldIconButton(
semanticsLabel:
"Clear Button. Clears The Memo Field Input.",
key: const Key(
"sendViewClearMemoFieldButtonKey"),
onTap: () {
memoController.text = "";
setState(() {});
},
child: const XIcon(),
)
: TextFieldIconButton(
semanticsLabel:
"Paste Button. Pastes From Clipboard To Memo Field Input.",
key: const Key(
"sendViewPasteMemoFieldButtonKey"),
onTap: () async {
final ClipboardData? data =
await clipboard.getData(
Clipboard
.kTextPlain);
if (data?.text != null &&
data!
.text!.isNotEmpty) {
String content =
data.text!.trim();
memoController.text =
content.trim();
setState(() {});
}
},
child: const ClipboardIcon(),
),
],
),
),
),
),
),
),
Builder(
builder: (_) {
final error = _updateInvalidAddressText(
_address ?? "",
);
if (error == null || error.isEmpty) {
return Container();
} else {
return Align(
alignment: Alignment.topLeft,
child: Padding(
padding: const EdgeInsets.only(
left: 12.0,
top: 4.0,
),
child: Text(
error,
textAlign: TextAlign.left,
style:
STextStyles.label(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textError,
),
2022-08-26 08:11:35 +00:00
),
),
);
}
},
2022-09-06 23:27:14 +00:00
),
if (coin == Coin.firo)
const SizedBox(
height: 12,
),
if (coin == Coin.firo)
Text(
"Send from",
style: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
if (coin == Coin.firo)
const SizedBox(
height: 8,
),
if (coin == Coin.firo)
Stack(
children: [
TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions:
Util.isDesktop ? false : true,
readOnly: true,
textInputAction: TextInputAction.none,
2022-09-06 23:27:14 +00:00
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
2022-09-06 23:27:14 +00:00
),
child: RawMaterialButton(
splashColor: Theme.of(context)
.extension<StackColors>()!
.highlight,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
2022-09-06 23:27:14 +00:00
),
),
onPressed: () {
showModalBottomSheet<dynamic>(
backgroundColor: Colors.transparent,
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
2022-09-06 23:27:14 +00:00
),
),
builder: (_) =>
FiroBalanceSelectionSheet(
walletId: walletId,
),
);
},
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
"${ref.watch(publicPrivateBalanceStateProvider.state).state} balance",
style: STextStyles.itemSubtitle12(
context),
),
const SizedBox(
width: 10,
),
if (ref
.read(
publicPrivateBalanceStateProvider
.state)
.state ==
"Private")
Text(
ref
.watch(
pAmountFormatter(coin))
.format(
ref
.watch(pWalletBalance(
walletId))
.spendable,
),
style: STextStyles.itemSubtitle(
context),
)
else
Text(
ref
.watch(
pAmountFormatter(coin))
.format(
ref
.watch(
pWalletBalanceSecondary(
walletId))
.spendable,
),
style: STextStyles.itemSubtitle(
context),
),
],
),
SvgPicture.asset(
Assets.svg.chevronDown,
width: 8,
height: 4,
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle2,
),
],
),
2022-09-06 23:27:14 +00:00
),
)
],
2022-08-26 08:11:35 +00:00
),
const SizedBox(
height: 12,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Amount",
style: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
2023-03-31 21:42:28 +00:00
if (coin != Coin.ethereum)
CustomTextButton(
text: "Send all ${coin.ticker}",
onTap: () async {
if ((coin == Coin.firo ||
coin == Coin.firoTestNet) &&
ref
.read(
publicPrivateBalanceStateProvider
.state)
.state ==
"Public") {
cryptoAmountController.text = ref
.read(pAmountFormatter(coin))
.format(
ref
.read(pWalletBalanceSecondary(
walletId))
.spendable,
withUnitName: false,
);
} else {
2023-04-05 22:06:31 +00:00
cryptoAmountController.text = ref
2023-05-29 21:10:55 +00:00
.read(pAmountFormatter(coin))
.format(
ref
.read(pWalletBalance(walletId))
2023-05-29 21:10:55 +00:00
.spendable,
withUnitName: false,
);
}
2023-03-31 21:42:28 +00:00
_cryptoAmountChanged();
},
),
],
2022-08-26 08:11:35 +00:00
),
const SizedBox(
height: 8,
2022-08-26 08:11:35 +00:00
),
TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
style: STextStyles.smallMed14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
2022-08-26 08:11:35 +00:00
),
key:
const Key("amountInputFieldCryptoTextFieldKey"),
controller: cryptoAmountController,
focusNode: _cryptoFocus,
2022-12-30 17:36:00 +00:00
keyboardType: Util.isDesktop
? null
: const TextInputType.numberWithOptions(
signed: false,
decimal: true,
),
textAlign: TextAlign.right,
inputFormatters: [
AmountInputFormatter(
decimals: coin.decimals,
2023-06-16 23:04:39 +00:00
unit: ref.watch(pAmountUnit(coin)),
locale: locale,
),
// 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})$')
// // RegExp(r'^\d{1,3}([,\.]\d+)?|[,\.\d]+$')
// getAmountRegex(locale, coin.decimals)
// .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: Padding(
padding: const EdgeInsets.all(12),
child: Text(
2023-05-30 15:02:09 +00:00
ref
.watch(pAmountUnit(coin))
.unitForCoin(coin),
style: STextStyles.smallMed14(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
2022-08-26 08:11:35 +00:00
),
),
),
),
if (Prefs.instance.externalCalls)
const SizedBox(
height: 8,
2022-08-26 08:11:35 +00:00
),
if (Prefs.instance.externalCalls)
2022-08-26 08:11:35 +00:00
TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
style: STextStyles.smallMed14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
key:
const Key("amountInputFieldFiatTextFieldKey"),
controller: baseAmountController,
focusNode: _baseFocus,
2022-12-30 17:36:00 +00:00
keyboardType: Util.isDesktop
? null
: const TextInputType.numberWithOptions(
signed: false,
decimal: true,
),
textAlign: TextAlign.right,
inputFormatters: [
AmountInputFormatter(
decimals: 2,
locale: locale,
),
// regex to validate a fiat amount with 2 decimal places
// TextInputFormatter.withFunction((oldValue,
// newValue) =>
// // RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$')
// getAmountRegex(locale, 2)
// .hasMatch(newValue.text)
// ? newValue
// : oldValue),
],
onChanged: (baseAmountString) {
final baseAmount = Amount.tryParseFiatString(
baseAmountString,
locale: locale,
);
if (baseAmount != null) {
2023-04-05 22:06:31 +00:00
final Decimal _price = ref
.read(priceAnd24hChangeNotifierProvider)
.getPrice(coin)
.item1;
if (_price == Decimal.zero) {
2023-04-06 20:21:49 +00:00
_amountToSend = 0.toAmountAsRaw(
2023-04-05 22:06:31 +00:00
fractionDigits: coin.decimals);
} else {
2023-04-05 22:06:31 +00:00
_amountToSend = baseAmount <= Amount.zero
2023-04-06 20:21:49 +00:00
? 0.toAmountAsRaw(
2023-04-05 22:06:31 +00:00
fractionDigits: coin.decimals)
: (baseAmount.decimal / _price)
2023-04-06 23:49:13 +00:00
.toDecimal(
scaleOnInfinitePrecision:
coin.decimals,
)
2023-04-05 22:06:31 +00:00
.toAmount(
fractionDigits: coin.decimals);
}
if (_cachedAmountToSend != null &&
_cachedAmountToSend == _amountToSend) {
return;
}
_cachedAmountToSend = _amountToSend;
Logging.instance.log(
"it changed $_amountToSend $_cachedAmountToSend",
level: LogLevel.Info);
final amountString =
2023-05-29 21:10:55 +00:00
ref.read(pAmountFormatter(coin)).format(
_amountToSend!,
withUnitName: false,
);
_cryptoAmountChangeLock = true;
cryptoAmountController.text = amountString;
_cryptoAmountChangeLock = false;
} else {
2023-04-06 20:21:49 +00:00
_amountToSend = 0.toAmountAsRaw(
fractionDigits: coin.decimals);
_cryptoAmountChangeLock = true;
cryptoAmountController.text = "";
_cryptoAmountChangeLock = false;
}
// setState(() {
// _calculateFeesFuture = calculateFees(
// Format.decimalAmountToSatoshis(
// _amountToSend!));
// });
_updatePreviewButtonState(
_address, _amountToSend);
},
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: Padding(
padding: const EdgeInsets.all(12),
child: Text(
ref.watch(prefsChangeNotifierProvider
.select((value) => value.currency)),
style: STextStyles.smallMed14(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
),
),
),
),
2023-03-07 21:45:22 +00:00
if (showCoinControl)
const SizedBox(
height: 8,
),
if (showCoinControl)
RoundedWhiteContainer(
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
"Coin control",
style:
STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
CustomTextButton(
text: selectedUTXOs.isEmpty
? "Select coins"
: "Selected coins (${selectedUTXOs.length})",
onTap: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 100),
);
}
if (mounted) {
final spendable = ref
.read(pWalletBalance(walletId))
.spendable;
2023-04-05 22:06:31 +00:00
Amount? amount;
if (_amountToSend != null) {
2023-04-05 22:06:31 +00:00
amount = _amountToSend!;
if (spendable == amount) {
// this is now a send all
} else {
amount += _currentFee;
}
}
final result =
await Navigator.of(context)
.pushNamed(
CoinControlView.routeName,
arguments: Tuple4(
walletId,
CoinControlViewType.use,
amount,
selectedUTXOs,
),
);
2023-03-07 21:45:22 +00:00
if (result is Set<UTXO>) {
setState(() {
selectedUTXOs = result;
});
}
2023-03-07 21:45:22 +00:00
}
},
),
],
),
),
const SizedBox(
height: 12,
),
2023-07-05 12:57:17 +00:00
if (coin == Coin.epicCash)
Text(
"On chain Note (optional)",
style: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
if (coin == Coin.epicCash)
2023-07-05 12:57:17 +00:00
const SizedBox(
height: 8,
),
if (coin == Coin.epicCash)
2023-07-05 12:57:17 +00:00
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions:
Util.isDesktop ? false : true,
2023-07-05 12:57:17 +00:00
maxLength: 256,
controller: onChainNoteController,
focusNode: _onChainNoteFocusNode,
style: STextStyles.field(context),
onChanged: (_) => setState(() {}),
decoration: standardInputDecoration(
"Type something...",
_onChainNoteFocusNode,
context,
).copyWith(
suffixIcon: onChainNoteController
.text.isNotEmpty
2023-07-05 12:57:17 +00:00
? Padding(
padding:
const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
onChainNoteController
.text = "";
});
},
),
],
),
2023-07-05 12:57:17 +00:00
),
)
2023-07-05 12:57:17 +00:00
: null,
),
),
),
if (coin == Coin.epicCash)
const SizedBox(
height: 12,
),
Text(
(coin == Coin.epicCash)
? "Local Note (optional)"
: "Note (optional)",
style: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
const SizedBox(
height: 8,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: noteController,
focusNode: _noteFocusNode,
style: STextStyles.field(context),
onChanged: (_) => setState(() {}),
decoration: standardInputDecoration(
"Type something...",
_noteFocusNode,
context,
).copyWith(
suffixIcon: noteController.text.isNotEmpty
? Padding(
padding:
const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
noteController.text = "";
});
},
),
],
),
),
)
: null,
2022-08-26 08:11:35 +00:00
),
),
),
const SizedBox(
height: 12,
),
2023-06-17 00:32:56 +00:00
if (coin != Coin.epicCash &&
coin != Coin.nano &&
coin != Coin.banano)
2022-12-06 14:36:03 +00:00
Text(
"Transaction fee (estimated)",
style: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
2023-06-17 00:32:56 +00:00
if (coin != Coin.epicCash &&
coin != Coin.nano &&
coin != Coin.banano)
2022-12-06 14:36:03 +00:00
const SizedBox(
height: 8,
),
2023-06-17 00:32:56 +00:00
if (coin != Coin.epicCash &&
coin != Coin.nano &&
coin != Coin.banano)
2022-12-06 14:36:03 +00:00
Stack(
children: [
TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions:
Util.isDesktop ? false : true,
controller: feeController,
readOnly: true,
textInputAction: TextInputAction.none,
),
2022-12-06 14:36:03 +00:00
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
),
2022-12-06 14:36:03 +00:00
child: RawMaterialButton(
splashColor: Theme.of(context)
.extension<StackColors>()!
.highlight,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
onPressed: (coin == Coin.firo ||
coin == Coin.firoTestNet) &&
ref
.watch(
publicPrivateBalanceStateProvider
.state)
.state ==
"Private"
? null
: () {
showModalBottomSheet<dynamic>(
backgroundColor:
Colors.transparent,
context: context,
shape:
const RoundedRectangleBorder(
borderRadius:
BorderRadius.vertical(
top: Radius.circular(20),
),
),
2022-12-06 14:36:03 +00:00
builder: (_) =>
TransactionFeeSelectionSheet(
walletId: walletId,
2023-04-05 22:06:31 +00:00
amount: (Decimal.tryParse(
cryptoAmountController
.text) ??
2023-05-30 15:02:09 +00:00
_amountToSend
?.decimal ??
2023-04-05 22:06:31 +00:00
Decimal.zero)
.toAmount(
fractionDigits: coin.decimals,
),
2022-12-06 14:36:03 +00:00
updateChosen: (String fee) {
if (fee == "custom") {
if (!isCustomFee) {
setState(() {
isCustomFee = true;
});
}
return;
}
_setCurrentFee(
fee,
true,
);
2022-12-06 14:36:03 +00:00
setState(() {
_calculateFeesFuture =
Future(() => fee);
if (isCustomFee) {
isCustomFee = false;
}
2022-12-06 14:36:03 +00:00
});
},
),
);
},
child: ((coin == Coin.firo ||
coin == Coin.firoTestNet) &&
ref
.watch(
publicPrivateBalanceStateProvider
.state)
.state ==
"Private")
? Row(
children: [
FutureBuilder(
future: _calculateFeesFuture,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState
.done &&
snapshot.hasData) {
_setCurrentFee(
2023-06-17 00:32:56 +00:00
snapshot.data!,
false,
);
2022-12-06 14:36:03 +00:00
return Text(
2023-06-17 00:32:56 +00:00
"~${snapshot.data!}",
2022-12-06 14:36:03 +00:00
style: STextStyles
.itemSubtitle(
context),
);
} else {
return AnimatedText(
stringsToLoopThrough: const [
"Calculating",
"Calculating.",
"Calculating..",
"Calculating...",
],
style: STextStyles
.itemSubtitle(
context),
);
}
},
),
],
)
: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
ref
.watch(
feeRateTypeStateProvider
.state)
.state
.prettyName,
style: STextStyles
2022-12-06 14:36:03 +00:00
.itemSubtitle12(
context),
),
const SizedBox(
width: 10,
),
FutureBuilder(
future:
_calculateFeesFuture,
builder:
(context, snapshot) {
if (snapshot.connectionState ==
ConnectionState
.done &&
snapshot.hasData) {
_setCurrentFee(
snapshot.data!,
false,
);
2022-12-06 14:36:03 +00:00
return Text(
isCustomFee
? ""
: "~${snapshot.data!}",
2022-12-06 14:36:03 +00:00
style: STextStyles
.itemSubtitle(
context),
);
} else {
return AnimatedText(
stringsToLoopThrough: const [
"Calculating",
"Calculating.",
"Calculating..",
"Calculating...",
],
style: STextStyles
.itemSubtitle(
context),
);
}
},
),
],
),
SvgPicture.asset(
Assets.svg.chevronDown,
width: 8,
height: 4,
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle2,
),
],
),
),
)
],
),
if (isCustomFee)
Padding(
padding: const EdgeInsets.only(
bottom: 12,
top: 16,
),
child: FeeSlider(
coin: coin,
onSatVByteChanged: (rate) {
customFeeRate = rate;
},
),
),
const Spacer(),
const SizedBox(
height: 12,
),
TextButton(
onPressed: ref
.watch(previewTxButtonStateProvider.state)
.state
2023-01-26 18:16:38 +00:00
? _previewTransaction
: null,
style: ref
.watch(previewTxButtonStateProvider.state)
.state
? Theme.of(context)
.extension<StackColors>()!
2023-01-24 19:29:12 +00:00
.getPrimaryEnabledButtonStyle(context)
: Theme.of(context)
.extension<StackColors>()!
2023-01-24 19:29:12 +00:00
.getPrimaryDisabledButtonStyle(context),
child: Text(
"Preview",
style: STextStyles.button(context),
),
),
const SizedBox(
height: 4,
2022-08-26 08:11:35 +00:00
),
],
),
2022-08-26 08:11:35 +00:00
),
),
),
),
);
},
),
2022-08-26 08:11:35 +00:00
),
);
}
}
2023-03-07 20:55:05 +00:00
String formatAddress(String epicAddress) {
// strip http:// or https:// prefixes if the address contains an @ symbol (and is thus an epicbox address)
if ((epicAddress.startsWith("http://") ||
epicAddress.startsWith("https://")) &&
epicAddress.contains("@")) {
epicAddress = epicAddress.replaceAll("http://", "");
epicAddress = epicAddress.replaceAll("https://", "");
}
2023-03-07 20:55:22 +00:00
// strip mailto: prefix
if (epicAddress.startsWith("mailto:")) {
epicAddress = epicAddress.replaceAll("mailto:", "");
}
2023-03-07 20:55:28 +00:00
// strip / suffix if the address contains an @ symbol (and is thus an epicbox address)
if (epicAddress.endsWith("/") && epicAddress.contains("@")) {
epicAddress = epicAddress.substring(0, epicAddress.length - 1);
}
return epicAddress;
}