Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
276 changes: 162 additions & 114 deletions lib/pages/send_view/confirm_transaction_view.dart

Large diffs are not rendered by default.

91 changes: 80 additions & 11 deletions lib/pages/send_view/send_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import '../../models/isar/models/isar_models.dart';
import '../../models/mwc_slatepack_models.dart';
import '../../models/paynym/paynym_account_lite.dart';
import '../../models/send_view_auto_fill_data.dart';
import '../../notifications/show_flush_bar.dart';
import '../../providers/providers.dart';
import '../../providers/ui/fee_rate_type_state_provider.dart';
import '../../providers/ui/preview_tx_button_state_provider.dart';
Expand Down Expand Up @@ -87,6 +88,7 @@ import 'confirm_transaction_view.dart';
import 'sub_widgets/building_transaction_dialog.dart';
import 'sub_widgets/dual_balance_selection_sheet.dart';
import 'sub_widgets/epic_slatepack_dialog.dart';
import 'sub_widgets/multi_recipient_summary.dart';
import 'sub_widgets/mwc_slatepack_dialog.dart';
import 'sub_widgets/transaction_fee_selection_sheet.dart';

Expand Down Expand Up @@ -148,6 +150,9 @@ class _SendViewState extends ConsumerState<SendView> {
Amount? _cachedAmountToSend;
String? _address;

/// Set while sending to the recipients of a multi-recipient payment URI.
List<SendRecipient>? _multiRecipients;

bool _addressToggleFlag = false;

bool _isFiroExWarningDisplayed = false;
Expand All @@ -157,8 +162,49 @@ class _SendViewState extends ConsumerState<SendView> {

Set<StandardInput> selectedUTXOs = {};

void _applyMultiRecipientUri(PaymentUriData paymentData) {
final recipients = parseUriRecipients(paymentData, coin);
if (recipients == null) {
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Invalid payment request amounts",
context: context,
);
return;
}

if (paymentData.message != null) {
noteController.text = paymentData.message!;
}
_setOpReturnData(null);
_address = null;
sendToController.text = "";
final total = recipients.total;
cryptoAmountController.text = ref
.read(pAmountFormatter(coin))
.formatEditable(total);
ref.read(pSendAmount.notifier).state = total;

setState(() {
_multiRecipients = recipients;
_addressToggleFlag = false;
});
_setValidAddressProviders(_address);
}

void _clearMultiRecipients() {
clearSendForm();
_setValidAddressProviders(_address);
}

void _applyUri(PaymentUriData paymentData) {
try {
if (paymentData.isMultiRecipient) {
_applyMultiRecipientUri(paymentData);
return;
}
_multiRecipients = null;

// auto fill address
_address = paymentData.address.trim();

Expand Down Expand Up @@ -260,6 +306,7 @@ class _SendViewState extends ConsumerState<SendView> {
final paymentData = AddressUtils.parsePaymentUri(
content,
logging: Logging.instance,
allowMultipleRecipients: true,
);

if (paymentData != null &&
Expand Down Expand Up @@ -328,6 +375,7 @@ class _SendViewState extends ConsumerState<SendView> {
final paymentData = AddressUtils.parsePaymentUri(
qrResult.rawContent!,
logging: Logging.instance,
allowMultipleRecipients: true,
);

if (paymentData != null &&
Expand Down Expand Up @@ -496,7 +544,10 @@ class _SendViewState extends ConsumerState<SendView> {
late Amount _currentFee;

void _setValidAddressProviders(String? address) {
if (isPaynymSend) {
if (_multiRecipients != null) {
ref.read(pValidSendToAddress.notifier).state = _multiRecipients!
.allValidFor(coin);
} else if (isPaynymSend) {
ref.read(pValidSendToAddress.notifier).state = true;
} else {
final wallet = ref.read(pWallets).getWallet(walletId);
Expand Down Expand Up @@ -1077,14 +1128,18 @@ class _SendViewState extends ConsumerState<SendView> {
final memo = coin is Stellar ? memoController.text : null;
txDataFuture = wallet.prepareSend(
txData: TxData(
recipients: [
TxRecipient(
address: _address!,
amount: amount,
isChange: false,
addressType: wallet.cryptoCurrency.getAddressType(_address!)!,
),
],
recipients:
_multiRecipients?.toTxRecipients(wallet.cryptoCurrency) ??
[
TxRecipient(
address: _address!,
amount: amount,
isChange: false,
addressType: wallet.cryptoCurrency.getAddressType(
_address!,
)!,
),
],
memo: memo,
feeRateType: feeRateType,
satsPerVByte: satsPerVByte,
Expand Down Expand Up @@ -1193,6 +1248,7 @@ class _SendViewState extends ConsumerState<SendView> {
memoController.text = "";
_address = "";
_addressToggleFlag = false;
_multiRecipients = null;
_setOpReturnData(null);
setState(() {});
}
Expand Down Expand Up @@ -1722,7 +1778,15 @@ class _SendViewState extends ConsumerState<SendView> {
readOnly: true,
style: STextStyles.fieldLabel(context),
),
if (!isPaynymSend && !isSlatepackMode)
if (_multiRecipients != null)
MultiRecipientSummary(
coin: coin,
recipients: _multiRecipients!,
onClear: _clearMultiRecipients,
),
if (!isPaynymSend &&
!isSlatepackMode &&
_multiRecipients == null)
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
Expand Down Expand Up @@ -1755,6 +1819,7 @@ class _SendViewState extends ConsumerState<SendView> {
AddressUtils.parsePaymentUri(
trimmed,
logging: Logging.instance,
allowMultipleRecipients: true,
);
if (parsed != null) {
_applyUri(parsed);
Expand Down Expand Up @@ -2190,7 +2255,9 @@ class _SendViewState extends ConsumerState<SendView> {
style: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
if (coin is! Ethereum && coin is! Tezos)
if (coin is! Ethereum &&
coin is! Tezos &&
_multiRecipients == null)
CustomTextButton(
text: _getSendAllTitle(
showCoinControl,
Expand All @@ -2203,6 +2270,7 @@ class _SendViewState extends ConsumerState<SendView> {
),
const SizedBox(height: 8),
TextField(
readOnly: _multiRecipients != null,
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
style: STextStyles.smallMed14(context).copyWith(
Expand Down Expand Up @@ -2271,6 +2339,7 @@ class _SendViewState extends ConsumerState<SendView> {
const SizedBox(height: 8),
if (Prefs.instance.externalCalls)
TextField(
readOnly: _multiRecipients != null,
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop
? false
Expand Down
126 changes: 126 additions & 0 deletions lib/pages/send_view/sub_widgets/multi_recipient_summary.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../../../themes/stack_colors.dart';
import '../../../utilities/address_utils.dart';
import '../../../utilities/amount/amount.dart';
import '../../../utilities/amount/amount_formatter.dart';
import '../../../utilities/text_styles.dart';
import '../../../utilities/util.dart';
import '../../../wallets/crypto_currency/crypto_currency.dart';
import '../../../wallets/models/tx_data.dart';
import '../../../widgets/custom_buttons/blue_text_button.dart';
import '../../../widgets/rounded_container.dart';
import '../../../widgets/rounded_white_container.dart';

/// A recipient of a multi-recipient payment URI, with its amount parsed for
/// the sending coin.
typedef SendRecipient = ({String address, Amount amount, String? label});

/// Converts the recipients of [paymentData] to amounts of [coin].
///
/// Returns null if any recipient has a missing, invalid, or zero amount.
List<SendRecipient>? parseUriRecipients(
PaymentUriData paymentData,
CryptoCurrency coin,
) {
final List<SendRecipient> recipients = [];
for (final recipient in paymentData.recipients) {
final amount = recipient.amount == null
? null
: Amount.tryParseCanonicalAmount(
recipient.amount!,
fractionDigits: coin.fractionDigits,
truncateOverprecision: true,
);
if (amount == null || amount <= Amount.zero) {
return null;
}
recipients.add((
address: recipient.address,
amount: amount,
label: recipient.label,
));
}
return recipients;
}

extension SendRecipientsExt on List<SendRecipient> {
Amount get total => map((e) => e.amount).reduce((a, b) => a + b);

bool allValidFor(CryptoCurrency coin) =>
every((e) => coin.validateAddress(e.address));

List<TxRecipient> toTxRecipients(CryptoCurrency coin) => [
for (final e in this)
TxRecipient(
address: e.address,
amount: e.amount,
isChange: false,
addressType: coin.getAddressType(e.address)!,
),
];
}

/// Read-only list of the recipients of a multi-recipient payment URI.
class MultiRecipientSummary extends ConsumerWidget {
const MultiRecipientSummary({
super.key,
required this.coin,
required this.recipients,
required this.onClear,
});

final CryptoCurrency coin;
final List<SendRecipient> recipients;
final VoidCallback onClear;

@override
Widget build(BuildContext context, WidgetRef ref) {
final colors = Theme.of(context).extension<StackColors>()!;
final labelStyle = Util.isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
: STextStyles.smallMed12(context);
final valueStyle = Util.isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(color: colors.textDark)
: STextStyles.itemSubtitle12(context);
final formatter = ref.watch(pAmountFormatter(coin));

final content = Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("${recipients.length} recipients", style: labelStyle),
CustomTextButton(text: "Clear", onTap: onClear),
],
),
for (final recipient in recipients) ...[
const SizedBox(height: 12),
if (recipient.label != null)
Text(recipient.label!, style: labelStyle),
SelectableText(recipient.address, style: valueStyle),
if (!coin.validateAddress(recipient.address))
Text(
"Invalid address",
style: STextStyles.label(context)
.copyWith(color: colors.textError),
),
const SizedBox(height: 4),
Text(formatter.format(recipient.amount), style: valueStyle),
],
],
);

if (Util.isDesktop) {
return RoundedContainer(
color: Colors.transparent,
borderColor: colors.textFieldDefaultBG,
child: content,
);
}
return RoundedWhiteContainer(child: content);
}
}
Loading