diff --git a/frontend/pshared/lib/data/dto/payment/payment.dart b/frontend/pshared/lib/data/dto/payment/payment.dart index 162b0e0f..f0d2d9b4 100644 --- a/frontend/pshared/lib/data/dto/payment/payment.dart +++ b/frontend/pshared/lib/data/dto/payment/payment.dart @@ -6,6 +6,7 @@ import 'package:pshared/data/dto/payment/response_endpoint.dart'; part 'payment.g.dart'; + @JsonSerializable() class PaymentDTO { final String? paymentRef; diff --git a/frontend/pshared/lib/data/mapper/payment/payment_response.dart b/frontend/pshared/lib/data/mapper/payment/payment_response.dart index 905db7da..7043dbdb 100644 --- a/frontend/pshared/lib/data/mapper/payment/payment_response.dart +++ b/frontend/pshared/lib/data/mapper/payment/payment_response.dart @@ -5,6 +5,7 @@ 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, diff --git a/frontend/pweb/lib/models/report/operation/document.dart b/frontend/pshared/lib/models/payment/operation_document.dart similarity index 68% rename from frontend/pweb/lib/models/report/operation/document.dart rename to frontend/pshared/lib/models/payment/operation_document.dart index 9379305e..9712a046 100644 --- a/frontend/pweb/lib/models/report/operation/document.dart +++ b/frontend/pshared/lib/models/payment/operation_document.dart @@ -1,9 +1,9 @@ -class OperationDocumentInfo { - final String operationRef; +class OperationDocumentRef { final String gatewayService; + final String operationRef; - const OperationDocumentInfo({ - required this.operationRef, + const OperationDocumentRef({ required this.gatewayService, + required this.operationRef, }); } diff --git a/frontend/pshared/lib/models/payment/payment.dart b/frontend/pshared/lib/models/payment/payment.dart index 69e4c469..3d89ca71 100644 --- a/frontend/pshared/lib/models/payment/payment.dart +++ b/frontend/pshared/lib/models/payment/payment.dart @@ -3,6 +3,7 @@ import 'package:pshared/models/payment/execution_operation.dart'; import 'package:pshared/models/payment/quote/quote.dart'; import 'package:pshared/models/payment/state.dart'; + class Payment { final String? paymentRef; final String? state; 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/app/router/payout_routes.dart b/frontend/pweb/lib/app/router/payout_routes.dart index f1e69a33..60543fb9 100644 --- a/frontend/pweb/lib/app/router/payout_routes.dart +++ b/frontend/pweb/lib/app/router/payout_routes.dart @@ -25,6 +25,7 @@ class PayoutRoutes { static const walletTopUp = 'payout-wallet-top-up'; static const paymentTypeQuery = 'paymentType'; + static const destinationLedgerAccountRefQuery = 'destinationLedgerAccountRef'; static const reportPaymentIdQuery = 'paymentId'; static const dashboardPath = '/dashboard'; @@ -40,7 +41,6 @@ class PayoutRoutes { static const editWalletPath = '/methods/edit'; static const walletTopUpPath = '/wallet/top-up'; - static String nameFor(PayoutDestination destination) { switch (destination) { case PayoutDestination.dashboard: @@ -126,9 +126,13 @@ class PayoutRoutes { static Map buildQueryParameters({ PaymentType? paymentType, + String? destinationLedgerAccountRef, }) { final params = { if (paymentType != null) paymentTypeQuery: paymentType.name, + if (destinationLedgerAccountRef != null && + destinationLedgerAccountRef.trim().isNotEmpty) + destinationLedgerAccountRefQuery: destinationLedgerAccountRef.trim(), }; return params; } @@ -140,35 +144,44 @@ class PayoutRoutes { ? null : PaymentType.values.firstWhereOrNull((type) => type.name == raw); + static String? destinationLedgerAccountRefFromState(GoRouterState state) => + destinationLedgerAccountRefFromRaw( + state.uri.queryParameters[destinationLedgerAccountRefQuery], + ); + + static String? destinationLedgerAccountRefFromRaw(String? raw) { + final value = raw?.trim(); + if (value == null || value.isEmpty) return null; + return value; + } } extension PayoutNavigation on BuildContext { - void goToPayout(PayoutDestination destination) => goNamed(PayoutRoutes.nameFor(destination)); + void goToPayout(PayoutDestination destination) => + goNamed(PayoutRoutes.nameFor(destination)); - void pushToPayout(PayoutDestination destination) => pushNamed(PayoutRoutes.nameFor(destination)); + void pushToPayout(PayoutDestination destination) => + pushNamed(PayoutRoutes.nameFor(destination)); void goToPayment({ PaymentType? paymentType, - }) => - goNamed( + String? destinationLedgerAccountRef, + }) => goNamed( PayoutRoutes.payment, queryParameters: PayoutRoutes.buildQueryParameters( paymentType: paymentType, + destinationLedgerAccountRef: destinationLedgerAccountRef, ), ); void goToReportPayment(String paymentId) => goNamed( PayoutRoutes.reportPayment, - queryParameters: { - PayoutRoutes.reportPaymentIdQuery: paymentId, - }, + queryParameters: {PayoutRoutes.reportPaymentIdQuery: paymentId}, ); void pushToReportPayment(String paymentId) => pushNamed( PayoutRoutes.reportPayment, - queryParameters: { - PayoutRoutes.reportPaymentIdQuery: paymentId, - }, + queryParameters: {PayoutRoutes.reportPaymentIdQuery: paymentId}, ); void pushToWalletTopUp() => pushNamed(PayoutRoutes.walletTopUp); diff --git a/frontend/pweb/lib/app/router/payout_shell.dart b/frontend/pweb/lib/app/router/payout_shell.dart index e9cc92c6..eda9c9b7 100644 --- a/frontend/pweb/lib/app/router/payout_shell.dart +++ b/frontend/pweb/lib/app/router/payout_shell.dart @@ -6,6 +6,7 @@ import 'package:provider/provider.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart'; import 'package:pshared/controllers/payment/source.dart'; +import 'package:pshared/models/ledger/account.dart'; import 'package:pshared/models/payment/type.dart'; import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/provider/ledger.dart'; @@ -227,7 +228,9 @@ RouteBase payoutShellRoute() => ShellRoute( onGoToPaymentWithoutRecipient: (type) => _startPayment(context, recipient: null, paymentType: type), onTopUp: (wallet) => _openWalletTopUp(context, wallet), - onWalletTap: (wallet) => _openWalletEdit(context, wallet), + onLedgerAddFunds: (account) => _openLedgerAddFunds(context, account), + onWalletTap: (wallet) => _openWalletTopUp(context, wallet), + onLedgerTap: (account) => _openLedgerEdit(context, account), ), ), ), @@ -304,6 +307,8 @@ RouteBase payoutShellRoute() => ShellRoute( child: PaymentPage( onBack: (_) => _popOrGo(context), initialPaymentType: PayoutRoutes.paymentTypeFromState(state), + initialDestinationLedgerAccountRef: + PayoutRoutes.destinationLedgerAccountRefFromState(state), fallbackDestination: fallbackDestination, ), ); @@ -340,17 +345,9 @@ RouteBase payoutShellRoute() => ShellRoute( GoRoute( name: PayoutRoutes.editWallet, path: PayoutRoutes.editWalletPath, - pageBuilder: (context, state) { - final walletsProvider = context.read(); - final wallet = walletsProvider.selectedWallet; - final loc = AppLocalizations.of(context)!; - - return NoTransitionPage( - child: wallet != null - ? WalletEditPage(onBack: () => _popOrGo(context)) - : Center(child: Text(loc.noWalletSelected)), - ); - }, + pageBuilder: (context, state) => NoTransitionPage( + child: WalletEditPage(onBack: () => _popOrGo(context)), + ), ), GoRoute( name: PayoutRoutes.walletTopUp, @@ -388,11 +385,27 @@ void _openEditRecipient(BuildContext context, {required Recipient recipient}) { context.pushNamed(PayoutRoutes.editRecipient); } -void _openWalletEdit(BuildContext context, Wallet wallet) { - context.read().selectWallet(wallet); +void _openLedgerEdit(BuildContext context, LedgerAccount account) { + context.read().selectLedgerByRef( + account.ledgerAccountRef, + ); context.pushToEditWallet(); } +void _openLedgerAddFunds(BuildContext context, LedgerAccount account) { + context.read().selectLedgerByRef( + account.ledgerAccountRef, + ); + context.read().setCurrentObject(null); + context.pushNamed( + PayoutRoutes.payment, + queryParameters: PayoutRoutes.buildQueryParameters( + paymentType: PaymentType.ledger, + destinationLedgerAccountRef: account.ledgerAccountRef, + ), + ); +} + void _openWalletTopUp(BuildContext context, Wallet wallet) { context.read().selectWallet(wallet); context.pushToWalletTopUp(); diff --git a/frontend/pweb/lib/controllers/dashboard/balance/actions_ui.dart b/frontend/pweb/lib/controllers/dashboard/balance/actions_ui.dart new file mode 100644 index 00000000..ff772cfd --- /dev/null +++ b/frontend/pweb/lib/controllers/dashboard/balance/actions_ui.dart @@ -0,0 +1,19 @@ +import 'package:flutter/foundation.dart'; + + +class BalanceActionsUiController extends ChangeNotifier { + int? _hoveredButtonIndex; + + int? get hoveredButtonIndex => _hoveredButtonIndex; + + bool isExpanded(int index) => _hoveredButtonIndex == index; + + void onHoverChanged(int index, bool hovered) { + final next = hovered + ? index + : (_hoveredButtonIndex == index ? null : _hoveredButtonIndex); + if (next == _hoveredButtonIndex) return; + _hoveredButtonIndex = next; + notifyListeners(); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/controller.dart b/frontend/pweb/lib/controllers/dashboard/balance/carousel.dart similarity index 62% rename from frontend/pweb/lib/pages/dashboard/buttons/balance/controller.dart rename to frontend/pweb/lib/controllers/dashboard/balance/carousel.dart index df2f6484..f7129118 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/controller.dart +++ b/frontend/pweb/lib/controllers/dashboard/balance/carousel.dart @@ -1,14 +1,22 @@ -import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart'; import 'package:pshared/provider/ledger.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/balance_item.dart'; +import 'package:pweb/models/dashboard/balance_item.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/config.dart'; + + +class BalanceCarouselController extends ChangeNotifier { + BalanceCarouselController() + : pageController = PageController( + viewportFraction: WalletCardConfig.viewportFraction, + ); -class BalanceCarouselController with ChangeNotifier { WalletsController? _walletsController; List _items = const [BalanceItem.addAction()]; int _index = 0; + final PageController pageController; List get items => _items; int get index => _index; @@ -31,6 +39,7 @@ class BalanceCarouselController with ChangeNotifier { _items = nextItems; _index = nextIndex; + _syncPageController(); if (hasItemsChanged || hasIndexChanged) { notifyListeners(); @@ -49,9 +58,24 @@ class BalanceCarouselController with ChangeNotifier { notifyListeners(); } - void goBack() => onPageChanged(_index - 1); + void goBack() => animateTo(_index - 1); - void goForward() => onPageChanged(_index + 1); + void goForward() => animateTo(_index + 1); + + void animateTo(int index) { + final target = _clampIndex(index, _items.length); + if (!pageController.hasClients) { + onPageChanged(target); + return; + } + pageController.animateToPage( + target, + duration: const Duration(milliseconds: 220), + curve: Curves.easeOut, + ); + } + + void syncPageController() => _syncPageController(); int _resolveNextIndex( List nextItems, @@ -73,14 +97,19 @@ class BalanceCarouselController with ChangeNotifier { String? _currentWalletRef(List items, int index) { if (items.isEmpty || index < 0 || index >= items.length) return null; final current = items[index]; - if (!current.isWallet) return null; - return current.wallet?.id; + return switch (current) { + WalletBalanceItem(:final wallet) => wallet.id, + _ => null, + }; } int? _walletIndexByRef(List items, String? walletRef) { if (walletRef == null || walletRef.isEmpty) return null; final idx = items.indexWhere( - (item) => item.isWallet && item.wallet?.id == walletRef, + (item) => switch (item) { + WalletBalanceItem(:final wallet) => wallet.id == walletRef, + _ => false, + }, ); if (idx < 0) return null; return idx; @@ -97,17 +126,17 @@ class BalanceCarouselController with ChangeNotifier { for (var i = 0; i < left.length; i++) { final a = left[i]; final b = right[i]; - if (a.type != b.type) return false; + if (a.runtimeType != b.runtimeType) return false; if (_itemIdentity(a) != _itemIdentity(b)) return false; } return true; } - String _itemIdentity(BalanceItem item) => switch (item.type) { - BalanceItemType.wallet => item.wallet?.id ?? '', - BalanceItemType.ledger => item.account?.ledgerAccountRef ?? '', - BalanceItemType.addAction => 'add', + String _itemIdentity(BalanceItem item) => switch (item) { + WalletBalanceItem(:final wallet) => wallet.id, + LedgerBalanceItem(:final account) => account.ledgerAccountRef, + AddBalanceActionItem() => 'add', }; void _syncSelectedWallet() { @@ -115,10 +144,23 @@ class BalanceCarouselController with ChangeNotifier { if (walletsController == null || _items.isEmpty) return; final current = _items[_index]; - if (!current.isWallet || current.wallet == null) return; - - final wallet = current.wallet!; + if (current is! WalletBalanceItem) return; + final wallet = current.wallet; if (walletsController.selectedWallet?.id == wallet.id) return; walletsController.selectWallet(wallet); } + + void _syncPageController() { + if (!pageController.hasClients || _items.isEmpty) return; + final current = pageController.page?.round(); + final target = _clampIndex(_index, _items.length); + if (current == target) return; + pageController.jumpToPage(target); + } + + @override + void dispose() { + pageController.dispose(); + super.dispose(); + } } diff --git a/frontend/pweb/lib/controllers/dashboard/balance/source_actions.dart b/frontend/pweb/lib/controllers/dashboard/balance/source_actions.dart new file mode 100644 index 00000000..fdf7deb5 --- /dev/null +++ b/frontend/pweb/lib/controllers/dashboard/balance/source_actions.dart @@ -0,0 +1,132 @@ +import 'package:flutter/material.dart'; + +import 'package:go_router/go_router.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/controllers/payment/source.dart'; +import 'package:pshared/models/payment/type.dart'; + +import 'package:pweb/app/router/payout_routes.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class BalanceActionButtonState { + final String label; + final IconData icon; + final VoidCallback onPressed; + + const BalanceActionButtonState({ + required this.label, + required this.icon, + required this.onPressed, + }); +} + +class BalanceActionsState { + final BalanceActionButtonState topLeading; + final BalanceActionButtonState topTrailing; + final BalanceActionButtonState bottom; + + const BalanceActionsState({ + required this.topLeading, + required this.topTrailing, + required this.bottom, + }); +} + +class BalanceSourceActionsController { + const BalanceSourceActionsController(); + + BalanceActionsState wallet({ + required BuildContext context, + required String walletRef, + required VoidCallback onAddFunds, + }) { + final l10n = AppLocalizations.of(context)!; + return BalanceActionsState( + topLeading: BalanceActionButtonState( + label: l10n.operationfryTitle, + icon: Icons.history_rounded, + onPressed: () => _openWalletOperationHistory(context, walletRef), + ), + topTrailing: BalanceActionButtonState( + label: l10n.send, + icon: Icons.send_rounded, + onPressed: () => _sendWalletPayout(context, walletRef), + ), + bottom: BalanceActionButtonState( + label: '${l10n.details} / ${l10n.addFunds}', + icon: Icons.account_balance_wallet_rounded, + onPressed: onAddFunds, + ), + ); + } + + BalanceActionsState ledger({ + required BuildContext context, + required String ledgerAccountRef, + required VoidCallback onAddFunds, + required VoidCallback onWalletDetails, + }) { + final l10n = AppLocalizations.of(context)!; + return BalanceActionsState( + topLeading: BalanceActionButtonState( + label: '${l10n.operationfryTitle} / ${l10n.details}', + icon: Icons.receipt_long_rounded, + onPressed: onWalletDetails, + ), + topTrailing: BalanceActionButtonState( + label: l10n.send, + icon: Icons.send_rounded, + onPressed: () => _sendLedgerPayout(context, ledgerAccountRef), + ), + bottom: BalanceActionButtonState( + label: l10n.addFunds, + icon: Icons.add_card_rounded, + onPressed: onAddFunds, + ), + ); + } + + void _openWalletOperationHistory(BuildContext context, String walletRef) { + _withSelectedWallet( + context, + walletRef, + () => context.pushNamed(PayoutRoutes.editWallet), + ); + } + + void _sendWalletPayout(BuildContext context, String walletRef) { + _withSelectedWallet( + context, + walletRef, + () => context.pushNamed( + PayoutRoutes.payment, + queryParameters: PayoutRoutes.buildQueryParameters( + paymentType: PaymentType.wallet, + ), + ), + ); + } + + void _sendLedgerPayout(BuildContext context, String ledgerAccountRef) { + context.read().selectLedgerByRef(ledgerAccountRef); + context.pushNamed( + PayoutRoutes.payment, + queryParameters: PayoutRoutes.buildQueryParameters( + paymentType: PaymentType.ledger, + ), + ); + } + + 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 new file mode 100644 index 00000000..9af9e4f5 --- /dev/null +++ b/frontend/pweb/lib/controllers/dashboard/balance/source_copy.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class BalanceCopyState { + final String label; + final String payload; + + const BalanceCopyState({required this.label, required this.payload}); + + bool get canCopy => payload.trim().isNotEmpty; +} + +class BalanceSourceCopyController { + const BalanceSourceCopyController(); + + BalanceCopyState wallet(BuildContext context, String? depositAddress) => + _buildCopyAddressState(context, depositAddress); + + 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 0ac5f1fe..51ded122 100644 --- a/frontend/pweb/lib/controllers/operations/report_operations.dart +++ b/frontend/pweb/lib/controllers/operations/report_operations.dart @@ -1,20 +1,26 @@ -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'; import 'package:pshared/models/payment/status.dart'; import 'package:pshared/provider/payment/payments.dart'; import 'package:pweb/models/state/load_more_state.dart'; import 'package:pweb/utils/report/operations/operations.dart'; import 'package:pweb/utils/report/payment_mapper.dart'; +import 'package:pweb/utils/report/source_filter.dart'; class ReportOperationsController extends ChangeNotifier { PaymentsProvider? _payments; + PaymentSourceType? _sourceType; + Set _sourceRefs = const {}; DateTimeRange? _selectedRange; final Set _selectedStatuses = {}; + List _paymentItems = const []; List _operations = const []; List _filtered = const []; @@ -36,10 +42,20 @@ class ReportOperationsController extends ChangeNotifier { return LoadMoreState.hidden; } - void update(PaymentsProvider provider) { + void update( + PaymentsProvider provider, { + PaymentSourceType? sourceType, + String? sourceRef, + List? sourceRefs, + }) { if (!identical(_payments, provider)) { _payments = provider; } + _sourceType = sourceType; + final effectiveSourceRefs = + sourceRefs ?? + (sourceRef == null ? const [] : [sourceRef]); + _sourceRefs = _normalizeRefs(effectiveSourceRefs); _rebuildOperations(); } @@ -74,13 +90,16 @@ class ReportOperationsController extends ChangeNotifier { } void _rebuildOperations() { - final items = _payments?.payments ?? const []; - _operations = items.map(mapPaymentToOperation).toList(); + _paymentItems = _payments?.payments ?? const []; + _operations = _paymentItems + .where(_matchesCurrentSource) + .map(mapPaymentToOperation) + .toList(); _rebuildFiltered(notify: true); } void _rebuildFiltered({bool notify = true}) { - _filtered = _applyFilters(_operations); + _filtered = _applyFilters(sortOperations(_operations)); if (notify) { notifyListeners(); } @@ -88,13 +107,14 @@ class ReportOperationsController extends ChangeNotifier { List _applyFilters(List operations) { if (_selectedRange == null && _selectedStatuses.isEmpty) { - return sortOperations(operations); + return operations; } final filtered = operations.where((op) { final statusMatch = _selectedStatuses.isEmpty || _selectedStatuses.contains(op.status); - final dateMatch = _selectedRange == null || + final dateMatch = + _selectedRange == null || isUnknownDate(op.date) || (op.date.isAfter( _selectedRange!.start.subtract(const Duration(seconds: 1)), @@ -105,7 +125,28 @@ class ReportOperationsController extends ChangeNotifier { return statusMatch && dateMatch; }).toList(); - return sortOperations(filtered); + return filtered; + } + + bool _matchesCurrentSource(Payment payment) { + final sourceType = _sourceType; + if (sourceType == null || _sourceRefs.isEmpty) return true; + return _sourceRefs.firstWhereOrNull( + (sourceRef) => paymentMatchesSource( + payment, + sourceType: sourceType, + sourceRef: sourceRef, + ), + ) != + null; + } + + Set _normalizeRefs(List refs) { + final normalized = refs + .map((value) => value.trim()) + .where((value) => value.isNotEmpty) + .toSet(); + return normalized; } bool _isSameRange(DateTimeRange? left, DateTimeRange? right) { diff --git a/frontend/pweb/lib/controllers/operations/wallet_transactions.dart b/frontend/pweb/lib/controllers/operations/wallet_transactions.dart index 2e7c5542..dac9006c 100644 --- a/frontend/pweb/lib/controllers/operations/wallet_transactions.dart +++ b/frontend/pweb/lib/controllers/operations/wallet_transactions.dart @@ -71,16 +71,24 @@ class WalletTransactionsController extends ChangeNotifier { void _rebuildFiltered({bool notify = true}) { final source = _provider?.transactions ?? const []; + final activeWalletId = _provider?.walletId; _filteredTransactions = source.where((tx) { + final walletMatch = + activeWalletId == null || tx.walletId == activeWalletId; final statusMatch = _selectedStatuses.isEmpty || _selectedStatuses.contains(tx.status); final typeMatch = _selectedTypes.isEmpty || _selectedTypes.contains(tx.type); - final dateMatch = _dateRange == null || - (tx.date.isAfter(_dateRange!.start.subtract(const Duration(seconds: 1))) && - tx.date.isBefore(_dateRange!.end.add(const Duration(seconds: 1)))); + final dateMatch = + _dateRange == null || + (tx.date.isAfter( + _dateRange!.start.subtract(const Duration(seconds: 1)), + ) && + tx.date.isBefore( + _dateRange!.end.add(const Duration(seconds: 1)), + )); - return statusMatch && typeMatch && dateMatch; + return walletMatch && statusMatch && typeMatch && dateMatch; }).toList(); if (notify) notifyListeners(); diff --git a/frontend/pweb/lib/controllers/payments/details.dart b/frontend/pweb/lib/controllers/payments/details.dart index 8108a742..5f3751a4 100644 --- a/frontend/pweb/lib/controllers/payments/details.dart +++ b/frontend/pweb/lib/controllers/payments/details.dart @@ -1,13 +1,12 @@ import 'package:flutter/foundation.dart'; +import 'package:pshared/models/payment/operation_document.dart'; import 'package:pshared/models/payment/execution_operation.dart'; import 'package:pshared/models/payment/payment.dart'; -import 'package:pshared/models/payment/status.dart'; import 'package:pshared/provider/payment/payments.dart'; -import 'package:pweb/models/documents/operation.dart'; import 'package:pweb/utils/report/operations/document_rule.dart'; -import 'package:pweb/utils/report/payment_mapper.dart'; + class PaymentDetailsController extends ChangeNotifier { PaymentDetailsController({required String paymentId}) @@ -22,26 +21,7 @@ class PaymentDetailsController extends ChangeNotifier { bool get isLoading => _payments?.isLoading ?? false; Exception? get error => _payments?.error; - bool get canDownload { - final current = _payment; - if (current == null) return false; - if (statusFromPayment(current) != OperationStatus.success) return false; - return primaryOperationDocumentRequest != null; - } - - OperationDocumentRequestModel? get primaryOperationDocumentRequest { - final current = _payment; - if (current == null) return null; - for (final operation in current.operations) { - final request = operationDocumentRequest(operation); - if (request != null) { - return request; - } - } - return null; - } - - OperationDocumentRequestModel? operationDocumentRequest( + OperationDocumentRef? operationDocumentRequest( PaymentExecutionOperation operation, ) { final current = _payment; @@ -54,7 +34,7 @@ class PaymentDetailsController extends ChangeNotifier { if (!isOperationDocumentEligible(operation.code)) return null; - return OperationDocumentRequestModel( + return OperationDocumentRef( gatewayService: gatewayService, operationRef: operationRef, ); diff --git a/frontend/pweb/lib/l10n/en.arb b/frontend/pweb/lib/l10n/en.arb index 6c37a488..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", @@ -638,7 +639,7 @@ } } }, - "noFee": "No fee", + "noFee": "None", "recipientWillReceive": "Recipient will receive: {amount}", "@recipientWillReceive": { diff --git a/frontend/pweb/lib/l10n/ru.arb b/frontend/pweb/lib/l10n/ru.arb index a1777eb1..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": "Блокировка", @@ -638,7 +639,7 @@ } } }, - "noFee": "Нет комиссии", + "noFee": "Без оплаты", "recipientWillReceive": "Получатель получит: {amount}", "@recipientWillReceive": { diff --git a/frontend/pweb/lib/models/dashboard/balance_item.dart b/frontend/pweb/lib/models/dashboard/balance_item.dart new file mode 100644 index 00000000..713dc8fe --- /dev/null +++ b/frontend/pweb/lib/models/dashboard/balance_item.dart @@ -0,0 +1,26 @@ +import 'package:pshared/models/ledger/account.dart'; +import 'package:pshared/models/payment/wallet.dart'; + +sealed class BalanceItem { + const BalanceItem(); + + const factory BalanceItem.wallet(Wallet wallet) = WalletBalanceItem; + const factory BalanceItem.ledger(LedgerAccount account) = LedgerBalanceItem; + const factory BalanceItem.addAction() = AddBalanceActionItem; +} + +final class WalletBalanceItem extends BalanceItem { + final Wallet wallet; + + const WalletBalanceItem(this.wallet); +} + +final class LedgerBalanceItem extends BalanceItem { + final LedgerAccount account; + + const LedgerBalanceItem(this.account); +} + +final class AddBalanceActionItem extends BalanceItem { + const AddBalanceActionItem(); +} diff --git a/frontend/pweb/lib/models/documents/operation.dart b/frontend/pweb/lib/models/documents/operation.dart deleted file mode 100644 index c669d04c..00000000 --- a/frontend/pweb/lib/models/documents/operation.dart +++ /dev/null @@ -1,9 +0,0 @@ -class OperationDocumentRequestModel { - final String gatewayService; - final String operationRef; - - const OperationDocumentRequestModel({ - required this.gatewayService, - required this.operationRef, - }); -} diff --git a/frontend/pweb/lib/models/payment/payment_state.dart b/frontend/pweb/lib/models/payment/payment_state.dart deleted file mode 100644 index 814abf3c..00000000 --- a/frontend/pweb/lib/models/payment/payment_state.dart +++ /dev/null @@ -1,27 +0,0 @@ -enum PaymentState { - success, - failed, - cancelled, - processing, - unknown, -} - -PaymentState paymentStateFromRaw(String? raw) { - final trimmed = (raw ?? '').trim().toUpperCase(); - final normalized = trimmed.startsWith('PAYMENT_STATE_') - ? trimmed.substring('PAYMENT_STATE_'.length) - : trimmed; - - switch (normalized) { - case 'SUCCESS': - return PaymentState.success; - case 'FAILED': - return PaymentState.failed; - case 'CANCELLED': - return PaymentState.cancelled; - case 'PROCESSING': - return PaymentState.processing; - default: - return PaymentState.unknown; - } -} 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/actions/bar.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/actions/bar.dart new file mode 100644 index 00000000..e4c4b62c --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/actions/bar.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/controllers/dashboard/balance/actions_ui.dart'; +import 'package:pweb/controllers/dashboard/balance/source_actions.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/actions/hover_expandable_action_button.dart'; + + +class BalanceActionsBar extends StatefulWidget { + final BalanceActionsState state; + + const BalanceActionsBar({super.key, required this.state}); + + @override + State createState() => _BalanceActionsBarState(); +} + +class _BalanceActionsBarState extends State { + static const double _buttonHeight = 34.0; + static const double _buttonGap = 6.0; + static const double _iconSize = 18.0; + static const double _textGap = 8.0; + static const double _horizontalPadding = 6.0; + + final BalanceActionsUiController _uiController = BalanceActionsUiController(); + + @override + void dispose() { + _uiController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textStyle = Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w400, + color: colorScheme.onSecondary, + fontSize: 14, + ); + final buttons = [ + widget.state.topLeading, + widget.state.topTrailing, + widget.state.bottom, + ]; + + return ListenableBuilder( + listenable: _uiController, + builder: (context, _) { + return Align( + alignment: Alignment.centerRight, + child: OverflowBox( + alignment: Alignment.centerRight, + minWidth: 0, + maxWidth: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + for (var i = 0; i < buttons.length; i++) ...[ + HoverExpandableActionButton( + height: _buttonHeight, + icon: buttons[i].icon, + label: buttons[i].label, + iconSize: _iconSize, + textStyle: textStyle, + expanded: _uiController.isExpanded(i), + textGap: _textGap, + horizontalPadding: _horizontalPadding, + onHoverChanged: (hovered) => + _uiController.onHoverChanged(i, hovered), + onPressed: buttons[i].onPressed, + ), + if (i != buttons.length - 1) + const SizedBox(height: _buttonGap), + ], + ], + ), + ), + ); + }, + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/actions/hover_expandable_action_button.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/actions/hover_expandable_action_button.dart new file mode 100644 index 00000000..3dba3dea --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/actions/hover_expandable_action_button.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; + + +class HoverExpandableActionButton extends StatelessWidget { + final double height; + final IconData icon; + final String label; + final double iconSize; + final TextStyle? textStyle; + final bool expanded; + final double textGap; + final double horizontalPadding; + final ValueChanged onHoverChanged; + final VoidCallback onPressed; + + const HoverExpandableActionButton({ + super.key, + required this.height, + required this.icon, + required this.label, + required this.iconSize, + required this.textStyle, + required this.expanded, + required this.textGap, + required this.horizontalPadding, + required this.onHoverChanged, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return MouseRegion( + onEnter: (_) => onHoverChanged(true), + onExit: (_) => onHoverChanged(false), + child: AnimatedContainer( + duration: const Duration(milliseconds: 220), + curve: Curves.easeOutCubic, + height: height, + decoration: BoxDecoration( + color: colorScheme.primaryFixed, + borderRadius: BorderRadius.circular(999), + ), + child: Material( + type: MaterialType.transparency, + child: InkWell( + borderRadius: BorderRadius.circular(999), + onTap: onPressed, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: horizontalPadding), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: iconSize, color: colorScheme.onSecondary), + AnimatedSize( + duration: const Duration(milliseconds: 220), + curve: Curves.easeOutCubic, + alignment: Alignment.centerRight, + child: expanded + ? Padding( + padding: EdgeInsets.only(left: textGap), + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.visible, + style: textStyle, + ), + ) + : const SizedBox.shrink(), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/amount.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/amount.dart index 3871379d..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,16 +28,26 @@ 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); return Row( + mainAxisSize: MainAxisSize.min, children: [ Text( - isMasked ? '•••• $currencyBalance' : '${amountToString(wallet.balance)} $currencyBalance', - style: textTheme.headlineSmall?.copyWith( + isMasked ? '•••• $currencyBalance' : formattedBalance, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, - color: colorScheme.onSurface, + color: colorScheme.primary, ), ), const SizedBox(width: _iconSpacing), @@ -43,7 +56,7 @@ class BalanceAmount extends StatelessWidget { child: Icon( isMasked ? Icons.visibility_off : Icons.visibility, size: _iconSize, - color: colorScheme.onSurface, + color: colorScheme.primary, ), ), ], diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/balance_item.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/balance_item.dart deleted file mode 100644 index 0e992d8a..00000000 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/balance_item.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:pshared/models/ledger/account.dart'; -import 'package:pshared/models/payment/wallet.dart'; - - -enum BalanceItemType { wallet, ledger, addAction } - -class BalanceItem { - final BalanceItemType type; - final Wallet? wallet; - final LedgerAccount? account; - - const BalanceItem.wallet(this.wallet) : type = BalanceItemType.wallet, account = null; - - const BalanceItem.ledger(this.account) : type = BalanceItemType.ledger, wallet = null; - - const BalanceItem.addAction() : type = BalanceItemType.addAction, wallet = null, account = null; - - bool get isWallet => type == BalanceItemType.wallet; - bool get isLedger => type == BalanceItemType.ledger; - bool get isAdd => type == BalanceItemType.addAction; -} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/card.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/card.dart deleted file mode 100644 index 55602f4c..00000000 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/card.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:provider/provider.dart'; - -import 'package:pshared/controllers/balance_mask/wallets.dart'; -import 'package:pshared/models/payment/wallet.dart'; -import 'package:pshared/models/payment/chain_network.dart'; -import 'package:pshared/utils/l10n/chain.dart'; - -import 'package:pweb/pages/dashboard/buttons/balance/add_funds.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/amount.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/config.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/header.dart'; -import 'package:pweb/widgets/refresh_balance/wallet.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) { - final networkLabel = (wallet.network == null || wallet.network == ChainNetwork.unspecified) - ? null - : wallet.network!.localizedName(context); - final symbol = wallet.tokenSymbol?.trim(); - - return Card( - color: Theme.of(context).colorScheme.onSecondary, - elevation: WalletCardConfig.elevation, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius), - ), - child: InkWell( - borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius), - onTap: onTap, - child: SizedBox.expand( - child: Padding( - padding: WalletCardConfig.contentPadding, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - BalanceHeader( - title: wallet.name, - subtitle: networkLabel, - badge: (symbol == null || symbol.isEmpty) ? null : symbol, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - BalanceAmount( - wallet: wallet, - onToggleMask: () { - context.read().toggleBalanceMask(wallet.id); - }, - ), - Column( - children: [ - WalletBalanceRefreshButton( - walletRef: wallet.id, - ), - BalanceAddFunds(onTopUp: onTopUp), - ], - ), - ], - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel.dart deleted file mode 100644 index da3f2dc6..00000000 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel.dart +++ /dev/null @@ -1,146 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/gestures.dart'; - -import 'package:pshared/models/payment/wallet.dart'; - -import 'package:pweb/pages/dashboard/buttons/balance/add/card.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/balance_item.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/card.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/config.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/indicator.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/ledger.dart'; - - -class BalanceCarousel extends StatefulWidget { - final List items; - final int currentIndex; - final ValueChanged onIndexChanged; - final ValueChanged onTopUp; - final ValueChanged onWalletTap; - - const BalanceCarousel({ - super.key, - required this.items, - required this.currentIndex, - required this.onIndexChanged, - required this.onTopUp, - required this.onWalletTap, - }); - - @override - State createState() => _BalanceCarouselState(); -} - -class _BalanceCarouselState extends State { - late final PageController _controller; - - @override - void initState() { - super.initState(); - _controller = PageController( - initialPage: widget.currentIndex, - viewportFraction: WalletCardConfig.viewportFraction, - ); - } - - @override - void didUpdateWidget(covariant BalanceCarousel oldWidget) { - super.didUpdateWidget(oldWidget); - if (!mounted) return; - if (_controller.hasClients) { - final currentPage = _controller.page?.round(); - if (currentPage != widget.currentIndex) { - _controller.jumpToPage(widget.currentIndex); - } - } - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - void _goToPage(int index) { - if (!_controller.hasClients) return; - _controller.animateToPage( - index, - duration: const Duration(milliseconds: 220), - curve: Curves.easeOut, - ); - } - - @override - Widget build(BuildContext context) { - if (widget.items.isEmpty) { - return const SizedBox.shrink(); - } - - final safeIndex = widget.currentIndex.clamp(0, widget.items.length - 1); - final scrollBehavior = ScrollConfiguration.of(context).copyWith( - dragDevices: const { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, - PointerDeviceKind.trackpad, - }, - ); - - return Column( - children: [ - SizedBox( - height: WalletCardConfig.cardHeight, - child: MouseRegion( - cursor: SystemMouseCursors.grab, - child: ScrollConfiguration( - behavior: scrollBehavior, - child: PageView.builder( - controller: _controller, - onPageChanged: widget.onIndexChanged, - itemCount: widget.items.length, - itemBuilder: (context, index) { - final item = widget.items[index]; - final Widget card = switch (item.type) { - BalanceItemType.wallet => WalletCard( - wallet: item.wallet!, - onTopUp: () => widget.onTopUp(item.wallet!), - onTap: () => widget.onWalletTap(item.wallet!), - ), - BalanceItemType.ledger => LedgerAccountCard(account: item.account!), - BalanceItemType.addAction => const AddBalanceCard(), - }; - - return Padding( - padding: WalletCardConfig.cardPadding, - child: card, - ); - }, - ), - ), - ), - ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - onPressed: safeIndex > 0 - ? () => _goToPage(safeIndex - 1) - : null, - icon: const Icon(Icons.arrow_back), - ), - const SizedBox(width: 16), - CarouselIndicator( - itemCount: widget.items.length, - index: safeIndex, - ), - const SizedBox(width: 16), - IconButton( - onPressed: safeIndex < widget.items.length - 1 ? () => _goToPage(safeIndex + 1) : null, - icon: const Icon(Icons.arrow_forward), - ), - ], - ), - ], - ); - } -} 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 new file mode 100644 index 00000000..576e935b --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/card_item.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/ledger/account.dart'; +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'; + + +class BalanceCarouselCardItem extends StatelessWidget { + final BalanceItem item; + final ValueChanged onTopUp; + final ValueChanged onLedgerAddFunds; + final ValueChanged onWalletTap; + final ValueChanged onLedgerTap; + + const BalanceCarouselCardItem({ + super.key, + required this.item, + required this.onTopUp, + required this.onLedgerAddFunds, + required this.onWalletTap, + required this.onLedgerTap, + }); + + @override + Widget build(BuildContext context) { + final card = switch (item) { + WalletBalanceItem(:final wallet) => BalanceSourceCard.wallet( + wallet: wallet, + onAddFunds: () => onTopUp(wallet), + onTap: () => onWalletTap(wallet), + ), + LedgerBalanceItem(:final account) => LedgerAccountCard( + account: account, + onTap: () => onLedgerTap(account), + onAddFunds: () => onLedgerAddFunds(account), + ), + AddBalanceActionItem() => const AddBalanceCard(), + }; + + return Padding(padding: WalletCardConfig.cardPadding, child: card); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/cards_view.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/cards_view.dart new file mode 100644 index 00000000..94a5fee8 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/cards_view.dart @@ -0,0 +1,61 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +import 'package:pshared/models/ledger/account.dart'; +import 'package:pshared/models/payment/wallet.dart'; + +import 'package:pweb/controllers/dashboard/balance/carousel.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/carousel/card_item.dart'; + + +class BalanceCarouselCardsView extends StatelessWidget { + final BalanceCarouselController controller; + final ValueChanged onTopUp; + final ValueChanged onLedgerAddFunds; + final ValueChanged onWalletTap; + final ValueChanged onLedgerTap; + final double height; + + const BalanceCarouselCardsView({ + super.key, + required this.controller, + required this.onTopUp, + required this.onLedgerAddFunds, + required this.onWalletTap, + required this.onLedgerTap, + required this.height, + }); + + @override + Widget build(BuildContext context) { + final scrollBehavior = ScrollConfiguration.of(context).copyWith( + dragDevices: const { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + PointerDeviceKind.trackpad, + }, + ); + + return SizedBox( + height: height, + child: MouseRegion( + cursor: SystemMouseCursors.grab, + child: ScrollConfiguration( + behavior: scrollBehavior, + child: PageView.builder( + controller: controller.pageController, + onPageChanged: controller.onPageChanged, + itemCount: controller.items.length, + itemBuilder: (context, index) => BalanceCarouselCardItem( + item: controller.items[index], + onTopUp: onTopUp, + onLedgerAddFunds: onLedgerAddFunds, + onWalletTap: onWalletTap, + onLedgerTap: onLedgerTap, + ), + ), + ), + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/carousel.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/carousel.dart new file mode 100644 index 00000000..52544c17 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/carousel.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/ledger/account.dart'; +import 'package:pshared/models/payment/wallet.dart'; + +import 'package:pweb/controllers/dashboard/balance/carousel.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/carousel/cards_view.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/carousel/navigation.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/config.dart'; + + +class BalanceCarousel extends StatelessWidget { + final BalanceCarouselController controller; + final ValueChanged onTopUp; + final ValueChanged onLedgerAddFunds; + final ValueChanged onWalletTap; + final ValueChanged onLedgerTap; + + const BalanceCarousel({ + super.key, + required this.controller, + required this.onTopUp, + required this.onLedgerAddFunds, + required this.onWalletTap, + required this.onLedgerTap, + }); + + @override + Widget build(BuildContext context) { + if (controller.items.isEmpty) { + return const SizedBox.shrink(); + } + + WidgetsBinding.instance.addPostFrameCallback((_) { + controller.syncPageController(); + }); + + final safeIndex = controller.index.clamp(0, controller.items.length - 1); + + return LayoutBuilder( + builder: (context, constraints) { + final cardHeight = WalletCardConfig.cardHeightForWidth( + constraints.maxWidth, + ); + + return Column( + children: [ + BalanceCarouselCardsView( + controller: controller, + onTopUp: onTopUp, + onLedgerAddFunds: onLedgerAddFunds, + onWalletTap: onWalletTap, + onLedgerTap: onLedgerTap, + height: cardHeight, + ), + const SizedBox(height: 16), + BalanceCarouselNavigation(controller: controller, index: safeIndex), + ], + ); + }, + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/navigation.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/navigation.dart new file mode 100644 index 00000000..33118d22 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/navigation.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/controllers/dashboard/balance/carousel.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/indicator.dart'; + + +class BalanceCarouselNavigation extends StatelessWidget { + final BalanceCarouselController controller; + final int index; + + const BalanceCarouselNavigation({ + super.key, + required this.controller, + required this.index, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + onPressed: index > 0 ? controller.goBack : null, + icon: const Icon(Icons.arrow_back), + ), + const SizedBox(width: 16), + CarouselIndicator(itemCount: controller.items.length, index: index), + const SizedBox(width: 16), + IconButton( + onPressed: index < controller.items.length - 1 + ? controller.goForward + : null, + icon: const Icon(Icons.arrow_forward), + ), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/config.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/config.dart index a70b9234..269ae6c8 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/config.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/config.dart @@ -2,14 +2,21 @@ import 'package:flutter/material.dart'; abstract class WalletCardConfig { - static const double cardHeight = 145.0; static const double elevation = 4.0; static const double borderRadius = 16.0; - static const double viewportFraction = 0.9; - - static const EdgeInsets cardPadding = EdgeInsets.symmetric(horizontal: 8); - static const EdgeInsets contentPadding = EdgeInsets.all(16); - + static const double viewportFraction = 0.96; + + static const EdgeInsets cardPadding = EdgeInsets.symmetric(horizontal: 6); + static const EdgeInsets contentPadding = EdgeInsets.symmetric( + horizontal: 28, + vertical: 16, + ); + static const double dotSize = 8.0; static const EdgeInsets dotMargin = EdgeInsets.symmetric(horizontal: 4); + + static double cardHeightForWidth(double width) { + final adaptiveHeight = width * 0.18; + return adaptiveHeight.clamp(150.0, 230.0); + } } diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/header.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/header.dart index a00a2075..3e0eb4cb 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/header.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/header.dart @@ -20,46 +20,51 @@ class BalanceHeader extends StatelessWidget { final subtitleText = subtitle?.trim(); final badgeText = badge?.trim(); - return Row( + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( title, - style: textTheme.titleMedium?.copyWith( - color: colorScheme.onSurface, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: textTheme.titleLarge?.copyWith( + color: colorScheme.primary, + fontWeight: FontWeight.w700, ), ), - if (subtitleText != null && subtitleText.isNotEmpty) - Text( - subtitleText, - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - fontWeight: FontWeight.w500, + ), + if (badgeText != null && badgeText.isNotEmpty) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: colorScheme.primaryFixed, + borderRadius: BorderRadius.circular(999), + ), + child: Text( + badgeText, + style: textTheme.labelSmall?.copyWith( + color: colorScheme.onSecondary, + fontWeight: FontWeight.w700, ), ), - ], - ), - ), - if (badgeText != null && badgeText.isNotEmpty) ...[ - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), - decoration: BoxDecoration( - color: colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(999), - ), - child: Text( - badgeText, - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onPrimaryContainer, - fontWeight: FontWeight.w600, ), + ], + ], + ), + if (subtitleText != null && subtitleText.isNotEmpty) + Text( + subtitleText, + style: textTheme.titleSmall?.copyWith( + color: colorScheme.primaryFixed, + fontWeight: FontWeight.w500, ), + maxLines: 1, ), - ], ], ); } diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger.dart deleted file mode 100644 index 5748c7e0..00000000 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger.dart +++ /dev/null @@ -1,124 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:provider/provider.dart'; - -import 'package:pshared/controllers/balance_mask/ledger_accounts.dart'; -import 'package:pshared/models/ledger/account.dart'; -import 'package:pshared/utils/currency.dart'; -import 'package:pshared/utils/money.dart'; - -import 'package:pweb/pages/dashboard/buttons/balance/config.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/header.dart'; -import 'package:pweb/widgets/refresh_balance/ledger.dart'; - -import 'package:pweb/generated/i18n/app_localizations.dart'; - - -class LedgerAccountCard extends StatelessWidget { - final LedgerAccount account; - - const LedgerAccountCard({super.key, required this.account}); - - String _formatBalance() { - 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}'; - } - } - - String _formatMaskedBalance() { - final currency = account.currency.trim(); - if (currency.isEmpty) return '••••'; - try { - final symbol = currencyCodeToSymbol(currencyStringToCode(currency)); - if (symbol.trim().isEmpty) { - return '•••• $currency'; - } - return '•••• $symbol'; - } catch (_) { - return '•••• $currency'; - } - } - - @override - Widget build(BuildContext context) { - final textTheme = Theme.of(context).textTheme; - final colorScheme = Theme.of(context).colorScheme; - final loc = AppLocalizations.of(context)!; - final accountName = account.name.trim(); - final accountCode = account.accountCode.trim(); - final title = accountName.isNotEmpty ? accountName : loc.paymentTypeLedger; - final subtitle = accountCode.isNotEmpty ? accountCode : null; - final badge = account.currency.trim().isEmpty - ? null - : account.currency.toUpperCase(); - - return Card( - color: colorScheme.onSecondary, - elevation: WalletCardConfig.elevation, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius), - ), - child: Padding( - padding: WalletCardConfig.contentPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - BalanceHeader(title: title, subtitle: subtitle, badge: badge), - Row( - children: [ - Consumer( - builder: (context, controller, _) { - final isMasked = controller.isBalanceMasked( - account.ledgerAccountRef, - ); - return Row( - children: [ - Text( - isMasked ? _formatMaskedBalance() : _formatBalance(), - style: textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - ), - ), - const SizedBox(width: 12), - GestureDetector( - onTap: () => controller.toggleBalanceMask( - account.ledgerAccountRef, - ), - child: Icon( - isMasked ? Icons.visibility_off : Icons.visibility, - size: 24, - color: colorScheme.onSurface, - ), - ), - ], - ); - }, - ), - const SizedBox(width: 12), - LedgerBalanceRefreshButton( - ledgerAccountRef: account.ledgerAccountRef, - ), - ], - ), - ], - ), - ), - ); - } -} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger_amount.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger_amount.dart new file mode 100644 index 00000000..f6fc8ffa --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger_amount.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/controllers/balance_mask/ledger_accounts.dart'; +import 'package:pshared/models/ledger/account.dart'; + +import 'package:pweb/pages/payout_page/wallet/edit/fields/ledger/balance_formatter.dart'; + + +class LedgerBalanceAmount extends StatelessWidget { + final LedgerAccount account; + + const LedgerBalanceAmount({super.key, required this.account}); + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + final colorScheme = Theme.of(context).colorScheme; + + return Consumer( + builder: (context, controller, _) { + final isMasked = controller.isBalanceMasked(account.ledgerAccountRef); + final balance = isMasked + ? LedgerBalanceFormatter.formatMasked(account) + : LedgerBalanceFormatter.format(context, account); + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + balance, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme.primary, + ), + ), + const SizedBox(width: 12), + GestureDetector( + onTap: () { + controller.toggleBalanceMask(account.ledgerAccountRef); + }, + child: Icon( + isMasked ? Icons.visibility_off : Icons.visibility, + size: 24, + color: colorScheme.primary, + ), + ), + ], + ); + }, + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/providers.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/providers.dart index 1de0dac2..8f0f2b35 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/providers.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/providers.dart @@ -5,7 +5,8 @@ import 'package:provider/provider.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart'; import 'package:pshared/provider/ledger.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/controller.dart'; +import 'package:pweb/controllers/dashboard/balance/carousel.dart'; + class BalanceWidgetProviders extends StatelessWidget { final Widget child; diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/actions/ledger.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/actions/ledger.dart new file mode 100644 index 00000000..10e7711f --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/actions/ledger.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/controllers/dashboard/balance/source_actions.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/actions/bar.dart'; + + +class LedgerSourceActions extends StatelessWidget { + final String ledgerAccountRef; + final VoidCallback onAddFunds; + final VoidCallback onWalletDetails; + + const LedgerSourceActions({ + super.key, + required this.ledgerAccountRef, + required this.onAddFunds, + required this.onWalletDetails, + }); + + @override + Widget build(BuildContext context) { + const controller = BalanceSourceActionsController(); + final state = controller.ledger( + context: context, + ledgerAccountRef: ledgerAccountRef, + onAddFunds: onAddFunds, + onWalletDetails: onWalletDetails, + ); + + return BalanceActionsBar(state: state); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/actions/wallet.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/actions/wallet.dart new file mode 100644 index 00000000..e699e513 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/actions/wallet.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/controllers/dashboard/balance/source_actions.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/actions/bar.dart'; + + +class WalletSourceActions extends StatelessWidget { + final String walletRef; + final VoidCallback onAddFunds; + + const WalletSourceActions({ + super.key, + required this.walletRef, + required this.onAddFunds, + }); + + @override + Widget build(BuildContext context) { + const controller = BalanceSourceActionsController(); + final state = controller.wallet( + context: context, + walletRef: walletRef, + onAddFunds: onAddFunds, + ); + + return BalanceActionsBar(state: state); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card.dart new file mode 100644 index 00000000..f71cb67c --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card.dart @@ -0,0 +1,138 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/controllers/balance_mask/wallets.dart'; +import 'package:pshared/models/ledger/account.dart'; +import 'package:pshared/models/payment/chain_network.dart'; +import 'package:pshared/models/payment/source_type.dart'; +import 'package:pshared/models/payment/wallet.dart'; +import 'package:pshared/utils/l10n/chain.dart'; + +import 'package:pweb/controllers/dashboard/balance/source_copy.dart'; +import 'package:pweb/models/state/visibility.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/amount.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/ledger_amount.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/source/actions/ledger.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/source/actions/wallet.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/source/card_layout.dart'; +import 'package:pweb/widgets/refresh_balance/ledger.dart'; +import 'package:pweb/widgets/refresh_balance/wallet.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class BalanceSourceCard extends StatelessWidget { + final PaymentSourceType _type; + final Wallet? _wallet; + final LedgerAccount? _ledgerAccount; + final VoidCallback onTap; + final VoidCallback onAddFunds; + static const BalanceSourceCopyController _copyController = + BalanceSourceCopyController(); + + const BalanceSourceCard.wallet({ + super.key, + required Wallet wallet, + required this.onTap, + required this.onAddFunds, + }) : _type = PaymentSourceType.wallet, + _wallet = wallet, + _ledgerAccount = null; + + const BalanceSourceCard.ledger({ + super.key, + required LedgerAccount account, + required this.onTap, + required this.onAddFunds, + }) : _type = PaymentSourceType.ledger, + _wallet = null, + _ledgerAccount = account; + + @override + Widget build(BuildContext context) => switch (_type) { + PaymentSourceType.wallet => _buildWalletCard(context, _wallet!), + PaymentSourceType.ledger => _buildLedgerCard(context, _ledgerAccount!), + }; + + Widget _buildWalletCard(BuildContext context, Wallet wallet) { + final networkLabel = + (wallet.network == null || wallet.network == ChainNetwork.unspecified) + ? null + : wallet.network!.localizedName(context); + final symbol = wallet.tokenSymbol?.trim(); + final copyState = _copyController.wallet(context, wallet.depositAddress); + + return BalanceSourceCardLayout( + title: wallet.name, + subtitle: networkLabel, + badge: (symbol == null || symbol.isEmpty) ? null : symbol, + onTap: null, + copyLabel: copyState.label, + canCopy: copyState.canCopy, + onCopy: copyState.canCopy + ? () async { + final copied = await _copyController.copy(copyState); + if (!copied || !context.mounted) return; + final loc = AppLocalizations.of(context)!; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(loc.addressCopied))); + } + : null, + refreshButton: WalletBalanceRefreshButton( + walletRef: wallet.id, + iconOnly: VisibilityState.hidden, + ), + actions: WalletSourceActions( + walletRef: wallet.id, + onAddFunds: onAddFunds, + ), + amount: BalanceAmount( + wallet: wallet, + onToggleMask: () { + context.read().toggleBalanceMask(wallet.id); + }, + ), + ); + } + + Widget _buildLedgerCard(BuildContext context, LedgerAccount account) { + final loc = AppLocalizations.of(context)!; + final accountName = account.name.trim(); + final accountCode = account.accountCode.trim(); + final title = accountName.isNotEmpty ? accountName : loc.paymentTypeLedger; + final badge = account.currency.trim().isEmpty + ? null + : account.currency.toUpperCase(); + final copyState = _copyController.ledger(context, accountCode); + + return BalanceSourceCardLayout( + title: title, + subtitle: null, + badge: badge, + onTap: onTap, + copyLabel: copyState.label, + canCopy: copyState.canCopy, + onCopy: copyState.canCopy + ? () async { + final copied = await _copyController.copy(copyState); + if (!copied || !context.mounted) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(loc.addressCopied))); + } + : null, + refreshButton: LedgerBalanceRefreshButton( + ledgerAccountRef: account.ledgerAccountRef, + iconOnly: VisibilityState.hidden, + ), + actions: LedgerSourceActions( + ledgerAccountRef: account.ledgerAccountRef, + onAddFunds: onAddFunds, + onWalletDetails: onTap, + ), + amount: LedgerBalanceAmount(account: account), + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card_layout.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card_layout.dart new file mode 100644 index 00000000..ef3626ef --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card_layout.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/pages/dashboard/buttons/balance/config.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/source/layout/wide_body.dart'; + + +class BalanceSourceCardLayout extends StatelessWidget { + final String title; + final String? subtitle; + final String? badge; + final Widget amount; + final Widget refreshButton; + final Widget actions; + final VoidCallback? onTap; + final String copyLabel; + final bool canCopy; + final VoidCallback? onCopy; + + const BalanceSourceCardLayout({ + super.key, + required this.title, + required this.subtitle, + required this.badge, + required this.amount, + required this.refreshButton, + required this.actions, + required this.onTap, + required this.copyLabel, + required this.canCopy, + required this.onCopy, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final borderRadius = BorderRadius.circular(WalletCardConfig.borderRadius); + + return Card( + color: colorScheme.onSecondary, + elevation: WalletCardConfig.elevation, + shape: RoundedRectangleBorder(borderRadius: borderRadius), + child: InkWell( + borderRadius: borderRadius, + onTap: onTap, + child: SizedBox.expand( + child: Padding( + padding: WalletCardConfig.contentPadding, + child: BalanceSourceBody( + title: title, + subtitle: subtitle, + badge: badge, + amount: amount, + refreshButton: refreshButton, + actions: actions, + copyLabel: copyLabel, + canCopy: canCopy, + onCopy: onCopy, + ), + ), + ), + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/cards/ledger.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/cards/ledger.dart new file mode 100644 index 00000000..69771c33 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/cards/ledger.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/ledger/account.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/source/card.dart'; + + +class LedgerAccountCard extends StatelessWidget { + final LedgerAccount account; + final VoidCallback onAddFunds; + final VoidCallback? onTap; + + const LedgerAccountCard({ + super.key, + required this.account, + required this.onAddFunds, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return BalanceSourceCard.ledger( + account: account, + onTap: onTap ?? () {}, + onAddFunds: onAddFunds, + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/layout/amount_with_refresh.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/layout/amount_with_refresh.dart new file mode 100644 index 00000000..6d6303fb --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/layout/amount_with_refresh.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + + +class BalanceAmountWithRefresh extends StatelessWidget { + final Widget amount; + final Widget refreshButton; + + const BalanceAmountWithRefresh({ + super.key, + required this.amount, + required this.refreshButton, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButtonTheme( + data: IconButtonThemeData( + style: IconButton.styleFrom( + minimumSize: const Size(30, 30), + maximumSize: const Size(40, 40), + padding: EdgeInsets.zero, + foregroundColor: colorScheme.primary, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + child: refreshButton, + ), + const SizedBox(width: 8), + amount, + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/layout/copyable_field.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/layout/copyable_field.dart new file mode 100644 index 00000000..4b709f5e --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/layout/copyable_field.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + + +class BalanceCopyableField extends StatelessWidget { + final String label; + final bool canCopy; + final VoidCallback? onCopy; + + const BalanceCopyableField({ + super.key, + required this.label, + required this.canCopy, + required this.onCopy, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + return Container( + decoration: BoxDecoration( + color: colorScheme.onSecondary, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: colorScheme.primaryFixed, width: 0.6), + ), + child: InkWell( + onTap: canCopy ? onCopy : null, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.copy_rounded, + size: 16, + color: canCopy + ? colorScheme.primaryFixed + : colorScheme.primary.withValues(alpha: 0.35), + ), + const SizedBox(width: 6), + Flexible( + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.labelMedium?.copyWith( + color: canCopy + ? colorScheme.primaryFixed + : colorScheme.primary.withValues(alpha: 0.45), + fontWeight: FontWeight.normal, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/layout/wide_body.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/layout/wide_body.dart new file mode 100644 index 00000000..648e6a61 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/layout/wide_body.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/pages/dashboard/buttons/balance/header.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/source/layout/amount_with_refresh.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/source/layout/copyable_field.dart'; + + +class BalanceSourceBody extends StatelessWidget { + final String title; + final String? subtitle; + final String? badge; + final Widget amount; + final Widget refreshButton; + final Widget actions; + final String copyLabel; + final bool canCopy; + final VoidCallback? onCopy; + + const BalanceSourceBody({ + super.key, + required this.title, + required this.subtitle, + required this.badge, + required this.amount, + required this.refreshButton, + required this.actions, + required this.copyLabel, + required this.canCopy, + required this.onCopy, + }); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final sideMaxWidth = constraints.maxWidth * 0.30; + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Flexible( + fit: FlexFit.loose, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: sideMaxWidth), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + BalanceHeader( + title: title, + subtitle: subtitle, + badge: badge, + ), + SizedBox(height: constraints.maxHeight * 0.06), + BalanceCopyableField( + label: copyLabel, + canCopy: canCopy, + onCopy: onCopy, + ), + ], + ), + ), + ), + Expanded( + child: Align( + alignment: Alignment.center, + child: FittedBox( + fit: BoxFit.scaleDown, + child: BalanceAmountWithRefresh( + amount: amount, + refreshButton: refreshButton, + ), + ), + ), + ), + Flexible( + fit: FlexFit.loose, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: sideMaxWidth), + child: SizedBox(height: constraints.maxHeight, child: actions), + ), + ), + ], + ); + }, + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/widget.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/widget.dart index a719d3a2..ca9ce1c5 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/widget.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/widget.dart @@ -3,22 +3,28 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart'; +import 'package:pshared/models/ledger/account.dart'; import 'package:pshared/provider/ledger.dart'; import 'package:pshared/models/payment/wallet.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/carousel.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/controller.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/carousel/carousel.dart'; +import 'package:pweb/controllers/dashboard/balance/carousel.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; + class BalanceWidget extends StatelessWidget { final ValueChanged onTopUp; + final ValueChanged onLedgerAddFunds; final ValueChanged onWalletTap; + final ValueChanged onLedgerTap; const BalanceWidget({ super.key, required this.onTopUp, + required this.onLedgerAddFunds, required this.onWalletTap, + required this.onLedgerTap, }); @override @@ -41,11 +47,11 @@ class BalanceWidget extends StatelessWidget { } final carouselWidget = BalanceCarousel( - items: carousel.items, - currentIndex: carousel.index, - onIndexChanged: carousel.onPageChanged, + controller: carousel, onTopUp: onTopUp, + onLedgerAddFunds: onLedgerAddFunds, onWalletTap: onWalletTap, + onLedgerTap: onLedgerTap, ); if (wallets.isEmpty && accounts.isEmpty) { diff --git a/frontend/pweb/lib/pages/dashboard/dashboard.dart b/frontend/pweb/lib/pages/dashboard/dashboard.dart index bc709b6d..1de0a860 100644 --- a/frontend/pweb/lib/pages/dashboard/dashboard.dart +++ b/frontend/pweb/lib/pages/dashboard/dashboard.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:pshared/models/ledger/account.dart'; import 'package:pshared/models/payment/type.dart'; import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/models/payment/wallet.dart'; @@ -15,6 +16,7 @@ import 'package:pweb/pages/loader.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; + class AppSpacing { static const double small = 10; static const double medium = 16; @@ -25,14 +27,18 @@ class DashboardPage extends StatefulWidget { final ValueChanged onRecipientSelected; final void Function(PaymentType type) onGoToPaymentWithoutRecipient; final ValueChanged onTopUp; + final ValueChanged onLedgerAddFunds; final ValueChanged onWalletTap; + final ValueChanged onLedgerTap; const DashboardPage({ super.key, required this.onRecipientSelected, required this.onGoToPaymentWithoutRecipient, required this.onTopUp, + required this.onLedgerAddFunds, required this.onWalletTap, + required this.onLedgerTap, }); @override @@ -86,7 +92,9 @@ class _DashboardPageState extends State { BalanceWidgetProviders( child: BalanceWidget( onTopUp: widget.onTopUp, + onLedgerAddFunds: widget.onLedgerAddFunds, onWalletTap: widget.onWalletTap, + onLedgerTap: widget.onLedgerTap, ), ), const SizedBox(height: AppSpacing.small), diff --git a/frontend/pweb/lib/pages/dashboard/payouts/amount/feild.dart b/frontend/pweb/lib/pages/dashboard/payouts/amount/field.dart similarity index 80% rename from frontend/pweb/lib/pages/dashboard/payouts/amount/feild.dart rename to frontend/pweb/lib/pages/dashboard/payouts/amount/field.dart index 8c670ef9..aeb78d7c 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/amount/feild.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/amount/field.dart @@ -5,11 +5,11 @@ import 'package:provider/provider.dart'; import 'package:pshared/utils/currency.dart'; import 'package:pweb/controllers/payments/amount_field.dart'; -import 'package:pweb/models/payment/amount/mode.dart'; import 'package:pweb/pages/dashboard/payouts/amount/mode/selector.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; + class PaymentAmountField extends StatelessWidget { const PaymentAmountField(); @@ -36,11 +36,9 @@ class PaymentAmountField extends StatelessWidget { decoration: InputDecoration( labelText: loc.amount, border: const OutlineInputBorder(), - prefixText: symbol == null ? null : '$symbol\u00A0', - helperText: switch (ui.mode) { - PaymentAmountMode.debit => loc.debitAmountLabel, - PaymentAmountMode.settlement => loc.expectedSettlementAmountLabel, - }, + prefixText: symbol == null + ? null + : withTrailingNonBreakingSpace(symbol), ), onChanged: ui.handleChanged, ), diff --git a/frontend/pweb/lib/pages/dashboard/payouts/amount/widget.dart b/frontend/pweb/lib/pages/dashboard/payouts/amount/widget.dart index 4ddaa3e7..7176d668 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/amount/widget.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/amount/widget.dart @@ -6,7 +6,8 @@ import 'package:pshared/controllers/payment/source.dart'; import 'package:pshared/provider/payment/amount.dart'; import 'package:pweb/controllers/payments/amount_field.dart'; -import 'package:pweb/pages/dashboard/payouts/amount/feild.dart'; +import 'package:pweb/pages/dashboard/payouts/amount/field.dart'; + class PaymentAmountWidget extends StatelessWidget { const PaymentAmountWidget({super.key}); diff --git a/frontend/pweb/lib/pages/dashboard/payouts/form.dart b/frontend/pweb/lib/pages/dashboard/payouts/form.dart index 4fa94eb8..f0ff3e93 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/form.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/form.dart @@ -102,7 +102,7 @@ class PaymentFormWidget extends StatelessWidget { children: [ detailsHeader, Row( - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( flex: 3, @@ -114,7 +114,7 @@ class PaymentFormWidget extends StatelessWidget { ), const SizedBox(height: _smallSpacing), Row( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( flex: 3, diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/actions.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/actions.dart index 9030afb9..9a09c800 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/actions.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/actions.dart @@ -6,7 +6,7 @@ import 'package:pshared/provider/payment/multiple/quotation.dart'; import 'package:pweb/controllers/payouts/multiple_payouts.dart'; import 'package:pweb/controllers/payouts/payout_verification.dart'; -import 'package:pweb/utils/payment/payout_verification_flow.dart'; +import 'package:pweb/utils/payment/verification_flow.dart'; import 'package:pweb/widgets/dialogs/payment_status_dialog.dart'; 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/quote_status/widgets/card.dart b/frontend/pweb/lib/pages/dashboard/payouts/quote_status/widgets/card.dart index 738e92d8..d74537a5 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/quote_status/widgets/card.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/quote_status/widgets/card.dart @@ -26,15 +26,18 @@ class QuoteStatusCard extends StatelessWidget { }); static const double _cardRadius = 12; - static const double _cardSpacing = 12; + static const double _cardSpacing = 8; static const double _iconSize = 18; @override Widget build(BuildContext context) { final theme = Theme.of(context); + final loc = AppLocalizations.of(context)!; final foregroundColor = _resolveForegroundColor(theme, statusType); final elementColor = _resolveElementColor(theme, statusType); - final statusStyle = theme.textTheme.bodyMedium?.copyWith(color: elementColor); + final statusStyle = theme.textTheme.bodyMedium?.copyWith( + color: elementColor, + ); final helperStyle = theme.textTheme.bodySmall?.copyWith( color: foregroundColor.withValues(alpha: 0.8), ); @@ -44,12 +47,10 @@ class QuoteStatusCard extends StatelessWidget { decoration: BoxDecoration( color: _resolveCardColor(theme, statusType), borderRadius: BorderRadius.circular(_cardRadius), - border: Border.all( - color: elementColor.withValues(alpha: 0.5), - ), + border: Border.all(color: elementColor.withValues(alpha: 0.5)), ), child: Row( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Padding( padding: const EdgeInsets.only(top: 2), @@ -59,7 +60,9 @@ class QuoteStatusCard extends StatelessWidget { height: _iconSize, child: CircularProgressIndicator( strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(foregroundColor), + valueColor: AlwaysStoppedAnimation( + foregroundColor, + ), ), ) : Icon( @@ -81,19 +84,15 @@ class QuoteStatusCard extends StatelessWidget { ], ), ), - if (canRefresh) - Padding( - padding: const EdgeInsets.only(left: _cardSpacing), - child: showPrimaryRefresh - ? ElevatedButton( - onPressed: canRefresh ? onRefresh : null, - child: Text(AppLocalizations.of(context)!.quoteRefresh), - ) - : TextButton( - onPressed: canRefresh ? onRefresh : null, - child: Text(AppLocalizations.of(context)!.quoteRefresh), - ), + if (canRefresh) ...[ + const SizedBox(width: _cardSpacing), + IconButton( + onPressed: onRefresh, + tooltip: loc.quoteRefresh, + icon: const Icon(Icons.refresh), + color: showPrimaryRefresh ? foregroundColor : elementColor, ), + ], ], ), ); diff --git a/frontend/pweb/lib/pages/dashboard/payouts/single/address_book/avatar.dart b/frontend/pweb/lib/pages/dashboard/payouts/single/address_book/avatar.dart index db03530e..118239f2 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/single/address_book/avatar.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/single/address_book/avatar.dart @@ -23,7 +23,7 @@ class RecipientAvatar extends StatelessWidget { @override Widget build(BuildContext context) { - final textColor = Theme.of(context).colorScheme.onPrimary; + final textColor = Theme.of(context).colorScheme.onSecondary; return Column( mainAxisAlignment: MainAxisAlignment.center, @@ -31,7 +31,7 @@ class RecipientAvatar extends StatelessWidget { CircleAvatar( radius: avatarRadius, backgroundImage: avatarUrl != null ? NetworkImage(avatarUrl!) : null, - backgroundColor: Theme.of(context).colorScheme.primary, + backgroundColor: Theme.of(context).colorScheme.primaryFixed, child: avatarUrl == null ? Text( getInitials(name), diff --git a/frontend/pweb/lib/pages/dashboard/payouts/single/address_book/short_list.dart b/frontend/pweb/lib/pages/dashboard/payouts/single/address_book/short_list.dart index 512bec73..1c281645 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/single/address_book/short_list.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/single/address_book/short_list.dart @@ -7,54 +7,56 @@ import 'package:pweb/pages/dashboard/payouts/single/address_book/avatar.dart'; class ShortListAddressBookPayout extends StatelessWidget { final List recipients; final ValueChanged onSelected; - final Widget? trailing; + final Widget? leading; const ShortListAddressBookPayout({ super.key, required this.recipients, required this.onSelected, - this.trailing, + this.leading, }); static const double _avatarRadius = 20; static const double _avatarSize = 80; - static const EdgeInsets _padding = EdgeInsets.symmetric(horizontal: 10, vertical: 8); + static const EdgeInsets _padding = EdgeInsets.symmetric( + horizontal: 10, + vertical: 8, + ); static const TextStyle _nameStyle = TextStyle(fontSize: 12); @override Widget build(BuildContext context) { - final trailingWidget = trailing; + final leadingWidget = leading; + final recipientItems = recipients.map((recipient) { + return Padding( + padding: _padding, + child: InkWell( + borderRadius: BorderRadius.circular(5), + hoverColor: Theme.of(context).colorScheme.onTertiary, + onTap: () => onSelected(recipient), + child: SizedBox( + height: _avatarSize, + width: _avatarSize, + child: RecipientAvatar( + isVisible: true, + name: recipient.name, + avatarUrl: recipient.avatarUrl, + avatarRadius: _avatarRadius, + nameStyle: _nameStyle, + ), + ), + ), + ); + }); return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( - children: - recipients.map((recipient) { - return Padding( - padding: _padding, - child: InkWell( - borderRadius: BorderRadius.circular(5), - hoverColor: Theme.of(context).colorScheme.primaryContainer, - onTap: () => onSelected(recipient), - child: SizedBox( - height: _avatarSize, - width: _avatarSize, - child: RecipientAvatar( - isVisible: true, - name: recipient.name, - avatarUrl: recipient.avatarUrl, - avatarRadius: _avatarRadius, - nameStyle: _nameStyle, - ), - ), - ), - ); - }).toList() - ..addAll( - trailingWidget == null - ? const [] - : [Padding(padding: _padding, child: trailingWidget)], - ), + children: [ + if (leadingWidget != null) + Padding(padding: _padding, child: leadingWidget), + ...recipientItems, + ], ), ); } diff --git a/frontend/pweb/lib/pages/dashboard/payouts/single/address_book/widget.dart b/frontend/pweb/lib/pages/dashboard/payouts/single/address_book/widget.dart index df80ca77..c6d42d95 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/single/address_book/widget.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/single/address_book/widget.dart @@ -21,10 +21,7 @@ import 'package:pweb/generated/i18n/app_localizations.dart'; class AddressBookPayout extends StatefulWidget { final ValueChanged onSelected; - const AddressBookPayout({ - super.key, - required this.onSelected, - }); + const AddressBookPayout({super.key, required this.onSelected}); @override State createState() => _AddressBookPayoutState(); @@ -71,6 +68,7 @@ class _AddressBookPayoutState extends State { provider.setCurrentObject(null); context.pushNamed(PayoutRoutes.addRecipient); } + final filteredRecipients = filterRecipients( recipients: recipients, query: _query, @@ -81,16 +79,18 @@ class _AddressBookPayoutState extends State { } if (provider.error != null) { - return Center(child: Text(loc.notificationError(provider.error ?? loc.noErrorInformation))); + return Center( + child: Text( + loc.notificationError(provider.error ?? loc.noErrorInformation), + ), + ); } return SizedBox( height: _isExpanded ? _expandedHeight : _collapsedHeight, child: Card( margin: const EdgeInsets.all(_cardMargin), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), elevation: 4, color: Theme.of(context).colorScheme.onSecondary, child: Padding( @@ -105,27 +105,27 @@ class _AddressBookPayoutState extends State { const SizedBox(height: _spacingBetween), Expanded( child: recipients.isEmpty - ? Center( - child: AddRecipientTile( - label: loc.addRecipient, - onTap: onAddRecipient, - ), - ) - : _isExpanded && filteredRecipients.isEmpty + ? Center( + child: AddRecipientTile( + label: loc.addRecipient, + onTap: onAddRecipient, + ), + ) + : _isExpanded && filteredRecipients.isEmpty ? AddressBookPlaceholder(text: loc.noRecipientsFound) : _isExpanded - ? LongListAddressBookPayout( - filteredRecipients: filteredRecipients, - onSelected: widget.onSelected, - ) - : ShortListAddressBookPayout( - recipients: recipients, - onSelected: widget.onSelected, - trailing: AddRecipientTile( - label: loc.addRecipient, - onTap: onAddRecipient, - ), + ? LongListAddressBookPayout( + filteredRecipients: filteredRecipients, + onSelected: widget.onSelected, + ) + : ShortListAddressBookPayout( + recipients: recipients, + onSelected: widget.onSelected, + leading: AddRecipientTile( + label: loc.addRecipient, + onTap: onAddRecipient, ), + ), ), ], ), 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/page.dart b/frontend/pweb/lib/pages/payout_page/send/page.dart index 7118ccf8..cea285e6 100644 --- a/frontend/pweb/lib/pages/payout_page/send/page.dart +++ b/frontend/pweb/lib/pages/payout_page/send/page.dart @@ -5,19 +5,21 @@ import 'package:pshared/models/recipient/recipient.dart'; import 'package:pweb/widgets/sidebar/destinations.dart'; import 'package:pweb/controllers/payments/page_ui.dart'; -import 'package:pweb/pages/payout_page/send/page_handlers.dart'; +import 'package:pweb/utils/payment/page_handlers.dart'; import 'package:pweb/pages/payout_page/send/page_view.dart'; class PaymentPage extends StatefulWidget { final ValueChanged? onBack; final PaymentType? initialPaymentType; + final String? initialDestinationLedgerAccountRef; final PayoutDestination fallbackDestination; const PaymentPage({ super.key, this.onBack, this.initialPaymentType, + this.initialDestinationLedgerAccountRef, this.fallbackDestination = PayoutDestination.dashboard, }); @@ -34,7 +36,11 @@ class _PaymentPageState extends State { _uiController = PaymentPageUiController(); WidgetsBinding.instance.addPostFrameCallback( - (_) => initializePaymentPage(context, widget.initialPaymentType), + (_) => initializePaymentPage( + context, + widget.initialPaymentType, + destinationLedgerAccountRef: widget.initialDestinationLedgerAccountRef, + ), ); } diff --git a/frontend/pweb/lib/pages/payout_page/send/page_view.dart b/frontend/pweb/lib/pages/payout_page/send/page_view.dart index 442719c9..7915609a 100644 --- a/frontend/pweb/lib/pages/payout_page/send/page_view.dart +++ b/frontend/pweb/lib/pages/payout_page/send/page_view.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:pshared/models/recipient/recipient.dart'; +import 'package:pshared/provider/payment/flow.dart'; import 'package:pshared/provider/payment/quotation/quotation.dart'; import 'package:pshared/provider/recipient/pmethods.dart'; import 'package:pshared/provider/recipient/provider.dart'; @@ -14,6 +15,7 @@ import 'package:pweb/controllers/payments/page_ui.dart'; import 'package:pweb/controllers/payouts/payout_verification.dart'; import 'package:pweb/models/state/control_state.dart'; + class PaymentPageView extends StatelessWidget { final PaymentPageUiController uiController; final ValueChanged? onBack; @@ -47,6 +49,7 @@ class PaymentPageView extends StatelessWidget { final uiController = context.watch(); final methodsProvider = context.watch(); final recipientProvider = context.watch(); + final flowProvider = context.watch(); final quotationProvider = context.watch(); final verificationController = context .watch(); @@ -58,10 +61,12 @@ class PaymentPageView extends StatelessWidget { recipients: recipientProvider.recipients, query: uiController.query, ); + final hasDestinationSelection = + flowProvider.selectedPaymentData != null; final sendState = verificationController.isCooldownActiveFor(verificationContextKey) ? ControlState.disabled - : (recipient == null + : (!hasDestinationSelection ? ControlState.disabled : ControlState.enabled); 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 new file mode 100644 index 00000000..ae741a7e --- /dev/null +++ b/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/manual_details.dart @@ -0,0 +1,49 @@ +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/payout_page/send/widgets/payment_info/header.dart'; +import 'package:pweb/models/state/control_state.dart'; +import 'package:pweb/models/state/visibility.dart'; +import 'package:pweb/utils/dimensions.dart'; + + +class PaymentInfoManualDetailsSection extends StatelessWidget { + final AppDimensions dimensions; + final String title; + final VisibilityState titleVisibility; + final PaymentMethodData data; + + const PaymentInfoManualDetailsSection({ + super.key, + required this.dimensions, + required this.title, + required this.titleVisibility, + required this.data, + }); + + @override + Widget build(BuildContext context) { + final entry = RecipientMethodDraft(type: data.type, data: data); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PaymentInfoHeader( + dimensions: dimensions, + title: title, + visibility: titleVisibility, + ), + PaymentMethodPanel( + selectedType: data.type, + selectedIndex: 0, + entries: [entry], + 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/send/widgets/payment_info/section.dart b/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/section.dart index fd4f6399..51419c09 100644 --- a/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/section.dart +++ b/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/section.dart @@ -6,6 +6,7 @@ import 'package:pshared/provider/payment/flow.dart'; import 'package:pweb/pages/payout_page/send/widgets/payment_info/methods_section.dart'; import 'package:pweb/pages/payout_page/send/widgets/payment_info/methods_state.dart'; +import 'package:pweb/pages/payout_page/send/widgets/payment_info/manual_details.dart'; import 'package:pweb/pages/payout_page/send/widgets/payment_info/no_methods.dart'; import 'package:pweb/pages/payout_page/send/widgets/payment_info/no_recipient.dart'; import 'package:pweb/models/state/visibility.dart'; @@ -35,8 +36,9 @@ class PaymentInfoSection extends StatelessWidget { Widget build(BuildContext context) { final loc = AppLocalizations.of(context)!; final flowProvider = context.watch(); + final manualData = flowProvider.manualPaymentData; - if (!flowProvider.hasRecipient) { + if (!flowProvider.hasRecipient && manualData == null) { return PaymentInfoNoRecipientSection( dimensions: dimensions, title: loc.paymentInfo, @@ -44,6 +46,15 @@ class PaymentInfoSection extends StatelessWidget { ); } + if (!flowProvider.hasRecipient && manualData != null) { + return PaymentInfoManualDetailsSection( + dimensions: dimensions, + title: loc.paymentInfo, + titleVisibility: titleVisibility, + data: manualData, + ); + } + final methods = flowProvider.methodsForRecipient; final types = visiblePaymentTypes; diff --git a/frontend/pweb/lib/pages/payout_page/send/widgets/recipient/section.dart b/frontend/pweb/lib/pages/payout_page/send/widgets/recipient/section.dart index ad5f8f0e..17094754 100644 --- a/frontend/pweb/lib/pages/payout_page/send/widgets/recipient/section.dart +++ b/frontend/pweb/lib/pages/payout_page/send/widgets/recipient/section.dart @@ -81,7 +81,7 @@ class RecipientSection extends StatelessWidget { ShortListAddressBookPayout( recipients: recipientProvider.recipients, onSelected: onRecipientSelected, - trailing: AddRecipientTile( + leading: AddRecipientTile( label: loc.addRecipient, onTap: onAddRecipient, ), diff --git a/frontend/pweb/lib/pages/payout_page/send/widgets/recipient_details_card.dart b/frontend/pweb/lib/pages/payout_page/send/widgets/recipient_details_card.dart index f5bd0cc3..971062ae 100644 --- a/frontend/pweb/lib/pages/payout_page/send/widgets/recipient_details_card.dart +++ b/frontend/pweb/lib/pages/payout_page/send/widgets/recipient_details_card.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + import 'package:pshared/models/recipient/recipient.dart'; +import 'package:pshared/provider/payment/flow.dart'; import 'package:pshared/provider/recipient/provider.dart'; import 'package:pweb/pages/payout_page/send/widgets/payment_info/section.dart'; @@ -46,24 +49,30 @@ class PaymentRecipientDetailsCard extends StatelessWidget { @override Widget build(BuildContext context) { + final flowProvider = context.watch(); + final isRecipientSelectionLocked = + !flowProvider.hasRecipient && flowProvider.manualPaymentData != null; + return PaymentSectionCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - RecipientSection( - recipient: recipient, - dimensions: dimensions, - recipientProvider: recipientProvider, - searchQuery: searchQuery, - filteredRecipients: filteredRecipients, - searchController: searchController, - searchFocusNode: searchFocusNode, - onSearchChanged: onSearchChanged, - onRecipientSelected: onRecipientSelected, - onRecipientCleared: onRecipientCleared, - onAddRecipient: onAddRecipient, - ), - SizedBox(height: dimensions.paddingMedium), + if (!isRecipientSelectionLocked) ...[ + RecipientSection( + recipient: recipient, + dimensions: dimensions, + recipientProvider: recipientProvider, + searchQuery: searchQuery, + filteredRecipients: filteredRecipients, + searchController: searchController, + searchFocusNode: searchFocusNode, + onSearchChanged: onSearchChanged, + onRecipientSelected: onRecipientSelected, + onRecipientCleared: onRecipientCleared, + onAddRecipient: onAddRecipient, + ), + SizedBox(height: dimensions.paddingMedium), + ], PaymentInfoSection( dimensions: dimensions, titleVisibility: VisibilityState.hidden, diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/buttons.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/buttons.dart index 4280a9b7..8f70a4e0 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/buttons.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/buttons.dart @@ -2,9 +2,10 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:pshared/controllers/payment/source.dart'; + import 'package:pweb/pages/payout_page/wallet/edit/buttons/send.dart'; import 'package:pweb/pages/payout_page/wallet/edit/buttons/top_up.dart'; -import 'package:pshared/provider/payment/wallets.dart'; class ButtonsWalletWidget extends StatelessWidget { @@ -12,25 +13,20 @@ class ButtonsWalletWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final provider = context.watch(); - - if (provider.wallets.isEmpty) return const SizedBox.shrink(); + final source = context.watch(); + if (!source.hasSources) return const SizedBox.shrink(); return Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Expanded( - child: SendPayoutButton(), - ), - VerticalDivider( - color: Theme.of(context).colorScheme.primary, - thickness: 1, - width: 10, - ), - Expanded( - child: TopUpButton(), - ), - ], + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Expanded(child: SendPayoutButton()), + VerticalDivider( + color: Theme.of(context).colorScheme.primary, + thickness: 1, + width: 10, + ), + Expanded(child: TopUpButton()), + ], ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/send.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/send.dart index 819c2e42..2d1b17b8 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/send.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/send.dart @@ -4,7 +4,8 @@ import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/controllers/balance_mask/wallets.dart'; +import 'package:pshared/controllers/payment/source.dart'; +import 'package:pshared/models/payment/source_type.dart'; import 'package:pshared/models/payment/type.dart'; import 'package:pweb/app/router/payout_routes.dart'; @@ -18,24 +19,27 @@ class SendPayoutButton extends StatelessWidget { @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context)!; + final source = context.watch(); + + final sourceType = source.selectedType; + final paymentType = switch (sourceType) { + PaymentSourceType.wallet => PaymentType.wallet, + PaymentSourceType.ledger => PaymentType.ledger, + _ => null, + }; + return ElevatedButton( - style: ElevatedButton.styleFrom( - shadowColor: null, - elevation: 0, - ), - onPressed: () { - final wallets = context.read(); - final wallet = wallets.selectedWallet; - - if (wallet != null) { - context.pushNamed( - PayoutRoutes.payment, - queryParameters: PayoutRoutes.buildQueryParameters( - paymentType: PaymentType.wallet, - ), - ); - } - }, + style: ElevatedButton.styleFrom(shadowColor: null, elevation: 0), + onPressed: paymentType == null + ? null + : () { + context.pushNamed( + PayoutRoutes.payment, + queryParameters: PayoutRoutes.buildQueryParameters( + paymentType: paymentType, + ), + ); + }, child: Text(loc.payoutNavSendPayout), ); } diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/top_up.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/top_up.dart index 618075eb..f70cf1f1 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/top_up.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/top_up.dart @@ -1,35 +1,55 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + import 'package:provider/provider.dart'; -import 'package:pshared/controllers/balance_mask/wallets.dart'; +import 'package:pshared/controllers/payment/source.dart'; +import 'package:pshared/models/payment/source_type.dart'; +import 'package:pshared/models/payment/type.dart'; +import 'package:pshared/provider/recipient/provider.dart'; import 'package:pweb/app/router/payout_routes.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; -class TopUpButton extends StatelessWidget{ +class TopUpButton extends StatelessWidget { const TopUpButton({super.key}); @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context)!; + final source = context.watch(); + final selectedType = source.selectedType; + final selectedLedger = source.selectedLedgerAccount; + final canTopUp = + selectedType == PaymentSourceType.wallet || + (selectedType == PaymentSourceType.ledger && selectedLedger != null); + return ElevatedButton( - style: ElevatedButton.styleFrom( - shadowColor: null, - elevation: 0, - ), - onPressed: () { - final wallet = context.read().selectedWallet; - if (wallet == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(loc.noWalletSelected)), - ); - return; - } - context.pushToWalletTopUp(); - }, + style: ElevatedButton.styleFrom(shadowColor: null, elevation: 0), + onPressed: !canTopUp + ? null + : () { + if (selectedType == PaymentSourceType.wallet) { + context.pushToWalletTopUp(); + return; + } + + if (selectedType == PaymentSourceType.ledger && + selectedLedger != null) { + context.read().setCurrentObject(null); + context.pushNamed( + PayoutRoutes.payment, + queryParameters: PayoutRoutes.buildQueryParameters( + paymentType: PaymentType.ledger, + destinationLedgerAccountRef: + selectedLedger.ledgerAccountRef, + ), + ); + } + }, child: Text(loc.topUpBalance), ); } diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/fields.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields.dart index 530ad26e..6331396b 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/fields.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields.dart @@ -1,55 +1,30 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/controllers/balance_mask/wallets.dart'; - -import 'package:pweb/pages/dashboard/buttons/balance/amount.dart'; -import 'package:pweb/widgets/refresh_balance/wallet.dart'; +import 'package:pshared/controllers/payment/source.dart'; +import 'package:pweb/pages/payout_page/wallet/edit/fields/ledger/section.dart'; +import 'package:pweb/pages/payout_page/wallet/edit/fields/wallet/wallet_section.dart'; class WalletEditFields extends StatelessWidget { const WalletEditFields({super.key}); @override Widget build(BuildContext context) { - return Consumer( - builder: (context, controller, _) { - final wallet = controller.selectedWallet; - - if (wallet == null) { - return SizedBox.shrink(); + return Consumer( + builder: (context, sourceController, _) { + final wallet = sourceController.selectedWallet; + if (wallet != null) { + return WalletSection(wallet: wallet); } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: BalanceAmount( - wallet: wallet, - onToggleMask: () => controller.toggleBalanceMask(wallet.id), - ), - ), - WalletBalanceRefreshButton(walletRef: wallet.id), - ], - ), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(wallet.walletUserID, style: Theme.of(context).textTheme.bodyLarge), - IconButton( - icon: Icon(Icons.copy), - iconSize: 18, - onPressed: () => Clipboard.setData(ClipboardData(text: wallet.walletUserID)), - ), - ], - ), - ], - ); + final ledger = sourceController.selectedLedgerAccount; + if (ledger != null) { + return LedgerSection(ledger: ledger); + } + + return const SizedBox.shrink(); }, ); } 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 new file mode 100644 index 00000000..a6639e1c --- /dev/null +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/balance_formatter.dart @@ -0,0 +1,30 @@ +import 'package:flutter/widgets.dart'; + +import 'package:pshared/models/ledger/account.dart'; +import 'package:pshared/utils/currency.dart'; + +import 'package:pweb/utils/money_display.dart'; + + +class LedgerBalanceFormatter { + const LedgerBalanceFormatter._(); + + static String format(BuildContext context, LedgerAccount account) { + return formatMoneyUi(context, account.balance?.balance); + } + + static String formatMasked(LedgerAccount account) { + final currency = account.currency.trim(); + if (currency.isEmpty) return '••••'; + + try { + final symbol = currencyCodeToSymbol(currencyStringToCode(currency)); + if (symbol.trim().isEmpty) { + return '•••• $currency'; + } + return '•••• $symbol'; + } catch (_) { + return '•••• $currency'; + } + } +} diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/balance_row.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/balance_row.dart new file mode 100644 index 00000000..322c3944 --- /dev/null +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/balance_row.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + + +class LedgerBalanceRow extends StatelessWidget { + final String balance; + final bool isMasked; + final VoidCallback onToggleMask; + + const LedgerBalanceRow({ + super.key, + required this.balance, + required this.isMasked, + required this.onToggleMask, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Flexible( + child: Text( + balance, + style: Theme.of( + context, + ).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), + ), + ), + const SizedBox(width: 8), + GestureDetector( + onTap: onToggleMask, + child: Icon( + isMasked ? Icons.visibility_off : Icons.visibility, + size: 22, + ), + ), + ], + ); + } +} 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 new file mode 100644 index 00000000..d53bdbb0 --- /dev/null +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/section.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/controllers/balance_mask/ledger_accounts.dart'; +import 'package:pshared/models/ledger/account.dart'; + +import 'package:pweb/pages/payout_page/wallet/edit/fields/ledger/balance_formatter.dart'; +import 'package:pweb/pages/payout_page/wallet/edit/fields/ledger/balance_row.dart'; +import 'package:pweb/pages/payout_page/wallet/edit/fields/shared/copyable_value_row.dart'; +import 'package:pweb/widgets/refresh_balance/ledger.dart'; + + +class LedgerSection extends StatelessWidget { + final LedgerAccount ledger; + + const LedgerSection({super.key, required this.ledger}); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, balanceMask, _) { + final isMasked = balanceMask.isBalanceMasked(ledger.ledgerAccountRef); + final accountCode = ledger.accountCode.trim(); + final hasAccountCode = accountCode.isNotEmpty; + final balance = isMasked + ? LedgerBalanceFormatter.formatMasked(ledger) + : LedgerBalanceFormatter.format(context, ledger); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: LedgerBalanceRow( + balance: balance, + isMasked: isMasked, + onToggleMask: () { + balanceMask.toggleBalanceMask(ledger.ledgerAccountRef); + }, + ), + ), + LedgerBalanceRefreshButton( + ledgerAccountRef: ledger.ledgerAccountRef, + ), + ], + ), + const SizedBox(height: 8), + CopyableValueRow( + value: hasAccountCode ? accountCode : '-', + canCopy: hasAccountCode, + onCopy: hasAccountCode + ? () { + Clipboard.setData(ClipboardData(text: accountCode)); + } + : null, + overflow: TextOverflow.ellipsis, + wrapValueWithFlexible: true, + ), + ], + ); + }, + ); + } +} diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/shared/copyable_value_row.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/shared/copyable_value_row.dart new file mode 100644 index 00000000..cb2dbd9a --- /dev/null +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/shared/copyable_value_row.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + + +class CopyableValueRow extends StatelessWidget { + final String value; + final bool canCopy; + final VoidCallback? onCopy; + final TextOverflow overflow; + final bool wrapValueWithFlexible; + + const CopyableValueRow({ + super.key, + required this.value, + required this.canCopy, + required this.onCopy, + this.overflow = TextOverflow.visible, + this.wrapValueWithFlexible = false, + }); + + @override + Widget build(BuildContext context) { + final valueText = Text( + value, + style: Theme.of(context).textTheme.bodyLarge, + overflow: overflow, + ); + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (wrapValueWithFlexible) Flexible(child: valueText) else valueText, + IconButton( + icon: const Icon(Icons.copy), + iconSize: 18, + onPressed: canCopy ? onCopy : null, + ), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/wallet/wallet_section.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/wallet/wallet_section.dart new file mode 100644 index 00000000..fe34e23a --- /dev/null +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/wallet/wallet_section.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/controllers/balance_mask/wallets.dart'; +import 'package:pshared/models/payment/wallet.dart'; + +import 'package:pweb/pages/dashboard/buttons/balance/amount.dart'; +import 'package:pweb/pages/payout_page/wallet/edit/fields/shared/copyable_value_row.dart'; +import 'package:pweb/widgets/refresh_balance/wallet.dart'; + + +class WalletSection extends StatelessWidget { + final Wallet wallet; + + const WalletSection({super.key, required this.wallet}); + + @override + Widget build(BuildContext context) { + final depositAddress = wallet.depositAddress?.trim(); + final hasDepositAddress = + depositAddress != null && depositAddress.isNotEmpty; + final copyAddress = hasDepositAddress ? depositAddress : ''; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: BalanceAmount( + wallet: wallet, + onToggleMask: () { + context.read().toggleBalanceMask( + wallet.id, + ); + }, + ), + ), + WalletBalanceRefreshButton(walletRef: wallet.id), + ], + ), + const SizedBox(height: 8), + CopyableValueRow( + value: hasDepositAddress ? depositAddress : '-', + canCopy: hasDepositAddress, + onCopy: hasDepositAddress + ? () { + Clipboard.setData(ClipboardData(text: copyAddress)); + } + : null, + ), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/header.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/header.dart index a725efad..9e92bb8c 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/header.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/header.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/controllers/balance_mask/wallets.dart'; +import 'package:pshared/controllers/payment/source.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; @@ -12,15 +12,20 @@ class WalletEditHeader extends StatelessWidget { @override Widget build(BuildContext context) { - final controller = context.watch(); + final controller = context.watch(); final wallet = controller.selectedWallet; + final ledger = controller.selectedLedgerAccount; final loc = AppLocalizations.of(context)!; - if (wallet == null) { + if (wallet == null && ledger == null) { return const SizedBox.shrink(); } final theme = Theme.of(context); + final title = wallet != null + ? loc.paymentTypeCryptoWallet + : loc.paymentTypeLedger; + final subtitle = wallet?.tokenSymbol; return Row( spacing: 8, @@ -32,14 +37,14 @@ class WalletEditHeader extends StatelessWidget { spacing: 4, children: [ Text( - loc.paymentTypeCryptoWallet, - style: theme.textTheme.headlineMedium!.copyWith( + title, + style: theme.textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, ), ), - if (wallet.tokenSymbol != null) + if (subtitle != null && subtitle.trim().isNotEmpty) Text( - wallet.tokenSymbol!, + subtitle, style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/page.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/page.dart index 949202d5..4328529d 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/page.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/page.dart @@ -2,17 +2,16 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/controllers/balance_mask/wallets.dart'; +import 'package:pshared/controllers/payment/source.dart'; import 'package:pweb/pages/payout_page/wallet/edit/buttons/buttons.dart'; import 'package:pweb/pages/payout_page/wallet/edit/fields.dart'; import 'package:pweb/pages/payout_page/wallet/edit/header.dart'; -import 'package:pweb/pages/payout_page/wallet/history/history.dart'; import 'package:pweb/utils/dimensions.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; - +//TODO make this page more generic and reusable class WalletEditPage extends StatelessWidget { final VoidCallback onBack; @@ -23,11 +22,11 @@ class WalletEditPage extends StatelessWidget { final dimensions = AppDimensions(); final loc = AppLocalizations.of(context)!; - return Consumer( + return Consumer( builder: (context, controller, child) { - final wallet = controller.selectedWallet; - - if (wallet == null) { + final sourceType = controller.selectedType; + + if (sourceType == null) { return Center(child: Text(loc.noWalletSelected)); } @@ -36,11 +35,15 @@ class WalletEditPage extends StatelessWidget { child: Column( children: [ ConstrainedBox( - constraints: BoxConstraints(maxWidth: dimensions.maxContentWidth), + constraints: BoxConstraints( + maxWidth: dimensions.maxContentWidth, + ), child: Material( elevation: dimensions.elevationSmall, color: Theme.of(context).colorScheme.onSecondary, - borderRadius: BorderRadius.circular(dimensions.borderRadiusMedium), + borderRadius: BorderRadius.circular( + dimensions.borderRadiusMedium, + ), child: Padding( padding: EdgeInsets.all(dimensions.paddingLarge), child: SingleChildScrollView( @@ -55,19 +58,12 @@ class WalletEditPage extends StatelessWidget { WalletEditFields(), const SizedBox(height: 24), ButtonsWalletWidget(), - const SizedBox(height: 24), ], ), ), ), ), ), - const SizedBox(height: 24), - Expanded( - child: SingleChildScrollView( - child: WalletHistory(wallet: wallet), - ), - ), ], ), ); diff --git a/frontend/pweb/lib/pages/payout_page/wallet/history/history.dart b/frontend/pweb/lib/pages/payout_page/wallet/history/history.dart index db656400..559e6b20 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/history/history.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/history/history.dart @@ -2,118 +2,87 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/models/payment/wallet.dart'; +import 'package:pshared/models/payment/source_type.dart'; +import 'package:pshared/provider/payment/payments.dart'; -import 'package:pweb/pages/payout_page/wallet/history/filters.dart'; -import 'package:pweb/pages/payout_page/wallet/history/table.dart'; -import 'package:pweb/controllers/operations/wallet_transactions.dart'; -import 'package:pweb/providers/wallet_transactions.dart'; +import 'package:pweb/controllers/operations/report_operations.dart'; +import 'package:pweb/models/state/load_more_state.dart'; +import 'package:pweb/pages/report/cards/list.dart'; +import 'package:pweb/pages/report/operations/actions.dart'; +import 'package:pweb/pages/report/operations/states/error.dart'; +import 'package:pweb/pages/report/operations/states/loading.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; -class WalletHistory extends StatefulWidget { - final Wallet wallet; +class WalletHistory extends StatelessWidget { + final String sourceRef; + final PaymentSourceType sourceType; + final List sourceRefs; - const WalletHistory({super.key, required this.wallet}); - - @override - State createState() => _WalletHistoryState(); -} - -class _WalletHistoryState extends State { - @override - void initState() { - super.initState(); - _load(); - } - - @override - void didUpdateWidget(covariant WalletHistory oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.wallet.id != widget.wallet.id) { - _load(); - } - } - - void _load() { - WidgetsBinding.instance.addPostFrameCallback((_) { - context - .read() - .load(walletId: widget.wallet.id); - }); - } - - Future _pickRange() async { - final provider = context.read(); - final now = DateTime.now(); - final initial = provider.dateRange ?? - DateTimeRange( - start: now.subtract(const Duration(days: 30)), - end: now, - ); - - final picked = await showDateRangePicker( - context: context, - firstDate: now.subtract(const Duration(days: 365)), - lastDate: now.add(const Duration(days: 1)), - initialDateRange: initial, - ); - - if (picked != null) { - provider.setDateRange(picked); - } - } + const WalletHistory({ + super.key, + required this.sourceRef, + required this.sourceType, + required this.sourceRefs, + }); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProxyProvider< + PaymentsProvider, + ReportOperationsController + >( + create: (_) => ReportOperationsController(), + update: (_, payments, controller) => controller! + ..update( + payments, + sourceType: sourceType, + sourceRef: sourceRef, + sourceRefs: sourceRefs, + ), + child: const _WalletHistoryContent(), + ); + } +} + +class _WalletHistoryContent extends StatelessWidget { + const _WalletHistoryContent(); @override Widget build(BuildContext context) { - final theme = Theme.of(context); final loc = AppLocalizations.of(context)!; - return Consumer( - builder: (context, provider, child) { - if (provider.isLoading) { - return const Padding( - padding: EdgeInsets.all(16.0), - child: Center(child: CircularProgressIndicator()), + return Consumer( + builder: (context, controller, child) { + if (controller.isLoading) { + return const OperationHistoryLoading(); + } + + if (controller.error != null) { + final message = + controller.error?.toString() ?? loc.noErrorInformation; + return OperationHistoryError( + message: loc.notificationError(message), + retryLabel: loc.retry, + onRetry: controller.refresh, ); } - if (provider.error != null) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - loc.failedToLoadHistory, - style: theme.textTheme.titleMedium! - .copyWith(color: theme.colorScheme.error), - ), - const SizedBox(height: 8), - Text(loc.notificationError(provider.error ?? loc.noErrorInformation)), - const SizedBox(height: 8), - OutlinedButton( - onPressed: _load, - child: Text(loc.retry), - ), - ], - ), - ); - } - - final transactions = provider.filteredTransactions; - - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - WalletHistoryFilters( - provider: provider, - onPickRange: _pickRange, - ), - const SizedBox(height: 12), - WalletTransactionsTable(transactions: transactions), - ], + final hasLoadMore = controller.loadMoreState != LoadMoreState.hidden; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + OperationsCardsList( + operations: controller.filteredOperations, + onTap: (operation) => openPaymentDetails(context, operation), + loadMoreState: controller.loadMoreState, + onLoadMore: hasLoadMore ? controller.loadMore : null, + ), + ], + ), ); }, ); 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/content.dart b/frontend/pweb/lib/pages/report/details/content.dart index d882b0b6..6e280ebf 100644 --- a/frontend/pweb/lib/pages/report/details/content.dart +++ b/frontend/pweb/lib/pages/report/details/content.dart @@ -13,7 +13,6 @@ import 'package:pweb/generated/i18n/app_localizations.dart'; class PaymentDetailsContent extends StatelessWidget { final Payment payment; final VoidCallback onBack; - final VoidCallback? onDownloadAct; final bool Function(PaymentExecutionOperation operation)? canDownloadOperationDocument; final ValueChanged? onDownloadOperationDocument; @@ -22,7 +21,6 @@ class PaymentDetailsContent extends StatelessWidget { super.key, required this.payment, required this.onBack, - this.onDownloadAct, this.canDownloadOperationDocument, this.onDownloadOperationDocument, }); @@ -37,7 +35,7 @@ class PaymentDetailsContent extends StatelessWidget { children: [ PaymentDetailsHeader(title: loc.paymentInfo, onBack: onBack), const SizedBox(height: 16), - PaymentSummaryCard(payment: payment, onDownloadAct: onDownloadAct), + PaymentSummaryCard(payment: payment), const SizedBox(height: 16), PaymentDetailsSections( payment: payment, diff --git a/frontend/pweb/lib/pages/report/details/page.dart b/frontend/pweb/lib/pages/report/details/page.dart index 998d2b3b..0e529d03 100644 --- a/frontend/pweb/lib/pages/report/details/page.dart +++ b/frontend/pweb/lib/pages/report/details/page.dart @@ -64,17 +64,6 @@ class _PaymentDetailsView extends StatelessWidget { return PaymentDetailsContent( payment: payment, onBack: () => _handleBack(context), - onDownloadAct: controller.canDownload - ? () { - final request = controller.primaryOperationDocumentRequest; - if (request == null) return; - downloadPaymentAct( - context, - gatewayService: request.gatewayService, - operationRef: request.operationRef, - ); - } - : null, canDownloadOperationDocument: controller.canDownloadOperationDocument, onDownloadOperationDocument: (operation) { 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/section.dart b/frontend/pweb/lib/pages/report/details/sections/operations/section.dart index 792907cf..0f993f5b 100644 --- a/frontend/pweb/lib/pages/report/details/sections/operations/section.dart +++ b/frontend/pweb/lib/pages/report/details/sections/operations/section.dart @@ -44,12 +44,12 @@ class PaymentOperationsSection extends StatelessWidget { ); if (i < operations.length - 1) { children.addAll([ - const SizedBox(height: 8), + const SizedBox(height: 10), Divider( height: 1, color: Theme.of(context).dividerColor.withAlpha(20), ), - const SizedBox(height: 8), + const SizedBox(height: 10), ]); } } 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 ae3c19f8..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,25 +9,23 @@ 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'; import 'package:pweb/generated/i18n/app_localizations.dart'; + class PaymentSummaryCard extends StatelessWidget { final Payment payment; - final VoidCallback? onDownloadAct; - const PaymentSummaryCard({ - super.key, - required this.payment, - this.onDownloadAct, - }); + const PaymentSummaryCard({super.key, required this.payment}); @override 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)); @@ -37,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); @@ -77,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), @@ -93,14 +93,6 @@ class PaymentSummaryCard extends StatelessWidget { text: feeText, muted: true, ), - if (onDownloadAct != null) ...[ - const SizedBox(height: 12), - OutlinedButton.icon( - onPressed: onDownloadAct, - icon: const Icon(Icons.download), - label: Text(loc.downloadAct), - ), - ], if (showPaymentId) ...[ const SizedBox(height: 16), Divider(color: theme.dividerColor.withAlpha(35), height: 1), 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/providers/multiple_payouts.dart b/frontend/pweb/lib/providers/multiple_payouts.dart index d82b8568..e3537670 100644 --- a/frontend/pweb/lib/providers/multiple_payouts.dart +++ b/frontend/pweb/lib/providers/multiple_payouts.dart @@ -13,8 +13,9 @@ import 'package:pshared/utils/payment/quote_helpers.dart'; import 'package:pweb/models/payment/multiple_payouts/csv_row.dart'; import 'package:pweb/models/payment/multiple_payouts/state.dart'; -import 'package:pweb/utils/payment/multiple_csv_parser.dart'; -import 'package:pweb/utils/payment/multiple_intent_builder.dart'; +import 'package:pweb/utils/payment/multiple/csv_parser.dart'; +import 'package:pweb/utils/payment/multiple/intent_builder.dart'; + class MultiplePayoutsProvider extends ChangeNotifier { final MultipleCsvParser _csvParser; diff --git a/frontend/pweb/lib/providers/wallet_transactions.dart b/frontend/pweb/lib/providers/wallet_transactions.dart index e9053e1a..7d153dbd 100644 --- a/frontend/pweb/lib/providers/wallet_transactions.dart +++ b/frontend/pweb/lib/providers/wallet_transactions.dart @@ -13,6 +13,7 @@ class WalletTransactionsProvider extends ChangeNotifier { bool _isLoading = false; String? _error; String? _walletId; + int _loadSeq = 0; List get transactions => List.unmodifiable(_transactions); bool get isLoading => _isLoading; @@ -20,18 +21,28 @@ class WalletTransactionsProvider extends ChangeNotifier { String? get walletId => _walletId; Future load({String? walletId}) async { + final targetWalletId = walletId ?? _walletId; + final requestSeq = ++_loadSeq; + _walletId = targetWalletId; _isLoading = true; _error = null; notifyListeners(); try { - _walletId = walletId ?? _walletId; - _transactions = await _service.fetchHistory(walletId: _walletId); + final fetched = await _service.fetchHistory(walletId: targetWalletId); + if (requestSeq != _loadSeq) return; + + _transactions = targetWalletId == null + ? fetched + : fetched.where((tx) => tx.walletId == targetWalletId).toList(); } catch (e) { + if (requestSeq != _loadSeq) return; _error = e.toString(); } finally { - _isLoading = false; - notifyListeners(); + if (requestSeq == _loadSeq) { + _isLoading = false; + notifyListeners(); + } } } } 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/multiple_csv_parser.dart b/frontend/pweb/lib/utils/payment/multiple/csv_parser.dart similarity index 100% rename from frontend/pweb/lib/utils/payment/multiple_csv_parser.dart rename to frontend/pweb/lib/utils/payment/multiple/csv_parser.dart diff --git a/frontend/pweb/lib/utils/payment/multiple_intent_builder.dart b/frontend/pweb/lib/utils/payment/multiple/intent_builder.dart similarity index 100% rename from frontend/pweb/lib/utils/payment/multiple_intent_builder.dart rename to frontend/pweb/lib/utils/payment/multiple/intent_builder.dart diff --git a/frontend/pweb/lib/pages/payout_page/send/page_handlers.dart b/frontend/pweb/lib/utils/payment/page_handlers.dart similarity index 82% rename from frontend/pweb/lib/pages/payout_page/send/page_handlers.dart rename to frontend/pweb/lib/utils/payment/page_handlers.dart index 76dac6b4..c29a1702 100644 --- a/frontend/pweb/lib/pages/payout_page/send/page_handlers.dart +++ b/frontend/pweb/lib/utils/payment/page_handlers.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; +import 'package:pshared/models/payment/methods/ledger.dart'; import 'package:pshared/models/payment/type.dart'; import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/provider/payment/flow.dart'; @@ -17,14 +18,30 @@ import 'package:pweb/widgets/dialogs/payment_status_dialog.dart'; import 'package:pweb/controllers/payments/page.dart'; import 'package:pweb/controllers/payments/page_ui.dart'; import 'package:pweb/controllers/payouts/payout_verification.dart'; -import 'package:pweb/utils/payment/payout_verification_flow.dart'; +import 'package:pweb/utils/payment/verification_flow.dart'; + void initializePaymentPage( BuildContext context, - PaymentType? initialPaymentType, -) { + PaymentType? initialPaymentType, { + String? destinationLedgerAccountRef, +}) { final flowProvider = context.read(); + final recipientsProvider = context.read(); + flowProvider.setPreferredType(initialPaymentType); + + final destinationRef = destinationLedgerAccountRef?.trim(); + if (destinationRef != null && destinationRef.isNotEmpty) { + recipientsProvider.setCurrentObject(null); + flowProvider.setPreferredType(PaymentType.ledger); + flowProvider.setManualPaymentData( + LedgerPaymentMethod(ledgerAccountRef: destinationRef), + ); + return; + } + + flowProvider.setManualPaymentData(null); } void handleSearchChanged(PaymentPageUiController uiController, String query) { 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/payment/payout_verification_flow.dart b/frontend/pweb/lib/utils/payment/verification_flow.dart similarity index 100% rename from frontend/pweb/lib/utils/payment/payout_verification_flow.dart rename to frontend/pweb/lib/utils/payment/verification_flow.dart 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/payment_mapper.dart b/frontend/pweb/lib/utils/report/payment_mapper.dart index ab054146..b57da710 100644 --- a/frontend/pweb/lib/utils/report/payment_mapper.dart +++ b/frontend/pweb/lib/utils/report/payment_mapper.dart @@ -1,12 +1,13 @@ +import 'package:pshared/models/payment/operation_document.dart'; import 'package:pshared/models/payment/operation.dart'; import 'package:pshared/models/payment/payment.dart'; import 'package:pshared/models/payment/state.dart'; import 'package:pshared/models/payment/status.dart'; import 'package:pshared/utils/money.dart'; -import 'package:pweb/models/report/operation/document.dart'; import 'package:pweb/utils/report/operations/document_rule.dart'; + OperationItem mapPaymentToOperation(Payment payment) { final debit = payment.lastQuote?.amounts?.sourceDebitTotal; final settlement = payment.lastQuote?.amounts?.destinationSettlement; @@ -49,7 +50,7 @@ OperationItem mapPaymentToOperation(Payment payment) { ); } -OperationDocumentInfo? _resolveOperationDocument(Payment payment) { +OperationDocumentRef? _resolveOperationDocument(Payment payment) { for (final operation in payment.operations) { final operationRef = operation.operationRef; final gatewayService = operation.gateway; @@ -58,7 +59,7 @@ OperationDocumentInfo? _resolveOperationDocument(Payment payment) { if (!isOperationDocumentEligible(operation.code)) continue; - return OperationDocumentInfo( + return OperationDocumentRef( operationRef: operationRef, gatewayService: gatewayService, ); diff --git a/frontend/pweb/lib/utils/report/source_filter.dart b/frontend/pweb/lib/utils/report/source_filter.dart new file mode 100644 index 00000000..bcc81c79 --- /dev/null +++ b/frontend/pweb/lib/utils/report/source_filter.dart @@ -0,0 +1,78 @@ +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'; +import 'package:pshared/models/payment/methods/wallet.dart'; +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, + required String sourceRef, +}) { + final normalizedSourceRef = _normalize(sourceRef); + if (normalizedSourceRef == null) return false; + + final paymentSourceRefs = _paymentSourceRefs(payment, sourceType); + if (paymentSourceRefs.isEmpty) return false; + + return paymentSourceRefs.contains(normalizedSourceRef); +} + +Set _paymentSourceRefs(Payment payment, PaymentSourceType sourceType) { + final fromSource = _sourceRefsFromEndpoint(payment.source, sourceType); + if (fromSource.isEmpty) return const {}; + return fromSource; +} + +Set _sourceRefsFromEndpoint( + PaymentEndpoint? endpoint, + PaymentSourceType sourceType, +) { + if (endpoint == null) return const {}; + + 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) => switch (source) { + ManagedWalletPaymentMethod(:final managedWalletRef) => _normalize( + managedWalletRef, + ), + WalletPaymentMethod(:final walletId) => _normalize(walletId), + _ => null, +}; + +String? _ledgerSourceRef(PaymentMethodData source) => switch (source) { + LedgerPaymentMethod(:final ledgerAccountRef) => _normalize(ledgerAccountRef), + _ => null, +}; + +String? _normalize(String? value) { + final normalized = value?.trim(); + if (normalized == null || normalized.isEmpty) return null; + return normalized; +} 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,