stack_wallet/lib/pages/send_view/send_view.dart

2194 lines
104 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 'dart:io';
import 'package:bip47/bip47.dart';
import 'package:cw_core/monero_transaction_priority.dart';
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';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/models/paynym/paynym_account_lite.dart';
import 'package:stackwallet/models/send_view_auto_fill_data.dart';
import 'package:stackwallet/pages/address_book_views/address_book_view.dart';
import 'package:stackwallet/pages/coin_control/coin_control_view.dart';
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';
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';
import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart';
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
import 'package:stackwallet/services/coins/manager.dart';
import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart';
import 'package:stackwallet/themes/coin_icon_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/address_utils.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/amount/amount_formatter.dart';
import 'package:stackwallet/utilities/amount/amount_input_formatter.dart';
import 'package:stackwallet/utilities/amount/amount_unit.dart';
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';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/animated_text.dart';
import 'package:stackwallet/widgets/background.dart';
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';
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';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
import 'package:tuple/tuple.dart';
class SendView extends ConsumerStatefulWidget {
const SendView({
Key? key,
required this.walletId,
required this.coin,
this.autoFillData,
this.clipboard = const ClipboardWrapper(),
this.barcodeScanner = const BarcodeScannerWrapper(),
this.accountLite,
}) : super(key: key);
static const String routeName = "/sendView";
final String walletId;
final Coin coin;
final SendViewAutoFillData? autoFillData;
final ClipboardInterface clipboard;
final BarcodeScannerInterface barcodeScanner;
final PaynymAccountLite? accountLite;
@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;
late TextEditingController onChainNoteController;
late TextEditingController feeController;
late final SendViewAutoFillData? _data;
final _addressFocusNode = FocusNode();
final _noteFocusNode = FocusNode();
final _onChainNoteFocusNode = FocusNode();
final _cryptoFocus = FocusNode();
final _baseFocus = FocusNode();
Amount? _amountToSend;
Amount? _cachedAmountToSend;
String? _address;
String? _privateBalanceString;
String? _publicBalanceString;
bool _addressToggleFlag = false;
bool _cryptoAmountChangeLock = false;
late VoidCallback onCryptoAmountChanged;
Amount? _cachedBalance;
Set<UTXO> selectedUTXOs = {};
void _cryptoAmountChanged() async {
if (!_cryptoAmountChangeLock) {
final cryptoAmount = ref.read(pAmountFormatter(coin)).tryParse(
cryptoAmountController.text,
);
if (cryptoAmount != null) {
_amountToSend = cryptoAmount;
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) {
baseAmountController.text = (_amountToSend!.decimal * price)
.toAmount(
fractionDigits: 2,
)
.fiatString(
locale: ref.read(localeServiceChangeNotifierProvider).locale,
);
}
} else {
_amountToSend = null;
baseAmountController.text = "";
}
_updatePreviewButtonState(_address, _amountToSend);
_cryptoAmountChangedFeeUpdateTimer?.cancel();
_cryptoAmountChangedFeeUpdateTimer = Timer(updateFeesTimerDuration, () {
if (coin != Coin.epicCash && !_baseFocus.hasFocus) {
setState(() {
_calculateFeesFuture = calculateFees(
_amountToSend == null
? 0.toAmountAsRaw(fractionDigits: coin.decimals)
: _amountToSend!,
);
});
}
});
}
}
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
? 0.toAmountAsRaw(fractionDigits: coin.decimals)
: _amountToSend!,
);
});
}
});
}
late Amount _currentFee;
void _setCurrentFee(String fee, bool shouldSetState) {
fee = fee.trim();
if (fee.startsWith("~")) {
fee = fee.substring(1);
}
if (fee.contains(" ")) {
fee = fee.split(" ").first;
}
final value = fee.contains(",")
? Decimal.parse(fee.replaceFirst(",", "."))
.toAmount(fractionDigits: coin.decimals)
: Decimal.parse(fee).toAmount(fractionDigits: coin.decimals);
if (shouldSetState) {
setState(() => _currentFee = value);
} else {
_currentFee = value;
}
}
String? _updateInvalidAddressText(String address, Manager manager) {
if (_data != null && _data!.contactLabel == address) {
return null;
}
if (address.isNotEmpty && !manager.validateAddress(address)) {
return "Invalid address";
}
return null;
}
void _updatePreviewButtonState(String? address, Amount? amount) {
if (isPaynymSend) {
ref.read(previewTxButtonStateProvider.state).state =
(amount != null && amount > Amount.zero);
} else {
final isValidAddress = ref
.read(walletsChangeNotifierProvider)
.getManager(walletId)
.validateAddress(address ?? "");
ref.read(previewTxButtonStateProvider.state).state =
(isValidAddress && amount != null && amount > Amount.zero);
}
}
late Future<String> _calculateFeesFuture;
Map<Amount, String> cachedFees = {};
Map<Amount, String> cachedFiroPrivateFees = {};
Map<Amount, String> cachedFiroPublicFees = {};
Future<String> calculateFees(Amount amount) async {
if (amount <= Amount.zero) {
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) {
return cachedFees[amount]!;
}
final manager =
ref.read(walletsChangeNotifierProvider).getManager(walletId);
final feeObject = await manager.fees;
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;
}
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 manager.estimateFeeFor(amount, specialMoneroId.raw!);
cachedFees[amount] = ref.read(pAmountFormatter(coin)).format(
fee,
withUnitName: true,
indicatePrecisionLoss: false,
);
return cachedFees[amount]!;
} else if (coin == Coin.firo || coin == Coin.firoTestNet) {
if (ref.read(publicPrivateBalanceStateProvider.state).state ==
"Private") {
fee = await manager.estimateFeeFor(amount, feeRate);
cachedFiroPrivateFees[amount] = ref.read(pAmountFormatter(coin)).format(
fee,
withUnitName: true,
indicatePrecisionLoss: false,
);
return cachedFiroPrivateFees[amount]!;
} else {
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 manager.estimateFeeFor(amount, feeRate);
cachedFees[amount] = ref.read(pAmountFormatter(coin)).format(
fee,
withUnitName: true,
indicatePrecisionLoss: false,
);
return cachedFees[amount]!;
}
}
Future<String?> _firoBalanceFuture(
ChangeNotifierProvider<Manager> provider, String locale) async {
final wallet = ref.read(provider).wallet as FiroWallet?;
if (wallet != null) {
Amount? balance;
if (ref.read(publicPrivateBalanceStateProvider.state).state ==
"Private") {
balance = wallet.availablePrivateBalance();
} else {
balance = wallet.availablePublicBalance();
}
return ref.read(pAmountFormatter(coin)).format(
balance,
);
}
return null;
}
Future<void> _previewTransaction() async {
// wait for keyboard to disappear
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 100),
);
final manager =
ref.read(walletsChangeNotifierProvider).getManager(walletId);
final Amount amount = _amountToSend!;
final Amount availableBalance;
if ((coin == Coin.firo || coin == Coin.firoTestNet)) {
if (ref.read(publicPrivateBalanceStateProvider.state).state ==
"Private") {
availableBalance =
(manager.wallet as FiroWallet).availablePrivateBalance();
} else {
availableBalance =
(manager.wallet as FiroWallet).availablePublicBalance();
}
} else {
availableBalance = manager.balance.spendable;
}
final coinControlEnabled =
ref.read(prefsChangeNotifierProvider).enableCoinControl;
if (coin != Coin.ethereum &&
!(manager.hasCoinControlSupport && coinControlEnabled) ||
(manager.hasCoinControlSupport &&
coinControlEnabled &&
selectedUTXOs.isEmpty)) {
// 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);
},
),
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context),
child: Text(
"Yes",
style: STextStyles.button(context),
),
onPressed: () {
Navigator.of(context).pop(true);
},
),
);
},
);
}
if (shouldSendAll == null || shouldSendAll == false) {
// cancel preview
return;
}
}
}
try {
bool wasCancelled = false;
if (mounted) {
unawaited(
showDialog<void>(
context: context,
useSafeArea: false,
barrierDismissible: false,
builder: (context) {
return BuildingTransactionDialog(
coin: manager.coin,
onCancel: () {
wasCancelled = true;
Navigator.of(context).pop();
},
);
},
),
);
}
final time = Future<dynamic>.delayed(
const Duration(
milliseconds: 2500,
),
);
Map<String, dynamic> txData;
Future<Map<String, dynamic>> txDataFuture;
if (isPaynymSend) {
final wallet = manager.wallet as PaynymWalletInterface;
final paymentCode = PaymentCode.fromPaymentCode(
widget.accountLite!.code,
networkType: wallet.networkType,
);
final feeRate = ref.read(feeRateTypeStateProvider);
txDataFuture = wallet.preparePaymentCodeSend(
paymentCode: paymentCode,
isSegwit: widget.accountLite!.segwit,
amount: amount,
args: {
"satsPerVByte": isCustomFee ? customFeeRate : null,
"feeRate": feeRate,
"UTXOs": (manager.hasCoinControlSupport &&
coinControlEnabled &&
selectedUTXOs.isNotEmpty)
? selectedUTXOs
: null,
},
);
} else if ((coin == Coin.firo || coin == Coin.firoTestNet) &&
ref.read(publicPrivateBalanceStateProvider.state).state !=
"Private") {
txDataFuture = (manager.wallet as FiroWallet).prepareSendPublic(
address: _address!,
amount: amount,
args: {
"feeRate": ref.read(feeRateTypeStateProvider),
"satsPerVByte": isCustomFee ? customFeeRate : null,
},
);
} else {
txDataFuture = manager.prepareSend(
address: _address!,
amount: amount,
args: {
"feeRate": ref.read(feeRateTypeStateProvider),
"satsPerVByte": isCustomFee ? customFeeRate : null,
"UTXOs": (manager.hasCoinControlSupport &&
coinControlEnabled &&
selectedUTXOs.isNotEmpty)
? selectedUTXOs
: null,
},
);
}
final results = await Future.wait([
txDataFuture,
time,
]);
txData = results.first as Map<String, dynamic>;
if (!wasCancelled && mounted) {
// pop building dialog
Navigator.of(context).pop();
txData["note"] = noteController.text;
txData["onChainNote"] = onChainNoteController.text;
if (isPaynymSend) {
txData["paynymAccountLite"] = widget.accountLite!;
} else {
txData["address"] = _address;
}
unawaited(Navigator.of(context).push(
RouteGenerator.getRoute(
shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute,
builder: (_) => ConfirmTransactionView(
transactionInfo: txData,
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();
},
),
);
},
));
}
}
}
bool get isPaynymSend => widget.accountLite != null;
bool isCustomFee = false;
int customFeeRate = 1;
@override
void initState() {
coin = widget.coin;
ref.refresh(feeSheetSessionCacheProvider);
_currentFee = 0.toAmountAsRaw(fractionDigits: coin.decimals);
_calculateFeesFuture =
calculateFees(0.toAmountAsRaw(fractionDigits: coin.decimals));
_data = widget.autoFillData;
walletId = widget.walletId;
clipboard = widget.clipboard;
scanner = widget.barcodeScanner;
sendToController = TextEditingController();
cryptoAmountController = TextEditingController();
baseAmountController = TextEditingController();
noteController = TextEditingController();
onChainNoteController = TextEditingController();
feeController = TextEditingController();
onCryptoAmountChanged = _cryptoAmountChanged;
cryptoAmountController.addListener(onCryptoAmountChanged);
baseAmountController.addListener(_baseAmountChanged);
if (_data != null) {
if (_data!.amount != null) {
final amount = Amount.fromDecimal(
_data!.amount!,
fractionDigits: coin.decimals,
);
cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format(
amount,
withUnitName: false,
);
}
sendToController.text = _data!.contactLabel;
_address = _data!.address.trim();
_addressToggleFlag = true;
}
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));
// });
// }
// }
// });
// }
super.initState();
}
@override
void dispose() {
_cryptoAmountChangedFeeUpdateTimer?.cancel();
_baseAmountChangedFeeUpdateTimer?.cancel();
cryptoAmountController.removeListener(onCryptoAmountChanged);
baseAmountController.removeListener(_baseAmountChanged);
sendToController.dispose();
cryptoAmountController.dispose();
baseAmountController.dispose();
noteController.dispose();
onChainNoteController.dispose();
feeController.dispose();
_noteFocusNode.dispose();
_onChainNoteFocusNode.dispose();
_addressFocusNode.dispose();
_cryptoFocus.dispose();
_baseFocus.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
final provider = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManagerProvider(walletId)));
final String locale = ref.watch(
localeServiceChangeNotifierProvider.select((value) => value.locale));
final showCoinControl = ref.watch(
walletsChangeNotifierProvider.select(
(value) => value.getManager(walletId).hasCoinControlSupport,
),
) &&
ref.watch(
prefsChangeNotifierProvider.select(
(value) => value.enableCoinControl,
),
);
if (coin == Coin.firo || coin == Coin.firoTestNet) {
ref.listen(publicPrivateBalanceStateProvider, (previous, next) {
if (_amountToSend == null) {
setState(() {
_calculateFeesFuture =
calculateFees(0.toAmountAsRaw(fractionDigits: coin.decimals));
});
} else {
setState(() {
_calculateFeesFuture = calculateFees(
_amountToSend!,
);
});
}
});
}
// 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"));
}
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),
),
),
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,
),
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: [
SvgPicture.file(
File(
ref.watch(
coinIconProvider(coin),
),
),
width: 22,
height: 22,
),
const SizedBox(
width: 6,
),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
ref.watch(provider.select(
(value) => value.walletName)),
style: STextStyles.titleBold12(context)
.copyWith(fontSize: 14),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
// 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),
),
if (coin != Coin.firo &&
coin != Coin.firoTestNet)
Text(
"Available balance",
style: STextStyles.label(context)
.copyWith(fontSize: 10),
),
],
),
const Spacer(),
FutureBuilder(
// TODO redo this widget now that its not actually a future
future: (coin != Coin.firo &&
coin != Coin.firoTestNet)
? Future(() => ref.watch(
provider.select((value) =>
value.balance.spendable)))
: ref.watch(publicPrivateBalanceStateProvider.state).state ==
"Private"
? Future(() => (ref
.watch(provider)
.wallet as FiroWallet)
.availablePrivateBalance())
: Future(() => (ref
.watch(provider)
.wallet as FiroWallet)
.availablePublicBalance()),
builder:
(_, AsyncSnapshot<Amount> snapshot) {
if (snapshot.connectionState ==
ConnectionState.done &&
snapshot.hasData) {
_cachedBalance = snapshot.data!;
}
if (_cachedBalance != null) {
return GestureDetector(
onTap: () {
cryptoAmountController.text = ref
.read(pAmountFormatter(coin))
.format(
_cachedBalance!,
withUnitName: false,
);
},
child: Container(
color: Colors.transparent,
child: Column(
crossAxisAlignment:
CrossAxisAlignment.end,
children: [
Text(
ref
.watch(pAmountFormatter(
coin))
.format(_cachedBalance!),
style:
STextStyles.titleBold12(
context)
.copyWith(
fontSize: 10,
),
textAlign: TextAlign.right,
),
Text(
"${(_cachedBalance!.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,
)
],
),
),
);
} else {
return Column(
crossAxisAlignment:
CrossAxisAlignment.end,
children: [
AnimatedText(
stringsToLoopThrough: const [
"Loading balance ",
"Loading balance. ",
"Loading balance.. ",
"Loading balance...",
],
style: STextStyles.itemSubtitle(
context)
.copyWith(
fontSize: 10,
),
),
const SizedBox(
height: 2,
),
AnimatedText(
stringsToLoopThrough: const [
"Loading balance ",
"Loading balance. ",
"Loading balance.. ",
"Loading balance...",
],
style: STextStyles.itemSubtitle(
context)
.copyWith(
fontSize: 8,
),
)
],
);
}
},
),
],
),
),
),
const SizedBox(
height: 16,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
isPaynymSend
? "Send to PayNym address"
: "Send to",
style: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
// if (coin == Coin.monero)
// CustomTextButton(
// text: "Use OpenAlias",
// onTap: () async {
// await showModalBottomSheet(
// context: context,
// builder: (context) =>
// OpenAliasBottomSheet(
// onSelected: (address) {
// sendToController.text = address;
// },
// ),
// );
// },
// ),
],
),
const SizedBox(
height: 8,
),
if (isPaynymSend)
TextField(
key: const Key("sendViewPaynymAddressFieldKey"),
controller: sendToController,
enabled: false,
readOnly: true,
style: STextStyles.fieldLabel(context),
),
if (!isPaynymSend)
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
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,
),
onChanged: (newValue) {
_address = newValue.trim();
_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.",
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.",
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 @
content = formatAddress(
content);
}
sendToController.text =
content.trim();
_address = content.trim();
_updatePreviewButtonState(
_address,
_amountToSend);
setState(() {
_addressToggleFlag =
sendToController
.text
.isNotEmpty;
});
}
},
child: sendToController
.text.isEmpty
? const ClipboardIcon()
: const XIcon(),
),
if (sendToController.text.isEmpty)
TextFieldIconButton(
semanticsLabel:
"Address Book Button. Opens Address Book For Address Field.",
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.",
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));
}
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 =
_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) {
final Amount amount =
Decimal.parse(results[
"amount"]!)
.toAmount(
fractionDigits:
coin.decimals,
);
cryptoAmountController
.text =
ref
.read(
pAmountFormatter(
coin))
.format(
amount,
withUnitName:
false,
);
_amountToSend = amount;
}
_updatePreviewButtonState(
_address,
_amountToSend);
setState(() {
_addressToggleFlag =
sendToController
.text.isNotEmpty;
});
// now check for non standard encoded basic address
} else if (ref
.read(
walletsChangeNotifierProvider)
.getManager(walletId)
.validateAddress(qrResult
.rawContent)) {
_address = qrResult
.rawContent
.trim();
sendToController.text =
_address ?? "";
_updatePreviewButtonState(
_address,
_amountToSend);
setState(() {
_addressToggleFlag =
sendToController
.text.isNotEmpty;
});
}
} 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);
}
},
child: const QrCodeIcon(),
)
],
),
),
),
),
),
),
Builder(
builder: (_) {
final error = _updateInvalidAddressText(
_address ?? "",
ref
.read(walletsChangeNotifierProvider)
.getManager(walletId),
);
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,
),
),
),
);
}
},
),
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,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
),
child: RawMaterialButton(
splashColor: Theme.of(context)
.extension<StackColors>()!
.highlight,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
onPressed: () {
showModalBottomSheet<dynamic>(
backgroundColor: Colors.transparent,
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
),
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,
),
FutureBuilder(
future: _firoBalanceFuture(
provider, locale),
builder: (context,
AsyncSnapshot<String?>
snapshot) {
if (snapshot.connectionState ==
ConnectionState.done &&
snapshot.hasData) {
if (ref
.read(
publicPrivateBalanceStateProvider
.state)
.state ==
"Private") {
_privateBalanceString =
snapshot.data!;
} else {
_publicBalanceString =
snapshot.data!;
}
}
if (ref
.read(
publicPrivateBalanceStateProvider
.state)
.state ==
"Private" &&
_privateBalanceString !=
null) {
return Text(
"$_privateBalanceString",
style: STextStyles
.itemSubtitle(context),
);
} else if (ref
.read(
publicPrivateBalanceStateProvider
.state)
.state ==
"Public" &&
_publicBalanceString !=
null) {
return Text(
"$_publicBalanceString",
style: STextStyles
.itemSubtitle(context),
);
} else {
return AnimatedText(
stringsToLoopThrough: const [
"Loading balance",
"Loading balance.",
"Loading balance..",
"Loading balance...",
],
style: STextStyles
.itemSubtitle(context),
);
}
},
),
],
),
SvgPicture.asset(
Assets.svg.chevronDown,
width: 8,
height: 4,
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle2,
),
],
),
),
)
],
),
const SizedBox(
height: 12,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Amount",
style: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
if (coin != Coin.ethereum)
CustomTextButton(
text: "Send all ${coin.ticker}",
onTap: () async {
if (coin == Coin.firo ||
coin == Coin.firoTestNet) {
final firoWallet = ref
.read(provider)
.wallet as FiroWallet;
if (ref
.read(
publicPrivateBalanceStateProvider
.state)
.state ==
"Private") {
cryptoAmountController.text = ref
.read(pAmountFormatter(coin))
.format(
firoWallet
.availablePrivateBalance(),
withUnitName: false,
);
} else {
cryptoAmountController.text = ref
.read(pAmountFormatter(coin))
.format(
firoWallet
.availablePublicBalance(),
withUnitName: false,
);
}
} else {
cryptoAmountController.text = ref
.read(pAmountFormatter(coin))
.format(
ref
.read(provider)
.balance
.spendable,
withUnitName: false,
);
}
_cryptoAmountChanged();
},
),
],
),
const SizedBox(
height: 8,
),
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("amountInputFieldCryptoTextFieldKey"),
controller: cryptoAmountController,
focusNode: _cryptoFocus,
keyboardType: Util.isDesktop
? null
: const TextInputType.numberWithOptions(
signed: false,
decimal: true,
),
textAlign: TextAlign.right,
inputFormatters: [
AmountInputFormatter(
decimals: coin.decimals,
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(
ref
.watch(pAmountUnit(coin))
.unitForCoin(coin),
style: STextStyles.smallMed14(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
),
),
),
),
if (Prefs.instance.externalCalls)
const SizedBox(
height: 8,
),
if (Prefs.instance.externalCalls)
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,
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) {
final Decimal _price = ref
.read(priceAnd24hChangeNotifierProvider)
.getPrice(coin)
.item1;
if (_price == Decimal.zero) {
_amountToSend = 0.toAmountAsRaw(
fractionDigits: coin.decimals);
} else {
_amountToSend = baseAmount <= Amount.zero
? 0.toAmountAsRaw(
fractionDigits: coin.decimals)
: (baseAmount.decimal / _price)
.toDecimal(
scaleOnInfinitePrecision:
coin.decimals,
)
.toAmount(
fractionDigits: coin.decimals);
}
if (_cachedAmountToSend != null &&
_cachedAmountToSend == _amountToSend) {
return;
}
_cachedAmountToSend = _amountToSend;
Logging.instance.log(
"it changed $_amountToSend $_cachedAmountToSend",
level: LogLevel.Info);
final amountString =
ref.read(pAmountFormatter(coin)).format(
_amountToSend!,
withUnitName: false,
);
_cryptoAmountChangeLock = true;
cryptoAmountController.text = amountString;
_cryptoAmountChangeLock = false;
} else {
_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),
),
),
),
),
),
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(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.balance
.spendable;
Amount? amount;
if (_amountToSend != null) {
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,
),
);
if (result is Set<UTXO>) {
setState(() {
selectedUTXOs = result;
});
}
}
},
),
],
),
),
const SizedBox(
height: 12,
),
if (coin == Coin.epicCash)
Text(
"On chain Note (optional)",
style: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
if (coin == Coin.epicCash)
const SizedBox(
height: 8,
),
if (coin == Coin.epicCash)
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
maxLength: 256,
controller: onChainNoteController,
focusNode: _onChainNoteFocusNode,
style: STextStyles.field(context),
onChanged: (_) => setState(() {}),
decoration: standardInputDecoration(
"Type something...",
_onChainNoteFocusNode,
context,
).copyWith(
suffixIcon: onChainNoteController.text.isNotEmpty
? Padding(
padding:
const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
onChainNoteController.text = "";
});
},
),
],
),
),
)
: 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,
),
),
),
const SizedBox(
height: 12,
),
if (coin != Coin.epicCash &&
coin != Coin.nano &&
coin != Coin.banano)
Text(
"Transaction fee (estimated)",
style: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
if (coin != Coin.epicCash &&
coin != Coin.nano &&
coin != Coin.banano)
const SizedBox(
height: 8,
),
if (coin != Coin.epicCash &&
coin != Coin.nano &&
coin != Coin.banano)
Stack(
children: [
TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions:
Util.isDesktop ? false : true,
controller: feeController,
readOnly: true,
textInputAction: TextInputAction.none,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
),
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),
),
),
builder: (_) =>
TransactionFeeSelectionSheet(
walletId: walletId,
amount: (Decimal.tryParse(
cryptoAmountController
.text) ??
_amountToSend
?.decimal ??
Decimal.zero)
.toAmount(
fractionDigits: coin.decimals,
),
updateChosen: (String fee) {
if (fee == "custom") {
if (!isCustomFee) {
setState(() {
isCustomFee = true;
});
}
return;
}
_setCurrentFee(
fee,
true,
);
setState(() {
_calculateFeesFuture =
Future(() => fee);
if (isCustomFee) {
isCustomFee = false;
}
});
},
),
);
},
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(
snapshot.data!,
false,
);
return Text(
"~${snapshot.data!}",
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
.itemSubtitle12(
context),
),
const SizedBox(
width: 10,
),
FutureBuilder(
future:
_calculateFeesFuture,
builder:
(context, snapshot) {
if (snapshot.connectionState ==
ConnectionState
.done &&
snapshot.hasData) {
_setCurrentFee(
snapshot.data!,
false,
);
return Text(
isCustomFee
? ""
: "~${snapshot.data!}",
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
? _previewTransaction
: null,
style: ref
.watch(previewTxButtonStateProvider.state)
.state
? Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context)
: Theme.of(context)
.extension<StackColors>()!
.getPrimaryDisabledButtonStyle(context),
child: Text(
"Preview",
style: STextStyles.button(context),
),
),
const SizedBox(
height: 4,
),
],
),
),
),
),
),
);
},
),
),
);
}
}
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://", "");
}
// strip mailto: prefix
if (epicAddress.startsWith("mailto:")) {
epicAddress = epicAddress.replaceAll("mailto:", "");
}
// 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;
}