fixes
This commit is contained in:
@@ -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<PaymentOperationDTO> operations;
|
||||
final PaymentQuoteDTO? lastQuote;
|
||||
final Map<String, String>? metadata;
|
||||
@@ -28,7 +27,6 @@ class PaymentDTO {
|
||||
this.destination,
|
||||
this.failureCode,
|
||||
this.failureReason,
|
||||
this.intent,
|
||||
this.operations = const <PaymentOperationDTO>[],
|
||||
this.lastQuote,
|
||||
this.metadata,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<PaymentExecutionOperation> operations;
|
||||
final PaymentQuote? lastQuote;
|
||||
final Map<String, String>? 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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -91,16 +91,22 @@ class BalanceSourceActionsController {
|
||||
}
|
||||
|
||||
void _openWalletOperationHistory(BuildContext context, String walletRef) {
|
||||
context.read<PaymentSourceController>().selectWalletByRef(walletRef);
|
||||
context.pushNamed(PayoutRoutes.editWallet);
|
||||
_withSelectedWallet(
|
||||
context,
|
||||
walletRef,
|
||||
() => context.pushNamed(PayoutRoutes.editWallet),
|
||||
);
|
||||
}
|
||||
|
||||
void _sendWalletPayout(BuildContext context, String walletRef) {
|
||||
context.read<PaymentSourceController>().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<PaymentSourceController>().selectWalletByRef(walletRef);
|
||||
action();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<bool> 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() ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> _normalizeRefs(List<String> refs) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Блокировка",
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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<RecipientMethodDraft> entries;
|
||||
final ValueChanged<int> 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<void> _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);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<RecipientMethodDraft> entries;
|
||||
final ValueChanged<int>? 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<PaymentMethodData>? 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);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<WalletsController>();
|
||||
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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<WalletTransaction> 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<Color?>(
|
||||
(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)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,19 +15,23 @@ class OperationCard extends StatelessWidget {
|
||||
final OperationItem operation;
|
||||
final ValueChanged<OperationItem>? 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);
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ class PayoutTotalsList extends StatelessWidget {
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
formatAmount(
|
||||
context,
|
||||
totals[index].amount,
|
||||
totals[index].currency,
|
||||
),
|
||||
|
||||
@@ -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<String?> values) {
|
||||
@@ -67,4 +69,8 @@ class PaymentFxSection extends StatelessWidget {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _normalizeAmount(String raw) {
|
||||
return raw.replaceAll(RegExp(r'\s+'), '').replaceAll(',', '.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, String>.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;
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)),
|
||||
|
||||
96
frontend/pweb/lib/utils/money_display.dart
Normal file
96
frontend/pweb/lib/utils/money_display.dart
Normal file
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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<void> 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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<PaymentSourceType, _SourceRefExtractor> _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<String> _paymentSourceRefs(Payment payment, PaymentSourceType sourceType) {
|
||||
final fromSource = _sourceRefsFromEndpoint(payment.source, sourceType);
|
||||
if (fromSource.isEmpty) return const <String>{};
|
||||
return fromSource;
|
||||
}
|
||||
|
||||
String? _sourceRefFromIntent(
|
||||
PaymentIntent? intent,
|
||||
Set<String> _sourceRefsFromEndpoint(
|
||||
PaymentEndpoint? endpoint,
|
||||
PaymentSourceType sourceType,
|
||||
) {
|
||||
final source = intent?.source;
|
||||
if (source == null) return null;
|
||||
if (endpoint == null) return const <String>{};
|
||||
|
||||
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 = <String>{};
|
||||
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<String, String>? metadata,
|
||||
PaymentSourceType sourceType,
|
||||
) {
|
||||
if (metadata == null || metadata.isEmpty) return null;
|
||||
|
||||
final keys = switch (sourceType) {
|
||||
PaymentSourceType.wallet => const <String>[
|
||||
'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 <String>[
|
||||
'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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
List<DropdownMenuItem<SourceOptionKey>> buildSourceSelectorItems({
|
||||
required BuildContext context,
|
||||
required List<Wallet> wallets,
|
||||
required List<LedgerAccount> ledgerAccounts,
|
||||
required AppLocalizations l10n,
|
||||
@@ -20,7 +21,7 @@ List<DropdownMenuItem<SourceOptionKey>> buildSourceSelectorItems({
|
||||
return DropdownMenuItem<SourceOptionKey>(
|
||||
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<DropdownMenuItem<SourceOptionKey>> buildSourceSelectorItems({
|
||||
return DropdownMenuItem<SourceOptionKey>(
|
||||
value: ledgerOptionKey(ledger.ledgerAccountRef),
|
||||
child: Text(
|
||||
'${ledgerDisplayName(ledger, l10n)} - ${ledgerBalance(ledger)}',
|
||||
'${ledgerDisplayName(ledger, l10n)} - ${ledgerBalance(context, ledger)}',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -25,6 +25,7 @@ Widget buildSourceSelectorField({
|
||||
}
|
||||
|
||||
final items = buildSourceSelectorItems(
|
||||
context: context,
|
||||
wallets: wallets,
|
||||
ledgerAccounts: ledgerAccounts,
|
||||
l10n: l10n,
|
||||
|
||||
Reference in New Issue
Block a user