diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 44dfcc0e..59d3bf79 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -207,5 +207,6 @@ "more": "More", "start": "Start", "connecting": "Connecting", - "accountNotFound": "Account not found" + "accountNotFound": "Account not found", + "sendTip": "Send Tip" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index cf42dcef..8caa754e 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -207,5 +207,6 @@ "more": "Más", "start": "Iniciar", "connecting": "Conectando", - "accountNotFound": "Cuenta no encontrada" + "accountNotFound": "Cuenta no encontrada", + "sendTip": "Enviar propina" } \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 4fa97e54..4be88eaa 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -206,5 +206,6 @@ "close": "Fermer", "start": "Commencer", "connecting": "Connexion", - "accountNotFound": "Compte non trouvé" + "accountNotFound": "Compte non trouvé", + "sendTip": "Envoyer un pourboire" } \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index f2b1e0a0..11b15a2c 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1349,6 +1349,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Learn More'** String get learnMore; + + /// No description provided for @sendTip. + /// + /// In en, this message translates to: + /// **'Send Tip'** + String get sendTip; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index fde6d134..6b0c24e9 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -676,4 +676,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get learnMore => 'Learn More'; + + @override + String get sendTip => 'Send Tip'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index e080fcab..33c46b33 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -680,4 +680,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get learnMore => 'Más información'; + + @override + String get sendTip => 'Enviar propina'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 1f88cc26..9e1a104a 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -681,4 +681,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get learnMore => 'En savoir plus'; + + @override + String get sendTip => 'Envoyer un pourboire'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 29cff167..b8ba37b9 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -683,4 +683,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get learnMore => 'Meer informatie'; + + @override + String get sendTip => 'Fooi versturen'; } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 4d32dbf1..40e71526 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -206,5 +206,6 @@ "close": "Sluiten", "start": "Start", "connecting": "Verbinding maken", - "accountNotFound": "Account niet gevonden" + "accountNotFound": "Account niet gevonden", + "sendTip": "Fooi versturen" } \ No newline at end of file diff --git a/lib/models/send_transaction.dart b/lib/models/send_transaction.dart deleted file mode 100644 index cf278cb0..00000000 --- a/lib/models/send_transaction.dart +++ /dev/null @@ -1,40 +0,0 @@ -class SendTransaction { - String? _to; - String? _amount; - String? _description; - String? _tipTo; - String? _tipAmount; - String? _tipDescription; - - SendTransaction({ - String? to, - String? amount, - String? description, - String? tipTo, - String? tipAmount, - String? tipDescription, - }) : _to = to, - _amount = amount, - _description = description, - _tipTo = tipTo, - _tipAmount = tipAmount, - _tipDescription = tipDescription; - - String? get to => _to; - set to(String? value) => _to = value; - - String? get amount => _amount; - set amount(String? value) => _amount = value; - - String? get description => _description; - set description(String? value) => _description = value; - - String? get tipTo => _tipTo; - set tipTo(String? value) => _tipTo = value; - - String? get tipAmount => _tipAmount; - set tipAmount(String? value) => _tipAmount = value; - - String? get tipDescription => _tipDescription; - set tipDescription(String? value) => _tipDescription = value; -} diff --git a/lib/router/router.dart b/lib/router/router.dart index a5a5daca..1e085cf9 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -90,6 +90,14 @@ GoRouter createRouter( sendToParams += '&description=${uri.queryParameters['description']}'; } + if (uri.queryParameters['tipAmount'] != null) { + sendToParams += + '&tipAmount=${uri.queryParameters['tipAmount']}'; + } + if (uri.queryParameters['tipDescription'] != null) { + sendToParams += + '&tipDescription=${uri.queryParameters['tipDescription']}'; + } } else if (eip681 != null) { sendToParams = encodeParams(uri.toString().replaceFirst('/?', '')); @@ -181,6 +189,9 @@ GoRouter createRouter( final tipTo = state.uri.queryParameters['tipTo']; final amount = state.uri.queryParameters['amount']; final description = state.uri.queryParameters['description']; + final tipAmount = state.uri.queryParameters['tipAmount']; + final tipDescription = + state.uri.queryParameters['tipDescription']; if (sendTo != null) { String params = 'sendto=$sendTo'; if (tipTo != null) { @@ -192,6 +203,12 @@ GoRouter createRouter( if (description != null) { params += '&description=$description'; } + if (tipAmount != null) { + params += '&tipAmount=$tipAmount'; + } + if (tipDescription != null) { + params += '&tipDescription=$tipDescription'; + } sendToURL = 'https://app.citizenwallet.xyz/?$params'; } } @@ -259,7 +276,6 @@ GoRouter createRouter( voucherLogic: extra['voucherLogic'], isMinting: extra['isMinting'] ?? false, sendToURL: extra['sendToURL'], - sendTransaction: extra['sendTransaction'], ); }, ), @@ -316,7 +332,6 @@ GoRouter createRouter( walletLogic: extra['walletLogic'], profilesLogic: extra['profilesLogic'], isMinting: extra['isMinting'] ?? false, - sendTransaction: extra['sendTransaction'], ); }, ), @@ -347,7 +362,6 @@ GoRouter createRouter( to: state.pathParameters['to'], isMinting: extra?['isMinting'] ?? false, profilesLogic: extra?['profilesLogic'], - sendTransaction: extra?['sendTransaction'], walletLogic: extra?['walletLogic'], ); }, @@ -544,7 +558,6 @@ GoRouter createWebRouter( } } - return WebLandingScreen( voucher: state.uri.queryParameters['voucher'], voucherParams: state.uri.queryParameters['params'], diff --git a/lib/screens/send/send_details.dart b/lib/screens/send/send_details.dart index 121a5e5d..a197ee6c 100644 --- a/lib/screens/send/send_details.dart +++ b/lib/screens/send/send_details.dart @@ -1,5 +1,4 @@ // import 'package:citizenwallet/l10n/app_localizations.dart'; -import 'package:citizenwallet/models/send_transaction.dart'; import 'package:citizenwallet/services/config/config.dart'; import 'package:citizenwallet/services/wallet/utils.dart'; import 'package:citizenwallet/state/profiles/logic.dart'; @@ -230,17 +229,13 @@ class _SendDetailsScreenState extends State { final toAccount = selectedAddress ?? walletLogic.addressController.value.text; - - final sendTransaction = SendTransaction( - amount: walletLogic.amountController.value.text, - to: toAccount, - description: walletLogic.messageController.value.text.trim(), - ); + final amount = walletLogic.amountController.value.text; + final description = walletLogic.messageController.value.text.trim(); walletLogic.sendTransaction( - sendTransaction.amount!, - sendTransaction.to!, - message: sendTransaction.description!, + amount, + toAccount, + message: description, ); await Future.delayed(const Duration(milliseconds: 50)); @@ -253,7 +248,6 @@ class _SendDetailsScreenState extends State { 'isMinting': widget.isMinting, 'walletLogic': walletLogic, 'profilesLogic': widget.profilesLogic, - 'sendTransaction': sendTransaction, }); walletLogic.clearInProgressTransaction(); diff --git a/lib/screens/send/send_progress.dart b/lib/screens/send/send_progress.dart index 136c97f7..a9003e5a 100644 --- a/lib/screens/send/send_progress.dart +++ b/lib/screens/send/send_progress.dart @@ -1,6 +1,5 @@ // import 'package:citizenwallet/l10n/app_localizations.dart'; import 'dart:async'; -import 'package:citizenwallet/models/send_transaction.dart'; import 'package:citizenwallet/models/transaction.dart'; import 'package:citizenwallet/services/wallet/utils.dart'; import 'package:citizenwallet/state/profiles/logic.dart'; @@ -8,6 +7,7 @@ import 'package:citizenwallet/state/profiles/state.dart'; import 'package:citizenwallet/state/wallet/logic.dart'; import 'package:citizenwallet/state/wallet/state.dart'; import 'package:citizenwallet/theme/provider.dart'; +import 'package:citizenwallet/utils/send.dart'; import 'package:citizenwallet/widgets/button.dart'; import 'package:citizenwallet/widgets/coin_logo.dart'; import 'package:citizenwallet/widgets/loaders/progress_circle.dart'; @@ -23,7 +23,6 @@ class SendProgress extends StatefulWidget { final bool isMinting; final WalletLogic? walletLogic; final ProfilesLogic? profilesLogic; - final SendTransaction? sendTransaction; const SendProgress({ super.key, @@ -31,7 +30,6 @@ class SendProgress extends StatefulWidget { this.isMinting = false, this.walletLogic, this.profilesLogic, - this.sendTransaction, }); @override @@ -76,15 +74,17 @@ class _SendProgressState extends State { }); } - Future handleSendTip(BuildContext context) async { + Future handleSendTip( + BuildContext context, + SendDestination tipping, + ) async { if (!context.mounted) { return; } final navigator = GoRouter.of(context); - final toAccount = widget.sendTransaction?.to ?? - widget.walletLogic?.addressController.value.text; + final toAccount = tipping.to; await navigator.push( '/wallet/${widget.walletLogic?.account}/send/$toAccount/tip', @@ -92,7 +92,6 @@ class _SendProgressState extends State { 'walletLogic': widget.walletLogic, 'profilesLogic': widget.profilesLogic, 'isMinting': widget.isMinting, - 'sendTransaction': widget.sendTransaction, }, ); } @@ -118,9 +117,12 @@ class _SendProgressState extends State { (WalletState state) => state.inProgressTransactionError, ); + final tipping = context.select((WalletState state) => state.tipping); + if (inProgressTransaction.state == TransactionState.success && _previousState != TransactionState.success) { - final hasTip = context.read().hasTip; + final hasTip = + context.select((WalletState state) => state.tipping) != null; if (!hasTip) { handleStartCloseScreenTimer(context); } @@ -135,7 +137,8 @@ class _SendProgressState extends State { if (inProgressTransaction.state == TransactionState.fail && _previousState != TransactionState.fail && !_isClosing) { - final hasTip = context.read().hasTip; + final hasTip = + context.select((WalletState state) => state.tipping) != null; if (!hasTip) { handleStartCloseScreenTimer(context); } @@ -359,12 +362,11 @@ class _SendProgressState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - context.select((WalletState state) => state.hasTip) + tipping != null ? Column( children: [ Button( - text: - "${AppLocalizations.of(context)!.send} Tip", + text: AppLocalizations.of(context)!.sendTip, color: Theme.of(context) .colors .primary @@ -373,7 +375,8 @@ class _SendProgressState extends State { .colors .white .resolveFrom(context), - onPressed: () => handleSendTip(context), + onPressed: () => + handleSendTip(context, tipping), minWidth: 200, maxWidth: width - 60, ), @@ -384,6 +387,8 @@ class _SendProgressState extends State { onPressed: () { final navigator = GoRouter.of(context); + widget.walletLogic?.clearTipping(); + navigator.go( '/wallet/${widget.walletLogic?.account}'); }, diff --git a/lib/screens/send/send_to.dart b/lib/screens/send/send_to.dart index 2774855d..75c74cd5 100644 --- a/lib/screens/send/send_to.dart +++ b/lib/screens/send/send_to.dart @@ -1,5 +1,4 @@ // import 'package:citizenwallet/l10n/app_localizations.dart'; -import 'package:citizenwallet/models/send_transaction.dart'; import 'package:citizenwallet/services/wallet/contracts/profile.dart'; import 'package:citizenwallet/services/wallet/utils.dart'; import 'package:citizenwallet/state/profiles/logic.dart'; @@ -34,7 +33,6 @@ class SendToScreen extends StatefulWidget { final ProfilesLogic profilesLogic; final VoucherLogic? voucherLogic; final String? sendToURL; - final SendTransaction? sendTransaction; final bool isMinting; @@ -45,7 +43,6 @@ class SendToScreen extends StatefulWidget { this.voucherLogic, this.isMinting = false, this.sendToURL, - this.sendTransaction, }); @override @@ -57,7 +54,6 @@ class _SendToScreenState extends State { final ScanLogic _scanLogic = ScanLogic(); String? _currentSendToURL; final _scrollController = ScrollController(); - // late SendTransaction _sendTransaction; late void Function() debouncedAddressUpdate; diff --git a/lib/screens/send/tip_details.dart b/lib/screens/send/tip_details.dart index 312e0e41..e305f372 100644 --- a/lib/screens/send/tip_details.dart +++ b/lib/screens/send/tip_details.dart @@ -1,5 +1,4 @@ // import 'package:citizenwallet/l10n/app_localizations.dart'; -import 'package:citizenwallet/models/send_transaction.dart'; import 'package:citizenwallet/services/config/config.dart'; import 'package:citizenwallet/services/wallet/utils.dart'; import 'package:citizenwallet/state/profiles/logic.dart'; @@ -29,7 +28,6 @@ class TipDetailsScreen extends StatefulWidget { final WalletLogic walletLogic; final ProfilesLogic profilesLogic; final VoucherLogic? voucherLogic; - final SendTransaction? sendTransaction; final bool isMinting; final bool isLink; @@ -39,7 +37,6 @@ class TipDetailsScreen extends StatefulWidget { required this.walletLogic, required this.profilesLogic, this.voucherLogic, - this.sendTransaction, this.isMinting = false, this.isLink = false, }); @@ -60,12 +57,10 @@ class _TipDetailsScreenState extends State { late void Function() debouncedAmountUpdate; bool _isSending = false; - late SendTransaction _sendTransaction; @override void initState() { super.initState(); - _sendTransaction = widget.sendTransaction ?? SendTransaction(); // Clear amount controller when tip screen initializes // to ensure hasAmount state is reset @@ -73,10 +68,25 @@ class _TipDetailsScreenState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { final walletLogic = widget.walletLogic; - final tipTo = context.read().tipTo; + final tipping = context.read().tipping; - if (tipTo != null) { - widget.profilesLogic.getProfile(tipTo).then((profile) { + if (tipping != null) { + // Set address state + walletLogic.setHasAddress(true); + + // Pre-fill amount and description if provided + if (tipping.amount != null) { + walletLogic.amountController.text = tipping.amount!; + } + if (tipping.description != null) { + walletLogic.messageController.text = tipping.description!; + } + + // Update amount after setting text + walletLogic.updateAmount(unlimited: widget.isMinting); + + // Load profile for tip recipient + widget.profilesLogic.getProfile(tipping.to).then((profile) { if (profile != null) { widget.profilesLogic.selectProfile(profile); } @@ -92,18 +102,6 @@ class _TipDetailsScreenState extends State { }); } - @override - void didChangeDependencies() { - super.didChangeDependencies(); - final tipTo = context.read().tipTo; - if (tipTo != null) { - widget.walletLogic.setHasTip(true); - widget.walletLogic.setHasAddress(true); - // Reset amount state since tip screen starts with empty amount - widget.walletLogic.updateAmount(unlimited: widget.isMinting); - } - } - @override void dispose() { amountFocusNode.dispose(); @@ -112,8 +110,12 @@ class _TipDetailsScreenState extends State { final walletLogic = widget.walletLogic; - walletLogic.clearAmountController(); - walletLogic.resetInputErrorState(); + // Schedule controller clearing after the current frame to avoid + // triggering rebuilds during navigation + WidgetsBinding.instance.addPostFrameCallback((_) { + walletLogic.clearAmountController(); + walletLogic.resetInputErrorState(); + }); super.dispose(); } @@ -225,15 +227,14 @@ class _TipDetailsScreenState extends State { } } - void handleSend( - BuildContext context, String? selectedAddress, String? tipTo) async { + void handleSend(BuildContext context, String? selectedAddress) async { if (_isSending) { return; } final walletLogic = widget.walletLogic; - if (tipTo == null) { + if (selectedAddress == null) { return; } @@ -247,12 +248,9 @@ class _TipDetailsScreenState extends State { final navigator = GoRouter.of(context); - // For tips, use tipTo address (profile is optional, address is required) - final addressToValidate = tipTo; - final isValid = walletLogic.validateSendFields( walletLogic.amountController.value.text, - addressToValidate, + selectedAddress, ); if (!isValid) { @@ -262,41 +260,30 @@ class _TipDetailsScreenState extends State { return; } - // For tips, use tipTo as the account address - final toAccount = tipTo; - - final sendTip = SendTransaction( - tipAmount: walletLogic.amountController.value.text, - tipTo: tipTo, - tipDescription: walletLogic.messageController.value.text.trim(), - ); - try { walletLogic.sendTransaction( - sendTip.tipAmount!, - sendTip.tipTo!, - message: sendTip.tipDescription!, + walletLogic.amountController.value.text, + selectedAddress, + message: walletLogic.messageController.value.text.trim(), ); } catch (e, stackTrace) { - print('error: $e'); - print('stack: $stackTrace'); + debugPrint('error: $e'); + debugPrint('stack: $stackTrace'); } - widget.walletLogic.setHasTip(false); + widget.walletLogic.clearTipping(); widget.walletLogic.setHasAddress(false); - widget.walletLogic.setTipTo(null); await Future.delayed(const Duration(milliseconds: 50)); HapticFeedback.heavyImpact(); final sent = await navigator.push( - '/wallet/${walletLogic.account}/send/$toAccount/progress', + '/wallet/${walletLogic.account}/send/$selectedAddress/progress', extra: { 'isMinting': widget.isMinting, 'walletLogic': walletLogic, 'profilesLogic': widget.profilesLogic, - 'sendTransaction': sendTip, }); if (sent == true) { @@ -419,10 +406,6 @@ class _TipDetailsScreenState extends State { (WalletState state) => state.wallet, ); - final tipTo = context.select( - (WalletState state) => state.tipTo, - ); - final balance = double.tryParse(wallet != null ? wallet.balance : '0.0') ?? 0.0; @@ -472,9 +455,8 @@ class _TipDetailsScreenState extends State { walletLogic.addressController.value.text.length == 42) || selectedProfile != null; - // For tips, use tipTo address if available, otherwise use address controller - final addressToDisplay = tipTo ?? walletLogic.addressController.value.text; - final formattedAddress = formatHexAddress(addressToDisplay); + final formattedAddress = + formatHexAddress(walletLogic.addressController.value.text); final hasAmountText = walletLogic.amountController.value.text.isNotEmpty; final isSendingValid = (hasAddress || isLink) && @@ -837,7 +819,6 @@ class _TipDetailsScreenState extends State { selectedProfile?.account ?? searchedProfile ?.account, - tipTo, ) : null, enabled: isSendingValid, @@ -846,8 +827,10 @@ class _TipDetailsScreenState extends State { ? AppLocalizations.of(context)! .swipeToMint : isLink - ? "${AppLocalizations.of(context)!.swipeToConfirm} Tip" - : "${AppLocalizations.of(context)!.swipeToSend} Tip", + ? AppLocalizations.of(context)! + .swipeToConfirm + : AppLocalizations.of(context)! + .swipeToSend, completionLabelColor: Theme.of(context) .colors .primary diff --git a/lib/screens/wallet/receive.dart b/lib/screens/wallet/receive.dart index 30afe332..00e6c080 100644 --- a/lib/screens/wallet/receive.dart +++ b/lib/screens/wallet/receive.dart @@ -117,7 +117,7 @@ class ReceiveScreenState extends State { _selectedProfile = context.read().selectedProfile; }); - widget.logic.setTipTo(result); + widget.logic.setTipping(to: result); // Update QR code with new tip information widget.logic.updateReceiveQR(); } @@ -130,7 +130,7 @@ class ReceiveScreenState extends State { }); widget.profilesLogic.deSelectProfile(); widget.logic.clearAddressController(); - widget.logic.setTipTo(null); + widget.logic.clearTipping(); widget.logic.updateReceiveQR(); } diff --git a/lib/screens/wallet/screen.dart b/lib/screens/wallet/screen.dart index de525f8e..86278c61 100644 --- a/lib/screens/wallet/screen.dart +++ b/lib/screens/wallet/screen.dart @@ -144,6 +144,7 @@ class WalletScreenState extends State _receiveParams = widget.receiveParams; _deepLink = widget.deepLink; _deepLinkParams = widget.deepLinkParams; + _sendToURL = widget.sendToURL; WidgetsBinding.instance.addPostFrameCallback((_) { onLoad(); @@ -587,7 +588,7 @@ class WalletScreenState extends State }); if (result != true && sendToURL != null) { - _logic.clearTipTo(); + _logic.clearTipping(); _sendToURL = null; } diff --git a/lib/state/wallet/logic.dart b/lib/state/wallet/logic.dart index fac4784d..41e6fbe5 100644 --- a/lib/state/wallet/logic.dart +++ b/lib/state/wallet/logic.dart @@ -1687,16 +1687,20 @@ class WalletLogic extends WidgetsBindingObserver { _amountController.clear(); } - void clearTipTo() { - _state.clearTipTo(); - } - - void setTipTo(String? tipTo) { - _state.setTipTo(tipTo); + void setTipping({ + required String to, + String? amount, + String? description, + }) { + _state.setTipping( + to: to, + amount: amount, + description: description, + ); } - void setHasTip(bool value) { - _state.setHasTip(value); + void clearTipping() { + _state.clearTipping(); } void setHasAddress(bool value) { @@ -1816,15 +1820,21 @@ class WalletLogic extends WidgetsBindingObserver { } if (parsedData.amount != null) { - if (format == QRFormat.eip681Transfer) { - final amount = fromDoubleUnit( - parsedData.amount!, - decimals: _wallet.currency.decimals, - ); - _amountController.text = amount; + // Parse amount value + final numValue = double.tryParse(parsedData.amount!) ?? 0; + + // Format amount based on community decimal support + final decimalDigits = _state.wallet?.decimalDigits ?? 0; + if (decimalDigits == 0) { + // No decimal support - use integer format + _amountController.text = numValue.toInt().toString(); } else { - _amountController.text = parsedData.amount!; + // Decimal support - format with appropriate precision + _amountController.text = numValue + .toStringAsFixed(decimalDigits) + .replaceAll(RegExp(r'\.?0+$'), ''); } + updateAmount(); } @@ -1851,9 +1861,26 @@ class WalletLogic extends WidgetsBindingObserver { } // Handle tip information if present - if (parsedData.tip != null) { - _state.setTipTo(parsedData.tip!.to); - _state.setHasTip(true); + if (parsedData.tip != null && parsedData.tip!.amount != null) { + // Format tip amount based on community decimal support + final tipNumValue = double.tryParse(parsedData.tip!.amount!) ?? 0; + final decimalDigits = _state.wallet?.decimalDigits ?? 0; + String formattedTipAmount; + if (decimalDigits == 0) { + // No decimal support - use integer format + formattedTipAmount = tipNumValue.toInt().toString(); + } else { + // Decimal support - format with appropriate precision + formattedTipAmount = tipNumValue + .toStringAsFixed(decimalDigits) + .replaceAll(RegExp(r'\.?0+$'), ''); + } + + _state.setTipping( + to: parsedData.tip!.to, + amount: formattedTipAmount, + description: parsedData.tip!.description, + ); } return addressToUse; @@ -1919,9 +1946,9 @@ class WalletLogic extends WidgetsBindingObserver { } // Add tipTo parameter if it exists in the state - final tipTo = _state.tipTo; - if (tipTo != null && tipTo.isNotEmpty) { - params += '&tipTo=$tipTo'; + final tipping = _state.tipping; + if (tipping != null && tipping.to.isNotEmpty) { + params += '&tipTo=${tipping.to}'; } // Check if URL already has query parameters in the fragment diff --git a/lib/state/wallet/state.dart b/lib/state/wallet/state.dart index 4eb75cba..d87d6292 100644 --- a/lib/state/wallet/state.dart +++ b/lib/state/wallet/state.dart @@ -4,6 +4,7 @@ import 'package:citizenwallet/services/config/config.dart'; import 'package:citizenwallet/services/engine/events.dart'; import 'package:citizenwallet/services/preferences/preferences.dart'; import 'package:citizenwallet/state/wallet/utils.dart'; +import 'package:citizenwallet/utils/send.dart'; import 'package:collection/collection.dart'; import 'package:flutter/cupertino.dart'; @@ -80,9 +81,7 @@ class WalletState with ChangeNotifier { bool cwWalletsLoading = false; bool cwWalletsError = false; - String? tipTo; - bool _hasTip = false; - bool get hasTip => _hasTip; + SendDestination? tipping; void setEventServiceState(EventServiceState state) { eventServiceState = state; @@ -701,19 +700,23 @@ class WalletState with ChangeNotifier { notifyListeners(); } - void setTipTo(String? tipTo) { - this.tipTo = tipTo; + void setTipping({ + required String to, + String? amount, + String? description, + }) { + tipping = SendDestination( + to: to, + amount: amount, + description: description, + ); notifyListeners(); } - void setHasTip(bool value) { - _hasTip = value; + void clearTipping() { + tipping = null; notifyListeners(); } - void clearTipTo() { - tipTo = null; - _hasTip = false; - notifyListeners(); - } + bool get hasTip => tipping != null; } diff --git a/lib/utils/qr.dart b/lib/utils/qr.dart index fb0507a7..cce1ee7a 100644 --- a/lib/utils/qr.dart +++ b/lib/utils/qr.dart @@ -133,7 +133,6 @@ ParsedQRData parseSendtoUrl(String raw) { } // Handle malformed query strings (convert ? to & after the first one) - // e.g., "alias=...?sendto=..." becomes "alias=...&sendto=..." if (queryString.contains('?')) { final firstQuestionMark = queryString.indexOf('?'); queryString = queryString.substring(0, firstQuestionMark) + @@ -148,24 +147,54 @@ ParsedQRData parseSendtoUrl(String raw) { uriData = parsedUri; } - final sendToParam = uriData.queryParameters['sendto']; - final amountParam = uriData.queryParameters['amount']; - final descriptionParam = uriData.queryParameters['description']; + // Use 'var' so we can update them if hidden params are found + var sendToParam = uriData.queryParameters['sendto']; + var amountParam = uriData.queryParameters['amount']; + var descriptionParam = uriData.queryParameters['description']; - final tipToParam = uriData.queryParameters['tipTo']; - final tipAmountParam = uriData.queryParameters['tipAmount']; - final tipDescriptionParam = uriData.queryParameters['tipDescription']; + var tipToParam = uriData.queryParameters['tipTo']; + var tipAmountParam = uriData.queryParameters['tipAmount']; + var tipDescriptionParam = uriData.queryParameters['tipDescription']; + + // 1. Handle encoded params inside sendto + if (sendToParam != null && sendToParam.contains('&')) { + final firstAmpIndex = sendToParam.indexOf('&'); + final realSendTo = sendToParam.substring(0, firstAmpIndex); + final hiddenParamsString = sendToParam.substring(firstAmpIndex + 1); + + sendToParam = realSendTo; + + final hiddenParams = Uri.splitQueryString(hiddenParamsString); + amountParam ??= hiddenParams['amount']; + descriptionParam ??= hiddenParams['description']; + } + + // 2. Handle encoded params inside tipTo (NEW LOGIC) + if (tipToParam != null && tipToParam.contains('&')) { + final firstAmpIndex = tipToParam.indexOf('&'); + final realTipTo = tipToParam.substring(0, firstAmpIndex); + final hiddenParamsString = tipToParam.substring(firstAmpIndex + 1); + + // Clean the tipTo param + tipToParam = realTipTo; + + // Parse the hidden string + final hiddenTipParams = Uri.splitQueryString(hiddenParamsString); + + // Populate tipAmount and tipDescription if they are null + tipAmountParam ??= hiddenTipParams['tipAmount']; + tipDescriptionParam ??= hiddenTipParams['tipDescription']; + } if (sendToParam == null) { return ParsedQRData(address: ''); } final address = sendToParam.split('@').first; - // Extract alias from sendto parameter (after @) as fallback - final aliasFromSendto = sendToParam.contains('@') - ? sendToParam.split('@').last - : null; - // Use explicit alias parameter if present, otherwise fallback to alias from sendto + + final aliasFromSendto = + sendToParam.contains('@') ? sendToParam.split('@').last : null; + final alias = uriData.queryParameters['alias'] ?? aliasFromSendto; final tip = tipToParam != null