stack_wallet/lib/pages/send_view/send_view.dart

1992 lines
95 KiB
Dart
Raw Normal View History

import 'dart:async';
2023-01-26 18:16:38 +00:00
import 'package:bip47/bip47.dart';
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';
2022-09-06 23:27:14 +00:00
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
2022-08-26 08:11:35 +00:00
import 'package:stackwallet/services/coins/manager.dart';
2023-01-26 18:16:38 +00:00
import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart';
2022-08-26 08:11:35 +00:00
import 'package:stackwallet/utilities/address_utils.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/format.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/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.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/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;
late TextEditingController feeController;
late final SendViewAutoFillData? _data;
final _addressFocusNode = FocusNode();
final _noteFocusNode = FocusNode();
final _cryptoFocus = FocusNode();
final _baseFocus = FocusNode();
Decimal? _amountToSend;
Decimal? _cachedAmountToSend;
String? _address;
2022-09-06 23:27:14 +00:00
String? _privateBalanceString;
String? _publicBalanceString;
2022-08-26 08:11:35 +00:00
bool _addressToggleFlag = false;
bool _cryptoAmountChangeLock = false;
late VoidCallback onCryptoAmountChanged;
Decimal? _cachedBalance;
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 String cryptoAmount = cryptoAmountController.text;
if (cryptoAmount.isNotEmpty &&
cryptoAmount != "." &&
cryptoAmount != ",") {
_amountToSend = cryptoAmount.contains(",")
? Decimal.parse(cryptoAmount.replaceFirst(",", "."))
: Decimal.parse(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) {
final String fiatAmountString = Format.localizedStringAsFixed(
value: _amountToSend! * price,
locale: ref.read(localeServiceChangeNotifierProvider).locale,
decimalPlaces: 2,
);
baseAmountController.text = fiatAmountString;
}
} 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
: Format.decimalAmountToSatoshis(
_amountToSend!,
coin,
),
);
});
}
});
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
? 0
: Format.decimalAmountToSatoshis(
_amountToSend!,
coin,
),
);
});
}
});
}
int _currentFee = 0;
void _setCurrentFee(String fee, bool shouldSetState) {
final value = Format.decimalAmountToSatoshis(
Decimal.parse(fee),
coin,
);
if (shouldSetState) {
setState(() => _currentFee = value);
} else {
_currentFee = value;
}
}
2022-08-26 08:11:35 +00:00
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, Decimal? amount) {
2023-01-26 18:16:38 +00:00
if (isPaynymSend) {
ref.read(previewTxButtonStateProvider.state).state =
(amount != null && amount > Decimal.zero);
} else {
final isValidAddress = ref
.read(walletsChangeNotifierProvider)
.getManager(walletId)
.validateAddress(address ?? "");
ref.read(previewTxButtonStateProvider.state).state =
(isValidAddress && amount != null && amount > Decimal.zero);
}
2022-08-26 08:11:35 +00:00
}
late Future<String> _calculateFeesFuture;
Map<int, String> cachedFees = {};
Map<int, String> cachedFiroPrivateFees = {};
Map<int, String> cachedFiroPublicFees = {};
2022-08-26 08:11:35 +00:00
Future<String> calculateFees(int amount) async {
if (amount <= 0) {
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 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;
}
int 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;
}
fee = await manager.estimateFeeFor(amount, specialMoneroId.raw!);
cachedFees[amount] = Format.satoshisToAmount(fee, coin: coin)
.toStringAsFixed(Constants.decimalPlacesForCoin(coin));
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] = Format.satoshisToAmount(fee, coin: coin)
.toStringAsFixed(Constants.decimalPlacesForCoin(coin));
2022-08-26 08:11:35 +00:00
return cachedFiroPrivateFees[amount]!;
} else {
fee = await (manager.wallet as FiroWallet)
.estimateFeeForPublic(amount, feeRate);
cachedFiroPublicFees[amount] = Format.satoshisToAmount(fee, coin: coin)
.toStringAsFixed(Constants.decimalPlacesForCoin(coin));
2022-08-26 08:11:35 +00:00
return cachedFiroPublicFees[amount]!;
}
} else {
fee = await manager.estimateFeeFor(amount, feeRate);
cachedFees[amount] = Format.satoshisToAmount(fee, coin: coin)
.toStringAsFixed(Constants.decimalPlacesForCoin(coin));
return cachedFees[amount]!;
}
2022-08-26 08:11:35 +00:00
}
2022-09-06 23:27:14 +00:00
Future<String?> _firoBalanceFuture(
ChangeNotifierProvider<Manager> provider, String locale) async {
final wallet = ref.read(provider).wallet as FiroWallet?;
if (wallet != null) {
Decimal? balance;
if (ref.read(publicPrivateBalanceStateProvider.state).state ==
"Private") {
balance = wallet.availablePrivateBalance();
2022-09-06 23:27:14 +00:00
} else {
balance = wallet.availablePublicBalance();
2022-09-06 23:27:14 +00:00
}
return Format.localizedStringAsFixed(
value: balance, locale: locale, decimalPlaces: 8);
}
return null;
}
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 manager =
ref.read(walletsChangeNotifierProvider).getManager(walletId);
final amount = Format.decimalAmountToSatoshis(_amountToSend!, coin);
int availableBalance;
if ((coin == Coin.firo || coin == Coin.firoTestNet)) {
if (ref.read(publicPrivateBalanceStateProvider.state).state ==
"Private") {
availableBalance = Format.decimalAmountToSatoshis(
(manager.wallet as FiroWallet).availablePrivateBalance(), coin);
} else {
availableBalance = Format.decimalAmountToSatoshis(
(manager.wallet as FiroWallet).availablePublicBalance(), coin);
}
} else {
availableBalance =
Format.decimalAmountToSatoshis(manager.balance.getSpendable(), coin);
}
2023-03-08 21:19:43 +00:00
final coinControlEnabled =
ref.read(prefsChangeNotifierProvider).enableCoinControl;
if (!(manager.hasCoinControlSupport && coinControlEnabled) ||
(manager.hasCoinControlSupport &&
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: manager.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,
),
);
2023-01-26 18:16:38 +00:00
Map<String, dynamic> txData;
Future<Map<String, dynamic>> txDataFuture;
2023-01-26 18:16:38 +00:00
if (isPaynymSend) {
final wallet = manager.wallet as PaynymWalletInterface;
final paymentCode = PaymentCode.fromPaymentCode(
widget.accountLite!.code,
wallet.networkType,
);
final feeRate = ref.read(feeRateTypeStateProvider);
txDataFuture = wallet.preparePaymentCodeSend(
2023-01-26 18:16:38 +00:00
paymentCode: paymentCode,
satoshiAmount: amount,
2023-03-07 21:45:22 +00:00
args: {
"feeRate": feeRate,
2023-03-08 21:19:43 +00:00
"UTXOs": (manager.hasCoinControlSupport &&
coinControlEnabled &&
selectedUTXOs.isNotEmpty)
2023-03-07 21:45:22 +00:00
? selectedUTXOs
: null,
},
2023-01-26 18:16:38 +00:00
);
} else if ((coin == Coin.firo || coin == Coin.firoTestNet) &&
ref.read(publicPrivateBalanceStateProvider.state).state !=
"Private") {
txDataFuture = (manager.wallet as FiroWallet).prepareSendPublic(
2023-01-26 18:16:38 +00:00
address: _address!,
satoshiAmount: amount,
args: {"feeRate": ref.read(feeRateTypeStateProvider)},
);
} else {
txDataFuture = manager.prepareSend(
2023-01-26 18:16:38 +00:00
address: _address!,
satoshiAmount: amount,
2023-03-07 21:45:22 +00:00
args: {
"feeRate": ref.read(feeRateTypeStateProvider),
2023-03-08 21:19:43 +00:00
"UTXOs": (manager.hasCoinControlSupport &&
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 = results.first as Map<String, dynamic>;
2023-01-26 18:16:38 +00:00
if (!wasCancelled && mounted) {
// pop building dialog
Navigator.of(context).pop();
txData["note"] = noteController.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();
},
),
);
},
));
}
}
}
2023-01-26 17:46:16 +00:00
bool get isPaynymSend => widget.accountLite != null;
2022-08-26 08:11:35 +00:00
@override
void initState() {
ref.refresh(feeSheetSessionCacheProvider);
_calculateFeesFuture = calculateFees(0);
_data = widget.autoFillData;
walletId = widget.walletId;
coin = widget.coin;
clipboard = widget.clipboard;
scanner = widget.barcodeScanner;
sendToController = TextEditingController();
cryptoAmountController = TextEditingController();
baseAmountController = TextEditingController();
noteController = TextEditingController();
feeController = TextEditingController();
onCryptoAmountChanged = _cryptoAmountChanged;
cryptoAmountController.addListener(onCryptoAmountChanged);
baseAmountController.addListener(_baseAmountChanged);
2022-08-26 08:11:35 +00:00
if (_data != null) {
if (_data!.amount != null) {
cryptoAmountController.text = _data!.amount!.toString();
}
sendToController.text = _data!.contactLabel;
_address = _data!.address;
_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();
feeController.dispose();
_noteFocusNode.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));
2023-03-07 21:45:22 +00:00
final showCoinControl = ref.watch(
2023-03-08 21:19:43 +00:00
walletsChangeNotifierProvider.select(
(value) => value.getManager(walletId).hasCoinControlSupport,
),
) &&
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(() {
_calculateFeesFuture = calculateFees(0);
});
} else {
setState(() {
_calculateFeesFuture = calculateFees(
Format.decimalAmountToSatoshis(_amountToSend!, coin));
});
}
});
}
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;
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.asset(
Assets.svg.iconFor(coin: coin),
width: 22,
height: 22,
2022-09-06 23:27:14 +00:00
),
const SizedBox(
width: 6,
),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
2022-09-06 23:27:14 +00:00
ref.watch(provider.select(
(value) => value.walletName)),
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(),
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.getSpendable())))
: ref.watch(publicPrivateBalanceStateProvider.state).state ==
"Private"
? Future(() => (ref
.watch(provider)
.wallet as FiroWallet)
.availablePrivateBalance())
: Future(() => (ref
.watch(provider)
.wallet as FiroWallet)
.availablePublicBalance()),
builder:
(_, AsyncSnapshot<Decimal> snapshot) {
if (snapshot.connectionState ==
ConnectionState.done &&
snapshot.hasData) {
_cachedBalance = snapshot.data!;
}
if (_cachedBalance != null) {
return GestureDetector(
onTap: () {
cryptoAmountController.text =
_cachedBalance!.toStringAsFixed(
Constants
.decimalPlacesForCoin(
coin));
},
child: Container(
color: Colors.transparent,
child: Column(
crossAxisAlignment:
CrossAxisAlignment.end,
children: [
Text(
"${Format.localizedStringAsFixed(
value: _cachedBalance!,
locale: locale,
decimalPlaces: 8,
)} ${coin.ticker}",
style:
STextStyles.titleBold12(
context)
.copyWith(
fontSize: 10,
),
textAlign: TextAlign.right,
),
Text(
"${Format.localizedStringAsFixed(
value: _cachedBalance! *
ref.watch(priceAnd24hChangeNotifierProvider
.select((value) =>
value
.getPrice(
coin)
.item1)),
locale: locale,
decimalPlaces: 2,
)} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}",
2023-03-17 14:53:31 +00:00
style: STextStyles.subtitle(
context)
.copyWith(
fontSize: 8,
),
textAlign: TextAlign.right,
)
],
2022-08-26 08:11:35 +00:00
),
),
);
} 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,
),
2022-08-26 08:11:35 +00:00
),
const SizedBox(
height: 2,
),
AnimatedText(
stringsToLoopThrough: const [
"Loading balance ",
"Loading balance. ",
"Loading balance.. ",
"Loading balance...",
],
style: STextStyles.itemSubtitle(
context)
.copyWith(
fontSize: 8,
),
)
],
);
}
},
),
],
),
2022-08-26 08:11:35 +00:00
),
),
const SizedBox(
height: 16,
2022-08-26 08:11:35 +00:00
),
Text(
2023-01-26 17:46:16 +00:00
isPaynymSend ? "Send to PayNym address" : "Send to",
style: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
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;
_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(
key: const Key(
"sendViewClearAddressFieldButtonKey"),
onTap: () {
sendToController.text = "";
_address = "";
_updatePreviewButtonState(
_address,
_amountToSend);
setState(() {
_addressToggleFlag =
false;
});
},
child: const XIcon(),
)
: TextFieldIconButton(
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;
_address = content;
_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(
key: const Key(
"sendViewAddressBookButtonKey"),
onTap: () {
Navigator.of(context).pushNamed(
AddressBookView.routeName,
arguments: widget.coin,
);
},
child: const AddressBookIcon(),
),
if (sendToController.text.isEmpty)
TextFieldIconButton(
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"] ??
"";
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) {
final amount =
Decimal.parse(results[
"amount"]!);
cryptoAmountController
.text =
Format
.localizedStringAsFixed(
value: amount,
locale: ref
.read(
localeServiceChangeNotifierProvider)
.locale,
decimalPlaces: Constants
.decimalPlacesForCoin(
coin),
);
amount.toString();
_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(
walletsChangeNotifierProvider)
.getManager(walletId)
.validateAddress(qrResult
.rawContent)) {
_address =
qrResult.rawContent;
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
),
),
),
),
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,
),
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,
),
FutureBuilder(
future: _firoBalanceFuture(
provider, locale),
builder: (context,
AsyncSnapshot<String?>
snapshot) {
if (snapshot.connectionState ==
ConnectionState.done &&
snapshot.hasData) {
if (ref
2022-09-06 23:27:14 +00:00
.read(
publicPrivateBalanceStateProvider
.state)
.state ==
"Private") {
_privateBalanceString =
snapshot.data!;
} else {
_publicBalanceString =
snapshot.data!;
}
}
if (ref
.read(
publicPrivateBalanceStateProvider
.state)
.state ==
"Private" &&
_privateBalanceString !=
null) {
return Text(
"$_privateBalanceString ${coin.ticker}",
style: STextStyles
.itemSubtitle(context),
);
} else if (ref
.read(
publicPrivateBalanceStateProvider
.state)
.state ==
"Public" &&
_publicBalanceString !=
null) {
return Text(
"$_publicBalanceString ${coin.ticker}",
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,
),
],
),
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,
),
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 = firoWallet
.availablePrivateBalance()
.toStringAsFixed(
Constants.decimalPlacesForCoin(
coin));
} else {
cryptoAmountController.text = firoWallet
.availablePublicBalance()
.toStringAsFixed(
Constants.decimalPlacesForCoin(
coin));
}
2022-09-06 23:27:14 +00:00
} else {
cryptoAmountController.text = (ref
.read(provider)
.balance
.getSpendable())
.toStringAsFixed(
Constants.decimalPlacesForCoin(
coin));
2022-09-06 23:27:14 +00:00
}
_cryptoAmountChanged();
},
2022-08-26 08:11:35 +00:00
),
],
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: [
// regex to validate a crypto amount with 8 decimal places
TextInputFormatter.withFunction((oldValue,
newValue) =>
RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$')
.hasMatch(newValue.text)
? newValue
: oldValue),
],
decoration: InputDecoration(
contentPadding: const EdgeInsets.only(
top: 12,
right: 12,
),
hintText: "0",
hintStyle:
STextStyles.fieldLabel(context).copyWith(
fontSize: 14,
),
prefixIcon: FittedBox(
fit: BoxFit.scaleDown,
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
coin.ticker,
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: [
// 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})$')
.hasMatch(newValue.text)
? newValue
: oldValue),
],
onChanged: (baseAmountString) {
if (baseAmountString.isNotEmpty &&
baseAmountString != "." &&
baseAmountString != ",") {
final baseAmount =
baseAmountString.contains(",")
? Decimal.parse(baseAmountString
.replaceFirst(",", "."))
: Decimal.parse(baseAmountString);
var _price = ref
.read(priceAnd24hChangeNotifierProvider)
.getPrice(coin)
.item1;
if (_price == Decimal.zero) {
_amountToSend = Decimal.zero;
} else {
_amountToSend = baseAmount <= Decimal.zero
? Decimal.zero
: (baseAmount / _price).toDecimal(
scaleOnInfinitePrecision:
Constants.decimalPlacesForCoin(
coin));
}
if (_cachedAmountToSend != null &&
_cachedAmountToSend == _amountToSend) {
return;
}
_cachedAmountToSend = _amountToSend;
Logging.instance.log(
"it changed $_amountToSend $_cachedAmountToSend",
level: LogLevel.Info);
final amountString =
Format.localizedStringAsFixed(
value: _amountToSend!,
locale: ref
.read(
localeServiceChangeNotifierProvider)
.locale,
decimalPlaces:
Constants.decimalPlacesForCoin(coin),
);
_cryptoAmountChangeLock = true;
cryptoAmountController.text = amountString;
_cryptoAmountChangeLock = false;
} else {
_amountToSend = Decimal.zero;
_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(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.balance
.spendable;
int? amount;
if (_amountToSend != null) {
amount =
Format.decimalAmountToSatoshis(
_amountToSend!,
coin,
);
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,
),
Text(
"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,
),
2022-12-06 14:36:03 +00:00
if (coin != Coin.epicCash)
Text(
"Transaction fee (estimated)",
style: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
if (coin != Coin.epicCash)
const SizedBox(
height: 8,
),
if (coin != Coin.epicCash)
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,
amount: Decimal.tryParse(
cryptoAmountController
.text) ??
Decimal.zero,
updateChosen: (String fee) {
_setCurrentFee(
fee,
true,
);
2022-12-06 14:36:03 +00:00
setState(() {
_calculateFeesFuture =
Future(() => fee);
});
},
),
);
},
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! as String,
false,
);
2022-12-06 14:36:03 +00:00
return Text(
"~${snapshot.data! as String} ${coin.ticker}",
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!
as String,
false,
);
2022-12-06 14:36:03 +00:00
return Text(
"~${snapshot.data! as String} ${coin.ticker}",
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,
),
],
),
),
)
],
),
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;
}