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
],