import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../providers/global/locale_provider.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/address_utils.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/amount/amount_formatter.dart'; import '../../../utilities/amount/amount_input_formatter.dart'; import '../../../utilities/amount/amount_unit.dart'; import '../../../utilities/barcode_scanner_interface.dart'; import '../../../utilities/clipboard_interface.dart'; import '../../../utilities/constants.dart'; import '../../../utilities/logger.dart'; import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; import '../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../widgets/custom_buttons/blue_text_button.dart'; import '../../../widgets/icon_widgets/clipboard_icon.dart'; import '../../../widgets/icon_widgets/qrcode_icon.dart'; import '../../../widgets/icon_widgets/x_icon.dart'; import '../../../widgets/rounded_container.dart'; import '../../../widgets/stack_text_field.dart'; import '../../../widgets/textfield_icon_button.dart'; //TODO: move the following two providers elsewhere final pClipboard = Provider((ref) => const ClipboardWrapper()); final pBarcodeScanner = Provider((ref) => const BarcodeScannerWrapper()); // final _pPrice = Provider.family((ref, coin) { // return ref.watch( // priceAnd24hChangeNotifierProvider // .select((value) => value.getPrice(coin).item1), // ); // }); final pRecipient = StateProvider.family<({String address, Amount? amount})?, int>( (ref, index) => null, ); class Recipient extends ConsumerStatefulWidget { const Recipient({ super.key, required this.index, required this.displayNumber, required this.coin, this.remove, this.onChanged, required this.addAnotherRecipientTapped, required this.sendAllTapped, }); final int index; final int displayNumber; final CryptoCurrency coin; final VoidCallback? remove; final VoidCallback? onChanged; final VoidCallback addAnotherRecipientTapped; final String Function() sendAllTapped; @override ConsumerState createState() => _RecipientState(); } class _RecipientState extends ConsumerState { late final TextEditingController addressController, amountController; late final FocusNode addressFocusNode, amountFocusNode; bool _addressIsEmpty = true; final bool _cryptoAmountChangeLock = false; bool get isSingle => widget.remove == null; void _updateRecipientData() { final address = addressController.text; final amount = ref.read(pAmountFormatter(widget.coin)).tryParse(amountController.text); ref.read(pRecipient(widget.index).notifier).state = ( address: address, amount: amount, ); widget.onChanged?.call(); } void _cryptoAmountChanged() async { if (!_cryptoAmountChangeLock) { Amount? cryptoAmount = ref.read(pAmountFormatter(widget.coin)).tryParse( amountController.text, ); if (cryptoAmount != null) { if (ref.read(pRecipient(widget.index))?.amount != null && ref.read(pRecipient(widget.index))?.amount == cryptoAmount) { return; } // final price = ref.read(_pPrice(widget.coin)); // // if (price > Decimal.zero) { // baseController.text = (cryptoAmount.decimal * price) // .toAmount( // fractionDigits: 2, // ) // .fiatString( // locale: ref.read(localeServiceChangeNotifierProvider).locale, // ); // } } else { cryptoAmount = null; // baseController.text = ""; } _updateRecipientData(); } } void _onQrTapped() async { try { if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); await Future.delayed( const Duration( milliseconds: 75, ), ); } final qrResult = await ref.read(pBarcodeScanner).scan(); Logging.instance.log( "qrResult content: ${qrResult.rawContent}", level: LogLevel.Info, ); final paymentData = AddressUtils.parsePaymentUri( qrResult.rawContent, logging: Logging.instance, ); Logging.instance.log( "qrResult parsed: $paymentData", level: LogLevel.Info, ); if (paymentData != null && paymentData.coin?.uriScheme == widget.coin.uriScheme) { // auto fill address addressController.text = paymentData.address.trim(); // autofill amount field if (paymentData.amount != null) { final Amount amount = Decimal.parse(paymentData.amount!).toAmount( fractionDigits: widget.coin.fractionDigits, ); amountController.text = ref.read(pAmountFormatter(widget.coin)).format( amount, withUnitName: false, ); } } else { addressController.text = qrResult.rawContent.trim(); } setState(() { _addressIsEmpty = addressController.text.isEmpty; }); _updateRecipientData(); } on PlatformException catch (e, s) { Logging.instance.log( "Failed to get camera permissions while " "trying to scan qr code in SendView: $e\n$s", level: LogLevel.Warning, ); } } @override void initState() { addressController = TextEditingController(); amountController = TextEditingController(); // baseController = TextEditingController(); final amount = ref.read(pRecipient(widget.index))?.amount; if (amount != null) { amountController.text = ref .read(pAmountFormatter(widget.coin)) .format(amount, withUnitName: false); } addressController.text = ref.read(pRecipient(widget.index))?.address ?? ""; _addressIsEmpty = addressController.text.isEmpty; addressFocusNode = FocusNode(); amountFocusNode = FocusNode(); // baseFocusNode = FocusNode(); amountController.addListener(_cryptoAmountChanged); super.initState(); } @override void dispose() { amountController.removeListener(_cryptoAmountChanged); addressController.dispose(); amountController.dispose(); // baseController.dispose(); addressFocusNode.dispose(); amountFocusNode.dispose(); // baseFocusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final String locale = ref.watch( localeServiceChangeNotifierProvider.select( (value) => value.locale, ), ); return RoundedContainer( color: Colors.transparent, padding: const EdgeInsets.all(0), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( isSingle ? "Send to" : "Recipient ${widget.displayNumber}", style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), CustomTextButton( text: isSingle ? "Add another recipient" : "Remove", onTap: isSingle ? widget.addAnotherRecipientTapped : widget.remove, ), ], ), const SizedBox( height: 8, ), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), child: TextField( key: const Key("sendViewAddressFieldKey"), controller: addressController, readOnly: false, autocorrect: false, enableSuggestions: false, focusNode: addressFocusNode, style: STextStyles.field(context), onChanged: (_) { _updateRecipientData(); setState(() { _addressIsEmpty = addressController.text.isEmpty; }); }, decoration: standardInputDecoration( "Enter ${widget.coin.ticker} address", addressFocusNode, context, ).copyWith( contentPadding: const EdgeInsets.only( left: 16, top: 6, bottom: 8, right: 5, ), suffixIcon: Padding( padding: _addressIsEmpty ? const EdgeInsets.only(right: 8) : const EdgeInsets.only(right: 0), child: UnconstrainedBox( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ !_addressIsEmpty ? TextFieldIconButton( semanticsLabel: "Clear Button. Clears The Address Field Input.", key: const Key( "sendViewClearAddressFieldButtonKey", ), onTap: () { addressController.text = ""; setState(() { _addressIsEmpty = true; }); _updateRecipientData(); }, child: const XIcon(), ) : TextFieldIconButton( semanticsLabel: "Paste Button. Pastes From Clipboard To Address Field Input.", key: const Key( "sendViewPasteAddressFieldButtonKey", ), onTap: () async { final ClipboardData? data = await ref .read(pClipboard) .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"), ); } addressController.text = content.trim(); setState(() { _addressIsEmpty = addressController.text.isEmpty; }); _updateRecipientData(); } }, child: _addressIsEmpty ? const ClipboardIcon() : const XIcon(), ), if (_addressIsEmpty) TextFieldIconButton( semanticsLabel: "Scan QR Button. " "Opens Camera For Scanning QR Code.", key: const Key( "sendViewScanQrButtonKey", ), onTap: _onQrTapped, child: const QrCodeIcon(), ), ], ), ), ), ), ), ), SizedBox( height: isSingle ? 12 : 8, ), if (isSingle) Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Amount", style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), // disable send all since the frost tx creation logic isn't there (yet?) const Spacer(), // CustomTextButton( // text: "Send all ${widget.coin.ticker}", // onTap: () { // amountController.text = widget.sendAllTapped(); // _cryptoAmountChanged(); // }, // ), ], ), if (isSingle) const SizedBox( height: 8, ), TextField( autocorrect: false, enableSuggestions: false, style: STextStyles.smallMed14(context).copyWith( color: Theme.of(context).extension()!.textDark, ), key: const Key("amountInputFieldCryptoTextFieldKey"), controller: amountController, focusNode: amountFocusNode, onChanged: (_) { _updateRecipientData(); }, keyboardType: Util.isDesktop ? null : const TextInputType.numberWithOptions( signed: false, decimal: true, ), textAlign: TextAlign.right, inputFormatters: [ AmountInputFormatter( decimals: widget.coin.fractionDigits, unit: ref.watch(pAmountUnit(widget.coin)), locale: locale, ), ], 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(widget.coin)) .unitForCoin(widget.coin), style: STextStyles.smallMed14(context).copyWith( color: Theme.of(context) .extension()! .accentColorDark, ), ), ), ), ), ), ], ), ); } }