diff --git a/lib/new-ui/pages/swap_page.dart b/lib/new-ui/pages/swap_page.dart index 0c533b358..0cd331351 100644 --- a/lib/new-ui/pages/swap_page.dart +++ b/lib/new-ui/pages/swap_page.dart @@ -480,133 +480,135 @@ class _NewSwapPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.max, children: [ - Form( - key: formKey, - child: Column( - children: [ - Observer( - builder: (_) => SwapAmountBox( - isReceiverCard: false, - walletSwitcherViewModel: widget.walletSwitcherViewModel, - exchangeViewModel: widget.exchangeViewModel, - onDispose: disposeBestRateSync, - hasAllAmount: widget.exchangeViewModel.hasAllAmount, - allAmount: widget.exchangeViewModel.hasAllAmount - ? () => widget.exchangeViewModel.enableSendAllAmount() - : null, - key: depositKey, - title: S.of(context).send, - initialCurrency: widget.exchangeViewModel.depositCurrency, - hasRefundAddress: true, - currencies: widget.exchangeViewModel.depositCurrencies, - onCurrencySelected: (currency) { - if (currency is CryptoCurrency) { - widget.exchangeViewModel - .changeDepositCurrency(currency: currency); - } - }, - currencyValueValidator: (value) { - return !widget.exchangeViewModel.isFixedRateMode && - value != S.of(context).all - ? AmountValidator( - isAutovalidate: true, - currency: widget.exchangeViewModel.isFixedRateMode - ? widget.exchangeViewModel.receiveCurrency - : widget.exchangeViewModel.depositCurrency, - minValue: widget.exchangeViewModel.limits.min.toString(), - maxValue: widget.exchangeViewModel.limits.max.toString(), - amountParsingProxy: - widget.exchangeViewModel.amountParsingProxy, - ).call(value) - : null; - }, - addressTextFieldValidator: AddressValidator( - type: widget.exchangeViewModel.depositCurrency), - onPushPasteButton: (context) async { - final clipboard = await Clipboard.getData('text/plain'); - widget.exchangeViewModel.depositAddress = clipboard?.text ?? ''; + RepaintBoundary( + child: Form( + key: formKey, + child: Column( + children: [ + Observer( + builder: (_) => SwapAmountBox( + isReceiverCard: false, + walletSwitcherViewModel: widget.walletSwitcherViewModel, + exchangeViewModel: widget.exchangeViewModel, + onDispose: disposeBestRateSync, + hasAllAmount: widget.exchangeViewModel.hasAllAmount, + allAmount: widget.exchangeViewModel.hasAllAmount + ? () => widget.exchangeViewModel.enableSendAllAmount() + : null, + key: depositKey, + title: S.of(context).send, + initialCurrency: widget.exchangeViewModel.depositCurrency, + hasRefundAddress: true, + currencies: widget.exchangeViewModel.depositCurrencies, + onCurrencySelected: (currency) { + if (currency is CryptoCurrency) { + widget.exchangeViewModel + .changeDepositCurrency(currency: currency); + } + }, + currencyValueValidator: (value) { + return !widget.exchangeViewModel.isFixedRateMode && + value != S.of(context).all + ? AmountValidator( + isAutovalidate: true, + currency: widget.exchangeViewModel.isFixedRateMode + ? widget.exchangeViewModel.receiveCurrency + : widget.exchangeViewModel.depositCurrency, + minValue: widget.exchangeViewModel.limits.min.toString(), + maxValue: widget.exchangeViewModel.limits.max.toString(), + amountParsingProxy: + widget.exchangeViewModel.amountParsingProxy, + ).call(value) + : null; + }, + addressTextFieldValidator: AddressValidator( + type: widget.exchangeViewModel.depositCurrency), + onPushPasteButton: (context) async { + final clipboard = await Clipboard.getData('text/plain'); + widget.exchangeViewModel.depositAddress = clipboard?.text ?? ''; - final domain = widget.exchangeViewModel.depositAddress; - widget.exchangeViewModel.depositAddress = - await fetchParsedAddress(context, domain, - widget.exchangeViewModel.depositCurrency); - }, - onPushAddressBookButton: (context) async { - final domain = widget.exchangeViewModel.depositAddress; - widget.exchangeViewModel.depositAddress = - await fetchParsedAddress(context, domain, - widget.exchangeViewModel.depositCurrency); - }, + final domain = widget.exchangeViewModel.depositAddress; + widget.exchangeViewModel.depositAddress = + await fetchParsedAddress(context, domain, + widget.exchangeViewModel.depositCurrency); + }, + onPushAddressBookButton: (context) async { + final domain = widget.exchangeViewModel.depositAddress; + widget.exchangeViewModel.depositAddress = + await fetchParsedAddress(context, domain, + widget.exchangeViewModel.depositCurrency); + }, + ), ), - ), - SwapLimitPopup(exchangeViewModel: widget.exchangeViewModel), - Padding( - padding: const EdgeInsets.symmetric(vertical: 12.0), - child: Stack( - alignment: Alignment.center, - children: [ - Container( - height: 1, - width: double.infinity, - color: Theme.of(context).colorScheme.surfaceContainerHigh, - ), - ModernButton.svg( - size: 36, - iconSize: 24, - svgPath: "assets/new-ui/swap_amounts.svg", - onPressed: widget.exchangeViewModel.reverseSwapDirection, - ), - ], + SwapLimitPopup(exchangeViewModel: widget.exchangeViewModel), + Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: Stack( + alignment: Alignment.center, + children: [ + Container( + height: 1, + width: double.infinity, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + ModernButton.svg( + size: 36, + iconSize: 24, + svgPath: "assets/new-ui/swap_amounts.svg", + onPressed: widget.exchangeViewModel.reverseSwapDirection, + ), + ], + ), ), - ), - Observer( - builder: (_) => SwapAmountBox( - isReceiverCard: true, - walletSwitcherViewModel: widget.walletSwitcherViewModel, - exchangeViewModel: widget.exchangeViewModel, - onDispose: disposeBestRateSync, - key: receiveKey, - title: S.of(context).receive, - initialCurrency: widget.exchangeViewModel.receiveCurrency, - currencies: widget.exchangeViewModel.receiveCurrencies, - onCurrencySelected: (currency) { - if (currency is CryptoCurrency) { - widget.exchangeViewModel - .changeReceiveCurrency(currency: currency); - } - }, - currencyValueValidator: (value) { - return widget.exchangeViewModel.isFixedRateMode - ? AmountValidator( - isAutovalidate: true, - currency: widget.exchangeViewModel.receiveCurrency, - minValue: widget.exchangeViewModel.limits.min.toString(), - maxValue: widget.exchangeViewModel.limits.max.toString(), - amountParsingProxy: - widget.exchangeViewModel.amountParsingProxy, - ).call(value) - : null; - }, - addressTextFieldValidator: AddressValidator( - type: widget.exchangeViewModel.receiveCurrency), - onPushPasteButton: (context) async { - final clipboard = await Clipboard.getData('text/plain'); - widget.exchangeViewModel.receiveAddress = clipboard?.text ?? ''; + Observer( + builder: (_) => SwapAmountBox( + isReceiverCard: true, + walletSwitcherViewModel: widget.walletSwitcherViewModel, + exchangeViewModel: widget.exchangeViewModel, + onDispose: disposeBestRateSync, + key: receiveKey, + title: S.of(context).receive, + initialCurrency: widget.exchangeViewModel.receiveCurrency, + currencies: widget.exchangeViewModel.receiveCurrencies, + onCurrencySelected: (currency) { + if (currency is CryptoCurrency) { + widget.exchangeViewModel + .changeReceiveCurrency(currency: currency); + } + }, + currencyValueValidator: (value) { + return widget.exchangeViewModel.isFixedRateMode + ? AmountValidator( + isAutovalidate: true, + currency: widget.exchangeViewModel.receiveCurrency, + minValue: widget.exchangeViewModel.limits.min.toString(), + maxValue: widget.exchangeViewModel.limits.max.toString(), + amountParsingProxy: + widget.exchangeViewModel.amountParsingProxy, + ).call(value) + : null; + }, + addressTextFieldValidator: AddressValidator( + type: widget.exchangeViewModel.receiveCurrency), + onPushPasteButton: (context) async { + final clipboard = await Clipboard.getData('text/plain'); + widget.exchangeViewModel.receiveAddress = clipboard?.text ?? ''; - final domain = widget.exchangeViewModel.receiveAddress; - widget.exchangeViewModel.receiveAddress = - await fetchParsedAddress(context, domain, - widget.exchangeViewModel.receiveCurrency); - }, - onPushAddressBookButton: (context) async { - final domain = widget.exchangeViewModel.receiveAddress; - widget.exchangeViewModel.receiveAddress = - await fetchParsedAddress(context, domain, - widget.exchangeViewModel.receiveCurrency); - }, - ), - ) - ], + final domain = widget.exchangeViewModel.receiveAddress; + widget.exchangeViewModel.receiveAddress = + await fetchParsedAddress(context, domain, + widget.exchangeViewModel.receiveCurrency); + }, + onPushAddressBookButton: (context) async { + final domain = widget.exchangeViewModel.receiveAddress; + widget.exchangeViewModel.receiveAddress = + await fetchParsedAddress(context, domain, + widget.exchangeViewModel.receiveCurrency); + }, + ), + ) + ], + ), ), ), Observer( diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index fc98e9eca..7e44e413d 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -227,262 +227,264 @@ class SendPage extends BasePage { !sendViewModel.balanceViewModel.isReversing, onLongPressUp: () => sendViewModel.balanceViewModel.isReversing = !sendViewModel.balanceViewModel.isReversing, - child: Form( - key: _formKey, - child: ScrollableWithBottomSection( - contentPadding: EdgeInsets.only(bottom: 24), - content: FocusTraversalGroup( - policy: OrderedTraversalPolicy(), - child: Column( - children: [ - PageViewHeightAdaptable( - controller: controller, - children: sendCards, - ), - SizedBox(height: 10), - Padding( - padding: EdgeInsets.only(left: 24, right: 24, bottom: 10), - child: Container( - height: 10, - child: Observer( - builder: (_) { - final count = sendViewModel.outputs.length; - - return count > 1 - ? Semantics( - label: 'Page Indicator', - hint: 'Swipe to change receiver', - excludeSemantics: true, - child: SmoothPageIndicator( - controller: controller, - count: count, - effect: ScrollingDotsEffect( - spacing: 6.0, - radius: 6.0, - dotWidth: 6.0, - dotHeight: 6.0, - dotColor: Theme.of(context) - .colorScheme - .primary - .withAlpha(100), - activeDotColor: Theme.of(context).colorScheme.primary, + child: RepaintBoundary( + child: Form( + key: _formKey, + child: ScrollableWithBottomSection( + contentPadding: EdgeInsets.only(bottom: 24), + content: FocusTraversalGroup( + policy: OrderedTraversalPolicy(), + child: Column( + children: [ + PageViewHeightAdaptable( + controller: controller, + children: sendCards, + ), + SizedBox(height: 10), + Padding( + padding: EdgeInsets.only(left: 24, right: 24, bottom: 10), + child: Container( + height: 10, + child: Observer( + builder: (_) { + final count = sendViewModel.outputs.length; + + return count > 1 + ? Semantics( + label: 'Page Indicator', + hint: 'Swipe to change receiver', + excludeSemantics: true, + child: SmoothPageIndicator( + controller: controller, + count: count, + effect: ScrollingDotsEffect( + spacing: 6.0, + radius: 6.0, + dotWidth: 6.0, + dotHeight: 6.0, + dotColor: Theme.of(context) + .colorScheme + .primary + .withAlpha(100), + activeDotColor: Theme.of(context).colorScheme.primary, + ), ), - ), - ) - : Offstage(); - }, + ) + : Offstage(); + }, + ), ), ), - ), - Container( - height: 40, - width: double.infinity, - padding: EdgeInsets.only(left: 24), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Observer( - builder: (_) { - final templates = sendViewModel.templates; - final itemCount = templates.length; - - return Row( - children: [ - AddTemplateButton( - key: ValueKey('send_page_add_template_button_key'), - onTap: () => - Navigator.of(context).pushNamed(Routes.sendTemplate), - currentTemplatesLength: templates.length, - ), - ListView.builder( - scrollDirection: Axis.horizontal, - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: itemCount, - itemBuilder: (context, index) { - final template = templates[index]; - return TemplateTile( - key: UniqueKey(), - to: template.name, - hasMultipleRecipients: - template.additionalRecipients != null && - template.additionalRecipients!.length > 1, - amount: template.isCurrencySelected - ? template.amount - : template.amountFiat, - from: template.isCurrencySelected - ? template.cryptoCurrency - : template.fiatCurrency, - onTap: () async { - sendViewModel.state = LoadingTemplateExecutingState(); - if (template.additionalRecipients?.isNotEmpty ?? - false) { - sendViewModel.clearOutputs(); - - for (int i = 0; - i < template.additionalRecipients!.length; - i++) { - Output output; - try { - output = sendViewModel.outputs[i]; - } catch (e) { - sendViewModel.addOutput(); - output = sendViewModel.outputs[i]; + Container( + height: 40, + width: double.infinity, + padding: EdgeInsets.only(left: 24), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Observer( + builder: (_) { + final templates = sendViewModel.templates; + final itemCount = templates.length; + + return Row( + children: [ + AddTemplateButton( + key: ValueKey('send_page_add_template_button_key'), + onTap: () => + Navigator.of(context).pushNamed(Routes.sendTemplate), + currentTemplatesLength: templates.length, + ), + ListView.builder( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: itemCount, + itemBuilder: (context, index) { + final template = templates[index]; + return TemplateTile( + key: UniqueKey(), + to: template.name, + hasMultipleRecipients: + template.additionalRecipients != null && + template.additionalRecipients!.length > 1, + amount: template.isCurrencySelected + ? template.amount + : template.amountFiat, + from: template.isCurrencySelected + ? template.cryptoCurrency + : template.fiatCurrency, + onTap: () async { + sendViewModel.state = LoadingTemplateExecutingState(); + if (template.additionalRecipients?.isNotEmpty ?? + false) { + sendViewModel.clearOutputs(); + + for (int i = 0; + i < template.additionalRecipients!.length; + i++) { + Output output; + try { + output = sendViewModel.outputs[i]; + } catch (e) { + sendViewModel.addOutput(); + output = sendViewModel.outputs[i]; + } + + await _setInputsFromTemplate( + context, + output: output, + template: template.additionalRecipients![i], + ); } - + } else { + final output = _defineCurrentOutput(); await _setInputsFromTemplate( context, output: output, - template: template.additionalRecipients![i], + template: template, ); } - } else { - final output = _defineCurrentOutput(); - await _setInputsFromTemplate( - context, - output: output, - template: template, + sendViewModel.state = InitialExecutionState(); + }, + onRemove: () { + showPopUp( + context: context, + builder: (dialogContext) { + return AlertWithTwoActions( + alertTitle: S.of(context).template, + alertContent: + S.of(context).confirm_delete_template, + rightButtonText: S.of(context).delete, + leftButtonText: S.of(context).cancel, + actionRightButton: () { + Navigator.of(dialogContext).pop(); + sendViewModel.sendTemplateViewModel + .removeTemplate(template: template); + }, + actionLeftButton: () => + Navigator.of(dialogContext).pop()); + }, ); - } - sendViewModel.state = InitialExecutionState(); - }, - onRemove: () { - showPopUp( - context: context, - builder: (dialogContext) { - return AlertWithTwoActions( - alertTitle: S.of(context).template, - alertContent: - S.of(context).confirm_delete_template, - rightButtonText: S.of(context).delete, - leftButtonText: S.of(context).cancel, - actionRightButton: () { - Navigator.of(dialogContext).pop(); - sendViewModel.sendTemplateViewModel - .removeTemplate(template: template); - }, - actionLeftButton: () => - Navigator.of(dialogContext).pop()); - }, - ); - }, - ); - }, - ), - ], - ); - }, + }, + ); + }, + ), + ], + ); + }, + ), ), ), - ), - ], + ], + ), ), - ), - bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), - bottomSection: Column( - children: [ - if (sendViewModel.sendTemplateViewModel.hasMultiRecipient) - Padding( - padding: EdgeInsets.only(bottom: 12), - child: PrimaryButton( - key: ValueKey('send_page_add_receiver_button_key'), - onPressed: () { - sendViewModel.addOutput(); - Future.delayed(const Duration(milliseconds: 250), () { - controller.jumpToPage(sendViewModel.outputs.length - 1); - }); - }, - text: S.of(context).add_receiver, - color: Colors.transparent, - textColor: Theme.of(context).colorScheme.onSurfaceVariant, - isDottedBorder: true, - borderColor: Theme.of(context).colorScheme.outline, - )), - 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()); - - if (sendViewModel.state is IsExecutingState) return; - if (_formKey.currentState != null && - !_formKey.currentState!.validate()) { - if (sendViewModel.outputs.length > 1) { - showErrorValidationAlert(context); - } - - return; - } - - final hasInvalidItems = sendViewModel.outputs.any((item) => - item.address.isEmpty || - (item.cryptoAmount.isEmpty && !item.sendAll)); - - if (hasInvalidItems) { - showErrorValidationAlert(context); - return; - } - - if (sendViewModel.wallet.isHardwareWallet) { - if (!sendViewModel.hardwareWalletViewModel!.isConnected) { - await Navigator.of(context).pushNamed(Routes.connectDevices, - arguments: ConnectDevicePageParams( - walletType: sendViewModel.walletType, - hardwareWalletType: - sendViewModel.wallet.walletInfo.hardwareWalletType!, - onConnectDevice: (BuildContext context, _) { - sendViewModel.hardwareWalletViewModel! - .initWallet(sendViewModel.wallet); - Navigator.of(context).pop(); - }, - )); - } else { - sendViewModel.hardwareWalletViewModel! - .initWallet(sendViewModel.wallet); - } - } - - if (sendViewModel.wallet.type == WalletType.monero) { - int amount = 0; - for (var item in sendViewModel.outputs) { - amount += item.formattedCryptoAmount; - } - if (monero!.needExportOutputs(sendViewModel.wallet, amount)) { - await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, - arguments: monero!.exportOutputsUR(sendViewModel.wallet)); - await Future.delayed( - Duration(seconds: 1)); // wait for monero to refresh the state - } - if (monero!.needExportOutputs(sendViewModel.wallet, amount)) { + bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), + bottomSection: Column( + children: [ + if (sendViewModel.sendTemplateViewModel.hasMultiRecipient) + Padding( + padding: EdgeInsets.only(bottom: 12), + child: PrimaryButton( + key: ValueKey('send_page_add_receiver_button_key'), + onPressed: () { + sendViewModel.addOutput(); + Future.delayed(const Duration(milliseconds: 250), () { + controller.jumpToPage(sendViewModel.outputs.length - 1); + }); + }, + text: S.of(context).add_receiver, + color: Colors.transparent, + textColor: Theme.of(context).colorScheme.onSurfaceVariant, + isDottedBorder: true, + borderColor: Theme.of(context).colorScheme.outline, + )), + 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()); + + if (sendViewModel.state is IsExecutingState) return; + if (_formKey.currentState != null && + !_formKey.currentState!.validate()) { + if (sendViewModel.outputs.length > 1) { + showErrorValidationAlert(context); + } + return; } - } - - final check = sendViewModel.shouldDisplayTotp(); - authService.authenticateAction( - context, - conditionToDetermineIfToUse2FA: check, - onAuthSuccess: (value) async { - if (value) { - await sendViewModel.createTransaction(); + + final hasInvalidItems = sendViewModel.outputs.any((item) => + item.address.isEmpty || + (item.cryptoAmount.isEmpty && !item.sendAll)); + + if (hasInvalidItems) { + showErrorValidationAlert(context); + return; + } + + if (sendViewModel.wallet.isHardwareWallet) { + if (!sendViewModel.hardwareWalletViewModel!.isConnected) { + await Navigator.of(context).pushNamed(Routes.connectDevices, + arguments: ConnectDevicePageParams( + walletType: sendViewModel.walletType, + hardwareWalletType: + sendViewModel.wallet.walletInfo.hardwareWalletType!, + onConnectDevice: (BuildContext context, _) { + sendViewModel.hardwareWalletViewModel! + .initWallet(sendViewModel.wallet); + Navigator.of(context).pop(); + }, + )); + } else { + sendViewModel.hardwareWalletViewModel! + .initWallet(sendViewModel.wallet); } - }, - ); - }, - text: _sendButtonText(context), - color: Theme.of(context).colorScheme.primary, - textColor: Theme.of(context).colorScheme.onPrimary, - isLoading: sendViewModel.state is IsExecutingState || - sendViewModel.state is TransactionCommitting || - sendViewModel.state is IsAwaitingDeviceResponseState || - sendViewModel.state is LoadingTemplateExecutingState, - isDisabled: !sendViewModel.isReadyForSend || sendViewModel.state is ExecutedSuccessfullyState, - ); - }, - ) - ], - )), + } + + if (sendViewModel.wallet.type == WalletType.monero) { + int amount = 0; + for (var item in sendViewModel.outputs) { + amount += item.formattedCryptoAmount; + } + if (monero!.needExportOutputs(sendViewModel.wallet, amount)) { + await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, + arguments: monero!.exportOutputsUR(sendViewModel.wallet)); + await Future.delayed( + Duration(seconds: 1)); // wait for monero to refresh the state + } + if (monero!.needExportOutputs(sendViewModel.wallet, amount)) { + return; + } + } + + final check = sendViewModel.shouldDisplayTotp(); + authService.authenticateAction( + context, + conditionToDetermineIfToUse2FA: check, + onAuthSuccess: (value) async { + if (value) { + await sendViewModel.createTransaction(); + } + }, + ); + }, + text: _sendButtonText(context), + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + isLoading: sendViewModel.state is IsExecutingState || + sendViewModel.state is TransactionCommitting || + sendViewModel.state is IsAwaitingDeviceResponseState || + sendViewModel.state is LoadingTemplateExecutingState, + isDisabled: !sendViewModel.isReadyForSend || sendViewModel.state is ExecutedSuccessfullyState, + ); + }, + ) + ], + )), + ), ), ), ],