From 6209e36dfcf536e9dad8f7d8b86144fd9a20f434 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Mon, 20 Jul 2026 13:19:09 +0200 Subject: [PATCH 1/5] fix-<-01-fiat-amounts (#3421) * fix: handling of "< 0.1" Amounts in send flow * refactor: clean up imports and restructure classes in `send_page.dart` apply lint --- lib/new-ui/pages/send_page.dart | 1424 +++++++++-------- .../widgets/send_page/fiat_amount_bar.dart | 123 +- .../widgets/send_page/send_amount_input.dart | 176 +- 3 files changed, 898 insertions(+), 825 deletions(-) diff --git a/lib/new-ui/pages/send_page.dart b/lib/new-ui/pages/send_page.dart index 6ee37d2ee..3ab80929e 100644 --- a/lib/new-ui/pages/send_page.dart +++ b/lib/new-ui/pages/send_page.dart @@ -1,85 +1,97 @@ -import 'package:cake_wallet/core/address_validator.dart'; -import 'package:cake_wallet/core/auth_service.dart'; -import 'package:cake_wallet/core/execution_state.dart'; -import 'package:cake_wallet/core/open_crypto_pay/open_cryptopay_service.dart'; -import 'package:cake_wallet/di.dart'; -import 'package:cake_wallet/entities/contact_record.dart'; -import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; -import 'package:cake_wallet/evm/evm.dart'; -import 'package:cake_wallet/exchange/trade.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/new-ui/modal_navigator.dart'; -import 'package:cake_wallet/new-ui/pages/coin_control_page.dart'; -import 'package:cake_wallet/new-ui/widgets/animated_dropdown.dart'; -import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart'; -import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart'; -import 'package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart'; -import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart'; -import 'package:cake_wallet/new-ui/widgets/picker.dart'; -import 'package:cake_wallet/new-ui/widgets/send_page/fiat_amount_bar.dart'; -import 'package:cake_wallet/new-ui/widgets/send_page/send_confirm_sheet.dart'; -import 'package:cake_wallet/new-ui/widgets/send_page/send_memo_input.dart'; -import 'package:cake_wallet/reactions/wallet_connect.dart'; -import 'package:cake_wallet/src/widgets/bottom_sheet/token_selection_bottom_sheet.dart'; -import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; -import 'package:cake_wallet/src/widgets/new_list_row/list_item_regular_row_widget.dart'; -import 'package:cake_wallet/src/widgets/standard_checkbox.dart'; -import 'package:cake_wallet/store/app_store.dart'; -import 'package:cw_core/amount/amount_sanitizer.dart'; -import 'package:cw_core/amount/money.dart'; -import 'package:cw_core/lnurl.dart'; -import 'package:cw_core/wallet_info.dart'; +import "package:cake_wallet/core/address_resolver/parsed_address.dart"; +import "package:cake_wallet/core/address_validator.dart"; +import "package:cake_wallet/core/auth_service.dart"; +import "package:cake_wallet/core/execution_state.dart"; +import "package:cake_wallet/core/open_crypto_pay/open_cryptopay_service.dart"; +import "package:cake_wallet/di.dart"; +import "package:cake_wallet/entities/contact_record.dart"; +import "package:cake_wallet/entities/priority_for_wallet_type.dart"; +import "package:cake_wallet/evm/evm.dart"; +import "package:cake_wallet/exchange/trade.dart"; +import "package:cake_wallet/generated/i18n.dart"; +import "package:cake_wallet/main.dart"; +import "package:cake_wallet/monero/monero.dart"; +import "package:cake_wallet/new-ui/modal_navigator.dart"; +import "package:cake_wallet/new-ui/pages/coin_control_page.dart"; +import "package:cake_wallet/new-ui/widgets/animated_dropdown.dart"; +import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart"; +import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart"; +import "package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart"; +import "package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart"; +import "package:cake_wallet/new-ui/widgets/modern_button.dart"; +import "package:cake_wallet/new-ui/widgets/new_primary_button.dart"; +import "package:cake_wallet/new-ui/widgets/picker.dart"; +import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart"; +import "package:cake_wallet/new-ui/widgets/send_page/directional_switcher.dart"; +import "package:cake_wallet/new-ui/widgets/send_page/fiat_amount_bar.dart"; +import "package:cake_wallet/new-ui/widgets/send_page/l2_action_wallet_selector.dart"; +import "package:cake_wallet/new-ui/widgets/send_page/recipient_dot_row.dart"; +import "package:cake_wallet/new-ui/widgets/send_page/send_address_input.dart"; +import "package:cake_wallet/new-ui/widgets/send_page/send_amount_input.dart"; +import "package:cake_wallet/new-ui/widgets/send_page/send_confirm_sheet.dart"; +import "package:cake_wallet/new-ui/widgets/send_page/send_memo_input.dart"; +import "package:cake_wallet/new-ui/widgets/send_page/send_syncing_indicator.dart"; +import "package:cake_wallet/reactions/wallet_connect.dart"; +import "package:cake_wallet/routes.dart" show Routes; +import "package:cake_wallet/src/screens/connect_device/connect_device_page.dart"; +import "package:cake_wallet/src/widgets/alert_with_one_action.dart"; +import "package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart"; +import "package:cake_wallet/src/widgets/bottom_sheet/payment_confirmation_bottom_sheet.dart"; +import "package:cake_wallet/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart"; +import "package:cake_wallet/src/widgets/bottom_sheet/token_selection_bottom_sheet.dart"; +import "package:cake_wallet/src/widgets/bottom_sheet/wallet_switcher_bottom_sheet.dart"; +import "package:cake_wallet/src/widgets/cake_image_widget.dart"; +import "package:cake_wallet/src/widgets/new_list_row/list_item_regular_row_widget.dart"; +import "package:cake_wallet/src/widgets/primary_button.dart"; +import "package:cake_wallet/src/widgets/standard_checkbox.dart"; +import "package:cake_wallet/store/app_store.dart"; +import "package:cake_wallet/utils/payment_request.dart"; +import "package:cake_wallet/utils/show_pop_up.dart"; +import "package:cake_wallet/view_model/contact_list/contact_list_view_model.dart"; +import "package:cake_wallet/view_model/payment/payment_view_model.dart"; +import "package:cake_wallet/view_model/send/output.dart"; +import "package:cake_wallet/view_model/send/send_view_model.dart"; +import "package:cake_wallet/view_model/send/send_view_model_state.dart"; +import "package:cake_wallet/view_model/wallet_switcher_view_model.dart"; +import "package:cw_core/amount/amount_sanitizer.dart"; +import "package:cw_core/amount/money.dart"; +import "package:cw_core/crypto_currency.dart"; +import "package:cw_core/lnurl.dart"; +import "package:cw_core/transaction_priority.dart"; +import "package:cw_core/unspent_coin_type.dart"; +import "package:cw_core/utils/print_verbose.dart"; +import "package:cw_core/wallet_info.dart"; import "package:cw_core/wallet_type.dart"; -import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; -import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; -import 'package:cake_wallet/new-ui/widgets/send_page/directional_switcher.dart'; -import 'package:cake_wallet/new-ui/widgets/send_page/recipient_dot_row.dart'; -import 'package:cake_wallet/new-ui/widgets/send_page/send_address_input.dart'; -import 'package:cake_wallet/new-ui/widgets/send_page/send_amount_input.dart'; -import 'package:cake_wallet/new-ui/widgets/send_page/send_syncing_indicator.dart'; -import 'package:cake_wallet/routes.dart' show Routes; -import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; -import 'package:cake_wallet/src/widgets/bottom_sheet/payment_confirmation_bottom_sheet.dart'; -import 'package:cake_wallet/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart'; -import 'package:cake_wallet/src/widgets/bottom_sheet/wallet_switcher_bottom_sheet.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/utils/payment_request.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cake_wallet/view_model/payment/payment_view_model.dart'; -import 'package:cake_wallet/view_model/send/output.dart'; -import 'package:cake_wallet/core/address_resolver/parsed_address.dart'; -import 'package:cake_wallet/view_model/send/send_view_model.dart'; -import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; -import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/transaction_priority.dart'; -import 'package:cw_core/utils/print_verbose.dart'; - -import 'package:cake_wallet/main.dart'; -import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; -import 'package:cake_wallet/new-ui/widgets/send_page/l2_action_wallet_selector.dart'; -import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; -import 'package:cw_core/unspent_coin_type.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:mobx/mobx.dart'; -import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; +import "package:flutter/cupertino.dart"; +import "package:flutter/material.dart"; +import "package:flutter_mobx/flutter_mobx.dart"; +import "package:mobx/mobx.dart"; +import "package:modal_bottom_sheet/modal_bottom_sheet.dart"; class SendPageHelpContent { + const SendPageHelpContent({ + required this.title, + required this.imagePath, + required this.description, + this.disclaimer, + }); + final String imagePath; final String title; final String description; final String? disclaimer; - - const SendPageHelpContent( - {required this.title, required this.imagePath, required this.description, this.disclaimer}); } class SendPageModes { + const SendPageModes({ + required this.title, + required this.showAddressField, + this.description, + this.confirmSheetIconPath, + this.helpContent, + this.popOnConfirmation = true, + }); + final bool showAddressField; final String title; final String? description; @@ -87,87 +99,87 @@ class SendPageModes { final SendPageHelpContent? helpContent; final bool popOnConfirmation; - const SendPageModes( - {required this.title, - this.description, - required this.showAddressField, - this.confirmSheetIconPath, - this.helpContent, - this.popOnConfirmation = true}); - static final SendPageModes normal = SendPageModes(title: S.current.send, showAddressField: true); static final SendPageModes lightningDeposit = SendPageModes( + title: S.current.bitcoin_lightning_deposit, + description: S.current.to_lightning, + showAddressField: false, + helpContent: SendPageHelpContent( title: S.current.bitcoin_lightning_deposit, - description: S.current.to_lightning, - showAddressField: false, - helpContent: SendPageHelpContent( - title: S.current.bitcoin_lightning_deposit, - imagePath: "assets/new-ui/lightning_deposit_help.svg", - description: S.current.lightning_deposit_desc, - disclaimer: S.current.lightning_deposit_disclaimer), - popOnConfirmation: false); + imagePath: "assets/new-ui/lightning_deposit_help.svg", + description: S.current.lightning_deposit_desc, + disclaimer: S.current.lightning_deposit_disclaimer, + ), + popOnConfirmation: false, + ); static final SendPageModes lightningWithdrawal = SendPageModes( + title: S.current.bitcoin_lightning_withdraw, + description: S.current.to_on_chain, + showAddressField: false, + helpContent: SendPageHelpContent( title: S.current.bitcoin_lightning_withdraw, - description: S.current.to_on_chain, - showAddressField: false, - helpContent: SendPageHelpContent( - title: S.current.bitcoin_lightning_withdraw, - imagePath: "assets/new-ui/lightning_withdraw_help.svg", - description: S.current.lightning_withdraw_desc, - disclaimer: S.current.lightning_withdraw_disclaimer), - popOnConfirmation: false); + imagePath: "assets/new-ui/lightning_withdraw_help.svg", + description: S.current.lightning_withdraw_desc, + disclaimer: S.current.lightning_withdraw_disclaimer, + ), + popOnConfirmation: false, + ); static final SendPageModes mwebDeposit = SendPageModes( - title: "${S.current.mask} Litecoin", - showAddressField: false, - confirmSheetIconPath: "assets/new-ui/mask.svg", - helpContent: SendPageHelpContent( - title: S.current.about_litecoin_privacy, - imagePath: "assets/new-ui/mweb_help.svg", - description: "${S.current.mweb_help_desc_1}\n\n${S.current.mweb_help_desc_2}", - disclaimer: S.current.mweb_help_disclaimer), - popOnConfirmation: false); + title: "${S.current.mask} Litecoin", + showAddressField: false, + confirmSheetIconPath: "assets/new-ui/mask.svg", + helpContent: SendPageHelpContent( + title: S.current.about_litecoin_privacy, + imagePath: "assets/new-ui/mweb_help.svg", + description: "${S.current.mweb_help_desc_1}\n\n${S.current.mweb_help_desc_2}", + disclaimer: S.current.mweb_help_disclaimer, + ), + popOnConfirmation: false, + ); static final SendPageModes mwebWithdrawal = SendPageModes( - title: "${S.current.unmask} Litecoin", - showAddressField: false, - confirmSheetIconPath: "assets/new-ui/unmask.svg", - helpContent: SendPageHelpContent( - title: S.current.about_litecoin_privacy, - imagePath: "assets/new-ui/mweb_help.svg", - description: "${S.current.mweb_help_desc_1}\n\n${S.current.mweb_help_desc_2}", - disclaimer: S.current.mweb_help_disclaimer), - popOnConfirmation: false); + title: "${S.current.unmask} Litecoin", + showAddressField: false, + confirmSheetIconPath: "assets/new-ui/unmask.svg", + helpContent: SendPageHelpContent( + title: S.current.about_litecoin_privacy, + imagePath: "assets/new-ui/mweb_help.svg", + description: "${S.current.mweb_help_desc_1}\n\n${S.current.mweb_help_desc_2}", + disclaimer: S.current.mweb_help_disclaimer, + ), + popOnConfirmation: false, + ); static final all = [normal, lightningDeposit, lightningWithdrawal, mwebDeposit, mwebWithdrawal]; } class SendPageParams { - final PaymentRequest? initialPaymentRequest; - final SendPageModes mode; - final CryptoCurrency? initialCurrency; - final UnspentCoinType unspentCoinType; - SendPageParams({ this.initialPaymentRequest, SendPageModes? mode, this.unspentCoinType = UnspentCoinType.any, this.initialCurrency, }) : mode = mode ?? SendPageModes.normal; + + final PaymentRequest? initialPaymentRequest; + final SendPageModes mode; + final CryptoCurrency? initialCurrency; + final UnspentCoinType unspentCoinType; } class NewSendPage extends StatefulWidget { - NewSendPage( - {super.key, - required this.sendViewModel, - required this.paymentViewModel, - required this.walletSwitcherViewModel, - required this.contactListViewModel, - required this.authService, - required SendPageParams params}) - : initialPaymentRequest = params.initialPaymentRequest, + NewSendPage({ + required this.sendViewModel, + required this.paymentViewModel, + required this.walletSwitcherViewModel, + required this.contactListViewModel, + required this.authService, + required SendPageParams params, + super.key, + }) : initialPaymentRequest = params.initialPaymentRequest, mode = params.mode { if (params.initialCurrency != null) { sendViewModel.selectedCryptoCurrency = params.initialCurrency!; @@ -205,20 +217,20 @@ class _NewSendPageState extends State { super.initState(); _addInputControllers(); - reaction((_) => widget.sendViewModel.outputs[_selectedOutput].sendAll, ((bool all) { + reaction((_) => widget.sendViewModel.outputs[_selectedOutput].sendAll, (all) { if (all) { widget.sendViewModel.outputs[_selectedOutput].isFiatEntry = false; _amountControllers[_selectedOutput].text = S.current.all; } - })); + }); - reaction((_) => widget.sendViewModel.outputs[_selectedOutput].address, ((address) { + reaction((_) => widget.sendViewModel.outputs[_selectedOutput].address, (address) { if (_addressControllers[_selectedOutput].text != address) { _addressControllers[_selectedOutput].text = address; } - })); + }); - reaction((_) => widget.sendViewModel.outputs[_selectedOutput].memo, (String memo) { + reaction((_) => widget.sendViewModel.outputs[_selectedOutput].memo, (memo) { if (memo != _memoControllers[_selectedOutput].text) { _memoControllers[_selectedOutput].text = memo; } @@ -270,7 +282,9 @@ class _NewSendPageState extends State { Future _resolveAddressForOutput(Output output) async { final result = await widget.sendViewModel.resolveAddressForOutput(output); - if (result == null) return; + if (result == null || !mounted) { + return; + } final confirmed = await showParsedAddressConfirmationAlert(context, result); @@ -291,308 +305,320 @@ class _NewSendPageState extends State { } @override - Widget build(BuildContext context) { - return Observer(builder: (_) { - final output = widget.sendViewModel.outputs[_selectedOutput]; - return SafeArea( - bottom: false, - child: KeyboardHideOverlay( - unfocusOnTap: true, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.vertical(top: Radius.circular(24))), - child: SafeArea( - child: Column( - spacing: 12, - mainAxisSize: MainAxisSize.max, - children: [ - ModalTopBar( - title: widget.mode.title, - subtitle: widget.mode.description, - leadingIcon: Icon(Icons.close), - onLeadingPressed: Navigator.of(context, rootNavigator: true).pop, - trailingWidget: Observer( - builder: (_) => Row( - spacing: 8, - children: [ - if (widget.sendViewModel.outputs.length > 1) - ModernButton( - size: 36, - icon: CakeImageWidget( - imageUrl: "assets/new-ui/remove_recipient.svg", - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.primary, - BlendMode.srcIn, + Widget build(BuildContext context) => Observer( + builder: (_) { + final output = widget.sendViewModel.outputs[_selectedOutput]; + return SafeArea( + bottom: false, + child: KeyboardHideOverlay( + unfocusOnTap: true, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), + ), + child: SafeArea( + child: Column( + spacing: 12, + mainAxisSize: MainAxisSize.max, + children: [ + ModalTopBar( + title: widget.mode.title, + subtitle: widget.mode.description, + leadingIcon: const Icon(Icons.close), + onLeadingPressed: Navigator.of(context, rootNavigator: true).pop, + trailingWidget: Observer( + builder: (_) => Row( + spacing: 8, + children: [ + if (widget.sendViewModel.outputs.length > 1) + ModernButton( + size: 36, + icon: CakeImageWidget( + imageUrl: "assets/new-ui/remove_recipient.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), ), + onPressed: () { + final outputIndex = _selectedOutput; + if (_selectedOutput != 0) { + _setOutput(_selectedOutput - 1); + } else { + _setOutput(1); + } + _removeInputControllers(outputIndex); + widget.sendViewModel.removeOutput(output); + if (outputIndex == 0) { + _setOutput(0); + } + }, ), - onPressed: () { - final outputIndex = _selectedOutput; - if (_selectedOutput != 0) { - _setOutput(_selectedOutput - 1); - } else { - _setOutput(1); - } - _removeInputControllers(outputIndex); - widget.sendViewModel.removeOutput(output); - if (outputIndex == 0) _setOutput(0); - }), - if (widget.mode == SendPageModes.normal && - widget.sendViewModel.hasMultiRecipient) - ModernButton( - size: 36, - icon: Icon(Icons.add), - onPressed: () { - _addInputControllers(); - widget.sendViewModel.addOutput(); - _setOutput(widget.sendViewModel.outputs.length - 1); - }), - if (widget.mode.helpContent != null) - ModernButton( - size: 36, - icon: CakeImageWidget( - imageUrl: "assets/new-ui/help.svg", - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.primary, BlendMode.srcIn), + if (widget.mode == SendPageModes.normal && + widget.sendViewModel.hasMultiRecipient) + ModernButton( + size: 36, + icon: const Icon(Icons.add), + onPressed: () { + _addInputControllers(); + widget.sendViewModel.addOutput(); + _setOutput(widget.sendViewModel.outputs.length - 1); + }, ), - onPressed: () { - Navigator.of(context).push(CupertinoPageRoute( - builder: (context) => Material( - child: SendHelpPage(content: widget.mode.helpContent!)))); - }) - ], - ), - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.max, - children: [ - Form( - key: _formKey, - child: DirectionalAnimatedSwitcher( - duration: Duration(milliseconds: 300), - child: Column( - key: ValueKey(_selectedOutput), - spacing: 24, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.mode.showAddressField) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 12, - children: [ - Text(S.of(context).address_or_alias), - NewSendAddressInput( - displayName: output.displayName, - validator: output.isParsedAddress - ? widget.sendViewModel.textValidator - : widget.sendViewModel.addressValidator, - addressController: _addressControllers[_selectedOutput], - focusNode: _addressFocusNode, - onURIScanned: (uri) async { - output.resetParsedAddress(); - await _resolveAddressForOutput(output); - - // Process the payment through the new flow - await _handlePaymentFlow( - uri.toString(), - PaymentRequest.fromString(uri.toString()), - ); - }, - onEditingComplete: () { - _addressFocusNode.unfocus(); - // output.fetchParsedAddress(context).then((val){ - // if(_addressControllers[_selectedOutput].text != output.extractedAddress) { - // _addressControllers[_selectedOutput].text = output.extractedAddress; - // } - // }); - }, - onPushAddressBookButton: (context) async { - output.resetParsedAddress(); - }, - onSelectedContact: (contact) { - output.loadContact(contact); - }, - onPushPasteButton: (context) async { - if (_justHandledPasteButton) return; - _justHandledPasteButton = true; - try { - output.resetParsedAddress(); - await _resolveAddressForOutput(output); - - final address = output.isParsedAddress - ? output.extractedAddress - : output.address; - - await _handlePaymentFlow( - address, - PaymentRequest( - address, - _amountControllers[_selectedOutput].text, - _memoControllers[_selectedOutput].text, - "", - null, - ), - ); - } finally { - _justHandledPasteButton = false; - } - - _handleLightningInvoicePaste(); - }, - selectedCurrency: - widget.sendViewModel.selectedCryptoCurrency, + if (widget.mode.helpContent != null) + ModernButton( + size: 36, + icon: CakeImageWidget( + imageUrl: "assets/new-ui/help.svg", + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + onPressed: () { + Navigator.of(context).push( + CupertinoPageRoute( + builder: (context) => Material( + child: SendHelpPage(content: widget.mode.helpContent!), ), - ], - ), - Column( + ), + ); + }, + ), + ], + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + children: [ + Form( + key: _formKey, + child: DirectionalAnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: Column( + key: ValueKey(_selectedOutput), + spacing: 24, crossAxisAlignment: CrossAxisAlignment.start, - spacing: 12, children: [ - Text(S.of(context).amount), - NewSendAmountInput( - validator: output.sendAll - ? widget.sendViewModel.allAmountValidator - : widget.sendViewModel.amountValidator(output), - amountController: _amountControllers[_selectedOutput], - currency: output.isFiatEntry - ? widget.sendViewModel.fiatCurrency.title - : widget.sendViewModel.selectedCryptoCurrencySymbol, - currencyIconPath: output.isFiatEntry - ? "" - : widget.sendViewModel.selectedCryptoCurrency - .iconPath ?? - "", - hasPicker: (output.isFiatEntry || - widget.sendViewModel.hasMultipleTokens), - onPickerClicked: () => _presentCurrencyPicker(context), - maxDecimals: output.isFiatEntry - ? widget.sendViewModel.fiatCurrency.decimals - : widget.sendViewModel.useBaseUnits - ? 0 - : widget - .sendViewModel.selectedCryptoCurrency.decimals, - ), - FiatAmountBar( - fiatInputMode: output.isFiatEntry, - onSwitchButtonPressed: () { - widget.sendViewModel.outputs[_selectedOutput] - .isFiatEntry = !output.isFiatEntry; - _amountControllers[_selectedOutput].text = - output.isFiatEntry - ? output.fiatAmount - : output.displayCryptoAmount; - }, - fiatAmount: _wrapAmount(output.roundedFiatAmount(6), 20), - cryptoAmount: - _wrapAmount(output.roundedCryptoAmount(6), 20), - allAmount: widget.sendViewModel.balance, - cryptoCurrencySymbol: - widget.sendViewModel.selectedCryptoCurrencySymbol, - fiatCurrencySymbol: - widget.sendViewModel.fiatCurrency.symbol, - onAllButtonPressed: () async { - output.setSendAll( - await widget.sendViewModel.sendingBalance); - await output.calculateEstimatedFee(); - }, - ), - ], - ), - if (widget.sendViewModel.isMwebAvailable && - widget.mode == SendPageModes.normal) - StandardCheckbox( - caption: S.of(context).litecoin_mweb_allow_coins, - captionColor: Theme.of(context).colorScheme.onSurface, - borderColor: Theme.of(context).colorScheme.primary, - iconColor: Theme.of(context).colorScheme.primary, - value: [UnspentCoinType.any, UnspentCoinType.mweb] - .contains(widget.sendViewModel.coinTypeToSpendFrom), - onChanged: (value) => - widget.sendViewModel.setAllowMwebCoins(value), - ), - if (widget.sendViewModel.hasMemos) - Observer( - builder: (_) => NewSendMemoInput( - memoController: _memoControllers[_selectedOutput], - maxMemoLength: widget.sendViewModel.maxMemoLength, - memoLength: output.memo.length, - )), - if (widget.sendViewModel.hasCoinControl || - widget.sendViewModel.hasFees) - AnimatedDropdown( - dropdownText: S.of(context).advanced_settings, - content: Column(children: [ - if (widget.sendViewModel.hasFees) - ListItemRegularRowWidget( - keyValue: "", - label: S.of(context).fees, - subtitle: - "~${output.estimatedFee} ${widget.sendViewModel.currencySymbol} (${output.estimatedFeeFiatAmount} ${widget.sendViewModel.fiatCurrency})", - onTap: () { - if (widget - .sendViewModel.feesViewModel.hasFeesPriority) - pickTransactionPriority(context, output); + if (widget.mode.showAddressField) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 12, + children: [ + Text(S.of(context).address_or_alias), + NewSendAddressInput( + displayName: output.displayName, + validator: output.isParsedAddress + ? widget.sendViewModel.textValidator + : widget.sendViewModel.addressValidator, + addressController: + _addressControllers[_selectedOutput], + focusNode: _addressFocusNode, + onURIScanned: (uri) async { + output.resetParsedAddress(); + await _resolveAddressForOutput(output); + + // Process the payment through the new flow + await _handlePaymentFlow( + uri.toString(), + PaymentRequest.fromString(uri.toString()), + ); + }, + onEditingComplete: _addressFocusNode.unfocus, + onPushAddressBookButton: (_) { + output.resetParsedAddress(); + }, + onSelectedContact: (contact) { + output.loadContact(contact); + }, + onPushPasteButton: (context) async { + if (_justHandledPasteButton) { + return; + } + _justHandledPasteButton = true; + try { + output.resetParsedAddress(); + await _resolveAddressForOutput(output); + + final address = output.isParsedAddress + ? output.extractedAddress + : output.address; + + await _handlePaymentFlow( + address, + PaymentRequest( + address, + _amountControllers[_selectedOutput].text, + _memoControllers[_selectedOutput].text, + "", + null, + ), + ); + } finally { + _justHandledPasteButton = false; + } + + _handleLightningInvoicePaste(); + }, + selectedCurrency: + widget.sendViewModel.selectedCryptoCurrency, + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 12, + children: [ + Text(S.of(context).amount), + NewSendAmountInput( + validator: output.sendAll + ? widget.sendViewModel.allAmountValidator + : widget.sendViewModel.amountValidator(output), + amountController: _amountControllers[_selectedOutput], + currency: output.isFiatEntry + ? widget.sendViewModel.fiatCurrency.title + : widget.sendViewModel.selectedCryptoCurrencySymbol, + currencyIconPath: output.isFiatEntry + ? "" + : widget.sendViewModel.selectedCryptoCurrency + .iconPath ?? + "", + hasPicker: output.isFiatEntry || + widget.sendViewModel.hasMultipleTokens, + onPickerClicked: () => _presentCurrencyPicker(context), + maxDecimals: output.isFiatEntry + ? widget.sendViewModel.fiatCurrency.decimals + : widget.sendViewModel.useBaseUnits + ? 0 + : widget.sendViewModel.selectedCryptoCurrency + .decimals, + ), + FiatAmountBar( + fiatInputMode: output.isFiatEntry, + onSwitchButtonPressed: _onFiatSwitchPressed, + fiatAmount: + _wrapAmount(output.roundedFiatAmount(6), 20), + cryptoAmount: + _wrapAmount(output.roundedCryptoAmount(6), 20), + allAmount: widget.sendViewModel.balance, + cryptoCurrencySymbol: + widget.sendViewModel.selectedCryptoCurrencySymbol, + fiatCurrencySymbol: + widget.sendViewModel.fiatCurrency.symbol, + onAllButtonPressed: () async { + output.setSendAll( + await widget.sendViewModel.sendingBalance, + ); + await output.calculateEstimatedFee(); }, ), - if (widget.sendViewModel.hasCoinControl) - ListItemRegularRowWidget( - keyValue: "", - label: "Coin Control", - onTap: () { - showCupertinoModalBottomSheet( - enableDrag: false, - useRootNavigator: true, - isDismissible: false, - context: context, - builder: (context) { - return NewCoinControlPage( - unspentCoinsListViewModel: widget - .sendViewModel - .unspentCoinsListViewModel, - canEdit: true); - }); - }), - ]), - ) - ], - ), - ), - ), - Observer( - builder: (_) => Column( - spacing: 12, - children: [ - if (!widget.sendViewModel.isReadyForSend) - SendSyncingIndicator( - status: widget.sendViewModel.wallet.syncStatus), - if (widget.sendViewModel.outputs.length > 1) - RecipientDotRow( - numDots: widget.sendViewModel.outputs.length, - onSelected: _setOutput, - selectedDot: _selectedOutput, + ], + ), + if (widget.sendViewModel.isMwebAvailable && + widget.mode == SendPageModes.normal) + StandardCheckbox( + caption: S.of(context).litecoin_mweb_allow_coins, + captionColor: Theme.of(context).colorScheme.onSurface, + borderColor: Theme.of(context).colorScheme.primary, + iconColor: Theme.of(context).colorScheme.primary, + value: [UnspentCoinType.any, UnspentCoinType.mweb] + .contains(widget.sendViewModel.coinTypeToSpendFrom), + onChanged: (value) => + widget.sendViewModel.setAllowMwebCoins(value), + ), + if (widget.sendViewModel.hasMemos) + Observer( + builder: (_) => NewSendMemoInput( + memoController: _memoControllers[_selectedOutput], + maxMemoLength: widget.sendViewModel.maxMemoLength, + memoLength: output.memo.length, + ), + ), + if (widget.sendViewModel.hasCoinControl || + widget.sendViewModel.hasFees) + AnimatedDropdown( + dropdownText: S.of(context).advanced_settings, + content: Column( + children: [ + if (widget.sendViewModel.hasFees) + ListItemRegularRowWidget( + keyValue: "", + label: S.of(context).fees, + subtitle: + "~${output.estimatedFee} ${widget.sendViewModel.currencySymbol} (${output.estimatedFeeFiatAmount} ${widget.sendViewModel.fiatCurrency})", + onTap: () { + if (widget.sendViewModel.feesViewModel + .hasFeesPriority) { + pickTransactionPriority(context, output); + } + }, + ), + if (widget.sendViewModel.hasCoinControl) + ListItemRegularRowWidget( + keyValue: "", + label: "Coin Control", + onTap: () { + showCupertinoModalBottomSheet( + enableDrag: false, + useRootNavigator: true, + isDismissible: false, + context: context, + builder: (context) => NewCoinControlPage( + unspentCoinsListViewModel: widget + .sendViewModel + .unspentCoinsListViewModel, + canEdit: true, + ), + ); + }, + ), + ], + ), + ), + ], ), - Observer( - builder: (_) { - return LoadingPrimaryButton( - key: ValueKey('send_page_send_button_key'), - onPressed: () async { - //Request dummy node to get the focus out of the text fields - FocusScope.of(context).requestFocus(FocusNode()); + ), + ), + Observer( + builder: (_) => Column( + spacing: 12, + children: [ + if (!widget.sendViewModel.isReadyForSend) + SendSyncingIndicator( + status: widget.sendViewModel.wallet.syncStatus, + ), + if (widget.sendViewModel.outputs.length > 1) + RecipientDotRow( + numDots: widget.sendViewModel.outputs.length, + onSelected: _setOutput, + selectedDot: _selectedOutput, + ), + Observer( + builder: (_) => LoadingPrimaryButton( + key: const ValueKey("send_page_send_button_key"), + onPressed: () { + //Request dummy node to get the focus out of the text fields + FocusScope.of(context).requestFocus(FocusNode()); - if (widget.sendViewModel.state is IsExecutingState) return; + if (widget.sendViewModel.state is IsExecutingState) { + return; + } - if (widget.mode == SendPageModes.normal) { - _handleSend(); - } else if (widget.mode == SendPageModes.lightningDeposit || - widget.mode == SendPageModes.mwebDeposit) { - Navigator.of(context).push(CupertinoPageRoute( - builder: (context) => Material( - child: L2ActionWalletSelector( + if (widget.mode == SendPageModes.normal) { + _handleSend(); + } else if (widget.mode == + SendPageModes.lightningDeposit || + widget.mode == SendPageModes.mwebDeposit) { + Navigator.of(context).push( + CupertinoPageRoute( + builder: (context) => Material( + child: L2ActionWalletSelector( showOtherWallets: false, action: L2Actions.deposit, sendViewModel: widget.sendViewModel, @@ -601,13 +627,17 @@ class _NewSendPageState extends State { walletSwitcherViewModel: widget.walletSwitcherViewModel, onSendInitiated: _handleSend, - )))); - } else if (widget.mode == - SendPageModes.lightningWithdrawal || - widget.mode == SendPageModes.mwebWithdrawal) { - Navigator.of(context).push(CupertinoPageRoute( - builder: (context) => Material( - child: L2ActionWalletSelector( + ), + ), + ), + ); + } else if (widget.mode == + SendPageModes.lightningWithdrawal || + widget.mode == SendPageModes.mwebWithdrawal) { + Navigator.of(context).push( + CupertinoPageRoute( + builder: (context) => Material( + child: L2ActionWalletSelector( showOtherWallets: false, action: L2Actions.withdraw, sendViewModel: widget.sendViewModel, @@ -616,38 +646,48 @@ class _NewSendPageState extends State { walletSwitcherViewModel: widget.walletSwitcherViewModel, onSendInitiated: _handleSend, - )))); - } - }, - text: S.of(context).continue_text, - color: Theme.of(context).colorScheme.primary, - textColor: Theme.of(context).colorScheme.onPrimary, - isLoading: widget.sendViewModel.state is IsExecutingState || - widget.sendViewModel.state is TransactionCommitting || - widget.sendViewModel.state - is IsAwaitingDeviceResponseState || - widget.sendViewModel.state - is LoadingTemplateExecutingState, - isDisabled: !widget.sendViewModel.isReadyForSend || - widget.sendViewModel.state is ExecutedSuccessfullyState, - ); - }, + ), + ), + ), + ); + } + }, + text: S.of(context).continue_text, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + isLoading: widget.sendViewModel.state is IsExecutingState || + widget.sendViewModel.state is TransactionCommitting || + widget.sendViewModel.state + is IsAwaitingDeviceResponseState || + widget.sendViewModel.state + is LoadingTemplateExecutingState, + isDisabled: !widget.sendViewModel.isReadyForSend || + widget.sendViewModel.state is ExecutedSuccessfullyState, + ), + ), + const SizedBox(), + ], ), - SizedBox(), - ], - ), - ) - ], + ), + ], + ), + ), ), - ), - ) - ], + ], + ), + ), ), ), - ), - ), + ); + }, ); - }); + + void _onFiatSwitchPressed() { + final output = widget.sendViewModel.outputs[_selectedOutput]; + widget.sendViewModel.outputs[_selectedOutput].isFiatEntry = !output.isFiatEntry; + + final amount = output.isFiatEntry ? output.fiatAmount : output.displayCryptoAmount; + _amountControllers[_selectedOutput].text = amount.startsWith("<") ? "0" : amount; } void _setOutput(int index) { @@ -667,7 +707,8 @@ class _NewSendPageState extends State { _amountControllers[_amountControllers.length - 1].addListener(() { if (_selectedOutput > widget.sendViewModel.outputs.length - 1) { printV( - "_selectedOutput > widget.sendViewModel.outputs.length - 1! this should NOT happen!"); + "_selectedOutput > widget.sendViewModel.outputs.length - 1! this should NOT happen!", + ); return; } @@ -685,10 +726,14 @@ class _NewSendPageState extends State { output.sendAll = false; } - if (S.current.all.contains(amount)) return; + if (S.current.all.contains(amount)) { + return; + } final cAmount = widget.sendViewModel.amountParsingProxy.getDisplayCryptoAmount( - output.cryptoAmount, widget.sendViewModel.selectedCryptoCurrency); + output.cryptoAmount, + widget.sendViewModel.selectedCryptoCurrency, + ); if (amount != cAmount) { final newAmount = widget.sendViewModel.amountParsingProxy .getCanonicalCryptoAmount(amount, widget.sendViewModel.selectedCryptoCurrency); @@ -700,7 +745,8 @@ class _NewSendPageState extends State { _addressControllers[_amountControllers.length - 1].addListener(() { if (_selectedOutput > widget.sendViewModel.outputs.length - 1) { printV( - "_selectedOutput > widget.sendViewModel.outputs.length - 1! this should NOT happen!"); + "_selectedOutput > widget.sendViewModel.outputs.length - 1! this should NOT happen!", + ); return; } @@ -716,7 +762,8 @@ class _NewSendPageState extends State { _memoControllers[_amountControllers.length - 1].addListener(() { if (_selectedOutput > widget.sendViewModel.outputs.length - 1) { printV( - "_selectedOutput > widget.sendViewModel.outputs.length - 1! this should NOT happen!"); + "_selectedOutput > widget.sendViewModel.outputs.length - 1! this should NOT happen!", + ); return; } final memo = _memoControllers[_selectedOutput].text; @@ -728,8 +775,8 @@ class _NewSendPageState extends State { }); } - void _handleSend() async { - //TODO refactor this action. code was copied over from old ui. i don't like it. + Future _handleSend() async { + //TODO(malik1004x): refactor this action. code was copied over from old ui. i don't like it. for (var i = 0; i < widget.sendViewModel.outputs.length; i++) { if (i < _amountControllers.length && !widget.sendViewModel.outputs[i].sendAll) { @@ -737,7 +784,9 @@ class _NewSendPageState extends State { widget.sendViewModel.outputs[i].setFiatAmount(_amountControllers[i].text); } else { final amount = widget.sendViewModel.amountParsingProxy.getCanonicalCryptoAmount( - _amountControllers[i].text.sanitized(), widget.sendViewModel.selectedCryptoCurrency); + _amountControllers[i].text.sanitized(), + widget.sendViewModel.selectedCryptoCurrency, + ); widget.sendViewModel.outputs[i].setCryptoAmount(amount); } } @@ -763,34 +812,39 @@ class _NewSendPageState extends State { if (widget.sendViewModel.wallet.isHardwareWallet) { if (!widget.sendViewModel.hardwareWalletViewModel! .isConnected(widget.sendViewModel.walletType)) { - await Navigator.of(context).pushNamed(Routes.connectDevices, - arguments: ConnectDevicePageParams( - walletType: widget.sendViewModel.walletType, - hardwareWalletType: widget.sendViewModel.wallet.walletInfo.hardwareWalletType!, - onConnectDevice: (_, __) { - widget.sendViewModel.hardwareWalletViewModel! - .initWallet(widget.sendViewModel.wallet); - Navigator.of(context).pop(); - }, - )); + await Navigator.of(context).pushNamed( + Routes.connectDevices, + arguments: ConnectDevicePageParams( + walletType: widget.sendViewModel.walletType, + hardwareWalletType: widget.sendViewModel.wallet.walletInfo.hardwareWalletType!, + onConnectDevice: (_, __) { + widget.sendViewModel.hardwareWalletViewModel!.initWallet(widget.sendViewModel.wallet); + Navigator.of(context).pop(); + }, + ), + ); } else { - widget.sendViewModel.hardwareWalletViewModel!.initWallet(widget.sendViewModel.wallet); + await widget.sendViewModel.hardwareWalletViewModel!.initWallet(widget.sendViewModel.wallet); } } if (widget.sendViewModel.wallet.type == WalletType.monero) { var amount = Money.zero(widget.sendViewModel.wallet.currency); - for (var item in widget.sendViewModel.outputs) { + for (final item in widget.sendViewModel.outputs) { amount += item.cryptoAmountMoney; } if (monero!.needExportOutputs(widget.sendViewModel.wallet, amount)) { if (widget.sendViewModel.wallet.hardwareWalletType == HardwareWalletType.trezor) { await monero!.syncTrezor(widget.sendViewModel.wallet); } else { - await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, - arguments: monero!.exportOutputsUR(widget.sendViewModel.wallet)); + if (mounted) { + await Navigator.of(context).pushNamed( + Routes.urqrAnimatedPage, + arguments: monero!.exportOutputsUR(widget.sendViewModel.wallet), + ); + } } - await Future.delayed(Duration(seconds: 1)); // wait for monero to refresh the state + await Future.delayed(const Duration(seconds: 1)); // wait for monero to refresh the state } if (monero!.needExportOutputs(widget.sendViewModel.wallet, amount)) { return; @@ -807,21 +861,22 @@ class _NewSendPageState extends State { Navigator.of(context, rootNavigator: true).pop(); } showModalBottomSheet( - isScrollControlled: true, - isDismissible: false, - enableDrag: false, - context: navigatorKey.currentContext ?? context, - backgroundColor: Colors.transparent, - builder: (context) { - return SendConfirmSheet( - title: widget.mode.title, - iconPath: widget.mode.helpContent?.imagePath, - sendViewModel: widget.sendViewModel, - ); - }).then((value) async { + isScrollControlled: true, + isDismissible: false, + enableDrag: false, + context: navigatorKey.currentContext ?? context, + backgroundColor: Colors.transparent, + builder: (context) => SendConfirmSheet( + title: widget.mode.title, + iconPath: widget.mode.helpContent?.imagePath, + sendViewModel: widget.sendViewModel, + ), + ).then((value) { if (widget.sendViewModel.state is TransactionCommitted && widget.mode.popOnConfirmation) { - if (!mounted) return; + if (!mounted) { + return; + } Navigator.of(context, rootNavigator: true).pop(); } widget.sendViewModel.dismissTransaction(); @@ -842,7 +897,9 @@ class _NewSendPageState extends State { void _presentCurrencyPicker(BuildContext context) { final output = widget.sendViewModel.outputs[_selectedOutput]; - if (!output.isFiatEntry && !widget.sendViewModel.hasMultipleTokens) return; + if (!output.isFiatEntry && !widget.sendViewModel.hasMultipleTokens) { + return; + } if (output.isFiatEntry) { FiatCurrencyPickerSheet.show( @@ -860,8 +917,8 @@ class _NewSendPageState extends State { final balanceByAsset = { for (final r in widget.sendViewModel.balanceViewModel.formattedBalances) r.asset: CurrencyPickerBalance( - amount: '${r.availableBalance} ${r.asset.title}', - fiat: isFiatDisabled ? null : '${r.fiatAvailableBalanceRaw} ${r.fiatCurrency?.symbol}', + amount: "${r.availableBalance} ${r.asset.title}", + fiat: isFiatDisabled ? null : "${r.fiatAvailableBalanceRaw} ${r.fiatCurrency?.symbol}", fiatValue: isFiatDisabled ? null : double.tryParse(r.fiatAvailableBalanceRaw), ), }; @@ -894,12 +951,16 @@ class _NewSendPageState extends State { } Future _handlePaymentFlow(String uri, PaymentRequest paymentRequest) async { - if (uri.contains('@') || paymentRequest.address.contains('@')) return; + if (uri.contains("@") || paymentRequest.address.contains("@")) { + return; + } if (OpenCryptoPayService.isOpenCryptoPayQR(uri) && widget.sendViewModel.selectedCryptoCurrency != CryptoCurrency.btcln) { final request = await widget.sendViewModel.getOpenCryptoPayRequest(uri); - if (request == null) return; + if (request == null) { + return; + } _applyPaymentRequest(request); return; } @@ -960,7 +1021,7 @@ class _NewSendPageState extends State { break; } } catch (e) { - printV('Payment flow error: $e'); + printV("Payment flow error: $e"); _applyPaymentRequest(paymentRequest); } } @@ -979,34 +1040,32 @@ class _NewSendPageState extends State { context: context, isDismissible: true, isScrollControlled: true, - builder: (BuildContext context) { - return PaymentConfirmationBottomSheet( - paymentFlowResult: result, - paymentViewModel: paymentViewModel, - walletSwitcherViewModel: walletSwitcherViewModel, - paymentRequest: paymentRequest, - onSelectWallet: () => _handleSelectWallet( - paymentViewModel, - walletSwitcherViewModel, - paymentRequest, - result, - ), - onChangeWallet: () => _handleChangeWallet( - paymentViewModel, - walletSwitcherViewModel, - paymentRequest, - result, - ), - onSwap: (bottomSheetContext) => - _handleSwapFlow(paymentViewModel, result, bottomSheetContext), - onSwitchNetwork: () => _handleSwitchNetwork( - paymentViewModel, - walletSwitcherViewModel, - paymentRequest, - result, - ), - ); - }, + builder: (context) => PaymentConfirmationBottomSheet( + paymentFlowResult: result, + paymentViewModel: paymentViewModel, + walletSwitcherViewModel: walletSwitcherViewModel, + paymentRequest: paymentRequest, + onSelectWallet: () => _handleSelectWallet( + paymentViewModel, + walletSwitcherViewModel, + paymentRequest, + result, + ), + onChangeWallet: () => _handleChangeWallet( + paymentViewModel, + walletSwitcherViewModel, + paymentRequest, + result, + ), + onSwap: (bottomSheetContext) => + _handleSwapFlow(paymentViewModel, result, bottomSheetContext), + onSwitchNetwork: () => _handleSwitchNetwork( + paymentViewModel, + walletSwitcherViewModel, + paymentRequest, + result, + ), + ), ); } @@ -1024,32 +1083,30 @@ class _NewSendPageState extends State { context: context, isDismissible: true, isScrollControlled: true, - builder: (BuildContext context) { - return TokenSelectionBottomSheet( - paymentViewModel: paymentViewModel, - paymentRequest: paymentRequest, - fixedNetwork: fixedNetwork, - onNext: (PaymentFlowResult newResult) { - final selectedChainId = newResult.chainId; - final isCompatible = - selectedChainId == evm?.getSelectedChainId(widget.sendViewModel.wallet); + builder: (context) => TokenSelectionBottomSheet( + paymentViewModel: paymentViewModel, + paymentRequest: paymentRequest, + fixedNetwork: fixedNetwork, + onNext: (newResult) { + final selectedChainId = newResult.chainId; + final isCompatible = + selectedChainId == evm?.getSelectedChainId(widget.sendViewModel.wallet); - if (isCompatible) { - widget.sendViewModel.setSelectedCryptoCurrency( - newResult.addressDetectionResult!.detectedCurrency!.title, - ); - _applyPaymentRequest(paymentRequest); - } else { - _showPaymentConfirmation( - paymentViewModel, - walletSwitcherViewModel, - paymentRequest, - newResult, - ); - } - }, - ); - }, + if (isCompatible) { + widget.sendViewModel.setSelectedCryptoCurrency( + newResult.addressDetectionResult!.detectedCurrency!.title, + ); + _applyPaymentRequest(paymentRequest); + } else { + _showPaymentConfirmation( + paymentViewModel, + walletSwitcherViewModel, + paymentRequest, + newResult, + ); + } + }, + ), ); } @@ -1065,12 +1122,10 @@ class _NewSendPageState extends State { context: context, isDismissible: true, isScrollControlled: true, - builder: (BuildContext dialogContext) { - return WalletSwitcherBottomSheet( - viewModel: walletSwitcherViewModel, - filterWalletType: paymentViewModel.detectedWalletType, - ); - }, + builder: (dialogContext) => WalletSwitcherBottomSheet( + viewModel: walletSwitcherViewModel, + filterWalletType: paymentViewModel.detectedWalletType, + ), ); final success = await walletSwitcherViewModel.switchToSelectedWallet(); @@ -1115,7 +1170,7 @@ class _NewSendPageState extends State { showModalBottomSheet( context: context, isDismissible: false, - builder: (BuildContext context) { + builder: (context) { loadingBottomSheetContext = context; return LoadingBottomSheet( titleText: S.of(context).loading_your_wallet, @@ -1156,7 +1211,7 @@ class _NewSendPageState extends State { showModalBottomSheet( context: context, isDismissible: false, - builder: (BuildContext context) { + builder: (context) { loadingBottomSheetContext = context; return LoadingBottomSheet( titleText: S.of(context).loading_your_wallet, @@ -1195,7 +1250,9 @@ class _NewSendPageState extends State { PaymentRequest paymentRequest, PaymentFlowResult result, ) async { - if (result.type != PaymentFlowType.evmNetworkSelection || result.wallet == null) return; + if (result.type != PaymentFlowType.evmNetworkSelection || result.wallet == null) { + return; + } if (context.mounted && Navigator.of(context).canPop()) { Navigator.of(context).pop(); @@ -1207,11 +1264,9 @@ class _NewSendPageState extends State { showModalBottomSheet( context: context, isDismissible: false, - builder: (BuildContext context) { + builder: (context) { loadingBottomSheetContext = context; - return LoadingBottomSheet( - titleText: S.of(context).loading_your_wallet, - ); + return LoadingBottomSheet(titleText: S.of(context).loading_your_wallet); }, ); } @@ -1234,7 +1289,7 @@ class _NewSendPageState extends State { if (loadingBottomSheetContext != null && loadingBottomSheetContext!.mounted) { Navigator.of(loadingBottomSheetContext!).pop(); } - printV('Switch network error: $e'); + printV("Switch network error: $e"); } } @@ -1246,9 +1301,11 @@ class _NewSendPageState extends State { _addressControllers[_selectedOutput].text = paymentRequest.address; if (paymentRequest.amount.isNotEmpty) { try { - _amountControllers[_selectedOutput].text = widget.sendViewModel.amountParsingProxy - .getDisplayCryptoAmount( - paymentRequest.amount, widget.sendViewModel.selectedCryptoCurrency); + _amountControllers[_selectedOutput].text = + widget.sendViewModel.amountParsingProxy.getDisplayCryptoAmount( + paymentRequest.amount, + widget.sendViewModel.selectedCryptoCurrency, + ); } catch (e) {} } _memoControllers[_selectedOutput].text = paymentRequest.note; @@ -1263,14 +1320,16 @@ class _NewSendPageState extends State { await Future.delayed(const Duration(milliseconds: 100)); - if (!mounted) return; + if (!mounted) { + return; + } final bottomSheet = getIt.get(param1: result); await showModalBottomSheet( context: context, isDismissible: true, isScrollControlled: true, - builder: (BuildContext context) => bottomSheet, + builder: (context) => bottomSheet, ); } @@ -1314,7 +1373,7 @@ class _NewSendPageState extends State { for (final type in supportedTypes) { final addressPattern = AddressValidator.getAddressFromStringPattern(type); if (addressPattern != null) { - final regex = RegExp('^$addressPattern\$'); + final regex = RegExp("^$addressPattern\$"); if (regex.hasMatch(trimmed)) { isValid = true; break; @@ -1323,7 +1382,9 @@ class _NewSendPageState extends State { } for (final pattern in excludedPatterns) { - if (pattern.hasMatch(trimmed)) return false; + if (pattern.hasMatch(trimmed)) { + return false; + } } return isValid; @@ -1341,116 +1402,123 @@ class _NewSendPageState extends State { await showCupertinoModalBottomSheet( context: pageContext, expand: false, - builder: (BuildContext modalContext) { + builder: (modalContext) { int selectedIdx = selectedItem; - return Observer(builder: (context) { - double? customFeeRate = isBitcoinWallet - ? widget.sendViewModel.feesViewModel.customBitcoinFeeRate.toDouble() - : null; - return StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - return IntrinsicHeight( + return Observer( + builder: (context) { + final customFeeRate = isBitcoinWallet + ? widget.sendViewModel.feesViewModel.customBitcoinFeeRate.toDouble() + : null; + return StatefulBuilder( + builder: (context, setState) => IntrinsicHeight( // height: MediaQuery.of(context).size.height*0.4, child: ModalNavigator( - parentContext: modalContext, - heightMode: ModalHeightModes.natural, - rootPage: Material( - child: NewPicker( - title: S.of(context).set_fees, - description: S.of(context).set_fees_desc, - sliderPageTitle: S.of(context).custom_fee, - sliderInitialValue: customFeeRate, - sliderMaxValue: maxCustomFeeRate, - sliderValueDescription: "sat/byte", - items: items - .map((item) => PickerItem( - title: item.title, - subtitle: item.description, - hint: item.hint, - value: item, - isSliderItem: items.indexOf(item) == customItemIndex, - )) - .toList(), - onItemSelected: (TransactionPriority priority) async { - widget.sendViewModel.feesViewModel.setTransactionPriority(priority); - setState(() => selectedIdx = items.indexOf(priority)); - await output.calculateEstimatedFee(); - }, - onSliderChanged: (double value) { - widget.sendViewModel.feesViewModel.customBitcoinFeeRate = value.round(); - }, - selectedIndex: selectedIdx), - )), - ); - }, - ); - }); + parentContext: modalContext, + heightMode: ModalHeightModes.natural, + rootPage: Material( + child: NewPicker( + title: S.of(context).set_fees, + description: S.of(context).set_fees_desc, + sliderPageTitle: S.of(context).custom_fee, + sliderInitialValue: customFeeRate, + sliderMaxValue: maxCustomFeeRate, + sliderValueDescription: "sat/byte", + items: items + .map( + (item) => PickerItem( + title: item.title, + subtitle: item.description, + hint: item.hint, + value: item, + isSliderItem: items.indexOf(item) == customItemIndex, + ), + ) + .toList(), + onItemSelected: (priority) async { + widget.sendViewModel.feesViewModel.setTransactionPriority(priority); + setState(() => selectedIdx = items.indexOf(priority)); + await output.calculateEstimatedFee(); + }, + onSliderChanged: (value) { + widget.sendViewModel.feesViewModel.customBitcoinFeeRate = value.round(); + }, + selectedIndex: selectedIdx, + ), + ), + ), + ), + ); + }, + ); }, ); } - String _wrapAmount(String amount, int maxChars) { - return amount.length <= maxChars ? amount : "${amount.substring(0, maxChars - 3)}..."; - } + String _wrapAmount(String amount, int maxChars) => + amount.length <= maxChars ? amount : "${amount.substring(0, maxChars - 3)}..."; } class SendHelpPage extends StatelessWidget { - const SendHelpPage({super.key, required this.content}); + const SendHelpPage({required this.content, super.key}); final SendPageHelpContent content; @override - Widget build(BuildContext context) { - return SafeArea( - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ModalTopBar( - title: content.title, - leadingIcon: Icon(Icons.arrow_back_ios_new), - onLeadingPressed: Navigator.of(context).pop, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 18.0), - child: Column( - spacing: 12, - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CakeImageWidget(imageUrl: content.imagePath), - Text( - content.description, - textAlign: TextAlign.center, - style: TextStyle( + Widget build(BuildContext context) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ModalTopBar( + title: content.title, + leadingIcon: const Icon(Icons.arrow_back_ios_new), + onLeadingPressed: Navigator.of(context).pop, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18), + child: Column( + spacing: 12, + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CakeImageWidget(imageUrl: content.imagePath), + Text( + content.description, + textAlign: TextAlign.center, + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface), - ), - if (content.disclaimer != null) ...[ - SizedBox(), - SizedBox(), - Text(content.disclaimer!, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + if (content.disclaimer != null) ...[ + const SizedBox(), + const SizedBox(), + Text( + content.disclaimer!, textAlign: TextAlign.center, style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurfaceVariant)), - ] - ], + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ], + ), ), - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 18), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18), child: NewPrimaryButton( - onPressed: Navigator.of(context).pop, - text: S.of(context).i_understand, - color: Theme.of(context).colorScheme.primary, - textColor: Theme.of(context).colorScheme.onPrimary)) - ], - ), - ); - } + onPressed: Navigator.of(context).pop, + text: S.of(context).i_understand, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + ), + ), + ], + ), + ); } Future showParsedAddressConfirmationAlert( @@ -1459,20 +1527,18 @@ Future showParsedAddressConfirmationAlert( ) async { final confirmed = await showPopUp( context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.of(context).address_detected, - headerTitleText: parsedAddress.profileName.isEmpty ? null : parsedAddress.profileName, - headerImageProfileUrl: parsedAddress.profileImageUrl.isEmpty - ? parsedAddress.addressSource.iconPath - : parsedAddress.profileImageUrl, - alertContent: S.of(context).extracted_address_content( - '${parsedAddress.handle} (${parsedAddress.addressSource.label})', - ), - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop(true), - ); - }, + builder: (context) => AlertWithOneAction( + alertTitle: S.of(context).address_detected, + headerTitleText: parsedAddress.profileName.isEmpty ? null : parsedAddress.profileName, + headerImageProfileUrl: parsedAddress.profileImageUrl.isEmpty + ? parsedAddress.addressSource.iconPath + : parsedAddress.profileImageUrl, + alertContent: S.of(context).extracted_address_content( + "${parsedAddress.handle} (${parsedAddress.addressSource.label})", + ), + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(true), + ), ); return confirmed ?? false; diff --git a/lib/new-ui/widgets/send_page/fiat_amount_bar.dart b/lib/new-ui/widgets/send_page/fiat_amount_bar.dart index 06725a4a7..bd383b50f 100644 --- a/lib/new-ui/widgets/send_page/fiat_amount_bar.dart +++ b/lib/new-ui/widgets/send_page/fiat_amount_bar.dart @@ -1,23 +1,24 @@ -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; -import 'package:cw_core/crypto_amount_format.dart'; -import 'package:flutter/material.dart'; +import "package:cake_wallet/generated/i18n.dart"; +import "package:cake_wallet/new-ui/widgets/modern_button.dart"; +import "package:cw_core/crypto_amount_format.dart"; +import "package:flutter/material.dart"; class FiatAmountBar extends StatelessWidget { - const FiatAmountBar( - {super.key, - required this.fiatInputMode, - required this.onSwitchButtonPressed, - this.onAllButtonPressed, - required this.cryptoAmount, - required this.fiatAmount, - required this.cryptoCurrencySymbol, - required this.fiatCurrencySymbol, - this.allAmount, - this.foregroundElementColor, - this.textColor, - this.allAmountColor, - this.allAmountTextColor}); + const FiatAmountBar({ + required this.fiatInputMode, + required this.onSwitchButtonPressed, + required this.cryptoAmount, + required this.fiatAmount, + required this.cryptoCurrencySymbol, + required this.fiatCurrencySymbol, + super.key, + this.onAllButtonPressed, + this.allAmount, + this.foregroundElementColor, + this.textColor, + this.allAmountColor, + this.allAmountTextColor, + }); final bool fiatInputMode; final VoidCallback onSwitchButtonPressed; @@ -34,42 +35,41 @@ class FiatAmountBar extends StatelessWidget { final Color? allAmountTextColor; @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - spacing: 8, - children: [ - ModernButton.svg( - backgroundColor: foregroundElementColor, - size: 28, - svgPath: "assets/new-ui/switch.svg", - iconSize: 18, - onPressed: onSwitchButtonPressed, - ), - GestureDetector( - onTap: onSwitchButtonPressed, - child: Text( - fiatInputMode - ? "${cryptoAmount.isEmpty ? "0" : cryptoAmount.withMaxDecimals(8)} $cryptoCurrencySymbol" - : "${fiatAmount.isEmpty ? "0" : fiatAmount} $fiatCurrencySymbol", - style: TextStyle(color: textColor ?? Theme.of(context).colorScheme.onSurface), - ), - ), - ], - ), - if (allAmount != null && allAmount!.isNotEmpty) + Widget build(BuildContext context) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ Row( spacing: 8, children: [ - Text( - "${S.of(context).max}.", - style: TextStyle(color: textColor ?? Theme.of(context).colorScheme.onSurface), + ModernButton.svg( + backgroundColor: foregroundElementColor, + size: 28, + svgPath: "assets/new-ui/switch.svg", + iconSize: 18, + onPressed: onSwitchButtonPressed, ), - Container( - decoration: BoxDecoration(borderRadius: BorderRadius.circular(999999)), - child: Material( + GestureDetector( + onTap: onSwitchButtonPressed, + child: Text( + fiatInputMode + ? "${cryptoAmount.isEmpty ? "0" : cryptoAmount.withMaxDecimals(8)} $cryptoCurrencySymbol" + : "${fiatAmount.isEmpty ? "0" : fiatAmount} $fiatCurrencySymbol", + style: TextStyle(color: textColor ?? Theme.of(context).colorScheme.onSurface), + ), + ), + ], + ), + if (allAmount != null && allAmount!.isNotEmpty) + Row( + spacing: 8, + children: [ + Text( + "${S.of(context).max}.", + style: TextStyle(color: textColor ?? Theme.of(context).colorScheme.onSurface), + ), + Container( + decoration: BoxDecoration(borderRadius: BorderRadius.circular(999999)), + child: Material( color: allAmountColor ?? foregroundElementColor ?? Theme.of(context).colorScheme.surfaceContainer, @@ -78,22 +78,23 @@ class FiatAmountBar extends StatelessWidget { borderRadius: BorderRadius.circular(99999), onTap: onAllButtonPressed, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Text( - formatAmount(allAmount!), + _formatAmount(allAmount!), style: TextStyle( - color: allAmountTextColor ?? Theme.of(context).colorScheme.primary), + color: allAmountTextColor ?? Theme.of(context).colorScheme.primary, + ), ), ), - )), - ) - ], - ) - ], - ); - } + ), + ), + ), + ], + ), + ], + ); - String formatAmount(String amount) { + String _formatAmount(String amount) { try { return double.parse(amount).toStringAsPrecision(8).replaceFirst(RegExp(r"\.?0+$"), ""); } catch (e) { diff --git a/lib/new-ui/widgets/send_page/send_amount_input.dart b/lib/new-ui/widgets/send_page/send_amount_input.dart index c66006452..ff905d08e 100644 --- a/lib/new-ui/widgets/send_page/send_amount_input.dart +++ b/lib/new-ui/widgets/send_page/send_amount_input.dart @@ -1,20 +1,20 @@ -import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart'; -import 'package:cake_wallet/new-ui/widgets/send_page/floating_icon_button.dart'; -import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; -import 'package:cake_wallet/utils/decimal_input_formatter.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; +import "package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart"; +import "package:cake_wallet/new-ui/widgets/send_page/floating_icon_button.dart"; +import "package:cake_wallet/src/widgets/cake_image_widget.dart"; +import "package:cake_wallet/utils/decimal_input_formatter.dart"; +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:flutter_mobx/flutter_mobx.dart"; class NewSendAmountInput extends StatefulWidget { const NewSendAmountInput({ - super.key, required this.currency, required this.maxDecimals, required this.hasPicker, required this.onPickerClicked, required this.currencyIconPath, required this.amountController, + super.key, this.validator, }); @@ -41,27 +41,28 @@ class _NewSendAmountInputState extends State { } @override - Widget build(BuildContext context) { - return FormField( - key: formFieldKey, - initialValue: widget.amountController.text, - validator: widget.validator, - builder: (state) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( + Widget build(BuildContext context) => FormField( + key: formFieldKey, + initialValue: widget.amountController.text, + validator: widget.validator, + builder: (state) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( decoration: BoxDecoration( - color: widget.hasPicker - ? Theme.of(context).colorScheme.surfaceContainerHigh - : Theme.of(context).colorScheme.surfaceContainer, - borderRadius: BorderRadius.circular(18)), + color: widget.hasPicker + ? Theme.of(context).colorScheme.surfaceContainerHigh + : Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(18), + ), child: Row( children: [ Expanded( child: Container( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainer, - borderRadius: BorderRadius.horizontal(left: Radius.circular(18))), + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: const BorderRadius.horizontal(left: Radius.circular(18)), + ), child: Row( mainAxisSize: MainAxisSize.max, spacing: 8, @@ -79,82 +80,87 @@ class _NewSendAmountInputState extends State { ], controller: widget.amountController, decoration: InputDecoration( - hintText: widget.maxDecimals > 0 ? "0" : "0.00", + hintText: widget.maxDecimals == 0 ? "0" : "0.00", errorMaxLines: 3, ), onChanged: state.didChange, ), ), FloatingIconButton( - iconPath: "assets/new-ui/paste.svg", - onPressed: () async { - final data = await Clipboard.getData(Clipboard.kTextPlain); - if (data != null && data.text != null) { - final text = data.text!; - widget.amountController.value = TextEditingValue( - text: text, - selection: TextSelection.collapsed(offset: text.length), - ); - } - }), + iconPath: "assets/new-ui/paste.svg", + onPressed: () async { + final data = await Clipboard.getData(Clipboard.kTextPlain); + if (data != null && data.text != null) { + final text = data.text!; + widget.amountController.value = TextEditingValue( + text: text, + selection: TextSelection.collapsed(offset: text.length), + ); + } + }, + ), ], ), ), ), IntrinsicWidth( child: Observer( - builder: (_) { - return GestureDetector( - onTap: widget.onPickerClicked, - child: Container( - alignment: Alignment.center, - width: double.infinity, - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - topRight: Radius.circular(18), - bottomRight: Radius.circular(18)), - color: widget.hasPicker - ? Theme.of(context).colorScheme.surfaceContainerHigh - : Theme.of(context).colorScheme.surfaceContainer, - ), - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - spacing: 8, - children: [ - if (widget.hasPicker && widget.currencyIconPath.isNotEmpty) - TokenImageWidget( - imageUrl: widget.currencyIconPath, size: 24), - Text(widget.currency), - if (widget.hasPicker) - CakeImageWidget( - imageUrl: "assets/new-ui/chooser.svg", - width: 12, - height: 12, - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.primary, - BlendMode.srcIn), - ) - ], - ))), - ); - }, + builder: (_) => GestureDetector( + onTap: widget.onPickerClicked, + child: Container( + alignment: Alignment.center, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topRight: Radius.circular(18), + bottomRight: Radius.circular(18), + ), + color: widget.hasPicker + ? Theme.of(context).colorScheme.surfaceContainerHigh + : Theme.of(context).colorScheme.surfaceContainer, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + spacing: 8, + children: [ + if (widget.hasPicker && widget.currencyIconPath.isNotEmpty) + TokenImageWidget( + imageUrl: widget.currencyIconPath, + size: 24, + ), + Text(widget.currency), + if (widget.hasPicker) + CakeImageWidget( + imageUrl: "assets/new-ui/chooser.svg", + width: 12, + height: 12, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + ], + ), + ), + ), + ), ), ), ], - )), - if (state.hasError) - Padding( - padding: EdgeInsets.only(top: 6, left: 8), - child: Text( - state.errorText!, - style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.error), ), - ) - ], - ), - ); - } + ), + if (state.hasError) + Padding( + padding: const EdgeInsets.only(top: 6, left: 8), + child: Text( + state.errorText!, + style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.error), + ), + ), + ], + ), + ); } From 85bb70807c12a656cf984697787c2972fb73cefc Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Mon, 20 Jul 2026 16:27:23 +0200 Subject: [PATCH 2/5] fix: copy button behavior on receive page (#3424) --- .../receive_page/receive_bottom_buttons.dart | 69 ++++++++++--------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/lib/new-ui/widgets/receive_page/receive_bottom_buttons.dart b/lib/new-ui/widgets/receive_page/receive_bottom_buttons.dart index 3d7cb1f9f..1566ff88d 100644 --- a/lib/new-ui/widgets/receive_page/receive_bottom_buttons.dart +++ b/lib/new-ui/widgets/receive_page/receive_bottom_buttons.dart @@ -1,22 +1,10 @@ -import 'dart:io'; - -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; -import 'package:device_info_plus/device_info_plus.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:cake_wallet/new-ui/widgets/copy_wrapper.dart'; +import "package:cake_wallet/generated/i18n.dart"; +import "package:cake_wallet/new-ui/widgets/copy_wrapper.dart"; +import "package:cake_wallet/new-ui/widgets/modern_button.dart"; +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; class ReceiveBottomButtons extends StatefulWidget { - final bool largeQrMode; - final ClipboardData? copyData; - final VoidCallback onCopyButtonPressed; - final VoidCallback onAmountButtonPressed; - final VoidCallback onLabelButtonPressed; - final VoidCallback onAccountsButtonPressed; - final bool showLabelButton; - final bool showAccountsButton; - const ReceiveBottomButtons({ required this.largeQrMode, required this.onCopyButtonPressed, @@ -29,6 +17,15 @@ class ReceiveBottomButtons extends StatefulWidget { super.key, }); + final bool largeQrMode; + final ClipboardData? copyData; + final VoidCallback onCopyButtonPressed; + final VoidCallback onAmountButtonPressed; + final VoidCallback onLabelButtonPressed; + final VoidCallback onAccountsButtonPressed; + final bool showLabelButton; + final bool showAccountsButton; + @override State createState() => _ReceiveBottomButtonsState(); } @@ -49,7 +46,7 @@ class _ReceiveBottomButtonsState extends State { opacity: targetOpacity, curve: Curves.easeOut, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32.0), + padding: const EdgeInsets.symmetric(horizontal: 32), child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, @@ -57,12 +54,13 @@ class _ReceiveBottomButtonsState extends State { children: [ GestureDetector( onTap: widget.copyData == null ? widget.onCopyButtonPressed : null, + behavior: HitTestBehavior.opaque, child: IgnorePointer( ignoring: widget.copyData == null, child: CopyWrapper( data: widget.copyData, builder: (context, copied) => AnimatedSwitcher( - duration: Duration(milliseconds: 200), + duration: const Duration(milliseconds: 200), child: IgnorePointer( child: ModernButton.svg( key: ValueKey(copied), @@ -84,25 +82,28 @@ class _ReceiveBottomButtonsState extends State { ), ), ModernButton.svg( - size: 60, - iconSize: 32, - svgPath: "assets/new-ui/set-amount.svg", - onPressed: widget.onAmountButtonPressed, - label: S.of(context).set_amount), + size: 60, + iconSize: 32, + svgPath: "assets/new-ui/set-amount.svg", + onPressed: widget.onAmountButtonPressed, + label: S.of(context).set_amount, + ), if (widget.showLabelButton) ModernButton.svg( - size: 60, - iconSize: 32, - svgPath: "assets/new-ui/add-label.svg", - onPressed: widget.onLabelButtonPressed, - label: S.of(context).label), + size: 60, + iconSize: 32, + svgPath: "assets/new-ui/add-label.svg", + onPressed: widget.onLabelButtonPressed, + label: S.of(context).label, + ), if (widget.showAccountsButton) ModernButton.svg( - size: 60, - iconSize: 32, - svgPath: "assets/new-ui/addr-book.svg", - onPressed: widget.onAccountsButtonPressed, - label: S.of(context).addresses), + size: 60, + iconSize: 32, + svgPath: "assets/new-ui/addr-book.svg", + onPressed: widget.onAccountsButtonPressed, + label: S.of(context).addresses, + ), ], ), ), From 41e2cba322cb97581bdd8c83af2b6d2601b66da1 Mon Sep 17 00:00:00 2001 From: Seth For Privacy <40500387+sethforprivacy@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:08:26 -0400 Subject: [PATCH 3/5] security: harden vulnerability disclosure policy (#3419) * security: harden vulnerability disclosure policy + Slack routing * security: add discretionary reward policy and marketing-site out-of-scope --- docs/SECURITY.md | 101 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 95 insertions(+), 6 deletions(-) diff --git a/docs/SECURITY.md b/docs/SECURITY.md index e7c6baa02..0198e7286 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -1,12 +1,101 @@ # Security Policy -## Reporting a Vulnerability +Cake Wallet is developed by **Cake Labs LLC**. We take the security and privacy of +our users seriously and welcome reports from security researchers. -If you need to report a vulnerability, please either: +> **Cake Wallet is not affiliated with CAKE.com, Clockify, or security.cake.com.** +> Those services belong to unrelated companies. The only official vulnerability +> disclosure channels for Cake Wallet are the ones listed on this page and at +> https://cakewallet.com/security. -* Open a security advisory: https://github.com/cake-tech/cake_wallet/security/advisories/new -* Send an email to `dev@cakewallet.com` with details on the vulnerability +## Reporting a vulnerability -## Supported Versions +**Please do not open a public issue, pull request, or social-media post for a +security vulnerability.** Public disclosure before a fix is available puts users' +funds and privacy at risk. Use one of the private channels below and we will +coordinate a fix and disclosure with you. -As we don't maintain previous versions of the app, only the latest release for each platform is supported and any updates will bump the version number. +1. **GitHub private security advisory (preferred).** + [Report a vulnerability](https://github.com/cake-tech/cake_wallet/security/advisories/new). + This gives you a private, structured thread with the maintainers and is the + fastest way to reach us. +2. **Encrypted email.** Send details to **security@cakewallet.com**. For sensitive + reports, please encrypt with our PGP key: + - Key: https://cakewallet.com/.well-known/cakewallet-security.asc + - Fingerprint: `DC91 6520 0271 AC6A 0533 3D3C BFE7 D9A5 0E4D 3A0A` + +Both channels are monitored and automatically raise an alert in our internal +security channel, so reports will not be missed. + +### What to include + +- A clear description of the issue and its security impact. +- Step-by-step reproduction, ideally with a proof of concept. +- Affected platforms (iOS, Android, macOS, Linux, Windows) and app version. +- Affected wallet types / chains, if applicable. +- Any relevant logs, addresses, or transaction IDs (for on-chain issues). + +If you used AI tooling to find or write up the report, please say so. + +## Our commitment (safe harbor) + +We consider security research conducted in good faith under this policy to be +authorized. We will not pursue or support legal action against researchers who: + +- make a good-faith effort to avoid privacy violations, data destruction, and + interruption or degradation of our services; +- only interact with accounts they own or have explicit permission to access; and +- give us a reasonable opportunity to fix an issue before disclosing it publicly. + +If in doubt about whether an action is authorized, ask us first at +security@cakewallet.com. + +## What to expect + +- **Acknowledgement:** within **2 business days**. +- **Triage and initial assessment:** within **7 business days**. +- **Coordinated disclosure:** we aim to ship a fix and coordinate public + disclosure within **90 days** of the report. We will keep you updated on + progress and agree on a disclosure date with you. +- **Credit:** with your permission, we are happy to publicly credit you for the + report once a fix is released. + +## Scope + +**In scope:** the Cake Wallet and Monero.com applications and the code in this +repository and its sibling `cake-tech` repositories — anything that could lead to +loss of funds, exposure of keys or seeds, a privacy leak, or a +failed/incorrect transaction. + +**Out of scope:** issues in third-party services, exchange/swap providers, or +nodes we do not operate; reports generated solely by automated scanners without a +demonstrated impact; low-severity or informational issues on our marketing and +landing websites (for example reflected or self-XSS, missing security headers, +clickjacking on pages with no sensitive actions, or SPF/DMARC and cookie-flag +nitpicks) that do not affect the app or user funds; and social-engineering or +physical attacks. + +## Rewards + +At our **sole discretion**, we may offer a reward for a valid report. To be +eligible, a report must: + +- be submitted **privately** through one of the channels above (a GitHub private + security advisory or `security@cakewallet.com`) — anything disclosed publicly or + sent through other channels is not eligible; and +- identify a genuine vulnerability with real impact on users — typically loss of + funds, exposure of keys or seeds, a privacy leak, or a failed or incorrect + transaction. + +Trivial or low-impact findings are **not** eligible — for example, reflected XSS +or other low-severity issues on our marketing websites, missing security headers, +hardening or best-practice suggestions, automated-scanner output without a working +proof of concept, or already-known issues. There is no fixed bounty and no +guaranteed payout; whether a report qualifies, and any amount, are determined +solely by Cake Labs LLC. + +## Supported versions + +We do not maintain previous releases. Only the **latest release for each platform** +is supported; security fixes are delivered in new versions. Please keep Cake Wallet +up to date. From b336da4a9a44a8dd9f4bba51b73dc96440548f9e Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Mon, 20 Jul 2026 23:23:20 +0200 Subject: [PATCH 4/5] refactor: remove deprecated payment URI classes and unify URI handling with `ERC681URI` implementation (#3423) * refactor: remove deprecated payment URI classes and unify URI handling with `ERC681URI` implementation * refactor: apply lint [skip ci] --- cw_core/lib/payment_uris.dart | 300 +++++++----------- cw_evm/lib/evm_chain_wallet_addresses.dart | 39 +-- .../exchange/exchange_trade_view_model.dart | 7 +- 3 files changed, 133 insertions(+), 213 deletions(-) diff --git a/cw_core/lib/payment_uris.dart b/cw_core/lib/payment_uris.dart index 2703d5e21..43d452f8d 100644 --- a/cw_core/lib/payment_uris.dart +++ b/cw_core/lib/payment_uris.dart @@ -1,4 +1,4 @@ -import 'package:cw_core/format_fixed.dart'; +import "package:cw_core/format_fixed.dart"; abstract class PaymentURI { PaymentURI({required this.amount, required this.address}); @@ -19,7 +19,7 @@ class MoneroURI extends PaymentURI { @override String toString() { - var base = 'monero:$address'; + var base = "monero:$address"; if (amount.isNotEmpty) { base += '?tx_amount=${amount.replaceAll(',', '.')}'; @@ -34,7 +34,7 @@ class HavenURI extends PaymentURI { @override String toString() { - var base = 'haven:$address'; + var base = "haven:$address"; if (amount.isNotEmpty) { base += '?tx_amount=${amount.replaceAll(',', '.')}'; @@ -45,7 +45,7 @@ class HavenURI extends PaymentURI { } class BitcoinURI extends PaymentURI { - BitcoinURI({required super.amount, required super.address, this.pjUri = ''}); + BitcoinURI({required super.amount, required super.address, this.pjUri = ""}); final String pjUri; @@ -53,13 +53,16 @@ class BitcoinURI extends PaymentURI { String toString() { final qp = {}; - if (amount.isNotEmpty) qp['amount'] = amount.replaceAll(',', '.'); - if (pjUri.isNotEmpty && !address.startsWith("sp")) { - qp['pjos'] = '0'; - qp['pj'] = pjUri; + if (amount.isNotEmpty) { + qp["amount"] = amount.replaceAll(",", "."); } - return Uri(scheme: 'bitcoin', path: address, queryParameters: qp).toString(); + if (pjUri.isNotEmpty && !address.startsWith("sp")) { + qp["pjos"] = "0"; + qp["pj"] = pjUri; + } + + return Uri(scheme: "bitcoin", path: address, queryParameters: qp).toString(); } } @@ -68,7 +71,7 @@ class LitecoinURI extends PaymentURI { @override String toString() { - var base = 'litecoin:$address'; + var base = "litecoin:$address"; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; @@ -83,52 +86,7 @@ class EthereumURI extends PaymentURI { @override String toString() { - var base = 'ethereum:$address'; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } - - return base; - } -} - -class BaseURI extends PaymentURI { - BaseURI({required super.amount, required super.address}); - - @override - String toString() { - var base = 'base:$address'; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } - - return base; - } -} - -class ArbitrumURI extends PaymentURI { - ArbitrumURI({required super.amount, required super.address}); - - @override - String toString() { - var base = 'arbitrum:$address'; - - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } - - return base; - } -} - -class BSCURI extends PaymentURI { - BSCURI({required super.amount, required super.address}); - - @override - String toString() { - var base = 'bsc:$address'; + var base = "ethereum:$address"; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; @@ -158,22 +116,7 @@ class NanoURI extends PaymentURI { @override String toString() { - var base = 'nano:$address'; - if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; - } - - return base; - } -} - -class PolygonURI extends PaymentURI { - PolygonURI({required super.amount, required super.address}); - - @override - String toString() { - var base = 'polygon:$address'; - + var base = "nano:$address"; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; } @@ -189,14 +132,14 @@ class SolanaURI extends PaymentURI { @override String toString() { - var base = 'solana:$address'; + var base = "solana:$address"; final params = []; if (amount.isNotEmpty) { params.add('amount=${amount.replaceAll(',', '.')}'); } if (contractAddress != null && contractAddress!.isNotEmpty) { - params.add('spl-token=$contractAddress'); + params.add("spl-token=$contractAddress"); } if (params.isNotEmpty) { base += '?${params.join('&')}'; @@ -213,14 +156,14 @@ class TronURI extends PaymentURI { @override String toString() { - var base = 'tron:$address'; + var base = "tron:$address"; final params = []; if (amount.isNotEmpty) { params.add('amount=${amount.replaceAll(',', '.')}'); } if (contractAddress != null && contractAddress!.isNotEmpty) { - params.add('token=$contractAddress'); + params.add("token=$contractAddress"); } if (params.isNotEmpty) { base += '?${params.join('&')}'; @@ -235,7 +178,7 @@ class WowneroURI extends PaymentURI { @override String toString() { - var base = 'wownero:$address'; + var base = "wownero:$address"; if (amount.isNotEmpty) { base += '?tx_amount=${amount.replaceAll(',', '.')}'; @@ -246,12 +189,11 @@ class WowneroURI extends PaymentURI { } class ZanoURI extends PaymentURI { - ZanoURI({required String amount, required String address}) - : super(amount: amount, address: address); + ZanoURI({required super.amount, required super.address}); @override String toString() { - var base = 'zano:' + address; + var base = "zano:$address"; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; @@ -262,12 +204,11 @@ class ZanoURI extends PaymentURI { } class DecredURI extends PaymentURI { - DecredURI({required String amount, required String address}) - : super(amount: amount, address: address); + DecredURI({required super.amount, required super.address}); @override String toString() { - var base = 'decred:' + address; + var base = "decred:$address"; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; @@ -278,12 +219,11 @@ class DecredURI extends PaymentURI { } class DogeURI extends PaymentURI { - DogeURI({required String amount, required String address}) - : super(amount: amount, address: address); + DogeURI({required super.amount, required super.address}); @override String toString() { - var base = 'doge:' + address; + var base = "doge:$address"; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; @@ -298,7 +238,7 @@ class ZcashURI extends PaymentURI { @override String toString() { - var base = 'zcash:$address'; + var base = "zcash:$address"; if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; @@ -309,9 +249,6 @@ class ZcashURI extends PaymentURI { } class ERC681URI extends PaymentURI { - final int chainId; - final String? contractAddress; - ERC681URI({ required this.chainId, required super.address, @@ -319,74 +256,11 @@ class ERC681URI extends PaymentURI { required this.contractAddress, }); - @override - String toString() { - var uri = 'ethereum:'; - - final targetAddress = contractAddress ?? address; - uri += targetAddress; - - if (chainId != 1) { - uri += '@$chainId'; - } - - if (contractAddress != null) { - uri += '/transfer'; - } - - final params = {}; - - if (contractAddress != null) { - params['address'] = address; - if (amount.isNotEmpty) { - params['uint256'] = _formatAmountForERC20(amount); - } - } else { - if (amount.isNotEmpty) { - params['value'] = _formatAmountForNative(amount); - } - } - - if (params.isNotEmpty) { - uri += '?'; - uri += params.entries.map((e) => '${e.key}=${e.value}').join('&'); - } - - return uri; - } - - /// Formats amount for ERC-20 token transfers (in atomic units) - String _formatAmountForERC20(String amount) { - try { - // Convert decimal amount to BigInt (assuming 18 decimals) - final amountDouble = double.parse(amount.replaceAll(',', '.')); - final amountBigInt = BigInt.from(amountDouble * 1e18); - return amountBigInt.toString(); - } catch (e) { - // Fallback to original amount if parsing fails - return amount.replaceAll(',', '.'); - } - } - - /// Formats amount for native ETH payments (in wei using scientific notation) - String _formatAmountForNative(String amount) { - try { - // Convert decimal amount to double for scientific notation - final amountDouble = double.parse(amount.replaceAll(',', '.')); - - // Use scientific notation as recommended by ERC-681 - return '${amountDouble}e18'; - } catch (e) { - // Fallback to original amount if parsing fails - return amount.replaceAll(',', '.'); - } - } - factory ERC681URI.fromUri(Uri uri) { final (isContract, targetAddress) = _getTargetAddress(uri.path); final chainId = _getChainID(uri.path); - final address = isContract ? uri.queryParameters["address"] ?? '' : targetAddress; + final address = isContract ? uri.queryParameters["address"] ?? "" : targetAddress; final amountParam = isContract ? uri.queryParameters["uint256"] : uri.queryParameters["value"]; var formatedAmount = ""; @@ -406,16 +280,78 @@ class ERC681URI extends PaymentURI { ); } - static int _getChainID(String path) { - return int.parse(RegExp( - r'@\d*', - ).firstMatch(path)?.group(0)?.replaceAll("@", "") ?? - "1"); + final int chainId; + final String? contractAddress; + + @override + String toString() { + var uri = "ethereum:"; + + final targetAddress = contractAddress ?? address; + uri += targetAddress; + + if (chainId != 1) { + uri += "@$chainId"; + } + + if (contractAddress != null) { + uri += "/transfer"; + } + + final params = {}; + + if (contractAddress != null) { + params["address"] = address; + if (amount.isNotEmpty) { + params["uint256"] = _formatAmountForERC20(amount); + } + } else { + if (amount.isNotEmpty) { + params["value"] = _formatAmountForNative(amount); + } + } + + if (params.isNotEmpty) { + uri += "?${params.entries.map((e) => "${e.key}=${e.value}").join("&")}"; + } + + return uri; } + /// Formats amount for ERC-20 token transfers (in atomic units) + String _formatAmountForERC20(String amount) { + try { + // Convert decimal amount to BigInt (assuming 18 decimals) + final amountDouble = double.parse(amount.replaceAll(",", ".")); + final amountBigInt = BigInt.from(amountDouble * 1e18); + return amountBigInt.toString(); + } catch (e) { + // Fallback to original amount if parsing fails + return amount.replaceAll(",", "."); + } + } + + /// Formats amount for native ETH payments (in wei using scientific notation) + String _formatAmountForNative(String amount) { + try { + // Convert decimal amount to double for scientific notation + final amountDouble = double.parse(amount.replaceAll(",", ".")); + + // Use scientific notation as recommended by ERC-681 + return "${amountDouble}e18"; + } catch (e) { + // Fallback to original amount if parsing fails + return amount.replaceAll(",", "."); + } + } + + static int _getChainID(String path) => int.parse( + RegExp(r"@\d*").firstMatch(path)?.group(0)?.replaceAll("@", "") ?? "1", + ); + static (bool, String) _getTargetAddress(String path) { final targetAddress = - RegExp(r'^(0x)?[0-9a-f]{40}', caseSensitive: false).firstMatch(path)!.group(0)!; + RegExp(r"^(0x)?[0-9a-f]{40}", caseSensitive: false).firstMatch(path)!.group(0)!; return (path.contains("/"), targetAddress); } @@ -426,25 +362,27 @@ class ERC681URI extends PaymentURI { /// - Scientific notation: "0.123e18", "1e6" → expanded to integer /// - Decimal ETH: "0.123456" → shifted by 18 decimals static String _normalizeToIntegerWei(String input) { - final raw = input.replaceAll(',', '.').trim(); + final raw = input.replaceAll(",", ".").trim(); // First we check if it's already a plain integer (basically just a number with no dot, no exponent) try { - final isPlainInteger = RegExp(r'^[+-]?\d+$').hasMatch(raw) && - !raw.contains('.') && - !raw.toLowerCase().contains('e'); - if (isPlainInteger) return raw.replaceFirst(RegExp(r'^\+'), ''); + final isPlainInteger = RegExp(r"^[+-]?\d+$").hasMatch(raw) && + !raw.contains(".") && + !raw.toLowerCase().contains("e"); + if (isPlainInteger) { + return raw.replaceFirst(RegExp(r"^\+"), ""); + } // Then we check if it's a scientific notation - final sci = RegExp(r'^[+-]?(\d+\.?\d*|\d*\.?\d+)[eE][+-]?\d+$'); + final sci = RegExp(r"^[+-]?(\d+\.?\d*|\d*\.?\d+)[eE][+-]?\d+$"); if (sci.hasMatch(raw)) { - final mantissaStr = raw.toLowerCase().split('e')[0]; - final exp = int.parse(raw.toLowerCase().split('e')[1]); + final mantissaStr = raw.toLowerCase().split("e")[0]; + final exp = int.parse(raw.toLowerCase().split("e")[1]); return _expandDecimal(mantissaStr, exp); } // Lastly, we check if it's a fixed decimal ETH amount, here we shift by 18 to get wei for the amount - if (raw.contains('.')) { + if (raw.contains(".")) { return _expandDecimal(raw, 18); } return raw; @@ -462,34 +400,34 @@ class ERC681URI extends PaymentURI { /// _expandDecimal('1.2', 3) -> '1200' static String _expandDecimal(String decimalStr, int expShift) { var s = decimalStr.trim(); - var sign = ''; - if (s.startsWith('-') || s.startsWith('+')) { - sign = s[0] == '-' ? '-' : ''; + var sign = ""; + if (s.startsWith("-") || s.startsWith("+")) { + sign = s[0] == "-" ? "-" : ""; s = s.substring(1); } // First we split the integer and fractional parts - final parts = s.split('.'); - final intPart = parts[0].isEmpty ? '0' : parts[0]; - final fracPart = parts.length > 1 ? parts[1] : ''; - final digits = (intPart + fracPart).replaceFirst(RegExp(r'^0+'), ''); + final parts = s.split("."); + final intPart = parts[0].isEmpty ? "0" : parts[0]; + final fracPart = parts.length > 1 ? parts[1] : ""; + final digits = (intPart + fracPart).replaceFirst(RegExp(r"^0+"), ""); final fracLen = fracPart.length; // Then we calculate the effective shift = desired shift minus existing fractional digits final shift = expShift - fracLen; if (shift >= 0) { - final head = digits.isEmpty ? '0' : digits; - final zeros = List.filled(shift, '0').join(); + final head = digits.isEmpty ? "0" : digits; + final zeros = List.filled(shift, "0").join(); final res = head + zeros; - return sign + (res.isEmpty ? '0' : res); + return sign + (res.isEmpty ? "0" : res); } else { // Need to insert a decimal point within digits; return integer by truncating final cut = digits.length + shift; if (cut <= 0) { - return '0'; + return "0"; } final res = digits.substring(0, cut); - return sign + (res.isEmpty ? '0' : res); + return sign + (res.isEmpty ? "0" : res); } } } diff --git a/cw_evm/lib/evm_chain_wallet_addresses.dart b/cw_evm/lib/evm_chain_wallet_addresses.dart index f7cef7513..fad6eb688 100644 --- a/cw_evm/lib/evm_chain_wallet_addresses.dart +++ b/cw_evm/lib/evm_chain_wallet_addresses.dart @@ -1,18 +1,15 @@ -import 'dart:developer'; +import "dart:developer"; -import 'package:cw_core/payment_uris.dart'; -import 'package:cw_core/wallet_addresses.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:mobx/mobx.dart'; +import "package:cw_core/payment_uris.dart"; +import "package:cw_core/wallet_addresses.dart"; +import "package:mobx/mobx.dart"; -part 'evm_chain_wallet_addresses.g.dart'; +part "evm_chain_wallet_addresses.g.dart"; class EVMChainWalletAddresses = EVMChainWalletAddressesBase with _$EVMChainWalletAddresses; abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store { - EVMChainWalletAddressesBase(WalletInfo walletInfo, this._selectedChainId) - : address = '', - super(walletInfo); + EVMChainWalletAddressesBase(super.walletInfo, this._selectedChainId) : address = ""; @override @observable @@ -33,7 +30,7 @@ abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store { Future updateAddressesInBox() async { try { addressesMap.clear(); - addressesMap[address] = ''; + addressesMap[address] = ""; await saveAddressesInBox(); } catch (e) { log(e.toString()); @@ -41,20 +38,10 @@ abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store { } @override - PaymentURI getPaymentUri(String amount) { - switch (_selectedChainId) { - case 1: - return EthereumURI(amount: amount, address: address); - case 137: - return PolygonURI(amount: amount, address: address); - case 8453: - return BaseURI(amount: amount, address: address); - case 42161: - return ArbitrumURI(amount: amount, address: address); - case 56: - return BSCURI(amount: amount, address: address); - default: - return EthereumURI(amount: amount, address: address); - } - } + PaymentURI getPaymentUri(String amount) => ERC681URI( + address: address, + amount: amount, + chainId: _selectedChainId, + contractAddress: null, + ); } diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 6e039e271..5ca718eb4 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -442,16 +442,11 @@ abstract class ExchangeTradeViewModelBase with Store { case WalletType.dogecoin: return DogeURI(address: inputAddress, amount: amount); case WalletType.ethereum: - return _createERC681URI(fromCurrency, inputAddress, amount); - // TODO: Expand ERC681URI support to Polygon(modify decoding flow for QRs, pay anything, and deep link handling) case WalletType.polygon: - return PolygonURI(amount: amount, address: inputAddress); case WalletType.base: - return BaseURI(amount: amount, address: inputAddress); case WalletType.arbitrum: - return ArbitrumURI(amount: amount, address: inputAddress); case WalletType.bsc: - return BSCURI(amount: amount, address: inputAddress); + return _createERC681URI(fromCurrency, inputAddress, amount); case WalletType.solana: return SolanaURI(amount: amount, address: inputAddress); case WalletType.tron: From 544cbec0e0180e6792c44d07e037447647ce86e2 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Tue, 21 Jul 2026 04:54:48 +0300 Subject: [PATCH 5/5] v6.3.0 Release Candidate (#3414) * v6.3.0 Release Candidate - the new transaction history - Radar in Apps screen - Zcash update - Flutter update - Parts of the Refactor should improve how amounts are handled in the app and fix any issue with amounts improve handling sending to aliases - Improvements for Cake Pay mobile - Bug fixes * don't use memo for note/message values * save message in QR locally in the notes field * Make stealth addresses generated from silent payment, not reliant on output index (#3420) * - fix swaps showing only on primary account - switch tron default node - privacy fixes * - enlarge destination tag and make it copiable - migrate users on hashvault - minor fix --- assets/tron_node_list.yml | 2 +- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 2 +- cw_bitcoin/lib/electrum_wallet.dart | 5 +- .../lib/pending_bitcoin_transaction.dart | 11 ++++ cw_monero/lib/monero_wallet_addresses.dart | 4 +- ios/Podfile.lock | 41 ++++++------- lib/bitcoin/cw_bitcoin.dart | 28 ++++----- lib/entities/default_settings_migration.dart | 27 ++++++++- .../provider/trocador_exchange_provider.dart | 2 +- lib/main.dart | 2 +- lib/new-ui/pages/send_page.dart | 17 +++++- .../swap_page/swap_send_external_modal.dart | 59 +++++++++++++++++-- ...nero_hardware_wallet_passphrase_input.dart | 2 + scripts/android/app_env.sh | 8 +-- scripts/ios/app_env.sh | 8 +-- 15 files changed, 152 insertions(+), 66 deletions(-) diff --git a/assets/tron_node_list.yml b/assets/tron_node_list.yml index 3837ec4ee..ae30489d9 100644 --- a/assets/tron_node_list.yml +++ b/assets/tron_node_list.yml @@ -1,6 +1,5 @@ - uri: api.trongrid.io - isDefault: true useSSL: true isEnabledForAutoSwitching: true label: "TronGrid" @@ -10,6 +9,7 @@ label: "PublicNode" - uri: trx.nownodes.io + isDefault: true useSSL: true isEnabledForAutoSwitching: true label: "NOWNodes" \ No newline at end of file diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 0bc7f8c9c..c4c41f913 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -124,7 +124,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S Future getPaymentRequestUri(String amount) async { if (addressPageType is LightningAddressType && lightningWallet != null) { - final amountSats = amount.isNotEmpty ? parseFixed(amount, 8) : null; + final amountSats = amount.isNotEmpty ? tryParseFixed(amount, 8) : null; final lnUrl = getLnurlOfLightningAddress(address); if (amountSats == null) { return LightningPaymentRequest(address: address, lnURL: lnUrl, amount: amount); diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index bc7d45ec4..3ebbfa49e 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1536,7 +1536,7 @@ abstract class ElectrumWalletBase network: network, memo: estimatedTx.memo, inputOrdering: BitcoinOrdering.shuffle, - outputOrdering: BitcoinOrdering.none, + outputOrdering: BitcoinOrdering.shuffle, enableRBF: !estimatedTx.spendsUnconfirmedTX, ); } @@ -1594,6 +1594,7 @@ abstract class ElectrumWalletBase isSendAll: estimatedTx.isSendAll, hasTaprootInputs: hasTaprootInputs, utxos: estimatedTx.utxos, + derivedOutputs: updatedOutputs, publicKeys: estimatedTx.publicKeys, isViewOnly: keys.privateKey.isEmpty, )..addListener((transaction) async { @@ -2297,7 +2298,7 @@ abstract class ElectrumWalletBase network: network, memo: memo, inputOrdering: BitcoinOrdering.shuffle, - outputOrdering: BitcoinOrdering.none, + outputOrdering: BitcoinOrdering.shuffle, enableRBF: true, ); diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 565234291..c92e80780 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -31,6 +31,7 @@ class PendingBitcoinTransaction with PendingTransaction { this.hasTaprootInputs = false, this.isMweb = false, this.utxos = const [], + this.derivedOutputs = const [], this.publicKeys, this.commitOverride, this.unsignedPsbt, @@ -47,6 +48,9 @@ class PendingBitcoinTransaction with PendingTransaction { final bool hasTaprootInputs; final bool isViewOnly; List utxos; + + final List derivedOutputs; + bool isMweb; String? changeAddressOverride; String? idOverride; @@ -79,6 +83,13 @@ class PendingBitcoinTransaction with PendingTransaction { bool get hasSilentPayment => _tx.hasSilentPayment; + List get stealthAddresses => derivedOutputs + .where((out) => !out.isChange) + .map((out) => out.isSilentPayment + ? out.address.toAddress(network ?? BitcoinNetwork.mainnet) + : null) + .toList(); + PendingChange? get change { try { final change = _tx.outputs.firstWhere((out) => out.isChange); diff --git a/cw_monero/lib/monero_wallet_addresses.dart b/cw_monero/lib/monero_wallet_addresses.dart index ee211ef90..01f711bed 100644 --- a/cw_monero/lib/monero_wallet_addresses.dart +++ b/cw_monero/lib/monero_wallet_addresses.dart @@ -90,7 +90,7 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { addressesMap.clear(); addressInfos.clear(); - accountList.accounts.forEach((account) async { + for (final account in accountList.accounts) { await _subaddressList.update(accountIndex: account.id); _subaddressList.subaddresses.forEach((subaddress) { addressesMap[subaddress.address] = subaddress.label; @@ -102,7 +102,7 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { address: subaddress.address, label: subaddress.label)); }); - }); + } await saveAddressesInBox(); } catch (e) { diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d46513dab..295e3b96a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -188,8 +188,6 @@ PODS: - Flutter - wakelock_plus (0.0.1): - Flutter - - warp_api (0.0.1): - - Flutter - YttriumWrapper (0.8.35) DEPENDENCIES: @@ -236,7 +234,6 @@ DEPENDENCIES: - universal_ble (from `.symlinks/plugins/universal_ble/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) - - warp_api (from `.symlinks/plugins/warp_api/ios`) SPEC REPOS: https://github.com/CocoaPods/Specs.git: @@ -341,8 +338,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/url_launcher_ios/ios" wakelock_plus: :path: ".symlinks/plugins/wakelock_plus/ios" - warp_api: - :path: ".symlinks/plugins/warp_api/ios" SPEC CHECKSUMS: AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 @@ -351,17 +346,17 @@ SPEC CHECKSUMS: breez_sdk_spark_flutter: ed3b6709b8ce9b40309d8728640eade5b19e8a0d connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd CryptoSwift: e64e11850ede528a02a0f3e768cec8e9d92ecb90 - cw_decred: 9c0e1df74745b51a1289ec5e91fb9e24b68fa14a - cw_mweb: 22cd01dfb8ad2d39b15332006f22046aaa8352a3 - device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7 - device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 - devicelocale: 35ba84dc7f45f527c3001535d8c8d104edd5d926 + cw_decred: a02cf30175a46971c1e2fa22c48407534541edc6 + cw_mweb: 3aea2fb35b2bd04d8b2d21b83216f3b8fb768d85 + device_display_brightness: 04374ebd653619292c1d996f00f42877ea19f17f + device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89 + devicelocale: bd64aa714485a8afdaded0892c1e7d5b7f680cf8 DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 dnssec_proof: d461cac7bd3301eb7447f87936745a0c1ae0a67e fast_scanner: 2cb1ad3e69e645e9980fb4961396ce5804caa3e3 file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517 - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 flutter_local_authentication: 989278c681612f1ee0e36019e149137f114b9d7f flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb @@ -374,32 +369,32 @@ SPEC CHECKSUMS: GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 - image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a + image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937 integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e - local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19 + local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 - package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 payjoin_flutter: d9d4c8aa16bd5dfedb9b21d0edc8199e0187d96e permission_handler_apple: 92d754bbaa7361d436db2d6c3c1c2a0fdcec462e PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 - quick_actions_ios: 4b07fb49d8d8f3518d7565fbb7a91014067a7d82 + quick_actions_ios: 500fcc11711d9f646739093395c4ae8eec25f779 reown_yttrium: cee334ade64725b1d83f7b34c706a6aae2696d58 rive_common: dd421daaf9ae69f0125aa761dd96abd278399952 rlz: 3f4022361f540a5c26d9d2474fea1d2455b69d8d SDWebImage: 9f177d83116802728e122410fb25ad88f5c7608a - sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986 - share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f - shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6 - sp_scanner: eaa617fa827396b967116b7f1f43549ca62e9a12 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + sensitive_clipboard: 161e9abc3d56b3131309d8a321eb4690a803c16b + share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + sp_scanner: b1bc9321690980bdb44bba7ec85d5543e716d1b5 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b - sqlite3_flutter_libs: 86f82662868ee26ff3451f73cac9c5fc2a1f57fa + sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 torch_dart: f4620705d10f05492fab047f2fa1c3a600e7d17d uni_links: ed8c961e47ed9ce42b6d91e1de8049e38a4b3152 - universal_ble: 45519b2aeafe62761e2c6309f8927edb5288b914 - url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + universal_ble: a322ebabee64f0ec27a313e89a5dd6967f37a60f + url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 YttriumWrapper: 31e937fe9fbe0f1314d2ca6be9ce9b379a059966 diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 48dbfda84..ae1018319 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -687,28 +687,20 @@ class CWBitcoin extends Bitcoin { } List updateOutputs(PendingTransaction pendingTransaction, List outputs) { - if (pendingTransaction is PendingLightningTransaction) return outputs; - - final pendingTx = pendingTransaction as PendingBitcoinTransaction; - - if (!pendingTx.hasSilentPayment) { + if (pendingTransaction is! PendingBitcoinTransaction || !pendingTransaction.hasSilentPayment) { return outputs; } - final updatedOutputs = outputs.map((output) { - try { - final pendingOut = pendingTx.outputs[outputs.indexOf(output)]; - final updatedOutput = output; + final stealthAddresses = pendingTransaction.stealthAddresses; + if (stealthAddresses.length != outputs.length) { + printV("well, that shouldn't happen"); + return outputs; + } - updatedOutput.stealthAddress = P2trAddress.fromScriptPubkey(script: pendingOut.scriptPubKey) - .toAddress(BitcoinNetwork.mainnet); - return updatedOutput; - } catch (_) {} - - return output; - }).toList(); - - return updatedOutputs; + for (var i = 0; i < outputs.length; i++) { + outputs[i].stealthAddress = stealthAddresses[i]; + } + return outputs; } @override diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 910ea069f..ef8a213c8 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -10,7 +10,6 @@ import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/entities/fs_migration.dart'; import 'package:cake_wallet/entities/haven_seed_store.dart'; -import 'package:cake_wallet/entities/node_list.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/secret_store_key.dart'; import 'package:cake_wallet/monero/monero.dart'; @@ -46,7 +45,7 @@ const cakeWalletBitcoinCashDefaultNodeUri = 'bitcoincash.stackwallet.com:50002'; const nanoDefaultNodeUri = 'rpc.nano.to'; const nanoDefaultPowNodeUri = 'rpc.nano.to'; const solanaDefaultNodeUri = 'solana-mainnet.core.chainstack.com'; -const tronDefaultNodeUri = 'api.trongrid.io'; +const tronDefaultNodeUri = 'trx.nownodes.io'; const newCakeWalletBitcoinUri = 'btc-electrum.cakewallet.com:50002'; const wowneroDefaultNodeUri = 'node3.monerodevs.org:34568'; const zanoDefaultNodeUri = '37.27.100.59:10500'; @@ -607,6 +606,28 @@ Future defaultSettingsMigration( enabled: true, ); break; + case 68: + await _changeDefaultNode( + sharedPreferences: sharedPreferences, + type: WalletType.tron, + newDefaultUri: tronDefaultNodeUri, + currentNodePreferenceKey: PreferencesKey.currentTronNodeIdKey, + useSSL: true, + oldUri: [ + 'tron-rpc.publicnode.com:443', + 'api.trongrid.io', + ], + ); + await _changeDefaultNode( + sharedPreferences: sharedPreferences, + type: WalletType.monero, + newDefaultUri: newCakeWalletMoneroUri, + currentNodePreferenceKey: PreferencesKey.currentNodeIdKey, + useSSL: true, + trusted: true, + oldUri: ['nodes.hashvault.pro:18081'], + ); + break; default: break; } @@ -663,7 +684,7 @@ Future _changeDefaultNode({ } else { final currentNode = nodes.firstWhereOrNull((node) => node.id == currentNodeId); shouldReplace = - currentNode == null || (oldUri?.any((e) => currentNode!.uriRaw.contains(e)) ?? true); + currentNode == null || (oldUri?.any((e) => currentNode.uriRaw.contains(e)) ?? true); } if (shouldReplace) { diff --git a/lib/exchange/provider/trocador_exchange_provider.dart b/lib/exchange/provider/trocador_exchange_provider.dart index a9d977dd6..8b027b323 100644 --- a/lib/exchange/provider/trocador_exchange_provider.dart +++ b/lib/exchange/provider/trocador_exchange_provider.dart @@ -539,7 +539,7 @@ class TrocadorExchangeProvider extends ExchangeProvider { } Future _getUri(String path, Map queryParams) async { - final uri = Uri.http(onionApiAuthority, path, queryParams); + final uri = Uri.https(onionApiAuthority, path, queryParams); if (useTorOnly) return uri; diff --git a/lib/main.dart b/lib/main.dart index b6f9714f6..f292731f8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -315,7 +315,7 @@ Future initializeAppConfigs({bool loadWallet = true}) async { payjoinSessionSource: payjoinSessionSource, anonpayInvoiceInfo: anonpayInvoiceInfo, havenSeedStore: havenSeedStore, - initialMigrationVersion: 67, + initialMigrationVersion: 68, ); } diff --git a/lib/new-ui/pages/send_page.dart b/lib/new-ui/pages/send_page.dart index 3ab80929e..721e21840 100644 --- a/lib/new-ui/pages/send_page.dart +++ b/lib/new-ui/pages/send_page.dart @@ -241,7 +241,9 @@ class _NewSendPageState extends State { widget.initialPaymentRequest!.scheme.toLowerCase()) { _addressControllers[0].text = widget.initialPaymentRequest!.address; _amountControllers[0].text = widget.initialPaymentRequest!.amount; - _memoControllers[0].text = widget.initialPaymentRequest!.note; + // _memoControllers[0].text = widget.initialPaymentRequest!.note; + _applyNote(widget.initialPaymentRequest!.note, 0); + final contractAddress = widget.initialPaymentRequest!.contractAddress; if (contractAddress != null && contractAddress.isNotEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -1308,7 +1310,9 @@ class _NewSendPageState extends State { ); } catch (e) {} } - _memoControllers[_selectedOutput].text = paymentRequest.note; + // _memoControllers[_selectedOutput].text = paymentRequest.note; + + _applyNote(paymentRequest.note, _selectedOutput); } Future _handleSwapFlow( @@ -1456,6 +1460,15 @@ class _NewSendPageState extends State { String _wrapAmount(String amount, int maxChars) => amount.length <= maxChars ? amount : "${amount.substring(0, maxChars - 3)}..."; + + // TODO: make a separate variable for memo in payment request model + void _applyNote(String note, int selectedOutput) { + if (widget.sendViewModel.hasMemos && note.length <= widget.sendViewModel.maxMemoLength) { + widget.sendViewModel.outputs[selectedOutput].memo = note; + } else { + widget.sendViewModel.outputs[selectedOutput].note = note; + } + } } class SendHelpPage extends StatelessWidget { diff --git a/lib/new-ui/widgets/swap_page/swap_send_external_modal.dart b/lib/new-ui/widgets/swap_page/swap_send_external_modal.dart index 71d34304c..f48d96a04 100644 --- a/lib/new-ui/widgets/swap_page/swap_send_external_modal.dart +++ b/lib/new-ui/widgets/swap_page/swap_send_external_modal.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/copy_wrapper.dart'; import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; import 'package:cake_wallet/new-ui/widgets/swap_page/swap_modal_header.dart'; @@ -9,6 +10,7 @@ import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/payment_uris.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; class SwapSendExternalModal extends StatefulWidget { const SwapSendExternalModal( @@ -166,10 +168,59 @@ class _SwapSendExternalModalState extends State { ), )), if (widget.exchangeTradeViewModel.trade.extraId != null) - Text( - "${S.of(context).destination_tag} ${widget.exchangeTradeViewModel.trade.extraId}", - style: TextStyle( - fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), + CopyWrapper( + data: ClipboardData( + text: widget.exchangeTradeViewModel.trade.extraId!), + builder: (context, copied) => Container( + width: double.infinity, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + Text( + S.of(context).destination_tag, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8, + children: [ + Flexible( + child: Text( + widget.exchangeTradeViewModel.trade.extraId!, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.primary), + ), + ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: Icon( + copied ? Icons.check : Icons.copy, + key: ValueKey(copied), + size: 18, + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ), + ], + ), + ), + ), ), Row( spacing: 8, diff --git a/lib/src/screens/connect_device/monero_hardware_wallet_passphrase_input.dart b/lib/src/screens/connect_device/monero_hardware_wallet_passphrase_input.dart index 6dd277e97..5db520050 100644 --- a/lib/src/screens/connect_device/monero_hardware_wallet_passphrase_input.dart +++ b/lib/src/screens/connect_device/monero_hardware_wallet_passphrase_input.dart @@ -34,6 +34,8 @@ class MoneroHardwareWalletPassphraseInputModal extends StatelessWidget { BaseTextFormField( controller: controller, hintText: S.of(context).passphrase_raw, + autocorrect: false, + enableSuggestions: false, ), const SizedBox(), NewPrimaryButton( diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index bac26a104..014e2d49a 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -14,15 +14,15 @@ TYPES=($MONERO_COM $CAKEWALLET) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="6.2.1" -MONERO_COM_BUILD_NUMBER=4161 +MONERO_COM_VERSION="6.3.0" +MONERO_COM_BUILD_NUMBER=4162 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="6.2.1" -CAKEWALLET_BUILD_NUMBER=4413 +CAKEWALLET_VERSION="6.3.0" +CAKEWALLET_BUILD_NUMBER=4414 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index a8d022d9b..4c0650f3d 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -12,13 +12,13 @@ TYPES=($MONERO_COM $CAKEWALLET) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="6.2.1" -MONERO_COM_BUILD_NUMBER=167 +MONERO_COM_VERSION="6.3.0" +MONERO_COM_BUILD_NUMBER=168 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="6.2.1" -CAKEWALLET_BUILD_NUMBER=423 +CAKEWALLET_VERSION="6.3.0" +CAKEWALLET_BUILD_NUMBER=424 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet"