add RepaintBoundary to send and swap (#3084)

This commit is contained in:
malik1004x
2026-03-14 13:43:33 +01:00
committed by GitHub
parent 611ecc8ef7
commit 98d8161afc
2 changed files with 367 additions and 363 deletions
+125 -123
View File
@@ -480,133 +480,135 @@ class _NewSwapPageState extends State<NewSwapPage> {
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(
+242 -240
View File
@@ -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: <Widget>[
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: <Widget>[
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: <Widget>[
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: <Widget>[
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<void>(
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<void>(
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,
);
},
)
],
)),
),
),
),
],