add RepaintBoundary to send and swap (#3084)

malik1004x committed Mar 14, 2026 at 13:43 UTC 98d8161afc86ba287531d14fd08b156cea8cbe29
2 files changed +368 -364
lib/new-ui/pages/swap_page.dart
+127 -125
@@ -480,133 +480,135 @@ class _NewSwapPageState extends State<NewSwapPage> {
480 mainAxisAlignment: MainAxisAlignment.spaceBetween,
481 mainAxisSize: MainAxisSize.max,
482 children: [
483 - Form(
484 - key: formKey,
485 - child: Column(
486 - children: [
487 - Observer(
488 - builder: (_) => SwapAmountBox(
489 - isReceiverCard: false,
490 - walletSwitcherViewModel: widget.walletSwitcherViewModel,
491 - exchangeViewModel: widget.exchangeViewModel,
492 - onDispose: disposeBestRateSync,
493 - hasAllAmount: widget.exchangeViewModel.hasAllAmount,
494 - allAmount: widget.exchangeViewModel.hasAllAmount
495 - ? () => widget.exchangeViewModel.enableSendAllAmount()
496 - : null,
497 - key: depositKey,
498 - title: S.of(context).send,
499 - initialCurrency: widget.exchangeViewModel.depositCurrency,
500 - hasRefundAddress: true,
501 - currencies: widget.exchangeViewModel.depositCurrencies,
502 - onCurrencySelected: (currency) {
503 - if (currency is CryptoCurrency) {
504 - widget.exchangeViewModel
505 - .changeDepositCurrency(currency: currency);
506 - }
507 - },
508 - currencyValueValidator: (value) {
509 - return !widget.exchangeViewModel.isFixedRateMode &&
510 - value != S.of(context).all
511 - ? AmountValidator(
512 - isAutovalidate: true,
513 - currency: widget.exchangeViewModel.isFixedRateMode
514 - ? widget.exchangeViewModel.receiveCurrency
515 - : widget.exchangeViewModel.depositCurrency,
516 - minValue: widget.exchangeViewModel.limits.min.toString(),
517 - maxValue: widget.exchangeViewModel.limits.max.toString(),
518 - amountParsingProxy:
519 - widget.exchangeViewModel.amountParsingProxy,
520 - ).call(value)
521 - : null;
522 - },
523 - addressTextFieldValidator: AddressValidator(
524 - type: widget.exchangeViewModel.depositCurrency),
525 - onPushPasteButton: (context) async {
526 - final clipboard = await Clipboard.getData('text/plain');
527 - widget.exchangeViewModel.depositAddress = clipboard?.text ?? '';
528 -
529 - final domain = widget.exchangeViewModel.depositAddress;
530 - widget.exchangeViewModel.depositAddress =
531 - await fetchParsedAddress(context, domain,
532 - widget.exchangeViewModel.depositCurrency);
533 - },
534 - onPushAddressBookButton: (context) async {
535 - final domain = widget.exchangeViewModel.depositAddress;
536 - widget.exchangeViewModel.depositAddress =
537 - await fetchParsedAddress(context, domain,
538 - widget.exchangeViewModel.depositCurrency);
539 - },
540 - ),
541 - ),
542 - SwapLimitPopup(exchangeViewModel: widget.exchangeViewModel),
543 - Padding(
544 - padding: const EdgeInsets.symmetric(vertical: 12.0),
545 - child: Stack(
546 - alignment: Alignment.center,
547 - children: [
548 - Container(
549 - height: 1,
550 - width: double.infinity,
551 - color: Theme.of(context).colorScheme.surfaceContainerHigh,
552 - ),
553 - ModernButton.svg(
554 - size: 36,
555 - iconSize: 24,
556 - svgPath: "assets/new-ui/swap_amounts.svg",
557 - onPressed: widget.exchangeViewModel.reverseSwapDirection,
558 - ),
559 - ],
483 + RepaintBoundary(
484 + child: Form(
485 + key: formKey,
486 + child: Column(
487 + children: [
488 + Observer(
489 + builder: (_) => SwapAmountBox(
490 + isReceiverCard: false,
491 + walletSwitcherViewModel: widget.walletSwitcherViewModel,
492 + exchangeViewModel: widget.exchangeViewModel,
493 + onDispose: disposeBestRateSync,
494 + hasAllAmount: widget.exchangeViewModel.hasAllAmount,
495 + allAmount: widget.exchangeViewModel.hasAllAmount
496 + ? () => widget.exchangeViewModel.enableSendAllAmount()
497 + : null,
498 + key: depositKey,
499 + title: S.of(context).send,
500 + initialCurrency: widget.exchangeViewModel.depositCurrency,
501 + hasRefundAddress: true,
502 + currencies: widget.exchangeViewModel.depositCurrencies,
503 + onCurrencySelected: (currency) {
504 + if (currency is CryptoCurrency) {
505 + widget.exchangeViewModel
506 + .changeDepositCurrency(currency: currency);
507 + }
508 + },
509 + currencyValueValidator: (value) {
510 + return !widget.exchangeViewModel.isFixedRateMode &&
511 + value != S.of(context).all
512 + ? AmountValidator(
513 + isAutovalidate: true,
514 + currency: widget.exchangeViewModel.isFixedRateMode
515 + ? widget.exchangeViewModel.receiveCurrency
516 + : widget.exchangeViewModel.depositCurrency,
517 + minValue: widget.exchangeViewModel.limits.min.toString(),
518 + maxValue: widget.exchangeViewModel.limits.max.toString(),
519 + amountParsingProxy:
520 + widget.exchangeViewModel.amountParsingProxy,
521 + ).call(value)
522 + : null;
523 + },
524 + addressTextFieldValidator: AddressValidator(
525 + type: widget.exchangeViewModel.depositCurrency),
526 + onPushPasteButton: (context) async {
527 + final clipboard = await Clipboard.getData('text/plain');
528 + widget.exchangeViewModel.depositAddress = clipboard?.text ?? '';
529 +
530 + final domain = widget.exchangeViewModel.depositAddress;
531 + widget.exchangeViewModel.depositAddress =
532 + await fetchParsedAddress(context, domain,
533 + widget.exchangeViewModel.depositCurrency);
534 + },
535 + onPushAddressBookButton: (context) async {
536 + final domain = widget.exchangeViewModel.depositAddress;
537 + widget.exchangeViewModel.depositAddress =
538 + await fetchParsedAddress(context, domain,
539 + widget.exchangeViewModel.depositCurrency);
540 + },
541 + ),
542 ),
561 - ),
562 - Observer(
563 - builder: (_) => SwapAmountBox(
564 - isReceiverCard: true,
565 - walletSwitcherViewModel: widget.walletSwitcherViewModel,
566 - exchangeViewModel: widget.exchangeViewModel,
567 - onDispose: disposeBestRateSync,
568 - key: receiveKey,
569 - title: S.of(context).receive,
570 - initialCurrency: widget.exchangeViewModel.receiveCurrency,
571 - currencies: widget.exchangeViewModel.receiveCurrencies,
572 - onCurrencySelected: (currency) {
573 - if (currency is CryptoCurrency) {
574 - widget.exchangeViewModel
575 - .changeReceiveCurrency(currency: currency);
576 - }
577 - },
578 - currencyValueValidator: (value) {
579 - return widget.exchangeViewModel.isFixedRateMode
580 - ? AmountValidator(
581 - isAutovalidate: true,
582 - currency: widget.exchangeViewModel.receiveCurrency,
583 - minValue: widget.exchangeViewModel.limits.min.toString(),
584 - maxValue: widget.exchangeViewModel.limits.max.toString(),
585 - amountParsingProxy:
586 - widget.exchangeViewModel.amountParsingProxy,
587 - ).call(value)
588 - : null;
589 - },
590 - addressTextFieldValidator: AddressValidator(
591 - type: widget.exchangeViewModel.receiveCurrency),
592 - onPushPasteButton: (context) async {
593 - final clipboard = await Clipboard.getData('text/plain');
594 - widget.exchangeViewModel.receiveAddress = clipboard?.text ?? '';
595 -
596 - final domain = widget.exchangeViewModel.receiveAddress;
597 - widget.exchangeViewModel.receiveAddress =
598 - await fetchParsedAddress(context, domain,
599 - widget.exchangeViewModel.receiveCurrency);
600 - },
601 - onPushAddressBookButton: (context) async {
602 - final domain = widget.exchangeViewModel.receiveAddress;
603 - widget.exchangeViewModel.receiveAddress =
604 - await fetchParsedAddress(context, domain,
605 - widget.exchangeViewModel.receiveCurrency);
606 - },
543 + SwapLimitPopup(exchangeViewModel: widget.exchangeViewModel),
544 + Padding(
545 + padding: const EdgeInsets.symmetric(vertical: 12.0),
546 + child: Stack(
547 + alignment: Alignment.center,
548 + children: [
549 + Container(
550 + height: 1,
551 + width: double.infinity,
552 + color: Theme.of(context).colorScheme.surfaceContainerHigh,
553 + ),
554 + ModernButton.svg(
555 + size: 36,
556 + iconSize: 24,
557 + svgPath: "assets/new-ui/swap_amounts.svg",
558 + onPressed: widget.exchangeViewModel.reverseSwapDirection,
559 + ),
560 + ],
561 + ),
562 ),
608 - )
609 - ],
563 + Observer(
564 + builder: (_) => SwapAmountBox(
565 + isReceiverCard: true,
566 + walletSwitcherViewModel: widget.walletSwitcherViewModel,
567 + exchangeViewModel: widget.exchangeViewModel,
568 + onDispose: disposeBestRateSync,
569 + key: receiveKey,
570 + title: S.of(context).receive,
571 + initialCurrency: widget.exchangeViewModel.receiveCurrency,
572 + currencies: widget.exchangeViewModel.receiveCurrencies,
573 + onCurrencySelected: (currency) {
574 + if (currency is CryptoCurrency) {
575 + widget.exchangeViewModel
576 + .changeReceiveCurrency(currency: currency);
577 + }
578 + },
579 + currencyValueValidator: (value) {
580 + return widget.exchangeViewModel.isFixedRateMode
581 + ? AmountValidator(
582 + isAutovalidate: true,
583 + currency: widget.exchangeViewModel.receiveCurrency,
584 + minValue: widget.exchangeViewModel.limits.min.toString(),
585 + maxValue: widget.exchangeViewModel.limits.max.toString(),
586 + amountParsingProxy:
587 + widget.exchangeViewModel.amountParsingProxy,
588 + ).call(value)
589 + : null;
590 + },
591 + addressTextFieldValidator: AddressValidator(
592 + type: widget.exchangeViewModel.receiveCurrency),
593 + onPushPasteButton: (context) async {
594 + final clipboard = await Clipboard.getData('text/plain');
595 + widget.exchangeViewModel.receiveAddress = clipboard?.text ?? '';
596 +
597 + final domain = widget.exchangeViewModel.receiveAddress;
598 + widget.exchangeViewModel.receiveAddress =
599 + await fetchParsedAddress(context, domain,
600 + widget.exchangeViewModel.receiveCurrency);
601 + },
602 + onPushAddressBookButton: (context) async {
603 + final domain = widget.exchangeViewModel.receiveAddress;
604 + widget.exchangeViewModel.receiveAddress =
605 + await fetchParsedAddress(context, domain,
606 + widget.exchangeViewModel.receiveCurrency);
607 + },
608 + ),
609 + )
610 + ],
611 + ),
612 ),
613 ),
614 Observer(
lib/src/screens/send/send_page.dart
+241 -239
@@ -227,262 +227,264 @@ class SendPage extends BasePage {
227 !sendViewModel.balanceViewModel.isReversing,
228 onLongPressUp: () => sendViewModel.balanceViewModel.isReversing =
229 !sendViewModel.balanceViewModel.isReversing,
230 - child: Form(
231 - key: _formKey,
232 - child: ScrollableWithBottomSection(
233 - contentPadding: EdgeInsets.only(bottom: 24),
234 - content: FocusTraversalGroup(
235 - policy: OrderedTraversalPolicy(),
236 - child: Column(
237 - children: <Widget>[
238 - PageViewHeightAdaptable(
239 - controller: controller,
240 - children: sendCards,
241 - ),
242 - SizedBox(height: 10),
243 - Padding(
244 - padding: EdgeInsets.only(left: 24, right: 24, bottom: 10),
245 - child: Container(
246 - height: 10,
247 - child: Observer(
248 - builder: (_) {
249 - final count = sendViewModel.outputs.length;
250 -
251 - return count > 1
252 - ? Semantics(
253 - label: 'Page Indicator',
254 - hint: 'Swipe to change receiver',
255 - excludeSemantics: true,
256 - child: SmoothPageIndicator(
257 - controller: controller,
258 - count: count,
259 - effect: ScrollingDotsEffect(
260 - spacing: 6.0,
261 - radius: 6.0,
262 - dotWidth: 6.0,
263 - dotHeight: 6.0,
264 - dotColor: Theme.of(context)
265 - .colorScheme
266 - .primary
267 - .withAlpha(100),
268 - activeDotColor: Theme.of(context).colorScheme.primary,
230 + child: RepaintBoundary(
231 + child: Form(
232 + key: _formKey,
233 + child: ScrollableWithBottomSection(
234 + contentPadding: EdgeInsets.only(bottom: 24),
235 + content: FocusTraversalGroup(
236 + policy: OrderedTraversalPolicy(),
237 + child: Column(
238 + children: <Widget>[
239 + PageViewHeightAdaptable(
240 + controller: controller,
241 + children: sendCards,
242 + ),
243 + SizedBox(height: 10),
244 + Padding(
245 + padding: EdgeInsets.only(left: 24, right: 24, bottom: 10),
246 + child: Container(
247 + height: 10,
248 + child: Observer(
249 + builder: (_) {
250 + final count = sendViewModel.outputs.length;
251 +
252 + return count > 1
253 + ? Semantics(
254 + label: 'Page Indicator',
255 + hint: 'Swipe to change receiver',
256 + excludeSemantics: true,
257 + child: SmoothPageIndicator(
258 + controller: controller,
259 + count: count,
260 + effect: ScrollingDotsEffect(
261 + spacing: 6.0,
262 + radius: 6.0,
263 + dotWidth: 6.0,
264 + dotHeight: 6.0,
265 + dotColor: Theme.of(context)
266 + .colorScheme
267 + .primary
268 + .withAlpha(100),
269 + activeDotColor: Theme.of(context).colorScheme.primary,
270 + ),
271 ),
270 - ),
271 - )
272 - : Offstage();
273 - },
272 + )
273 + : Offstage();
274 + },
275 + ),
276 ),
277 ),
276 - ),
277 - Container(
278 - height: 40,
279 - width: double.infinity,
280 - padding: EdgeInsets.only(left: 24),
281 - child: SingleChildScrollView(
282 - scrollDirection: Axis.horizontal,
283 - child: Observer(
284 - builder: (_) {
285 - final templates = sendViewModel.templates;
286 - final itemCount = templates.length;
287 -
288 - return Row(
289 - children: <Widget>[
290 - AddTemplateButton(
291 - key: ValueKey('send_page_add_template_button_key'),
292 - onTap: () =>
293 - Navigator.of(context).pushNamed(Routes.sendTemplate),
294 - currentTemplatesLength: templates.length,
295 - ),
296 - ListView.builder(
297 - scrollDirection: Axis.horizontal,
298 - shrinkWrap: true,
299 - physics: NeverScrollableScrollPhysics(),
300 - itemCount: itemCount,
301 - itemBuilder: (context, index) {
302 - final template = templates[index];
303 - return TemplateTile(
304 - key: UniqueKey(),
305 - to: template.name,
306 - hasMultipleRecipients:
307 - template.additionalRecipients != null &&
308 - template.additionalRecipients!.length > 1,
309 - amount: template.isCurrencySelected
310 - ? template.amount
311 - : template.amountFiat,
312 - from: template.isCurrencySelected
313 - ? template.cryptoCurrency
314 - : template.fiatCurrency,
315 - onTap: () async {
316 - sendViewModel.state = LoadingTemplateExecutingState();
317 - if (template.additionalRecipients?.isNotEmpty ??
318 - false) {
319 - sendViewModel.clearOutputs();
320 -
321 - for (int i = 0;
322 - i < template.additionalRecipients!.length;
323 - i++) {
324 - Output output;
325 - try {
326 - output = sendViewModel.outputs[i];
327 - } catch (e) {
328 - sendViewModel.addOutput();
329 - output = sendViewModel.outputs[i];
278 + Container(
279 + height: 40,
280 + width: double.infinity,
281 + padding: EdgeInsets.only(left: 24),
282 + child: SingleChildScrollView(
283 + scrollDirection: Axis.horizontal,
284 + child: Observer(
285 + builder: (_) {
286 + final templates = sendViewModel.templates;
287 + final itemCount = templates.length;
288 +
289 + return Row(
290 + children: <Widget>[
291 + AddTemplateButton(
292 + key: ValueKey('send_page_add_template_button_key'),
293 + onTap: () =>
294 + Navigator.of(context).pushNamed(Routes.sendTemplate),
295 + currentTemplatesLength: templates.length,
296 + ),
297 + ListView.builder(
298 + scrollDirection: Axis.horizontal,
299 + shrinkWrap: true,
300 + physics: NeverScrollableScrollPhysics(),
301 + itemCount: itemCount,
302 + itemBuilder: (context, index) {
303 + final template = templates[index];
304 + return TemplateTile(
305 + key: UniqueKey(),
306 + to: template.name,
307 + hasMultipleRecipients:
308 + template.additionalRecipients != null &&
309 + template.additionalRecipients!.length > 1,
310 + amount: template.isCurrencySelected
311 + ? template.amount
312 + : template.amountFiat,
313 + from: template.isCurrencySelected
314 + ? template.cryptoCurrency
315 + : template.fiatCurrency,
316 + onTap: () async {
317 + sendViewModel.state = LoadingTemplateExecutingState();
318 + if (template.additionalRecipients?.isNotEmpty ??
319 + false) {
320 + sendViewModel.clearOutputs();
321 +
322 + for (int i = 0;
323 + i < template.additionalRecipients!.length;
324 + i++) {
325 + Output output;
326 + try {
327 + output = sendViewModel.outputs[i];
328 + } catch (e) {
329 + sendViewModel.addOutput();
330 + output = sendViewModel.outputs[i];
331 + }
332 +
333 + await _setInputsFromTemplate(
334 + context,
335 + output: output,
336 + template: template.additionalRecipients![i],
337 + );
338 }
331 -
339 + } else {
340 + final output = _defineCurrentOutput();
341 await _setInputsFromTemplate(
342 context,
343 output: output,
335 - template: template.additionalRecipients![i],
344 + template: template,
345 );
346 }
338 - } else {
339 - final output = _defineCurrentOutput();
340 - await _setInputsFromTemplate(
341 - context,
342 - output: output,
343 - template: template,
347 + sendViewModel.state = InitialExecutionState();
348 + },
349 + onRemove: () {
350 + showPopUp<void>(
351 + context: context,
352 + builder: (dialogContext) {
353 + return AlertWithTwoActions(
354 + alertTitle: S.of(context).template,
355 + alertContent:
356 + S.of(context).confirm_delete_template,
357 + rightButtonText: S.of(context).delete,
358 + leftButtonText: S.of(context).cancel,
359 + actionRightButton: () {
360 + Navigator.of(dialogContext).pop();
361 + sendViewModel.sendTemplateViewModel
362 + .removeTemplate(template: template);
363 + },
364 + actionLeftButton: () =>
365 + Navigator.of(dialogContext).pop());
366 + },
367 );
345 - }
346 - sendViewModel.state = InitialExecutionState();
347 - },
348 - onRemove: () {
349 - showPopUp<void>(
350 - context: context,
351 - builder: (dialogContext) {
352 - return AlertWithTwoActions(
353 - alertTitle: S.of(context).template,
354 - alertContent:
355 - S.of(context).confirm_delete_template,
356 - rightButtonText: S.of(context).delete,
357 - leftButtonText: S.of(context).cancel,
358 - actionRightButton: () {
359 - Navigator.of(dialogContext).pop();
360 - sendViewModel.sendTemplateViewModel
361 - .removeTemplate(template: template);
362 - },
363 - actionLeftButton: () =>
364 - Navigator.of(dialogContext).pop());
365 - },
366 - );
367 - },
368 - );
369 - },
370 - ),
371 - ],
372 - );
373 - },
368 + },
369 + );
370 + },
371 + ),
372 + ],
373 + );
374 + },
375 + ),
376 ),
377 ),
376 - ),
377 - ],
378 + ],
379 + ),
380 ),
379 - ),
380 - bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
381 - bottomSection: Column(
382 - children: [
383 - if (sendViewModel.sendTemplateViewModel.hasMultiRecipient)
384 - Padding(
385 - padding: EdgeInsets.only(bottom: 12),
386 - child: PrimaryButton(
387 - key: ValueKey('send_page_add_receiver_button_key'),
388 - onPressed: () {
389 - sendViewModel.addOutput();
390 - Future.delayed(const Duration(milliseconds: 250), () {
391 - controller.jumpToPage(sendViewModel.outputs.length - 1);
392 - });
393 - },
394 - text: S.of(context).add_receiver,
395 - color: Colors.transparent,
396 - textColor: Theme.of(context).colorScheme.onSurfaceVariant,
397 - isDottedBorder: true,
398 - borderColor: Theme.of(context).colorScheme.outline,
399 - )),
400 - Observer(
401 - builder: (_) {
402 - return LoadingPrimaryButton(
403 - key: ValueKey('send_page_send_button_key'),
404 - onPressed: () async {
405 - //Request dummy node to get the focus out of the text fields
406 - FocusScope.of(context).requestFocus(FocusNode());
407 -
408 - if (sendViewModel.state is IsExecutingState) return;
409 - if (_formKey.currentState != null &&
410 - !_formKey.currentState!.validate()) {
411 - if (sendViewModel.outputs.length > 1) {
412 - showErrorValidationAlert(context);
413 - }
414 -
415 - return;
416 - }
417 -
418 - final hasInvalidItems = sendViewModel.outputs.any((item) =>
419 - item.address.isEmpty ||
420 - (item.cryptoAmount.isEmpty && !item.sendAll));
421 -
422 - if (hasInvalidItems) {
423 - showErrorValidationAlert(context);
424 - return;
425 - }
426 -
427 - if (sendViewModel.wallet.isHardwareWallet) {
428 - if (!sendViewModel.hardwareWalletViewModel!.isConnected) {
429 - await Navigator.of(context).pushNamed(Routes.connectDevices,
430 - arguments: ConnectDevicePageParams(
431 - walletType: sendViewModel.walletType,
432 - hardwareWalletType:
433 - sendViewModel.wallet.walletInfo.hardwareWalletType!,
434 - onConnectDevice: (BuildContext context, _) {
435 - sendViewModel.hardwareWalletViewModel!
436 - .initWallet(sendViewModel.wallet);
437 - Navigator.of(context).pop();
438 - },
439 - ));
440 - } else {
441 - sendViewModel.hardwareWalletViewModel!
442 - .initWallet(sendViewModel.wallet);
443 - }
444 - }
445 -
446 - if (sendViewModel.wallet.type == WalletType.monero) {
447 - int amount = 0;
448 - for (var item in sendViewModel.outputs) {
449 - amount += item.formattedCryptoAmount;
450 - }
451 - if (monero!.needExportOutputs(sendViewModel.wallet, amount)) {
452 - await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage,
453 - arguments: monero!.exportOutputsUR(sendViewModel.wallet));
454 - await Future.delayed(
455 - Duration(seconds: 1)); // wait for monero to refresh the state
381 + bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
382 + bottomSection: Column(
383 + children: [
384 + if (sendViewModel.sendTemplateViewModel.hasMultiRecipient)
385 + Padding(
386 + padding: EdgeInsets.only(bottom: 12),
387 + child: PrimaryButton(
388 + key: ValueKey('send_page_add_receiver_button_key'),
389 + onPressed: () {
390 + sendViewModel.addOutput();
391 + Future.delayed(const Duration(milliseconds: 250), () {
392 + controller.jumpToPage(sendViewModel.outputs.length - 1);
393 + });
394 + },
395 + text: S.of(context).add_receiver,
396 + color: Colors.transparent,
397 + textColor: Theme.of(context).colorScheme.onSurfaceVariant,
398 + isDottedBorder: true,
399 + borderColor: Theme.of(context).colorScheme.outline,
400 + )),
401 + Observer(
402 + builder: (_) {
403 + return LoadingPrimaryButton(
404 + key: ValueKey('send_page_send_button_key'),
405 + onPressed: () async {
406 + //Request dummy node to get the focus out of the text fields
407 + FocusScope.of(context).requestFocus(FocusNode());
408 +
409 + if (sendViewModel.state is IsExecutingState) return;
410 + if (_formKey.currentState != null &&
411 + !_formKey.currentState!.validate()) {
412 + if (sendViewModel.outputs.length > 1) {
413 + showErrorValidationAlert(context);
414 + }
415 +
416 + return;
417 }
457 - if (monero!.needExportOutputs(sendViewModel.wallet, amount)) {
418 +
419 + final hasInvalidItems = sendViewModel.outputs.any((item) =>
420 + item.address.isEmpty ||
421 + (item.cryptoAmount.isEmpty && !item.sendAll));
422 +
423 + if (hasInvalidItems) {
424 + showErrorValidationAlert(context);
425 return;
426 }
460 - }
461 -
462 - final check = sendViewModel.shouldDisplayTotp();
463 - authService.authenticateAction(
464 - context,
465 - conditionToDetermineIfToUse2FA: check,
466 - onAuthSuccess: (value) async {
467 - if (value) {
468 - await sendViewModel.createTransaction();
427 +
428 + if (sendViewModel.wallet.isHardwareWallet) {
429 + if (!sendViewModel.hardwareWalletViewModel!.isConnected) {
430 + await Navigator.of(context).pushNamed(Routes.connectDevices,
431 + arguments: ConnectDevicePageParams(
432 + walletType: sendViewModel.walletType,
433 + hardwareWalletType:
434 + sendViewModel.wallet.walletInfo.hardwareWalletType!,
435 + onConnectDevice: (BuildContext context, _) {
436 + sendViewModel.hardwareWalletViewModel!
437 + .initWallet(sendViewModel.wallet);
438 + Navigator.of(context).pop();
439 + },
440 + ));
441 + } else {
442 + sendViewModel.hardwareWalletViewModel!
443 + .initWallet(sendViewModel.wallet);
444 }
470 - },
471 - );
472 - },
473 - text: _sendButtonText(context),
474 - color: Theme.of(context).colorScheme.primary,
475 - textColor: Theme.of(context).colorScheme.onPrimary,
476 - isLoading: sendViewModel.state is IsExecutingState ||
477 - sendViewModel.state is TransactionCommitting ||
478 - sendViewModel.state is IsAwaitingDeviceResponseState ||
479 - sendViewModel.state is LoadingTemplateExecutingState,
480 - isDisabled: !sendViewModel.isReadyForSend || sendViewModel.state is ExecutedSuccessfullyState,
481 - );
482 - },
483 - )
484 - ],
485 - )),
445 + }
446 +
447 + if (sendViewModel.wallet.type == WalletType.monero) {
448 + int amount = 0;
449 + for (var item in sendViewModel.outputs) {
450 + amount += item.formattedCryptoAmount;
451 + }
452 + if (monero!.needExportOutputs(sendViewModel.wallet, amount)) {
453 + await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage,
454 + arguments: monero!.exportOutputsUR(sendViewModel.wallet));
455 + await Future.delayed(
456 + Duration(seconds: 1)); // wait for monero to refresh the state
457 + }
458 + if (monero!.needExportOutputs(sendViewModel.wallet, amount)) {
459 + return;
460 + }
461 + }
462 +
463 + final check = sendViewModel.shouldDisplayTotp();
464 + authService.authenticateAction(
465 + context,
466 + conditionToDetermineIfToUse2FA: check,
467 + onAuthSuccess: (value) async {
468 + if (value) {
469 + await sendViewModel.createTransaction();
470 + }
471 + },
472 + );
473 + },
474 + text: _sendButtonText(context),
475 + color: Theme.of(context).colorScheme.primary,
476 + textColor: Theme.of(context).colorScheme.onPrimary,
477 + isLoading: sendViewModel.state is IsExecutingState ||
478 + sendViewModel.state is TransactionCommitting ||
479 + sendViewModel.state is IsAwaitingDeviceResponseState ||
480 + sendViewModel.state is LoadingTemplateExecutingState,
481 + isDisabled: !sendViewModel.isReadyForSend || sendViewModel.state is ExecutedSuccessfullyState,
482 + );
483 + },
484 + )
485 + ],
486 + )),
487 + ),
488 ),
489 ),
490 ],