diff --git a/frontend/pshared/lib/data/dto/payment/payment.dart b/frontend/pshared/lib/data/dto/payment/payment.dart index 23d7d449..f0d2d9b4 100644 --- a/frontend/pshared/lib/data/dto/payment/payment.dart +++ b/frontend/pshared/lib/data/dto/payment/payment.dart @@ -1,12 +1,12 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:pshared/data/dto/payment/operation.dart'; -import 'package:pshared/data/dto/payment/intent/payment.dart'; import 'package:pshared/data/dto/payment/payment_quote.dart'; import 'package:pshared/data/dto/payment/response_endpoint.dart'; part 'payment.g.dart'; + @JsonSerializable() class PaymentDTO { final String? paymentRef; @@ -15,7 +15,6 @@ class PaymentDTO { final PaymentResponseEndpointDTO? destination; final String? failureCode; final String? failureReason; - final PaymentIntentDTO? intent; final List operations; final PaymentQuoteDTO? lastQuote; final Map? metadata; @@ -28,7 +27,6 @@ class PaymentDTO { this.destination, this.failureCode, this.failureReason, - this.intent, this.operations = const [], this.lastQuote, this.metadata, diff --git a/frontend/pshared/lib/data/mapper/payment/payment_response.dart b/frontend/pshared/lib/data/mapper/payment/payment_response.dart index 242db1ff..7043dbdb 100644 --- a/frontend/pshared/lib/data/mapper/payment/payment_response.dart +++ b/frontend/pshared/lib/data/mapper/payment/payment_response.dart @@ -1,11 +1,11 @@ import 'package:pshared/data/dto/payment/payment.dart'; -import 'package:pshared/data/mapper/payment/intent/payment.dart'; import 'package:pshared/data/mapper/payment/operation.dart'; import 'package:pshared/data/mapper/payment/quote.dart'; import 'package:pshared/data/mapper/payment/response_endpoint.dart'; import 'package:pshared/models/payment/payment.dart'; import 'package:pshared/models/payment/state.dart'; + extension PaymentDTOMapper on PaymentDTO { Payment toDomain() => Payment( paymentRef: paymentRef, @@ -15,7 +15,6 @@ extension PaymentDTOMapper on PaymentDTO { orchestrationState: paymentOrchestrationStateFromValue(state), failureCode: failureCode, failureReason: failureReason, - intent: intent?.toDomain(), operations: operations.map((item) => item.toDomain()).toList(), lastQuote: lastQuote?.toDomain(), metadata: metadata, @@ -31,7 +30,6 @@ extension PaymentMapper on Payment { destination: destination?.toDTO(), failureCode: failureCode, failureReason: failureReason, - intent: intent?.toDTO(), operations: operations.map((item) => item.toDTO()).toList(), lastQuote: lastQuote?.toDTO(), metadata: metadata, diff --git a/frontend/pshared/lib/models/payment/payment.dart b/frontend/pshared/lib/models/payment/payment.dart index dad41559..3d89ca71 100644 --- a/frontend/pshared/lib/models/payment/payment.dart +++ b/frontend/pshared/lib/models/payment/payment.dart @@ -1,9 +1,9 @@ import 'package:pshared/models/payment/endpoint.dart'; import 'package:pshared/models/payment/execution_operation.dart'; -import 'package:pshared/models/payment/intent.dart'; import 'package:pshared/models/payment/quote/quote.dart'; import 'package:pshared/models/payment/state.dart'; + class Payment { final String? paymentRef; final String? state; @@ -12,7 +12,6 @@ class Payment { final PaymentOrchestrationState orchestrationState; final String? failureCode; final String? failureReason; - final PaymentIntent? intent; final List operations; final PaymentQuote? lastQuote; final Map? metadata; @@ -26,7 +25,6 @@ class Payment { required this.orchestrationState, required this.failureCode, required this.failureReason, - this.intent, required this.operations, required this.lastQuote, required this.metadata, diff --git a/frontend/pshared/lib/utils/currency.dart b/frontend/pshared/lib/utils/currency.dart index 26f5c722..f44580e0 100644 --- a/frontend/pshared/lib/utils/currency.dart +++ b/frontend/pshared/lib/utils/currency.dart @@ -4,6 +4,16 @@ import 'package:pshared/models/asset.dart'; import 'package:pshared/models/currency.dart'; +const nonBreakingSpace = '\u00A0'; + +String withTrailingNonBreakingSpace(String value) { + return '$value$nonBreakingSpace'; +} + +String joinWithNonBreakingSpace(String left, String right) { + return '$left$nonBreakingSpace$right'; +} + String currencyCodeToSymbol(Currency currencyCode) { switch (currencyCode) { case Currency.usd: @@ -24,7 +34,10 @@ String amountToString(double amount) { } String currencyToString(Currency currencyCode, double amount) { - return '${currencyCodeToSymbol(currencyCode)}\u00A0${amountToString(amount)}'; + return joinWithNonBreakingSpace( + currencyCodeToSymbol(currencyCode), + amountToString(amount), + ); } String assetToString(Asset asset) { diff --git a/frontend/pshared/lib/utils/money.dart b/frontend/pshared/lib/utils/money.dart index 925f5eb5..328d187f 100644 --- a/frontend/pshared/lib/utils/money.dart +++ b/frontend/pshared/lib/utils/money.dart @@ -1,4 +1,5 @@ import 'package:pshared/models/money.dart'; +import 'package:pshared/utils/currency.dart'; double parseMoneyAmount(String? raw, {double fallback = 0}) { @@ -7,6 +8,34 @@ double parseMoneyAmount(String? raw, {double fallback = 0}) { return double.tryParse(trimmed) ?? fallback; } +String formatMoneyDisplay( + Money? money, { + String fallback = '--', + String separator = ' ', + String invalidAmountFallback = '', +}) { + if (money == null) return fallback; + + final rawAmount = money.amount.trim(); + final rawCurrency = money.currency.trim(); + final parsedAmount = parseMoneyAmount(rawAmount, fallback: double.nan); + final amountToken = parsedAmount.isNaN + ? (rawAmount.isEmpty ? invalidAmountFallback : rawAmount) + : amountToString(parsedAmount); + + final symbol = currencySymbolFromCode(rawCurrency); + final normalizedSymbol = symbol?.trim() ?? ''; + final hasSymbol = normalizedSymbol.isNotEmpty; + final currencyToken = hasSymbol ? normalizedSymbol : rawCurrency; + final first = amountToken; + final second = currencyToken; + + if (first.isEmpty && second.isEmpty) return fallback; + if (first.isEmpty) return second; + if (second.isEmpty) return first; + return '$first$separator$second'; +} + extension MoneyAmountX on Money { double get amountValue => parseMoneyAmount(amount); } diff --git a/frontend/pweb/lib/controllers/dashboard/balance/source_actions.dart b/frontend/pweb/lib/controllers/dashboard/balance/source_actions.dart index a2df9f83..fdf7deb5 100644 --- a/frontend/pweb/lib/controllers/dashboard/balance/source_actions.dart +++ b/frontend/pweb/lib/controllers/dashboard/balance/source_actions.dart @@ -91,16 +91,22 @@ class BalanceSourceActionsController { } void _openWalletOperationHistory(BuildContext context, String walletRef) { - context.read().selectWalletByRef(walletRef); - context.pushNamed(PayoutRoutes.editWallet); + _withSelectedWallet( + context, + walletRef, + () => context.pushNamed(PayoutRoutes.editWallet), + ); } void _sendWalletPayout(BuildContext context, String walletRef) { - context.read().selectWalletByRef(walletRef); - context.pushNamed( - PayoutRoutes.payment, - queryParameters: PayoutRoutes.buildQueryParameters( - paymentType: PaymentType.wallet, + _withSelectedWallet( + context, + walletRef, + () => context.pushNamed( + PayoutRoutes.payment, + queryParameters: PayoutRoutes.buildQueryParameters( + paymentType: PaymentType.wallet, + ), ), ); } @@ -114,4 +120,13 @@ class BalanceSourceActionsController { ), ); } + + void _withSelectedWallet( + BuildContext context, + String walletRef, + VoidCallback action, + ) { + context.read().selectWalletByRef(walletRef); + action(); + } } diff --git a/frontend/pweb/lib/controllers/dashboard/balance/source_copy.dart b/frontend/pweb/lib/controllers/dashboard/balance/source_copy.dart index 787a54c7..9af9e4f5 100644 --- a/frontend/pweb/lib/controllers/dashboard/balance/source_copy.dart +++ b/frontend/pweb/lib/controllers/dashboard/balance/source_copy.dart @@ -1,5 +1,8 @@ +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class BalanceCopyState { final String label; @@ -13,23 +16,26 @@ class BalanceCopyState { class BalanceSourceCopyController { const BalanceSourceCopyController(); - BalanceCopyState wallet(String? depositAddress) { - return BalanceCopyState( - label: 'Copy Deposit Address', - payload: depositAddress?.trim() ?? '', - ); - } + BalanceCopyState wallet(BuildContext context, String? depositAddress) => + _buildCopyAddressState(context, depositAddress); - BalanceCopyState ledger(String? accountCode) { - return BalanceCopyState( - label: 'Copy Deposit Address', - payload: accountCode?.trim() ?? '', - ); - } + BalanceCopyState ledger(BuildContext context, String? accountCode) => + _buildCopyAddressState(context, accountCode); Future copy(BalanceCopyState state) async { if (!state.canCopy) return false; await Clipboard.setData(ClipboardData(text: state.payload)); return true; } + + BalanceCopyState _buildCopyAddressState( + BuildContext context, + String? payload, + ) { + final l10n = AppLocalizations.of(context)!; + return BalanceCopyState( + label: l10n.copyAddress, + payload: payload?.trim() ?? '', + ); + } } diff --git a/frontend/pweb/lib/controllers/operations/report_operations.dart b/frontend/pweb/lib/controllers/operations/report_operations.dart index e844b418..51ded122 100644 --- a/frontend/pweb/lib/controllers/operations/report_operations.dart +++ b/frontend/pweb/lib/controllers/operations/report_operations.dart @@ -1,7 +1,7 @@ -import 'dart:collection'; - import 'package:flutter/material.dart'; +import 'package:collection/collection.dart'; + import 'package:pshared/models/payment/operation.dart'; import 'package:pshared/models/payment/payment.dart'; import 'package:pshared/models/payment/source_type.dart'; @@ -131,16 +131,14 @@ class ReportOperationsController extends ChangeNotifier { bool _matchesCurrentSource(Payment payment) { final sourceType = _sourceType; if (sourceType == null || _sourceRefs.isEmpty) return true; - for (final sourceRef in _sourceRefs) { - if (paymentMatchesSource( - payment, - sourceType: sourceType, - sourceRef: sourceRef, - )) { - return true; - } - } - return false; + return _sourceRefs.firstWhereOrNull( + (sourceRef) => paymentMatchesSource( + payment, + sourceType: sourceType, + sourceRef: sourceRef, + ), + ) != + null; } Set _normalizeRefs(List refs) { diff --git a/frontend/pweb/lib/l10n/en.arb b/frontend/pweb/lib/l10n/en.arb index dbc8fee2..1537022d 100644 --- a/frontend/pweb/lib/l10n/en.arb +++ b/frontend/pweb/lib/l10n/en.arb @@ -235,6 +235,7 @@ "avatarUpdateError": "Failed to update profile photo", "settings": "Settings", "notSet": "not set", + "valueUnavailable": "Not available", "search": "Search...", "ok": "Ok", "cancel": "Cancel", @@ -393,7 +394,7 @@ "paymentDetailsNotFound": "Payment not found", "paymentDetailsIdentifiers": "Identifiers", "paymentDetailsAmounts": "Amounts", - "paymentDetailsFx": "FX quote", + "paymentDetailsFx": "Conversion rate", "paymentDetailsFailure": "Failure", "paymentDetailsMetadata": "Metadata", "metadataUploadFileName": "Upload file name", @@ -424,7 +425,7 @@ "paymentOperationSettlement": "Settlement", "paymentOperationLedger": "Ledger", "paymentOperationActionSend": "Send", - "paymentOperationActionObserve": "Observe", + "paymentOperationActionObserve": "Confirmation", "paymentOperationActionFxConvert": "FX convert", "paymentOperationActionCredit": "Credit", "paymentOperationActionBlock": "Block", diff --git a/frontend/pweb/lib/l10n/ru.arb b/frontend/pweb/lib/l10n/ru.arb index ef8b6f04..e7a98a52 100644 --- a/frontend/pweb/lib/l10n/ru.arb +++ b/frontend/pweb/lib/l10n/ru.arb @@ -235,6 +235,7 @@ "avatarUpdateError": "Не удалось обновить фото профиля", "settings": "Настройки", "notSet": "не задано", + "valueUnavailable": "Недоступно", "search": "Поиск...", "ok": "Ок", "cancel": "Отмена", @@ -393,7 +394,7 @@ "paymentDetailsNotFound": "Платеж не найден", "paymentDetailsIdentifiers": "Идентификаторы", "paymentDetailsAmounts": "Суммы", - "paymentDetailsFx": "Курс", + "paymentDetailsFx": "Курс обмена", "paymentDetailsFailure": "Ошибка", "paymentDetailsMetadata": "Метаданные", "metadataUploadFileName": "Имя файла загрузки", @@ -424,7 +425,7 @@ "paymentOperationSettlement": "Расчётный контур", "paymentOperationLedger": "Леджер", "paymentOperationActionSend": "Отправка", - "paymentOperationActionObserve": "Проверка", + "paymentOperationActionObserve": "Подтверждение", "paymentOperationActionFxConvert": "FX-конверсия", "paymentOperationActionCredit": "Зачисление", "paymentOperationActionBlock": "Блокировка", diff --git a/frontend/pweb/lib/pages/address_book/form/view.dart b/frontend/pweb/lib/pages/address_book/form/view.dart index 27cbf49b..cac0f727 100644 --- a/frontend/pweb/lib/pages/address_book/form/view.dart +++ b/frontend/pweb/lib/pages/address_book/form/view.dart @@ -7,7 +7,7 @@ import 'package:pshared/models/recipient/payment_method_draft.dart'; import 'package:pweb/pages/address_book/form/widgets/feilds/email.dart'; import 'package:pweb/pages/address_book/form/widgets/header.dart'; import 'package:pweb/pages/address_book/form/widgets/feilds/name.dart'; -import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel.dart'; +import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel/panel.dart'; import 'package:pweb/pages/address_book/form/widgets/payment_methods/selector_row.dart'; import 'package:pweb/pages/address_book/form/widgets/save_button.dart'; diff --git a/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel.dart b/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel.dart deleted file mode 100644 index e0fe261b..00000000 --- a/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:pshared/models/payment/methods/data.dart'; -import 'package:pshared/models/payment/type.dart'; -import 'package:pshared/models/recipient/payment_method_draft.dart'; - -import 'package:pweb/pages/payment_methods/form.dart'; -import 'package:pweb/pages/payment_methods/icon.dart'; -import 'package:pweb/widgets/dialogs/confirmation_dialog.dart'; -import 'package:pweb/models/state/control_state.dart'; -import 'package:pweb/models/state/visibility.dart'; - -import 'package:pweb/generated/i18n/app_localizations.dart'; - - -class PaymentMethodPanel extends StatelessWidget { - final PaymentType selectedType; - final int selectedIndex; - final List entries; - final ValueChanged onRemove; - final void Function(int, PaymentMethodData) onChanged; - final ControlState editState; - final VisibilityState deleteVisibility; - - final double padding; - - const PaymentMethodPanel({ - super.key, - required this.selectedType, - required this.selectedIndex, - required this.entries, - required this.onRemove, - required this.onChanged, - this.editState = ControlState.enabled, - this.deleteVisibility = VisibilityState.visible, - this.padding = 16, - }); - - Future _confirmDelete(BuildContext context, VoidCallback onConfirmed) async { - final l10n = AppLocalizations.of(context)!; - final confirmed = await showConfirmationDialog( - context: context, - title: l10n.delete, - message: l10n.deletePaymentConfirmation, - confirmLabel: l10n.delete, - ); - if (confirmed) { - onConfirmed(); - } - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final l10n = AppLocalizations.of(context)!; - final label = l10n.paymentMethodDetails; - final entry = selectedIndex >= 0 && selectedIndex < entries.length - ? entries[selectedIndex] - : null; - - return AnimatedContainer( - duration: const Duration(milliseconds: 3000), - padding: EdgeInsets.all(padding), - decoration: BoxDecoration( - color: theme.colorScheme.onSecondary, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: theme.colorScheme.onSurface.withAlpha(30)), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - iconForPaymentType(selectedType), - size: 18, - color: theme.colorScheme.primary, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - label, - style: theme.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, - ), - ), - ), - if (entry != null && deleteVisibility == VisibilityState.visible) - TextButton.icon( - onPressed: () => _confirmDelete(context, () => onRemove(selectedIndex)), - icon: Icon(Icons.delete, color: theme.colorScheme.error), - label: Text( - l10n.delete, - style: TextStyle(color: theme.colorScheme.error), - ), - ), - ], - ), - const SizedBox(height: 12), - if (entry != null) - PaymentMethodForm( - key: ValueKey('${selectedType.name}-${entry.existing?.id ?? selectedIndex}-form'), - selectedType: selectedType, - initialData: entry.data, - isEditable: editState == ControlState.enabled, - onChanged: (data) { - if (data == null) return; - onChanged(selectedIndex, data); - }, - ), - ], - ), - ); - } -} diff --git a/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel.dart b/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel.dart new file mode 100644 index 00000000..4ba9aae4 --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/methods/data.dart'; +import 'package:pshared/models/payment/type.dart'; +import 'package:pshared/models/recipient/payment_method_draft.dart'; + +import 'package:pweb/models/state/control_state.dart'; +import 'package:pweb/models/state/visibility.dart'; +import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel/panel_container.dart'; +import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel/panel_entry_form.dart'; +import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel/panel_header.dart'; +import 'package:pweb/utils/payment/method_delete_confirmation.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class PaymentMethodPanel extends StatelessWidget { + final PaymentType selectedType; + final int selectedIndex; + final List entries; + final ValueChanged? onRemove; + final void Function(int, PaymentMethodData)? onChanged; + final ControlState editState; + final VisibilityState deleteVisibility; + final double padding; + + const PaymentMethodPanel({ + super.key, + required this.selectedType, + required this.selectedIndex, + required this.entries, + this.onRemove, + this.onChanged, + this.editState = ControlState.enabled, + this.deleteVisibility = VisibilityState.visible, + this.padding = 16, + }) : assert(editState == ControlState.disabled || onChanged != null), + assert(deleteVisibility == VisibilityState.hidden || onRemove != null); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final entry = selectedIndex >= 0 && selectedIndex < entries.length + ? entries[selectedIndex] + : null; + final showDelete = + entry != null && + deleteVisibility == VisibilityState.visible && + onRemove != null; + + return PaymentMethodPanelContainer( + padding: padding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PaymentMethodPanelHeader( + selectedType: selectedType, + title: l10n.paymentMethodDetails, + deleteLabel: l10n.delete, + showDelete: showDelete, + onDelete: showDelete + ? () => confirmPaymentMethodDelete( + context, + () => onRemove!(selectedIndex), + ) + : null, + ), + const SizedBox(height: 12), + if (entry != null) + PaymentMethodPanelEntryForm( + selectedType: selectedType, + selectedIndex: selectedIndex, + entry: entry, + isEditable: editState == ControlState.enabled, + onChanged: onChanged == null + ? null + : (data) => onChanged!(selectedIndex, data), + ), + ], + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel_container.dart b/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel_container.dart new file mode 100644 index 00000000..20301e76 --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel_container.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + + +class PaymentMethodPanelContainer extends StatelessWidget { + final double padding; + final Widget child; + + const PaymentMethodPanelContainer({ + super.key, + required this.padding, + required this.child, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return AnimatedContainer( + duration: const Duration(milliseconds: 3000), + padding: EdgeInsets.all(padding), + decoration: BoxDecoration( + color: theme.colorScheme.onSecondary, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: theme.colorScheme.onSurface.withAlpha(30)), + ), + child: child, + ); + } +} diff --git a/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel_entry_form.dart b/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel_entry_form.dart new file mode 100644 index 00000000..1a29ef2f --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel_entry_form.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/methods/data.dart'; +import 'package:pshared/models/payment/type.dart'; +import 'package:pshared/models/recipient/payment_method_draft.dart'; + +import 'package:pweb/pages/payment_methods/form.dart'; + + +class PaymentMethodPanelEntryForm extends StatelessWidget { + final PaymentType selectedType; + final int selectedIndex; + final RecipientMethodDraft entry; + final bool isEditable; + final ValueChanged? onChanged; + + const PaymentMethodPanelEntryForm({ + super.key, + required this.selectedType, + required this.selectedIndex, + required this.entry, + required this.isEditable, + this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return PaymentMethodForm( + key: ValueKey( + '${selectedType.name}-${entry.existing?.id ?? selectedIndex}-form', + ), + selectedType: selectedType, + initialData: entry.data, + isEditable: isEditable, + onChanged: (data) { + if (data == null) return; + onChanged?.call(data); + }, + ); + } +} diff --git a/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel_header.dart b/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel_header.dart new file mode 100644 index 00000000..46b488d3 --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel_header.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/type.dart'; + +import 'package:pweb/pages/payment_methods/icon.dart'; + + +class PaymentMethodPanelHeader extends StatelessWidget { + final PaymentType selectedType; + final String title; + final String deleteLabel; + final bool showDelete; + final VoidCallback? onDelete; + + const PaymentMethodPanelHeader({ + super.key, + required this.selectedType, + required this.title, + required this.deleteLabel, + required this.showDelete, + this.onDelete, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Row( + children: [ + Icon( + iconForPaymentType(selectedType), + size: 18, + color: theme.colorScheme.primary, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + title, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ), + if (showDelete && onDelete != null) + TextButton.icon( + onPressed: onDelete, + icon: Icon(Icons.delete, color: theme.colorScheme.error), + label: Text( + deleteLabel, + style: TextStyle(color: theme.colorScheme.error), + ), + ), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/amount.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/amount.dart index 2eb8c096..0f32412c 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/amount.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/amount.dart @@ -3,9 +3,12 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart'; +import 'package:pshared/models/money.dart'; import 'package:pshared/models/payment/wallet.dart'; import 'package:pshared/utils/currency.dart'; +import 'package:pweb/utils/money_display.dart'; + class BalanceAmount extends StatelessWidget { final Wallet wallet; @@ -25,6 +28,13 @@ class BalanceAmount extends StatelessWidget { final textTheme = Theme.of(context).textTheme; final colorScheme = Theme.of(context).colorScheme; final currencyBalance = currencyCodeToSymbol(wallet.currency); + final formattedBalance = formatMoneyUi( + context, + Money( + amount: amountToString(wallet.balance), + currency: currencyCodeToString(wallet.currency), + ), + ); final wallets = context.watch(); final isMasked = wallets.isBalanceMasked(wallet.id); @@ -32,9 +42,7 @@ class BalanceAmount extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Text( - isMasked - ? '•••• $currencyBalance' - : '${amountToString(wallet.balance)} $currencyBalance', + isMasked ? '•••• $currencyBalance' : formattedBalance, maxLines: 1, overflow: TextOverflow.ellipsis, style: textTheme.headlineMedium?.copyWith( diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/card_item.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/card_item.dart index d672321e..576e935b 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/card_item.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/card_item.dart @@ -6,8 +6,8 @@ import 'package:pshared/models/payment/wallet.dart'; import 'package:pweb/models/dashboard/balance_item.dart'; import 'package:pweb/pages/dashboard/buttons/balance/add/card.dart'; import 'package:pweb/pages/dashboard/buttons/balance/config.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/source/card.dart'; import 'package:pweb/pages/dashboard/buttons/balance/source/cards/ledger.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/source/cards/wallet.dart'; class BalanceCarouselCardItem extends StatelessWidget { @@ -29,9 +29,9 @@ class BalanceCarouselCardItem extends StatelessWidget { @override Widget build(BuildContext context) { final card = switch (item) { - WalletBalanceItem(:final wallet) => WalletCard( + WalletBalanceItem(:final wallet) => BalanceSourceCard.wallet( wallet: wallet, - onTopUp: () => onTopUp(wallet), + onAddFunds: () => onTopUp(wallet), onTap: () => onWalletTap(wallet), ), LedgerBalanceItem(:final account) => LedgerAccountCard( diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger_amount.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger_amount.dart index 8922b636..f6fc8ffa 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger_amount.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger_amount.dart @@ -23,7 +23,7 @@ class LedgerBalanceAmount extends StatelessWidget { final isMasked = controller.isBalanceMasked(account.ledgerAccountRef); final balance = isMasked ? LedgerBalanceFormatter.formatMasked(account) - : LedgerBalanceFormatter.format(account); + : LedgerBalanceFormatter.format(context, account); return Row( mainAxisSize: MainAxisSize.min, diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card.dart index 5e511236..f71cb67c 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card.dart @@ -61,7 +61,7 @@ class BalanceSourceCard extends StatelessWidget { ? null : wallet.network!.localizedName(context); final symbol = wallet.tokenSymbol?.trim(); - final copyState = _copyController.wallet(wallet.depositAddress); + final copyState = _copyController.wallet(context, wallet.depositAddress); return BalanceSourceCardLayout( title: wallet.name, @@ -105,7 +105,7 @@ class BalanceSourceCard extends StatelessWidget { final badge = account.currency.trim().isEmpty ? null : account.currency.toUpperCase(); - final copyState = _copyController.ledger(accountCode); + final copyState = _copyController.ledger(context, accountCode); return BalanceSourceCardLayout( title: title, diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/cards/wallet.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/cards/wallet.dart deleted file mode 100644 index a23e98df..00000000 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/cards/wallet.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:pshared/models/payment/wallet.dart'; - -import 'package:pweb/pages/dashboard/buttons/balance/source/card.dart'; - - -class WalletCard extends StatelessWidget { - final Wallet wallet; - final VoidCallback onTopUp; - final VoidCallback onTap; - - const WalletCard({ - super.key, - required this.wallet, - required this.onTopUp, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return BalanceSourceCard.wallet( - wallet: wallet, - onTap: onTap, - onAddFunds: onTopUp, - ); - } -} diff --git a/frontend/pweb/lib/pages/dashboard/payouts/amount/field.dart b/frontend/pweb/lib/pages/dashboard/payouts/amount/field.dart index 6f293385..aeb78d7c 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/amount/field.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/amount/field.dart @@ -36,7 +36,9 @@ class PaymentAmountField extends StatelessWidget { decoration: InputDecoration( labelText: loc.amount, border: const OutlineInputBorder(), - prefixText: symbol == null ? null : '$symbol\u00A0', + prefixText: symbol == null + ? null + : withTrailingNonBreakingSpace(symbol), ), onChanged: ui.handleChanged, ), diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/helpers.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/helpers.dart deleted file mode 100644 index 47b3388c..00000000 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/helpers.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:pshared/models/asset.dart'; -import 'package:pshared/models/money.dart'; -import 'package:pshared/utils/currency.dart'; -import 'package:pshared/utils/money.dart'; - -import 'package:pweb/controllers/payouts/multiple_payouts.dart'; - -import 'package:pweb/generated/i18n/app_localizations.dart'; - - -String moneyLabel(Money? money) { - if (money == null) return 'N/A'; - final amount = parseMoneyAmount(money.amount, fallback: double.nan); - if (amount.isNaN) return '${money.amount} ${money.currency}'; - try { - return assetToString( - Asset(currency: currencyStringToCode(money.currency), amount: amount), - ); - } catch (_) { - return '${money.amount} ${money.currency}'; - } -} - -String sentAmountLabel(MultiplePayoutsController controller) { - final requested = controller.requestedSentAmount; - final sourceDebit = controller.aggregateDebitAmount; - - if (requested == null && sourceDebit == null) return 'N/A'; - if (sourceDebit != null) return moneyLabel(sourceDebit); - return moneyLabel(requested); -} - -String feeLabel(MultiplePayoutsController controller, AppLocalizations l10n) { - final fee = controller.aggregateFeeAmount; - if (fee == null) return l10n.noFee; - return moneyLabel(fee); -} diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/summary.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/summary.dart index af9bdc6b..5ea64671 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/summary.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/summary.dart @@ -1,11 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:pshared/utils/currency.dart'; + import 'package:pweb/controllers/payouts/multiple_payouts.dart'; import 'package:pweb/models/dashboard/summary_values.dart'; -import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/helpers.dart'; import 'package:pweb/pages/dashboard/payouts/summary/widget.dart'; +import 'package:pweb/utils/money_display.dart'; + import 'package:pweb/generated/i18n/app_localizations.dart'; + class SourceQuoteSummary extends StatelessWidget { const SourceQuoteSummary({ super.key, @@ -18,12 +22,27 @@ class SourceQuoteSummary extends StatelessWidget { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; return PaymentSummary( spacing: spacing, values: PaymentSummaryValues( - fee: feeLabel(controller, AppLocalizations.of(context)!), - recipientReceives: moneyLabel(controller.aggregateSettlementAmount), - total: moneyLabel(controller.aggregateDebitAmount), + fee: controller.aggregateFeeAmount == null + ? l10n.noFee + : formatMoneyUiWithL10n( + l10n, + controller.aggregateFeeAmount, + separator: nonBreakingSpace, + ), + recipientReceives: formatMoneyUiWithL10n( + l10n, + controller.aggregateSettlementAmount, + separator: nonBreakingSpace, + ), + total: formatMoneyUiWithL10n( + l10n, + controller.aggregateDebitAmount, + separator: nonBreakingSpace, + ), ), ); } diff --git a/frontend/pweb/lib/pages/dashboard/payouts/summary/row.dart b/frontend/pweb/lib/pages/dashboard/payouts/summary/row.dart index f459261d..7086b1a6 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/summary/row.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/summary/row.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:pshared/models/asset.dart'; -import 'package:pshared/utils/currency.dart'; + +import 'package:pweb/utils/money_display.dart'; class PaymentSummaryRow extends StatelessWidget { @@ -11,8 +12,8 @@ class PaymentSummaryRow extends StatelessWidget { final TextStyle? style; const PaymentSummaryRow({ - super.key, - required this.labelFactory, + super.key, + required this.labelFactory, required this.asset, this.value, this.style, @@ -20,8 +21,7 @@ class PaymentSummaryRow extends StatelessWidget { @override Widget build(BuildContext context) { - final formatted = value ?? - (asset == null ? 'N/A' : assetToString(asset!)); + final formatted = value ?? formatAssetUi(context, asset); return Text(labelFactory(formatted), style: style); } } diff --git a/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/manual_details.dart b/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/manual_details.dart index e9dfc9d7..ae741a7e 100644 --- a/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/manual_details.dart +++ b/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/manual_details.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:pshared/models/payment/methods/data.dart'; import 'package:pshared/models/recipient/payment_method_draft.dart'; +import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel/panel.dart'; -import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel.dart'; import 'package:pweb/pages/payout_page/send/widgets/payment_info/header.dart'; import 'package:pweb/models/state/control_state.dart'; import 'package:pweb/models/state/visibility.dart'; @@ -40,8 +40,6 @@ class PaymentInfoManualDetailsSection extends StatelessWidget { selectedType: data.type, selectedIndex: 0, entries: [entry], - onRemove: (_) {}, - onChanged: (_, ignored) {}, editState: ControlState.disabled, deleteVisibility: VisibilityState.hidden, ), diff --git a/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/methods_section.dart b/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/methods_section.dart index 266afcf0..3afaa1d0 100644 --- a/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/methods_section.dart +++ b/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/methods_section.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:pshared/models/payment/type.dart'; import 'package:pshared/models/recipient/payment_method_draft.dart'; +import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel/panel.dart'; -import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel.dart'; import 'package:pweb/pages/address_book/form/widgets/payment_methods/selector_row.dart'; import 'package:pweb/pages/payout_page/send/widgets/payment_info/details_builder.dart'; import 'package:pweb/pages/payout_page/send/widgets/payment_info/header.dart'; @@ -78,8 +78,6 @@ class PaymentInfoMethodsSection extends StatelessWidget { selectedType: state.selectedType, selectedIndex: state.selectedIndex!, entries: state.selectedEntries, - onRemove: (_) {}, - onChanged: (_, _) {}, editState: ControlState.disabled, deleteVisibility: VisibilityState.hidden, ), diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/balance_formatter.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/balance_formatter.dart index 85513ab6..a6639e1c 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/balance_formatter.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/balance_formatter.dart @@ -1,30 +1,16 @@ +import 'package:flutter/widgets.dart'; + import 'package:pshared/models/ledger/account.dart'; import 'package:pshared/utils/currency.dart'; -import 'package:pshared/utils/money.dart'; + +import 'package:pweb/utils/money_display.dart'; class LedgerBalanceFormatter { const LedgerBalanceFormatter._(); - static String format(LedgerAccount account) { - final money = account.balance?.balance; - if (money == null) return '--'; - - final amount = parseMoneyAmount(money.amount, fallback: double.nan); - if (amount.isNaN) { - return '${money.amount} ${money.currency}'; - } - - try { - final currency = currencyStringToCode(money.currency); - final symbol = currencyCodeToSymbol(currency); - if (symbol.trim().isEmpty) { - return '${amountToString(amount)} ${money.currency}'; - } - return '${amountToString(amount)} $symbol'; - } catch (_) { - return '${amountToString(amount)} ${money.currency}'; - } + static String format(BuildContext context, LedgerAccount account) { + return formatMoneyUi(context, account.balance?.balance); } static String formatMasked(LedgerAccount account) { diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/section.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/section.dart index 3458dd43..d53bdbb0 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/section.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/section.dart @@ -26,7 +26,7 @@ class LedgerSection extends StatelessWidget { final hasAccountCode = accountCode.isNotEmpty; final balance = isMasked ? LedgerBalanceFormatter.formatMasked(ledger) - : LedgerBalanceFormatter.format(ledger); + : LedgerBalanceFormatter.format(context, ledger); return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/frontend/pweb/lib/pages/payout_page/wallet/history/table.dart b/frontend/pweb/lib/pages/payout_page/wallet/history/table.dart deleted file mode 100644 index bd5a6a3d..00000000 --- a/frontend/pweb/lib/pages/payout_page/wallet/history/table.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:pshared/utils/currency.dart'; - -import 'package:pweb/models/wallet/wallet_transaction.dart'; -import 'package:pweb/pages/payout_page/wallet/history/chip.dart'; -import 'package:pweb/pages/report/table/badge.dart'; - -import 'package:pweb/generated/i18n/app_localizations.dart'; - - -class WalletTransactionsTable extends StatelessWidget { - final List transactions; - - const WalletTransactionsTable({super.key, required this.transactions}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final loc = AppLocalizations.of(context)!; - - if (transactions.isEmpty) { - return Card( - color: theme.colorScheme.onSecondary, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Padding( - padding: const EdgeInsets.all(16), - child: Text(loc.walletHistoryEmpty), - ), - ); - } - - return Card( - color: theme.colorScheme.onSecondary, - elevation: 2, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: DataTable( - columnSpacing: 18, - headingTextStyle: const TextStyle(fontWeight: FontWeight.w600), - columns: [ - DataColumn(label: Text(loc.colStatus)), - DataColumn(label: Text(loc.colType)), - DataColumn(label: Text(loc.colAmount)), - DataColumn(label: Text(loc.colBalance)), - DataColumn(label: Text(loc.colCounterparty)), - DataColumn(label: Text(loc.colDate)), - DataColumn(label: Text(loc.colComment)), - ], - rows: List.generate( - transactions.length, - (index) { - final tx = transactions[index]; - final color = WidgetStateProperty.resolveWith( - (states) => index.isEven - ? theme.colorScheme.surfaceContainerHighest - : null, - ); - - return DataRow.byIndex( - index: index, - color: color, - cells: [ - DataCell(OperationStatusBadge(status: tx.status)), - DataCell(TypeChip(type: tx.type)), - DataCell(Text( - '${tx.type.sign}${amountToString(tx.amount)} ${currencyCodeToSymbol(tx.currency)}')), - DataCell(Text( - tx.balanceAfter == null - ? '-' - : '${amountToString(tx.balanceAfter!)} ${currencyCodeToSymbol(tx.currency)}', - )), - DataCell(Text(tx.counterparty ?? '-')), - DataCell(Text( - '${TimeOfDay.fromDateTime(tx.date).format(context)}\n' - '${tx.date.toLocal().toIso8601String().split("T").first}', - )), - DataCell(Text(tx.description)), - ], - ); - }, - ), - ), - ), - ), - ); - } -} diff --git a/frontend/pweb/lib/pages/report/cards/operation_card.dart b/frontend/pweb/lib/pages/report/cards/operation_card.dart index 3b57c68f..53aafd81 100644 --- a/frontend/pweb/lib/pages/report/cards/operation_card.dart +++ b/frontend/pweb/lib/pages/report/cards/operation_card.dart @@ -15,19 +15,23 @@ class OperationCard extends StatelessWidget { final OperationItem operation; final ValueChanged? onTap; - const OperationCard({ - super.key, - required this.operation, - this.onTap, - }); + const OperationCard({super.key, required this.operation, this.onTap}); @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context)!; final theme = Theme.of(context); final canOpen = onTap != null && paymentIdFromOperation(operation) != null; - final amountLabel = formatAmount(operation.amount, operation.currency); - final toAmountLabel = formatAmount(operation.toAmount, operation.toCurrency); + final amountLabel = formatAmount( + context, + operation.amount, + operation.currency, + ); + final toAmountLabel = formatAmount( + context, + operation.toAmount, + operation.toCurrency, + ); final showToAmount = shouldShowToAmount(operation); final timeLabel = formatOperationTime(context, operation.date); diff --git a/frontend/pweb/lib/pages/report/charts/payout_volumes/totals_list.dart b/frontend/pweb/lib/pages/report/charts/payout_volumes/totals_list.dart index 38abb52e..a7ba3644 100644 --- a/frontend/pweb/lib/pages/report/charts/payout_volumes/totals_list.dart +++ b/frontend/pweb/lib/pages/report/charts/payout_volumes/totals_list.dart @@ -42,6 +42,7 @@ class PayoutTotalsList extends StatelessWidget { const SizedBox(width: 8), Text( formatAmount( + context, totals[index].amount, totals[index].currency, ), diff --git a/frontend/pweb/lib/pages/report/details/sections/fx.dart b/frontend/pweb/lib/pages/report/details/sections/fx.dart index f56527bd..50a8ea2c 100644 --- a/frontend/pweb/lib/pages/report/details/sections/fx.dart +++ b/frontend/pweb/lib/pages/report/details/sections/fx.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:pshared/models/money.dart'; import 'package:pshared/models/payment/payment.dart'; import 'package:pshared/models/payment/fx/quote.dart'; import 'package:pshared/utils/currency.dart'; +import 'package:pshared/utils/money.dart'; import 'package:pweb/pages/report/details/section.dart'; import 'package:pweb/pages/report/details/sections/rows.dart'; @@ -13,26 +15,17 @@ import 'package:pweb/generated/i18n/app_localizations.dart'; class PaymentFxSection extends StatelessWidget { final Payment payment; - const PaymentFxSection({ - super.key, - required this.payment, - }); + const PaymentFxSection({super.key, required this.payment}); @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context)!; final fx = payment.lastQuote?.fxQuote; final rows = buildDetailRows([ - DetailValue( - label: loc.fxRateLabel, - value: _formatRate(fx), - ), + DetailValue(label: loc.fxRateLabel, value: _formatRate(fx)), ]); - return DetailsSection( - title: loc.paymentDetailsFx, - children: rows, - ); + return DetailsSection(title: loc.paymentDetailsFx, children: rows); } String? _formatRate(FxQuote? fx) { @@ -40,24 +33,33 @@ class PaymentFxSection extends StatelessWidget { final price = fx.price?.trim(); if (price == null || price.isEmpty) return null; - final base = _firstNonEmpty([ - currencySymbolFromCode(fx.baseCurrency), - currencySymbolFromCode(fx.baseAmount?.currency), + final baseCurrency = _firstNonEmpty([ fx.baseCurrency, fx.baseAmount?.currency, + currencySymbolFromCode(fx.baseCurrency), + currencySymbolFromCode(fx.baseAmount?.currency), ]); - final quote = _firstNonEmpty([ - currencySymbolFromCode(fx.quoteCurrency), - currencySymbolFromCode(fx.quoteAmount?.currency), + final quoteCurrency = _firstNonEmpty([ fx.quoteCurrency, fx.quoteAmount?.currency, + currencySymbolFromCode(fx.quoteCurrency), + currencySymbolFromCode(fx.quoteAmount?.currency), ]); - if (base == null || quote == null) { - return price; - } + if (baseCurrency == null || quoteCurrency == null) return price; - return '1 $base = $price $quote'; + final baseDisplay = formatMoneyDisplay( + Money(amount: '1', currency: baseCurrency), + fallback: '1 $baseCurrency', + invalidAmountFallback: '1', + ); + final quoteDisplay = formatMoneyDisplay( + Money(amount: _normalizeAmount(price), currency: quoteCurrency), + fallback: '$price $quoteCurrency', + invalidAmountFallback: price, + ); + + return '$baseDisplay = $quoteDisplay'; } String? _firstNonEmpty(List values) { @@ -67,4 +69,8 @@ class PaymentFxSection extends StatelessWidget { } return null; } + + String _normalizeAmount(String raw) { + return raw.replaceAll(RegExp(r'\s+'), '').replaceAll(',', '.'); + } } diff --git a/frontend/pweb/lib/pages/report/details/sections/metadata.dart b/frontend/pweb/lib/pages/report/details/sections/metadata.dart deleted file mode 100644 index 63a48dc7..00000000 --- a/frontend/pweb/lib/pages/report/details/sections/metadata.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:pshared/models/payment/payment.dart'; - -import 'package:pweb/pages/report/details/row.dart'; -import 'package:pweb/pages/report/details/section.dart'; - -import 'package:pweb/generated/i18n/app_localizations.dart'; - - -class PaymentMetadataSection extends StatelessWidget { - final Payment payment; - - const PaymentMetadataSection({ - super.key, - required this.payment, - }); - - @override - Widget build(BuildContext context) { - final loc = AppLocalizations.of(context)!; - final metadata = payment.metadata ?? const {}; - const allowedKeys = {'upload_filename', 'upload_rows'}; - final filtered = Map.fromEntries( - metadata.entries.where((entry) => allowedKeys.contains(entry.key)), - ); - - if (filtered.isEmpty) { - return DetailsSection( - title: loc.paymentDetailsMetadata, - children: [ - Text( - loc.metadataEmpty, - style: Theme.of(context).textTheme.bodyMedium, - ), - ], - ); - } - - final entries = filtered.entries.toList() - ..sort((a, b) => a.key.compareTo(b.key)); - - return DetailsSection( - title: loc.paymentDetailsMetadata, - children: entries - .map( - (entry) => DetailRow( - label: _metadataLabel(loc, entry.key), - value: entry.value, - monospaced: true, - ), - ) - .toList(), - ); - } -} - -String _metadataLabel(AppLocalizations loc, String key) { - switch (key) { - case 'upload_filename': - return loc.metadataUploadFileName; - case 'upload_rows': - return loc.metadataTotalRecipients; - default: - return key; - } -} diff --git a/frontend/pweb/lib/pages/report/details/sections/operations/tile.dart b/frontend/pweb/lib/pages/report/details/sections/operations/tile.dart index 2afbf9f2..9a0dbd6c 100644 --- a/frontend/pweb/lib/pages/report/details/sections/operations/tile.dart +++ b/frontend/pweb/lib/pages/report/details/sections/operations/tile.dart @@ -27,6 +27,7 @@ class OperationHistoryTile extends StatelessWidget { final loc = AppLocalizations.of(context)!; final theme = Theme.of(context); final title = resolveOperationTitle(loc, operation.code); + final operationLabel = operation.label?.trim(); final stateView = resolveStepStateView(context, operation.state); final completedAt = formatCompletedAt(context, operation.completedAt); final canDownload = canDownloadDocument && onDownloadDocument != null; @@ -49,13 +50,24 @@ class OperationHistoryTile extends StatelessWidget { StepStateChip(view: stateView), ], ), + if (operationLabel != null && + operationLabel.isNotEmpty && + operationLabel != title) ...[ + const SizedBox(height: 4), + Text( + operationLabel, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], const SizedBox(height: 6), Text( '${loc.completedAtLabel}: $completedAt', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), - ), + ), if (canDownload) ...[ const SizedBox(height: 8), TextButton.icon( diff --git a/frontend/pweb/lib/pages/report/details/summary_card/widget.dart b/frontend/pweb/lib/pages/report/details/summary_card/widget.dart index 45af884e..ee6ade98 100644 --- a/frontend/pweb/lib/pages/report/details/summary_card/widget.dart +++ b/frontend/pweb/lib/pages/report/details/summary_card/widget.dart @@ -9,6 +9,7 @@ import 'package:pweb/pages/report/details/summary_card/info_line.dart'; import 'package:pweb/pages/report/table/badge.dart'; import 'package:pweb/utils/report/amount_parts.dart'; import 'package:pweb/utils/report/format.dart'; +import 'package:pweb/utils/money_display.dart'; import 'package:pweb/utils/report/payment_mapper.dart'; import 'package:pweb/utils/clipboard.dart'; @@ -24,6 +25,7 @@ class PaymentSummaryCard extends StatelessWidget { Widget build(BuildContext context) { final loc = AppLocalizations.of(context)!; final theme = Theme.of(context); + final unavailableValue = unavailableMoneyValue(context); final status = statusFromPayment(payment); final dateLabel = formatDateLabel(context, resolvePaymentDate(payment)); @@ -33,14 +35,16 @@ class PaymentSummaryCard extends StatelessWidget { final toAmount = payment.lastQuote?.amounts?.destinationSettlement; final fee = quoteFeeTotal(payment.lastQuote); - final amountLabel = formatMoney(primaryAmount); - final toAmountLabel = formatMoney(toAmount); - final feeLabel = formatMoney(fee); + final amountLabel = formatMoney(context, primaryAmount); + final toAmountLabel = formatMoney(context, toAmount); + final feeLabel = formatMoney(context, fee); final paymentRef = (payment.paymentRef ?? '').trim(); - final showToAmount = toAmountLabel != '-'; + final showToAmount = toAmountLabel != unavailableValue; final showFee = payment.lastQuote != null; - final feeText = feeLabel != '-' ? loc.fee(feeLabel) : loc.fee(loc.noFee); + final feeText = feeLabel != unavailableValue + ? loc.fee(feeLabel) + : loc.fee(loc.noFee); final showPaymentId = paymentRef.isNotEmpty; final amountParts = splitAmount(amountLabel); @@ -73,12 +77,12 @@ class PaymentSummaryCard extends StatelessWidget { currency: amountParts.currency, ), const SizedBox(height: 6), - if (amountLabel != '-') + if (amountLabel != unavailableValue) InfoLine( icon: Icons.send_outlined, text: loc.sentAmount(amountLabel), ), - if (showToAmount && toAmountLabel != '-') + if (showToAmount && toAmountLabel != unavailableValue) InfoLine( icon: Icons.south_east, text: loc.recipientWillReceive(toAmountLabel), diff --git a/frontend/pweb/lib/pages/report/table/row.dart b/frontend/pweb/lib/pages/report/table/row.dart index 3ee87b52..1c2eab6b 100644 --- a/frontend/pweb/lib/pages/report/table/row.dart +++ b/frontend/pweb/lib/pages/report/table/row.dart @@ -1,14 +1,17 @@ import 'package:flutter/material.dart'; +import 'package:pshared/models/money.dart'; import 'package:pshared/models/payment/operation.dart'; import 'package:pshared/models/payment/status.dart'; import 'package:pshared/utils/currency.dart'; import 'package:pweb/pages/report/table/badge.dart'; +import 'package:pweb/utils/money_display.dart'; import 'package:pweb/utils/report/download_act.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; + class OperationRow { static DataRow build(OperationItem op, BuildContext context) { final isUnknownDate = op.date.millisecondsSinceEpoch == 0; @@ -35,13 +38,21 @@ class OperationRow { label: Text(loc.downloadAct), ) : Text(op.fileName ?? ''); + final amountLabel = formatMoneyUiWithL10n( + loc, + Money(amount: amountToString(op.amount), currency: op.currency), + ); + final toAmountLabel = formatMoneyUiWithL10n( + loc, + Money(amount: amountToString(op.toAmount), currency: op.toCurrency), + ); return DataRow( cells: [ DataCell(OperationStatusBadge(status: op.status)), DataCell(documentCell), - DataCell(Text('${amountToString(op.amount)} ${op.currency}')), - DataCell(Text('${amountToString(op.toAmount)} ${op.toCurrency}')), + DataCell(Text(amountLabel)), + DataCell(Text(toAmountLabel)), DataCell(Text(op.payId)), DataCell(Text(op.cardNumber ?? '-')), DataCell(Text(op.name)), diff --git a/frontend/pweb/lib/utils/money_display.dart b/frontend/pweb/lib/utils/money_display.dart new file mode 100644 index 00000000..3d7010f6 --- /dev/null +++ b/frontend/pweb/lib/utils/money_display.dart @@ -0,0 +1,96 @@ +import 'package:flutter/widgets.dart'; + +import 'package:pshared/models/asset.dart'; +import 'package:pshared/models/money.dart'; +import 'package:pshared/utils/currency.dart'; +import 'package:pshared/utils/money.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +String unavailableMoneyValue(BuildContext context) { + return AppLocalizations.of(context)!.valueUnavailable; +} + +String unavailableMoneyValueFromL10n(AppLocalizations l10n) { + return l10n.valueUnavailable; +} + +String formatMoneyUi( + BuildContext context, + Money? money, { + String separator = ' ', +}) { + return formatMoneyUiWithL10n( + AppLocalizations.of(context)!, + money, + separator: separator, + ); +} + +String formatMoneyUiWithL10n( + AppLocalizations l10n, + Money? money, { + String separator = ' ', +}) { + final unavailableValue = unavailableMoneyValueFromL10n(l10n); + return formatMoneyDisplay( + money, + fallback: unavailableValue, + invalidAmountFallback: unavailableValue, + separator: separator, + ); +} + +String formatAmountUi( + BuildContext context, { + required double amount, + required String currency, + String separator = ' ', +}) { + return formatAmountUiWithL10n( + AppLocalizations.of(context)!, + amount: amount, + currency: currency, + separator: separator, + ); +} + +String formatAmountUiWithL10n( + AppLocalizations l10n, { + required double amount, + required String currency, + String separator = ' ', +}) { + return formatMoneyUiWithL10n( + l10n, + Money(amount: amountToString(amount), currency: currency), + separator: separator, + ); +} + +String formatAssetUi( + BuildContext context, + Asset? asset, { + String separator = ' ', +}) { + return formatAssetUiWithL10n( + AppLocalizations.of(context)!, + asset, + separator: separator, + ); +} + +String formatAssetUiWithL10n( + AppLocalizations l10n, + Asset? asset, { + String separator = ' ', +}) { + if (asset == null) return unavailableMoneyValueFromL10n(l10n); + return formatAmountUiWithL10n( + l10n, + amount: asset.amount, + currency: currencyCodeToString(asset.currency), + separator: separator, + ); +} diff --git a/frontend/pweb/lib/utils/payment/method_delete_confirmation.dart b/frontend/pweb/lib/utils/payment/method_delete_confirmation.dart new file mode 100644 index 00000000..f7300219 --- /dev/null +++ b/frontend/pweb/lib/utils/payment/method_delete_confirmation.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/widgets/dialogs/confirmation_dialog.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +Future confirmPaymentMethodDelete( + BuildContext context, + VoidCallback onConfirmed, +) async { + final l10n = AppLocalizations.of(context)!; + final confirmed = await showConfirmationDialog( + context: context, + title: l10n.delete, + message: l10n.deletePaymentConfirmation, + confirmLabel: l10n.delete, + ); + if (confirmed) { + onConfirmed(); + } +} diff --git a/frontend/pweb/lib/utils/payment/status_view.dart b/frontend/pweb/lib/utils/payment/status_view.dart index 71f90e87..c0c04392 100644 --- a/frontend/pweb/lib/utils/payment/status_view.dart +++ b/frontend/pweb/lib/utils/payment/status_view.dart @@ -50,31 +50,35 @@ StatusView operationStatusViewFromToken( case 'settled': return StatusView( label: l10n.operationStatusSuccessful, - backgroundColor: scheme.tertiaryContainer, - foregroundColor: scheme.onTertiaryContainer, + backgroundColor: Colors.green, + foregroundColor: Colors.white, ); + case 'skipped': return StatusView( label: l10n.operationStepStateSkipped, - backgroundColor: scheme.secondaryContainer, - foregroundColor: scheme.onSecondaryContainer, + backgroundColor: Colors.grey, + foregroundColor: Colors.white, ); + case 'error': case 'failed': case 'rejected': case 'aborted': return StatusView( label: l10n.operationStatusUnsuccessful, - backgroundColor: scheme.errorContainer, - foregroundColor: scheme.onErrorContainer, + backgroundColor: Colors.red, + foregroundColor: Colors.white, ); + case 'cancelled': case 'canceled': return StatusView( label: l10n.paymentStatusCancelled, - backgroundColor: scheme.surfaceContainerHighest, - foregroundColor: scheme.onSurfaceVariant, + backgroundColor: Colors.blueGrey, + foregroundColor: Colors.white, ); + case 'processing': case 'running': case 'executing': @@ -82,9 +86,10 @@ StatusView operationStatusViewFromToken( case 'started': return StatusView( label: l10n.paymentStatusProcessing, - backgroundColor: scheme.primaryContainer, - foregroundColor: scheme.onPrimaryContainer, + backgroundColor: Colors.orange, + foregroundColor: Colors.white, ); + case 'pending': case 'queued': case 'waiting': @@ -92,26 +97,29 @@ StatusView operationStatusViewFromToken( case 'scheduled': return StatusView( label: l10n.operationStatusPending, - backgroundColor: scheme.secondary, - foregroundColor: scheme.onSecondary, + backgroundColor: Colors.amber, + foregroundColor: Colors.black, ); + case 'needs_attention': return StatusView( label: l10n.operationStepStateNeedsAttention, - backgroundColor: scheme.tertiary, - foregroundColor: scheme.onTertiary, + backgroundColor: Colors.grey.shade800, + foregroundColor: Colors.white, ); + case 'retrying': return StatusView( label: l10n.operationStepStateRetrying, - backgroundColor: scheme.primary, - foregroundColor: scheme.onPrimary, + backgroundColor: Colors.purple, + foregroundColor: Colors.white, ); + default: return StatusView( label: fallbackLabel ?? humanizeOperationStatusToken(token), - backgroundColor: scheme.surfaceContainerHighest, - foregroundColor: scheme.onSurfaceVariant, + backgroundColor: Colors.grey.shade400, + foregroundColor: Colors.black, ); } } diff --git a/frontend/pweb/lib/utils/report/format.dart b/frontend/pweb/lib/utils/report/format.dart index 4cde3fc9..370ab889 100644 --- a/frontend/pweb/lib/utils/report/format.dart +++ b/frontend/pweb/lib/utils/report/format.dart @@ -3,34 +3,35 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:pshared/models/money.dart'; -import 'package:pshared/utils/currency.dart'; import 'package:pshared/utils/localization.dart'; +import 'package:pweb/utils/money_display.dart'; -String formatMoney(Money? money, {String fallback = '-'}) { - if (money == null) return fallback; - final amount = money.amount.trim(); - if (amount.isEmpty) return fallback; - final symbol = currencySymbolFromCode(money.currency); - final suffix = symbol ?? money.currency; - if (suffix.trim().isEmpty) return amount; - return '$amount $suffix'; +String formatMoney(BuildContext context, Money? money) { + if (money == null || money.amount.trim().isEmpty) { + return unavailableMoneyValue(context); + } + return formatMoneyUi(context, money); } -String formatAmount(double amount, String currency, {String fallback = '-'}) { - final trimmed = currency.trim(); - if (trimmed.isEmpty) return amountToString(amount); - final symbol = currencySymbolFromCode(trimmed); - final suffix = symbol ?? trimmed; - return '${amountToString(amount)} $suffix'; +String formatAmount(BuildContext context, double amount, String currency) { + return formatAmountUi(context, amount: amount, currency: currency); } -String formatDateLabel(BuildContext context, DateTime? date, {String fallback = '-'}) { +String formatDateLabel( + BuildContext context, + DateTime? date, { + String fallback = '-', +}) { if (date == null || date.millisecondsSinceEpoch == 0) return fallback; return dateTimeToLocalFormat(context, date.toLocal()); } -String formatLongDate(BuildContext context, DateTime? date, {String fallback = '-'}) { +String formatLongDate( + BuildContext context, + DateTime? date, { + String fallback = '-', +}) { if (date == null || date.millisecondsSinceEpoch == 0) return fallback; final locale = Localizations.localeOf(context).toString(); final formatter = DateFormat('d MMMM y', locale); diff --git a/frontend/pweb/lib/utils/report/source_filter.dart b/frontend/pweb/lib/utils/report/source_filter.dart index da7f1369..bcc81c79 100644 --- a/frontend/pweb/lib/utils/report/source_filter.dart +++ b/frontend/pweb/lib/utils/report/source_filter.dart @@ -1,4 +1,4 @@ -import 'package:pshared/models/payment/intent.dart'; +import 'package:pshared/models/payment/endpoint.dart'; import 'package:pshared/models/payment/methods/data.dart'; import 'package:pshared/models/payment/methods/ledger.dart'; import 'package:pshared/models/payment/methods/managed_wallet.dart'; @@ -7,6 +7,13 @@ import 'package:pshared/models/payment/payment.dart'; import 'package:pshared/models/payment/source_type.dart'; +typedef _SourceRefExtractor = String? Function(PaymentMethodData source); + +final Map _sourceExtractors = { + PaymentSourceType.wallet: _walletSourceRef, + PaymentSourceType.ledger: _ledgerSourceRef, +}; + bool paymentMatchesSource( Payment payment, { required PaymentSourceType sourceType, @@ -15,95 +22,54 @@ bool paymentMatchesSource( final normalizedSourceRef = _normalize(sourceRef); if (normalizedSourceRef == null) return false; - final paymentSourceRef = _paymentSourceRef(payment, sourceType); - return paymentSourceRef != null && paymentSourceRef == normalizedSourceRef; + final paymentSourceRefs = _paymentSourceRefs(payment, sourceType); + if (paymentSourceRefs.isEmpty) return false; + + return paymentSourceRefs.contains(normalizedSourceRef); } -String? _paymentSourceRef(Payment payment, PaymentSourceType sourceType) { - final fromIntent = _sourceRefFromIntent(payment.intent, sourceType); - if (fromIntent != null) return fromIntent; - return _sourceRefFromMetadata(payment.metadata, sourceType); +Set _paymentSourceRefs(Payment payment, PaymentSourceType sourceType) { + final fromSource = _sourceRefsFromEndpoint(payment.source, sourceType); + if (fromSource.isEmpty) return const {}; + return fromSource; } -String? _sourceRefFromIntent( - PaymentIntent? intent, +Set _sourceRefsFromEndpoint( + PaymentEndpoint? endpoint, PaymentSourceType sourceType, ) { - final source = intent?.source; - if (source == null) return null; + if (endpoint == null) return const {}; - final fromIntentAttributes = _sourceRefFromMetadata( - intent?.attributes, - sourceType, - ); - if (fromIntentAttributes != null) return fromIntentAttributes; - - switch (sourceType) { - case PaymentSourceType.wallet: - return _walletSourceRef(source); - case PaymentSourceType.ledger: - return _ledgerSourceRef(source); + final refs = {}; + void collect(String? value) { + final normalized = _normalize(value); + if (normalized == null) return; + refs.add(normalized); } + + final source = endpoint.method; + if (source != null) { + final fromMethod = _sourceExtractors[sourceType]?.call(source); + collect(fromMethod); + } + + collect(endpoint.paymentMethodRef); + + return refs; } -String? _walletSourceRef(PaymentMethodData source) { - if (source is ManagedWalletPaymentMethod) { - return _normalize(source.managedWalletRef) ?? - _sourceRefFromMetadata(source.metadata, PaymentSourceType.wallet); - } - if (source is WalletPaymentMethod) { - return _normalize(source.walletId) ?? - _sourceRefFromMetadata(source.metadata, PaymentSourceType.wallet); - } - return null; -} +String? _walletSourceRef(PaymentMethodData source) => switch (source) { + ManagedWalletPaymentMethod(:final managedWalletRef) => _normalize( + managedWalletRef, + ), + WalletPaymentMethod(:final walletId) => _normalize(walletId), + _ => null, +}; -String? _ledgerSourceRef(PaymentMethodData source) { - if (source is LedgerPaymentMethod) { - return _normalize(source.ledgerAccountRef) ?? - _sourceRefFromMetadata(source.metadata, PaymentSourceType.ledger); - } - return null; -} - -String? _sourceRefFromMetadata( - Map? metadata, - PaymentSourceType sourceType, -) { - if (metadata == null || metadata.isEmpty) return null; - - final keys = switch (sourceType) { - PaymentSourceType.wallet => const [ - 'source_wallet_ref', - 'managed_wallet_ref', - 'wallet_ref', - 'wallet_id', - 'source_wallet_id', - 'source_wallet_user_id', - 'wallet_user_id', - 'wallet_user_ref', - 'wallet_number', - 'source_wallet_number', - 'source_managed_wallet_ref', - 'source_ref', - ], - PaymentSourceType.ledger => const [ - 'source_ledger_account_ref', - 'ledger_account_ref', - 'source_account_code', - 'ledger_account_code', - 'account_code', - 'source_ref', - ], - }; - - for (final key in keys) { - final value = _normalize(metadata[key]); - if (value != null) return value; - } - - return null; -} +String? _ledgerSourceRef(PaymentMethodData source) => switch (source) { + LedgerPaymentMethod(:final ledgerAccountRef) => _normalize(ledgerAccountRef), + _ => null, +}; String? _normalize(String? value) { final normalized = value?.trim(); diff --git a/frontend/pweb/lib/utils/report/utils/format.dart b/frontend/pweb/lib/utils/report/utils/format.dart deleted file mode 100644 index f89d1428..00000000 --- a/frontend/pweb/lib/utils/report/utils/format.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:pshared/models/money.dart'; -import 'package:pshared/utils/currency.dart'; -import 'package:pshared/utils/localization.dart'; -import 'package:intl/intl.dart'; - - -String formatMoney(Money? money, {String fallback = '-'}) { - final amount = money?.amount.trim(); - if (amount == null || amount.isEmpty) return fallback; - return '$amount ${money!.currency}'; -} - -String formatAmount(double amount, String currency, {String fallback = '-'}) { - final trimmed = currency.trim(); - if (trimmed.isEmpty) return amountToString(amount); - final symbol = currencySymbolFromCode(trimmed); - final suffix = symbol ?? trimmed; - return '${amountToString(amount)} $suffix'; -} - -String formatDateLabel(BuildContext context, DateTime? date, {String fallback = '-'}) { - if (date == null || date.millisecondsSinceEpoch == 0) return fallback; - return dateTimeToLocalFormat(context, date.toLocal()); -} - -String formatLongDate(BuildContext context, DateTime? date, {String fallback = '-'}) { - if (date == null || date.millisecondsSinceEpoch == 0) return fallback; - final locale = Localizations.localeOf(context).toString(); - final formatter = DateFormat('d MMMM y', locale); - return formatter.format(date.toLocal()); -} - -String collapseWhitespace(String value) { - return value.replaceAll(RegExp(r'\s+'), ' ').trim(); -} diff --git a/frontend/pweb/lib/widgets/payment/source_wallet_selector/balance_formatter.dart b/frontend/pweb/lib/widgets/payment/source_wallet_selector/balance_formatter.dart index 37e68bc1..8fda011b 100644 --- a/frontend/pweb/lib/widgets/payment/source_wallet_selector/balance_formatter.dart +++ b/frontend/pweb/lib/widgets/payment/source_wallet_selector/balance_formatter.dart @@ -1,31 +1,31 @@ +import 'package:flutter/widgets.dart'; + import 'package:pshared/models/ledger/account.dart'; +import 'package:pshared/models/money.dart'; import 'package:pshared/models/payment/wallet.dart'; import 'package:pshared/utils/currency.dart'; -import 'package:pshared/utils/money.dart'; + +import 'package:pweb/utils/money_display.dart'; -String walletBalance(Wallet wallet) { - final symbol = currencyCodeToSymbol(wallet.currency); - return '$symbol ${amountToString(wallet.balance)}'; +String walletBalance(BuildContext context, Wallet wallet) { + return formatMoneyUi( + context, + Money( + amount: amountToString(wallet.balance), + currency: currencyCodeToString(wallet.currency), + ), + ); } -String ledgerBalance(LedgerAccount account) { +String ledgerBalance(BuildContext context, LedgerAccount account) { final money = account.balance?.balance; - final rawAmount = money?.amount.trim(); - final amount = parseMoneyAmount(rawAmount, fallback: double.nan); - final amountText = amount.isNaN - ? (rawAmount == null || rawAmount.isEmpty ? '--' : rawAmount) - : amountToString(amount); + final effectiveCurrency = (money?.currency.trim().isNotEmpty ?? false) + ? money!.currency + : account.currency; - final currencyCode = (money?.currency ?? account.currency) - .trim() - .toUpperCase(); - final symbol = currencySymbolFromCode(currencyCode); - if (symbol != null && symbol.trim().isNotEmpty) { - return '$symbol $amountText'; - } - if (currencyCode.isNotEmpty) { - return '$amountText $currencyCode'; - } - return amountText; + return formatMoneyUi( + context, + Money(amount: money?.amount ?? '', currency: effectiveCurrency), + ); } diff --git a/frontend/pweb/lib/widgets/payment/source_wallet_selector/dropdown_items.dart b/frontend/pweb/lib/widgets/payment/source_wallet_selector/dropdown_items.dart index 28405370..3df4721a 100644 --- a/frontend/pweb/lib/widgets/payment/source_wallet_selector/dropdown_items.dart +++ b/frontend/pweb/lib/widgets/payment/source_wallet_selector/dropdown_items.dart @@ -11,6 +11,7 @@ import 'package:pweb/generated/i18n/app_localizations.dart'; List> buildSourceSelectorItems({ + required BuildContext context, required List wallets, required List ledgerAccounts, required AppLocalizations l10n, @@ -20,7 +21,7 @@ List> buildSourceSelectorItems({ return DropdownMenuItem( value: walletOptionKey(wallet.id), child: Text( - '${walletDisplayName(wallet, l10n)} - ${walletBalance(wallet)}', + '${walletDisplayName(wallet, l10n)} - ${walletBalance(context, wallet)}', overflow: TextOverflow.ellipsis, ), ); @@ -29,7 +30,7 @@ List> buildSourceSelectorItems({ return DropdownMenuItem( value: ledgerOptionKey(ledger.ledgerAccountRef), child: Text( - '${ledgerDisplayName(ledger, l10n)} - ${ledgerBalance(ledger)}', + '${ledgerDisplayName(ledger, l10n)} - ${ledgerBalance(context, ledger)}', overflow: TextOverflow.ellipsis, ), ); diff --git a/frontend/pweb/lib/widgets/payment/source_wallet_selector/selector_field.dart b/frontend/pweb/lib/widgets/payment/source_wallet_selector/selector_field.dart index e897d151..e290dc10 100644 --- a/frontend/pweb/lib/widgets/payment/source_wallet_selector/selector_field.dart +++ b/frontend/pweb/lib/widgets/payment/source_wallet_selector/selector_field.dart @@ -25,6 +25,7 @@ Widget buildSourceSelectorField({ } final items = buildSourceSelectorItems( + context: context, wallets: wallets, ledgerAccounts: ledgerAccounts, l10n: l10n,