diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index d12071936..dee54419f 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -107,6 +107,8 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin Navigator.of(context).pushNamed(Routes.devQRTools), ), + if (FeatureFlag.hasDevOptions) + SettingsCellWithArrow( + title: '[dev] exchange provider logs', + handler: (BuildContext context) => + Navigator.of(context).pushNamed(Routes.devExchangeProviderLogs), + ), Spacer(), SettingsVersionCell( title: S.of(context).version(_otherSettingsViewModel.currentVersion), diff --git a/lib/src/widgets/bottom_sheet/payment_confirmation_bottom_sheet.dart b/lib/src/widgets/bottom_sheet/payment_confirmation_bottom_sheet.dart index 23913aa2a..afe2e404b 100644 --- a/lib/src/widgets/bottom_sheet/payment_confirmation_bottom_sheet.dart +++ b/lib/src/widgets/bottom_sheet/payment_confirmation_bottom_sheet.dart @@ -65,6 +65,21 @@ class _PaymentConfirmationContent extends StatelessWidget { final VoidCallback onChangeWallet; final VoidCallback onSwap; + /// Checks if the given address is a MWEB or SP (Silent Payment) address + bool _isMwebOrSpAddress(String address) { + if (address.isEmpty) return false; + + final lowerAddress = address.toLowerCase(); + + // Check for MWEB addresses (Litecoin MWEB addresses start with "ltcmweb1") + if (lowerAddress.startsWith('ltcmweb1')) return true; + + // Check for Silent Payment addresses (Bitcoin SP addresses start with "sp1", "tsp1") + if (lowerAddress.startsWith('sp1') || lowerAddress.startsWith('tsp1')) return true; + + return false; + } + @override Widget build(BuildContext context) { return Observer( @@ -78,6 +93,9 @@ class _PaymentConfirmationContent extends StatelessWidget { final hasAtLeastOneWallet = paymentFlowResult.type == PaymentFlowType.singleWallet || hasMultipleWallets; + final isMwebOrSpAddress = + _isMwebOrSpAddress(paymentFlowResult.addressDetectionResult?.address ?? ''); + return Container( padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), child: Column( @@ -114,13 +132,15 @@ class _PaymentConfirmationContent extends StatelessWidget { ), const SizedBox(height: 72), if (hasAtLeastOneWallet) ...[ - PrimaryButton( - onPressed: onSwap, - text: '${S.current.swap} $currentWalletName', - color: Theme.of(context).colorScheme.surfaceContainer, - textColor: Theme.of(context).colorScheme.onSecondaryContainer, - ), - const SizedBox(height: 10), + if (!isMwebOrSpAddress) ...[ + PrimaryButton( + onPressed: onSwap, + text: '${S.current.swap} $currentWalletName', + color: Theme.of(context).colorScheme.surfaceContainer, + textColor: Theme.of(context).colorScheme.onSecondaryContainer, + ), + const SizedBox(height: 10), + ], PrimaryButton( onPressed: hasMultipleWallets ? onSelectWallet : onChangeWallet, text: S.current.switch_wallet, @@ -136,16 +156,19 @@ class _PaymentConfirmationContent extends StatelessWidget { textColor: Theme.of(context).colorScheme.onSecondaryContainer, ), const SizedBox(height: 10), - PrimaryButton( - onPressed: onSwap, - text: '${S.current.swap} $currentWalletName', - color: hasAtLeastOneWallet - ? Theme.of(context).colorScheme.surfaceContainer - : Theme.of(context).colorScheme.primary, - textColor: hasAtLeastOneWallet - ? Theme.of(context).colorScheme.onSecondaryContainer - : Theme.of(context).colorScheme.onPrimary, - ), + if (!isMwebOrSpAddress) ...[ + PrimaryButton( + onPressed: onSwap, + text: '${S.current.swap} $currentWalletName', + color: hasAtLeastOneWallet + ? Theme.of(context).colorScheme.surfaceContainer + : Theme.of(context).colorScheme.primary, + textColor: hasAtLeastOneWallet + ? Theme.of(context).colorScheme.onSecondaryContainer + : Theme.of(context).colorScheme.onPrimary, + ), + const SizedBox(height: 10), + ], const SizedBox(height: 32), ], ], diff --git a/lib/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart b/lib/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart index 0864e882c..69f4abf0b 100644 --- a/lib/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart +++ b/lib/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:cake_wallet/core/amount_validator.dart'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/di.dart'; @@ -11,8 +9,10 @@ import 'package:cake_wallet/src/widgets/bottom_sheet/swap_details_bottom_sheet.d import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/utils/address_formatter.dart'; import 'package:cake_wallet/utils/debounce.dart'; +import 'package:cw_core/crypto_amount_format.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cw_core/wallet_type.dart'; @@ -76,14 +76,12 @@ class SwapConfirmationContentState extends State { late TextEditingController _amountController; late TextEditingController _amountFiatController; late TextEditingController _addressController; - late TextEditingController _noteController; final _receiveAmountDebounce = Debounce(Duration(milliseconds: 500)); final _receiveAmountFiatDebounce = Debounce(Duration(milliseconds: 500)); final FocusNode _amountFocus = FocusNode(); final FocusNode _amountFiatFocus = FocusNode(); final FocusNode _addressFocus = FocusNode(); - final FocusNode _noteFocus = FocusNode(); final _formKey = GlobalKey(); ReactionDisposer? _receiveAmountReaction; @@ -94,6 +92,7 @@ class SwapConfirmationContentState extends State { bool _showingFailureDialog = false; bool _showingSwapDetailsDialog = false; + bool _isUserTypingFiat = false; @override void initState() { @@ -106,8 +105,6 @@ class SwapConfirmationContentState extends State { : '0.00'); _amountFiatController = TextEditingController(text: widget.exchangeViewModel.receiveAmountFiatFormatted); - _noteController = - TextEditingController(text: widget.paymentFlowResult.addressDetectionResult?.note ?? ''); WidgetsBinding.instance.addPostFrameCallback( (_) => _setUpReactions( @@ -123,11 +120,9 @@ class SwapConfirmationContentState extends State { _amountController.dispose(); _amountFiatController.dispose(); _addressController.dispose(); - _noteController.dispose(); _amountFocus.dispose(); _amountFiatFocus.dispose(); _addressFocus.dispose(); - _noteFocus.dispose(); _receiveAmountReaction?.call(); _receiveAddressReaction?.call(); _tradeStateReaction?.call(); @@ -176,7 +171,23 @@ class SwapConfirmationContentState extends State { hintText: 'Amount (${detectedCurrencyName})', focusNode: _amountFocus, controller: _amountController, - keyboardType: TextInputType.numberWithOptions(decimal: true), + keyboardType: TextInputType.numberWithOptions(decimal: true, signed: false), + inputFormatters: [ + FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]')), + ], + onChanged: (value) { + final sanitized = value + .replaceAll(',', '.') + .withMaxDecimals(widget.exchangeViewModel.receiveCurrency.decimals); + if (sanitized != _amountController.text) { + // Update text while preserving a sane cursor position to avoid auto-selection + _amountController.value = _amountController.value.copyWith( + text: sanitized, + selection: TextSelection.collapsed(offset: sanitized.length), + composing: TextRange.empty, + ); + } + }, validator: (value) { return AmountValidator( isAutovalidate: true, @@ -260,14 +271,6 @@ class SwapConfirmationContentState extends State { focusNode: _addressFocus, controller: _addressController, ), - const SizedBox(height: 8), - SwapConfirmationTextfield( - maxLines: 1, - key: ValueKey('swap_confirmation_bottomsheet_note_textfield_key'), - hintText: 'Transaction Note', - focusNode: _noteFocus, - controller: _noteController, - ), SizedBox(height: 8), Center( child: Text( @@ -303,7 +306,7 @@ class SwapConfirmationContentState extends State { _receiveAmountFiatReaction = reaction((_) => exchangeViewModel.receiveAmountFiatFormatted, (String amount) { - if (_amountFiatController.text != amount) { + if (!_isUserTypingFiat && _amountFiatController.text != amount) { _amountFiatController.text = amount; } }); @@ -381,9 +384,14 @@ class SwapConfirmationContentState extends State { _amountFiatController.addListener(() { if (_amountFiatController.text != exchangeViewModel.receiveAmountFiatFormatted) { + _isUserTypingFiat = true; _receiveAmountFiatDebounce.run(() { exchangeViewModel.loadLimits(); exchangeViewModel.setReceiveAmountFromFiat(fiatAmount: _amountFiatController.text); + // Reset the flag after the debounced operation completes + Future.delayed(Duration(milliseconds: 100), () { + _isUserTypingFiat = false; + }); }); } }); @@ -394,6 +402,17 @@ class SwapConfirmationContentState extends State { } }); + _amountFiatFocus.addListener(() { + if (_amountFiatFocus.hasFocus) { + _isUserTypingFiat = true; + } else { + // Reset the flag when user stops focusing on the field + Future.delayed(Duration(milliseconds: 200), () { + _isUserTypingFiat = false; + }); + } + }); + exchangeViewModel.receiveCurrency = walletTypeToCryptoCurrency(paymentFlowResult.walletType!); await exchangeViewModel.fetchFiatPrice(exchangeViewModel.receiveCurrency); @@ -417,6 +436,8 @@ class SwapConfirmationTextfield extends StatelessWidget { this.maxLines = 1, this.validator, this.keyboardType, + this.onChanged, + this.inputFormatters, }); final FocusNode focusNode; @@ -427,7 +448,8 @@ class SwapConfirmationTextfield extends StatelessWidget { final int maxLines; final String? Function(String?)? validator; final TextInputType? keyboardType; - + final void Function(String)? onChanged; + final List? inputFormatters; @override Widget build(BuildContext context) { return Container( @@ -472,6 +494,8 @@ class SwapConfirmationTextfield extends StatelessWidget { maxLines: maxLines, validator: validator, keyboardType: keyboardType, + onChanged: onChanged, + inputFormatters: inputFormatters, ), ], ), diff --git a/lib/utils/payment_request.dart b/lib/utils/payment_request.dart index c6519897c..b574ab260 100644 --- a/lib/utils/payment_request.dart +++ b/lib/utils/payment_request.dart @@ -44,10 +44,12 @@ class PaymentRequest { if (nano != null) { if (amount.isNotEmpty) { - if (address.contains("nano")) { - amount = nanoUtil!.getRawAsUsableString(amount, nanoUtil!.rawPerNano); - } else if (address.contains("ban")) { - amount = nanoUtil!.getRawAsUsableString(amount, nanoUtil!.rawPerBanano); + if (!_isAlreadyUsableAmount(amount)) { + if (address.contains("nano")) { + amount = nanoUtil!.getRawAsUsableString(amount, nanoUtil!.rawPerNano); + } else if (address.contains("ban")) { + amount = nanoUtil!.getRawAsUsableString(amount, nanoUtil!.rawPerBanano); + } } } } @@ -72,4 +74,25 @@ class PaymentRequest { final String? callbackUrl; final String? callbackMessage; final String? contractAddress; + + /// Checks if the amount string is already in a usable format (e.g., "123.45") and doesn't need to be converted from raw format. + /// + /// This was causing an error for us when we scan Nano QRs with amounts in them, the amounts are already in usable format so when the parsing was done, it returns 0 wrongly. + static bool _isAlreadyUsableAmount(String amount) { + if (amount.isEmpty) return false; + + // Try to parse as double - if successful, it's already in usable format + final parsed = double.tryParse(amount.replaceAll(',', '.')); + if (parsed == null) return false; + + // Check if the amount contains a decimal point and is a reasonable number, + // it's likely already in usable format rather than raw format + // Raw amounts are typically very large integers without decimal points + if (amount.contains('.') && parsed > 0 && parsed < 1000000000) return true; + + // If it's a small integer (less than 1 billion), it's likely already usable + if (parsed == parsed.toInt() && parsed < 1000000000) return true; + + return false; + } } diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 390a0bec4..8c120887d 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -43,6 +43,7 @@ import 'package:cake_wallet/utils/token_utilities.dart'; import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; import 'package:cake_wallet/view_model/send/fees_view_model.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart'; +import 'package:cw_core/crypto_amount_format.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/spl_token.dart'; @@ -452,10 +453,13 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with @action void setReceiveAmountFromFiat({required String fiatAmount}) { final _enteredAmount = double.tryParse(fiatAmount.replaceAll(',', '.')) ?? 0.0; - final crypto = _enteredAmount / fiatConversionStore.prices[receiveCurrency]!; + final price = fiatConversionStore.prices[receiveCurrency]; + if (price == null || price == 0.0) return; + + final crypto = _enteredAmount / price; final receiveAmountTmp = _cryptoNumberFormat.format(crypto); if (receiveAmount != receiveAmountTmp) { - changeReceiveAmount(amount: receiveAmountTmp); + changeReceiveAmount(amount: receiveAmountTmp.withMaxDecimals(receiveCurrency.decimals)); } } diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index 9a9aa462b..5e7c8d45b 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -241,6 +241,7 @@ abstract class OutputBase with Store { void setSendAll(String fullBalance) { cryptoFullBalance = fullBalance; sendAll = true; + _updateFiatAmount(); } @action