From 6bb3ab5063759e99172dee4a7efb3be7d738afc3 Mon Sep 17 00:00:00 2001 From: Arseni Date: Mon, 2 Mar 2026 17:41:41 +0300 Subject: [PATCH] =?UTF-8?q?changed=20color=20theme=20to=20be=20black=20and?= =?UTF-8?q?=20added=20the=20ability=20to=20enter=20the=20amount=20in=20the?= =?UTF-8?q?=20recipient=E2=80=99s=20currency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/pshared/lib/config/common.dart | 2 +- .../lib/data/dto/payment/network_fee.dart | 20 ++++ .../lib/data/dto/payment/quote_aggregate.dart | 24 ++++ frontend/pshared/lib/models/resources.dart | 6 +- .../pshared/lib/provider/payment/amount.dart | 9 ++ .../provider/payment/multiple/provider.dart | 1 + .../payment/quotation/intent_builder.dart | 12 +- .../pshared/lib/service/payment/multiple.dart | 1 + .../pshared/lib/service/verification.dart | 1 + .../payment/quotation_currency_resolver.dart | 57 ++++++++++ frontend/pweb/lib/app/app.dart | 5 +- .../controllers/payments/amount_field.dart | 107 ++++++++++++++++-- frontend/pweb/lib/l10n/en.arb | 4 +- frontend/pweb/lib/l10n/ru.arb | 4 +- .../pweb/lib/models/payment/amount/mode.dart | 1 + frontend/pweb/lib/pages/2fa/input.dart | 16 ++- .../pages/dashboard/buttons/balance/card.dart | 56 ++++----- .../lib/pages/dashboard/buttons/buttons.dart | 13 ++- .../pages/dashboard/payouts/amount/feild.dart | 49 +++++--- .../dashboard/payouts/amount/mode/button.dart | 43 +++++++ .../payouts/amount/mode/selector.dart | 53 +++++++++ .../dashboard/payouts/amount/widget.dart | 14 ++- .../lib/pages/dashboard/payouts/form.dart | 28 ++--- .../sections/sample/download_button.dart | 37 ++++-- .../multiple/sections/sample/header.dart | 7 +- .../multiple/sections/sample/table.dart | 3 +- .../multiple/sections/sample/widget.dart | 15 +-- .../payouts/quote_status/widgets/card.dart | 32 ++++-- .../payouts/single/address_book/widget.dart | 19 +++- .../pages/invitations/widgets/form/form.dart | 1 - .../pages/invitations/widgets/form/view.dart | 2 - frontend/pweb/lib/pages/loaders/account.dart | 44 +++++-- .../pweb/lib/pages/loaders/organization.dart | 61 ++++++---- .../pweb/lib/pages/loaders/permissions.dart | 14 ++- .../pages/payout_page/send/page_handlers.dart | 1 - .../send/widgets/add_recipient_tile.dart | 21 +++- .../account/password/toggle_button.dart | 36 ------ .../pweb/lib/pages/signup/form/buttons.dart | 16 --- .../pweb/lib/widgets/sidebar/side_menu.dart | 5 +- .../pweb/lib/widgets/sidebar/sidebar.dart | 14 +-- frontend/pweb/lib/widgets/sidebar/user.dart | 3 +- 41 files changed, 618 insertions(+), 239 deletions(-) create mode 100644 frontend/pshared/lib/data/dto/payment/network_fee.dart create mode 100644 frontend/pshared/lib/data/dto/payment/quote_aggregate.dart create mode 100644 frontend/pshared/lib/utils/payment/quotation_currency_resolver.dart create mode 100644 frontend/pweb/lib/models/payment/amount/mode.dart create mode 100644 frontend/pweb/lib/pages/dashboard/payouts/amount/mode/button.dart create mode 100644 frontend/pweb/lib/pages/dashboard/payouts/amount/mode/selector.dart delete mode 100644 frontend/pweb/lib/pages/settings/profile/account/password/toggle_button.dart delete mode 100644 frontend/pweb/lib/pages/signup/form/buttons.dart diff --git a/frontend/pshared/lib/config/common.dart b/frontend/pshared/lib/config/common.dart index 71a9cd3f..c6b47267 100644 --- a/frontend/pshared/lib/config/common.dart +++ b/frontend/pshared/lib/config/common.dart @@ -15,7 +15,7 @@ class CommonConstants { static String clientId = ''; static String wsProto = 'ws'; static String wsEndpoint = '/ws'; - static Color themeColor = Color.fromARGB(255, 80, 63, 224); + static Color themeColor = Color.fromARGB(255, 0, 0, 0); static String nilObjectRef = '000000000000000000000000'; // Public getters for shared properties diff --git a/frontend/pshared/lib/data/dto/payment/network_fee.dart b/frontend/pshared/lib/data/dto/payment/network_fee.dart new file mode 100644 index 00000000..b9b2f344 --- /dev/null +++ b/frontend/pshared/lib/data/dto/payment/network_fee.dart @@ -0,0 +1,20 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/data/dto/money.dart'; + +part 'network_fee.g.dart'; + + +@JsonSerializable() +class NetworkFeeDTO { + final MoneyDTO? networkFee; + final String? estimationContext; + + const NetworkFeeDTO({ + this.networkFee, + this.estimationContext, + }); + + factory NetworkFeeDTO.fromJson(Map json) => _$NetworkFeeDTOFromJson(json); + Map toJson() => _$NetworkFeeDTOToJson(this); +} diff --git a/frontend/pshared/lib/data/dto/payment/quote_aggregate.dart b/frontend/pshared/lib/data/dto/payment/quote_aggregate.dart new file mode 100644 index 00000000..34dd779b --- /dev/null +++ b/frontend/pshared/lib/data/dto/payment/quote_aggregate.dart @@ -0,0 +1,24 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/data/dto/money.dart'; + +part 'quote_aggregate.g.dart'; + + +@JsonSerializable() +class PaymentQuoteAggregateDTO { + final List? debitAmounts; + final List? expectedSettlementAmounts; + final List? expectedFeeTotals; + final List? networkFeeTotals; + + const PaymentQuoteAggregateDTO({ + this.debitAmounts, + this.expectedSettlementAmounts, + this.expectedFeeTotals, + this.networkFeeTotals, + }); + + factory PaymentQuoteAggregateDTO.fromJson(Map json) => _$PaymentQuoteAggregateDTOFromJson(json); + Map toJson() => _$PaymentQuoteAggregateDTOToJson(this); +} diff --git a/frontend/pshared/lib/models/resources.dart b/frontend/pshared/lib/models/resources.dart index 662f9ab7..e0fcd182 100644 --- a/frontend/pshared/lib/models/resources.dart +++ b/frontend/pshared/lib/models/resources.dart @@ -73,7 +73,7 @@ enum ResourceType { @JsonValue('ledger_parties') ledgerParties, - @JsonValue('ledger_posing_lines') + @JsonValue('ledger_posting_lines') ledgerPostingLines, @JsonValue('payments') @@ -113,4 +113,8 @@ enum ResourceType { /// Represents steps in workflows or processes @JsonValue('steps') steps, + + /// Fallback for unknown values returned by backend. + @JsonValue('unknown') + unknown, } diff --git a/frontend/pshared/lib/provider/payment/amount.dart b/frontend/pshared/lib/provider/payment/amount.dart index f2cde541..19c7d5fc 100644 --- a/frontend/pshared/lib/provider/payment/amount.dart +++ b/frontend/pshared/lib/provider/payment/amount.dart @@ -1,12 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:pshared/models/payment/settlement_mode.dart'; class PaymentAmountProvider with ChangeNotifier { double _amount = 10.0; bool _payerCoversFee = true; + SettlementMode _settlementMode = SettlementMode.fixSource; double get amount => _amount; bool get payerCoversFee => _payerCoversFee; + SettlementMode get settlementMode => _settlementMode; void setAmount(double value) { _amount = value; @@ -17,4 +20,10 @@ class PaymentAmountProvider with ChangeNotifier { _payerCoversFee = value; notifyListeners(); } + + void setSettlementMode(SettlementMode value) { + if (_settlementMode == value) return; + _settlementMode = value; + notifyListeners(); + } } diff --git a/frontend/pshared/lib/provider/payment/multiple/provider.dart b/frontend/pshared/lib/provider/payment/multiple/provider.dart index 5d8c228e..9c0cd15c 100644 --- a/frontend/pshared/lib/provider/payment/multiple/provider.dart +++ b/frontend/pshared/lib/provider/payment/multiple/provider.dart @@ -7,6 +7,7 @@ import 'package:pshared/provider/resource.dart'; import 'package:pshared/service/payment/multiple.dart'; import 'package:pshared/utils/exception.dart'; + class MultiPaymentProvider extends ChangeNotifier { late OrganizationsProvider _organization; late MultiQuotationProvider _quotation; diff --git a/frontend/pshared/lib/provider/payment/quotation/intent_builder.dart b/frontend/pshared/lib/provider/payment/quotation/intent_builder.dart index ca26a8b0..e72a70fa 100644 --- a/frontend/pshared/lib/provider/payment/quotation/intent_builder.dart +++ b/frontend/pshared/lib/provider/payment/quotation/intent_builder.dart @@ -22,6 +22,8 @@ import 'package:pshared/utils/currency.dart'; import 'package:pshared/utils/payment/fx_helpers.dart'; class QuotationIntentBuilder { + static const String _settlementCurrency = 'RUB'; + PaymentIntent? build({ required PaymentAmountProvider payment, required WalletsController wallets, @@ -39,10 +41,12 @@ class QuotationIntentBuilder { data: paymentData, ); final sourceCurrency = currencyCodeToString(selectedWallet.currency); + final amountCurrency = payment.settlementMode == SettlementMode.fixReceived + ? _settlementCurrency + : sourceCurrency; final amount = Money( amount: payment.amount.toString(), - // TODO: adapt to possible other sources - currency: sourceCurrency, + currency: amountCurrency, ); final isCryptoToCrypto = paymentData is CryptoAddressPaymentMethod && @@ -50,7 +54,7 @@ class QuotationIntentBuilder { amount.currency; final fxIntent = FxIntentHelper.buildSellBaseBuyQuote( baseCurrency: sourceCurrency, - quoteCurrency: 'RUB', // TODO: exentd target currencies + quoteCurrency: _settlementCurrency, // TODO: exentd target currencies enabled: !isCryptoToCrypto, ); return PaymentIntent( @@ -68,7 +72,7 @@ class QuotationIntentBuilder { feeTreatment: payment.payerCoversFee ? FeeTreatment.addToSource : FeeTreatment.deductFromDestination, - settlementMode: SettlementMode.fixSource, + settlementMode: payment.settlementMode, customer: customer, ); } diff --git a/frontend/pshared/lib/service/payment/multiple.dart b/frontend/pshared/lib/service/payment/multiple.dart index 1a5350b9..a181e68d 100644 --- a/frontend/pshared/lib/service/payment/multiple.dart +++ b/frontend/pshared/lib/service/payment/multiple.dart @@ -13,6 +13,7 @@ import 'package:pshared/models/payment/quote/quotes.dart'; import 'package:pshared/service/authorization/service.dart'; import 'package:pshared/service/services.dart'; + class MultiplePaymentsService { static final _logger = Logger('service.payment.multiple'); static const String _objectType = Services.payments; diff --git a/frontend/pshared/lib/service/verification.dart b/frontend/pshared/lib/service/verification.dart index 10fab535..016b54a8 100644 --- a/frontend/pshared/lib/service/verification.dart +++ b/frontend/pshared/lib/service/verification.dart @@ -17,6 +17,7 @@ import 'package:pshared/service/authorization/storage.dart'; import 'package:pshared/service/services.dart'; import 'package:pshared/utils/http/requests.dart'; + class VerificationService { static final _logger = Logger('service.verification'); static const String _objectType = Services.verification; diff --git a/frontend/pshared/lib/utils/payment/quotation_currency_resolver.dart b/frontend/pshared/lib/utils/payment/quotation_currency_resolver.dart new file mode 100644 index 00000000..bc21f0b0 --- /dev/null +++ b/frontend/pshared/lib/utils/payment/quotation_currency_resolver.dart @@ -0,0 +1,57 @@ +import 'package:pshared/models/payment/methods/card.dart'; +import 'package:pshared/models/payment/methods/crypto_address.dart'; +import 'package:pshared/models/payment/methods/data.dart'; +import 'package:pshared/models/payment/methods/iban.dart'; +import 'package:pshared/models/payment/methods/russian_bank.dart'; +import 'package:pshared/models/payment/quote/quote.dart'; + + +class PaymentQuotationCurrencyResolver { + static String? resolveQuoteCurrency({ + PaymentQuote? quote, + PaymentMethodData? paymentData, + }) { + final quoteCurrency = _normalizeCurrency( + quote?.amounts?.destinationSettlement?.currency, + ); + if (quoteCurrency != null) return quoteCurrency; + + if (paymentData == null) return null; + + final metadataCurrency = _normalizeCurrency( + paymentData.metadata?['currency'], + ); + if (metadataCurrency != null) return metadataCurrency; + + if (paymentData is CryptoAddressPaymentMethod) { + return _normalizeCurrency(paymentData.asset?.tokenSymbol); + } + + if (paymentData is RussianBankAccountPaymentMethod || + paymentData is CardPaymentMethod) { + return 'RUB'; + } + + if (paymentData is IbanPaymentMethod) { + return 'EUR'; + } + + return null; + } + + static bool isFxEnabled({ + required String? sourceCurrency, + required String? quoteCurrency, + }) { + final normalizedSource = _normalizeCurrency(sourceCurrency); + final normalizedQuote = _normalizeCurrency(quoteCurrency); + if (normalizedSource == null || normalizedQuote == null) return false; + return normalizedSource != normalizedQuote; + } + + static String? _normalizeCurrency(String? value) { + final normalized = value?.trim().toUpperCase(); + if (normalized == null || normalized.isEmpty) return null; + return normalized; + } +} diff --git a/frontend/pweb/lib/app/app.dart b/frontend/pweb/lib/app/app.dart index 47e62054..44f80d45 100644 --- a/frontend/pweb/lib/app/app.dart +++ b/frontend/pweb/lib/app/app.dart @@ -20,7 +20,10 @@ class PayApp extends StatelessWidget { Widget build(BuildContext context) => MaterialApp.router( title: 'Sendico', theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Constants.themeColor), + colorScheme: ColorScheme.fromSeed( + seedColor: Constants.themeColor, + dynamicSchemeVariant: DynamicSchemeVariant.monochrome, + ), useMaterial3: true, ), routerConfig: _router, diff --git a/frontend/pweb/lib/controllers/payments/amount_field.dart b/frontend/pweb/lib/controllers/payments/amount_field.dart index da73f007..1a818cbf 100644 --- a/frontend/pweb/lib/controllers/payments/amount_field.dart +++ b/frontend/pweb/lib/controllers/payments/amount_field.dart @@ -1,25 +1,56 @@ import 'package:flutter/material.dart'; +import 'package:pshared/controllers/balance_mask/wallets.dart'; +import 'package:pshared/models/payment/settlement_mode.dart'; import 'package:pshared/provider/payment/amount.dart'; import 'package:pshared/utils/currency.dart'; import 'package:pshared/utils/money.dart'; +import 'package:pweb/models/payment/amount/mode.dart'; class PaymentAmountFieldController extends ChangeNotifier { + static const String _settlementCurrencyCode = 'RUB'; + final TextEditingController textController; + final FocusNode focusNode = FocusNode(); + PaymentAmountProvider? _provider; + WalletsController? _wallets; bool _isSyncingText = false; + PaymentAmountMode _mode = PaymentAmountMode.debit; PaymentAmountFieldController({required double initialAmount}) - : textController = TextEditingController( - text: amountToString(initialAmount), - ); + : textController = TextEditingController( + text: amountToString(initialAmount), + ); + + PaymentAmountMode get mode => _mode; + bool get isReverseModeAvailable { + final sourceCurrencyCode = _sourceCurrencyCode; + return sourceCurrencyCode != null && + sourceCurrencyCode != _settlementCurrencyCode; + } + + String? get activeCurrencyCode => switch (_mode) { + PaymentAmountMode.debit => _sourceCurrencyCode, + PaymentAmountMode.settlement => _settlementCurrencyCode, + }; + + void update(PaymentAmountProvider provider, WalletsController wallets) { + if (!identical(_provider, provider)) { + _provider?.removeListener(_handleProviderChanged); + _provider = provider; + _provider?.addListener(_handleProviderChanged); + _syncModeWithProvider(provider); + } + + if (!identical(_wallets, wallets)) { + _wallets?.removeListener(_handleWalletsChanged); + _wallets = wallets; + _wallets?.addListener(_handleWalletsChanged); + _normalizeModeForWallet(); + } - void update(PaymentAmountProvider provider) { - if (identical(_provider, provider)) return; - _provider?.removeListener(_handleProviderChanged); - _provider = provider; - _provider?.addListener(_handleProviderChanged); _syncTextWithAmount(provider.amount); } @@ -31,12 +62,70 @@ class PaymentAmountFieldController extends ChangeNotifier { } } + void handleModeChanged(PaymentAmountMode value) { + if (value == _mode) return; + if (!isReverseModeAvailable && value == PaymentAmountMode.settlement) { + return; + } + + _mode = value; + _provider?.setSettlementMode(_settlementModeFromMode(value)); + notifyListeners(); + } + void _handleProviderChanged() { final provider = _provider; if (provider == null) return; _syncTextWithAmount(provider.amount); + final changed = _syncModeWithProvider(provider); + if (changed) { + _normalizeModeForWallet(); + notifyListeners(); + } } + void _handleWalletsChanged() { + final changed = _normalizeModeForWallet(); + if (changed) { + notifyListeners(); + } + } + + bool _syncModeWithProvider(PaymentAmountProvider provider) { + final nextMode = _modeFromSettlementMode(provider.settlementMode); + if (nextMode == _mode) return false; + _mode = nextMode; + return true; + } + + bool _normalizeModeForWallet() { + if (isReverseModeAvailable || _mode != PaymentAmountMode.settlement) { + return false; + } + _mode = PaymentAmountMode.debit; + _provider?.setSettlementMode(SettlementMode.fixSource); + return true; + } + + String? get _sourceCurrencyCode { + final selectedWallet = _wallets?.selectedWallet; + if (selectedWallet == null) return null; + return currencyCodeToString(selectedWallet.currency); + } + + PaymentAmountMode _modeFromSettlementMode(SettlementMode mode) => + switch (mode) { + SettlementMode.fixReceived => PaymentAmountMode.settlement, + SettlementMode.fixSource || + SettlementMode.unspecified => PaymentAmountMode.debit, + }; + + SettlementMode _settlementModeFromMode(PaymentAmountMode mode) => + switch (mode) { + PaymentAmountMode.debit => SettlementMode.fixSource, + PaymentAmountMode.settlement => SettlementMode.fixReceived, + }; + double? _parseAmount(String value) { final parsed = parseMoneyAmount( value.replaceAll(',', '.'), @@ -61,6 +150,8 @@ class PaymentAmountFieldController extends ChangeNotifier { @override void dispose() { _provider?.removeListener(_handleProviderChanged); + _wallets?.removeListener(_handleWalletsChanged); + focusNode.dispose(); textController.dispose(); super.dispose(); } diff --git a/frontend/pweb/lib/l10n/en.arb b/frontend/pweb/lib/l10n/en.arb index 5fe8e274..07eea254 100644 --- a/frontend/pweb/lib/l10n/en.arb +++ b/frontend/pweb/lib/l10n/en.arb @@ -403,9 +403,9 @@ "idempotencyKeyLabel": "Idempotency key", "quoteIdLabel": "Quote ID", "createdAtLabel": "Created at", - "debitAmountLabel": "Debit amount", + "debitAmountLabel": "You pay", "debitSettlementAmountLabel": "Debit settlement amount", - "expectedSettlementAmountLabel": "Expected settlement amount", + "expectedSettlementAmountLabel": "Recipient gets", "feeTotalLabel": "Total fee", "networkFeeLabel": "Network fee", "fxRateLabel": "Rate", diff --git a/frontend/pweb/lib/l10n/ru.arb b/frontend/pweb/lib/l10n/ru.arb index 6e8ce47f..dbbf68e1 100644 --- a/frontend/pweb/lib/l10n/ru.arb +++ b/frontend/pweb/lib/l10n/ru.arb @@ -403,9 +403,9 @@ "idempotencyKeyLabel": "Ключ идемпотентности", "quoteIdLabel": "ID котировки", "createdAtLabel": "Создан", - "debitAmountLabel": "Списано", + "debitAmountLabel": "Вы платите", "debitSettlementAmountLabel": "Списано к зачислению", - "expectedSettlementAmountLabel": "Ожидаемая сумма зачисления", + "expectedSettlementAmountLabel": "Получателю поступит", "feeTotalLabel": "Комиссия", "networkFeeLabel": "Сетевая комиссия", "fxRateLabel": "Курс", diff --git a/frontend/pweb/lib/models/payment/amount/mode.dart b/frontend/pweb/lib/models/payment/amount/mode.dart new file mode 100644 index 00000000..d2b6ca27 --- /dev/null +++ b/frontend/pweb/lib/models/payment/amount/mode.dart @@ -0,0 +1 @@ +enum PaymentAmountMode {debit, settlement} diff --git a/frontend/pweb/lib/pages/2fa/input.dart b/frontend/pweb/lib/pages/2fa/input.dart index 2a14770e..f92fac19 100644 --- a/frontend/pweb/lib/pages/2fa/input.dart +++ b/frontend/pweb/lib/pages/2fa/input.dart @@ -9,7 +9,10 @@ class TwoFactorCodeInput extends StatelessWidget { const TwoFactorCodeInput({super.key, required this.onCompleted}); @override - Widget build(BuildContext context) => Center( + Widget build(BuildContext context){ + final theme = Theme.of(context).colorScheme; + + return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 300), child: MaterialPinField( @@ -21,13 +24,18 @@ class TwoFactorCodeInput extends StatelessWidget { shape: MaterialPinShape.outlined, borderRadius: BorderRadius.circular(4), cellSize: Size(40, 48), - borderColor: Theme.of(context).colorScheme.primaryContainer, - focusedBorderColor: Theme.of(context).colorScheme.primary, - cursorColor: Theme.of(context).colorScheme.primary, + borderColor: theme.primaryContainer.withValues(alpha: 0.2), + focusedBorderColor: theme.primary, + filledBorderColor: theme.primary, + cursorColor: theme.primary, + focusedFillColor: theme.onSecondary, + filledFillColor: theme.onSecondary, + fillColor: theme.onSecondary, ), onCompleted: onCompleted, onChanged: (_) {}, ), ), ); + } } diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/card.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/card.dart index af77978f..6f352655 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/card.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/card.dart @@ -45,37 +45,37 @@ class WalletCard extends StatelessWidget { 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: loc.paymentTypeCryptoWallet, - subtitle: networkLabel, - badge: (symbol == null || symbol.isEmpty) ? null : symbol, - ), - Row( - children: [ - BalanceAmount( - wallet: wallet, - onToggleMask: () { - context.read().toggleBalanceMask(wallet.id); - }, - ), - WalletBalanceRefreshButton( - walletRef: wallet.id, - ), - ], - ), - BalanceAddFunds(onTopUp: onTopUp), - ], + child: SizedBox.expand( + child: Padding( + padding: WalletCardConfig.contentPadding, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BalanceHeader( + title: loc.paymentTypeCryptoWallet, + subtitle: networkLabel, + badge: (symbol == null || symbol.isEmpty) ? null : symbol, + ), + Row( + children: [ + BalanceAmount( + wallet: wallet, + onToggleMask: () { + context.read().toggleBalanceMask(wallet.id); + }, + ), + WalletBalanceRefreshButton( + walletRef: wallet.id, + ), + ], + ), + BalanceAddFunds(onTopUp: onTopUp), + ], + ), ), ), ), - ), ); } } diff --git a/frontend/pweb/lib/pages/dashboard/buttons/buttons.dart b/frontend/pweb/lib/pages/dashboard/buttons/buttons.dart index a32b9d66..1194e737 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/buttons.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/buttons.dart @@ -24,11 +24,12 @@ class TransactionRefButton extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context).colorScheme; - - final backgroundColor = isActive ? theme.primary : theme.onSecondary; - final foregroundColor = isActive ? theme.onPrimary : theme.onPrimaryContainer; - final hoverColor = isActive ? theme.primary : theme.secondaryContainer; + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final selectedBackground = colorScheme.onSecondary; + final backgroundColor = isActive ? colorScheme.primary : selectedBackground; + final hoverColor = isActive ? colorScheme.primary : colorScheme.surfaceContainerHighest; + final foregroundColor = isActive ? colorScheme.onPrimary : colorScheme.primary; return Material( color: backgroundColor, @@ -65,4 +66,4 @@ class TransactionRefButton extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/dashboard/payouts/amount/feild.dart b/frontend/pweb/lib/pages/dashboard/payouts/amount/feild.dart index 3d5374ac..8c670ef9 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/amount/feild.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/amount/feild.dart @@ -2,36 +2,49 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/controllers/balance_mask/wallets.dart'; -import 'package:pshared/models/currency.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(); @override Widget build(BuildContext context) { - final currency = context.select( - (c) => c.selectedWallet?.currency, - ); - final symbol = currency == null ? null : currencyCodeToSymbol(currency); - final ui = context.watch(); + final loc = AppLocalizations.of(context)!; + final symbol = currencySymbolFromCode(ui.activeCurrencyCode); - return TextField( - controller: ui.textController, - keyboardType: const TextInputType.numberWithOptions(decimal: true), - decoration: InputDecoration( - labelText: AppLocalizations.of(context)!.amount, - border: const OutlineInputBorder(), - prefixText: symbol == null ? null : '$symbol\u00A0', - ), - onChanged: ui.handleChanged, + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (ui.isReverseModeAvailable) ...[ + PaymentAmountModeSelector( + selectedMode: ui.mode, + onModeChanged: ui.handleModeChanged, + ), + const SizedBox(height: 6), + ], + TextField( + controller: ui.textController, + focusNode: ui.focusNode, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + 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, + }, + ), + onChanged: ui.handleChanged, + ), + ], ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/dashboard/payouts/amount/mode/button.dart b/frontend/pweb/lib/pages/dashboard/payouts/amount/mode/button.dart new file mode 100644 index 00000000..7c6a7a69 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/payouts/amount/mode/button.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + + +class ModeButton extends StatelessWidget { + final String label; + final bool isSelected; + final VoidCallback onTap; + + const ModeButton({ + required this.label, + required this.isSelected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Material( + color: isSelected + ? theme.colorScheme.primary + : theme.colorScheme.onSecondary, + borderRadius: BorderRadius.circular(8), + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5), + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.labelSmall?.copyWith( + fontWeight: FontWeight.w500, + color: isSelected + ? theme.colorScheme.onSecondary + : theme.colorScheme.primary, + ), + ), + ), + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/payouts/amount/mode/selector.dart b/frontend/pweb/lib/pages/dashboard/payouts/amount/mode/selector.dart new file mode 100644 index 00000000..e9cb1f63 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/payouts/amount/mode/selector.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/models/payment/amount/mode.dart'; +import 'package:pweb/pages/dashboard/payouts/amount/mode/button.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class PaymentAmountModeSelector extends StatelessWidget { + final PaymentAmountMode selectedMode; + final ValueChanged onModeChanged; + + const PaymentAmountModeSelector({ + super.key, + required this.selectedMode, + required this.onModeChanged, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final loc = AppLocalizations.of(context)!; + return Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: theme.colorScheme.onSecondary, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: theme.colorScheme.outline, + ), + ), + child: Row( + children: [ + Expanded( + child: ModeButton( + label: loc.debitAmountLabel, + isSelected: selectedMode == PaymentAmountMode.debit, + onTap: () => onModeChanged(PaymentAmountMode.debit), + ), + ), + const SizedBox(width: 2), + Expanded( + child: ModeButton( + label: loc.expectedSettlementAmountLabel, + isSelected: selectedMode == PaymentAmountMode.settlement, + onTap: () => onModeChanged(PaymentAmountMode.settlement), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/dashboard/payouts/amount/widget.dart b/frontend/pweb/lib/pages/dashboard/payouts/amount/widget.dart index b0eb5c3d..c2e72503 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/amount/widget.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/amount/widget.dart @@ -2,27 +2,31 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:pshared/controllers/balance_mask/wallets.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'; - class PaymentAmountWidget extends StatelessWidget { const PaymentAmountWidget({super.key}); @override Widget build(BuildContext context) { - return ChangeNotifierProxyProvider( + return ChangeNotifierProxyProvider2< + PaymentAmountProvider, + WalletsController, + PaymentAmountFieldController + >( create: (ctx) { final initialAmount = ctx.read().amount; return PaymentAmountFieldController(initialAmount: initialAmount); }, - update: (ctx, amountProvider, controller) { - controller!.update(amountProvider); + update: (ctx, amountProvider, wallets, controller) { + controller!.update(amountProvider, wallets); return controller; }, child: const PaymentAmountField(), ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/dashboard/payouts/form.dart b/frontend/pweb/lib/pages/dashboard/payouts/form.dart index 16ebc690..4fa94eb8 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/form.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/form.dart @@ -101,6 +101,18 @@ class PaymentFormWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ detailsHeader, + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + flex: 3, + child: const PaymentAmountWidget(), + ), + const SizedBox(width: _columnSpacing), + Expanded(flex: 2, child: quoteCard), + ], + ), + const SizedBox(height: _smallSpacing), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -109,28 +121,16 @@ class PaymentFormWidget extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const PaymentAmountWidget(), - const SizedBox(height: _smallSpacing), FeePayerSwitch( spacing: _smallSpacing, style: theme.textTheme.bodySmall, ), + const SizedBox(height: _mediumSpacing), + const PaymentSummary(spacing: _extraSpacing), ], ), ), const SizedBox(width: _columnSpacing), - Expanded(flex: 2, child: quoteCard), - ], - ), - const SizedBox(height: _mediumSpacing), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Expanded( - flex: 3, - child: PaymentSummary(spacing: _extraSpacing), - ), - const SizedBox(width: _columnSpacing), Expanded( flex: 2, child: Column( diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/sample/download_button.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/sample/download_button.dart index 3b4309c3..a13220b5 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/sample/download_button.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/sample/download_button.dart @@ -6,25 +6,42 @@ import 'package:pweb/generated/i18n/app_localizations.dart'; class FileFormatSampleDownloadButton extends StatelessWidget { const FileFormatSampleDownloadButton({ super.key, - required this.theme, - required this.l10n, required this.onPressed, }); - final ThemeData theme; - final AppLocalizations l10n; final VoidCallback onPressed; + final double buttonWidth = 220; @override Widget build(BuildContext context) { - final linkStyle = theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.primary, + final theme = Theme.of(context); + final l10n = AppLocalizations.of(context)!; + final textStyle = theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onPrimary, + fontWeight: FontWeight.w500, ); - return TextButton( - onPressed: onPressed, - style: TextButton.styleFrom(padding: EdgeInsets.zero), - child: Text(l10n.downloadSampleCSV, style: linkStyle), + return Align( + alignment: Alignment.center, + child: SizedBox( + width: buttonWidth, + child: FilledButton( + onPressed: onPressed, + style: FilledButton.styleFrom( + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: Text( + l10n.downloadSampleCSV, + textAlign: TextAlign.center, + style: textStyle, + ), + ), + ), ); } } diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/sample/header.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/sample/header.dart index 691a124c..d1c4e48b 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/sample/header.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/sample/header.dart @@ -6,15 +6,12 @@ import 'package:pweb/generated/i18n/app_localizations.dart'; class FileFormatSampleHeader extends StatelessWidget { const FileFormatSampleHeader({ super.key, - required this.theme, - required this.l10n, }); - final ThemeData theme; - final AppLocalizations l10n; - @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final l10n = AppLocalizations.of(context)!; final titleStyle = theme.textTheme.bodyLarge?.copyWith( fontSize: 18, fontWeight: FontWeight.w600, diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/sample/table.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/sample/table.dart index c902448b..f6781882 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/sample/table.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/sample/table.dart @@ -8,15 +8,14 @@ import 'package:pweb/generated/i18n/app_localizations.dart'; class FileFormatSampleTable extends StatelessWidget { const FileFormatSampleTable({ super.key, - required this.l10n, required this.rows, }); - final AppLocalizations l10n; final List rows; @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; return DataTable( columnSpacing: 20, columns: [ diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/sample/widget.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/sample/widget.dart index 5ced3fda..15f03a46 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/sample/widget.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/sample/widget.dart @@ -9,29 +9,20 @@ import 'package:pweb/pages/dashboard/payouts/multiple/sections/sample/header.dar import 'package:pweb/pages/dashboard/payouts/multiple/sections/sample/table.dart'; import 'package:pweb/utils/download.dart'; -import 'package:pweb/generated/i18n/app_localizations.dart'; - class FileFormatSampleSection extends StatelessWidget { const FileFormatSampleSection({super.key}); @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final l10n = AppLocalizations.of(context)!; - return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - FileFormatSampleHeader(theme: theme, l10n: l10n), + FileFormatSampleHeader(), const SizedBox(height: 12), - FileFormatSampleTable(l10n: l10n, rows: sampleRows), + FileFormatSampleTable(rows: sampleRows), const SizedBox(height: 10), - FileFormatSampleDownloadButton( - theme: theme, - l10n: l10n, - onPressed: _downloadSampleCsv, - ), + FileFormatSampleDownloadButton(onPressed: _downloadSampleCsv), ], ); } 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 e2ed8c55..738e92d8 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 @@ -33,7 +33,8 @@ class QuoteStatusCard extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); final foregroundColor = _resolveForegroundColor(theme, statusType); - final statusStyle = theme.textTheme.bodyMedium?.copyWith(color: foregroundColor); + final elementColor = _resolveElementColor(theme, statusType); + final statusStyle = theme.textTheme.bodyMedium?.copyWith(color: elementColor); final helperStyle = theme.textTheme.bodySmall?.copyWith( color: foregroundColor.withValues(alpha: 0.8), ); @@ -43,6 +44,9 @@ class QuoteStatusCard extends StatelessWidget { decoration: BoxDecoration( color: _resolveCardColor(theme, statusType), borderRadius: BorderRadius.circular(_cardRadius), + border: Border.all( + color: elementColor.withValues(alpha: 0.5), + ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -61,7 +65,7 @@ class QuoteStatusCard extends StatelessWidget { : Icon( _resolveIcon(statusType), size: _iconSize, - color: foregroundColor, + color: elementColor, ), ), const SizedBox(width: _cardSpacing), @@ -98,28 +102,36 @@ class QuoteStatusCard extends StatelessWidget { Color _resolveCardColor(ThemeData theme, QuoteStatusType status) { switch (status) { case QuoteStatusType.loading: - return theme.colorScheme.secondaryContainer; + case QuoteStatusType.active: + case QuoteStatusType.missing: + return theme.colorScheme.onSecondary; case QuoteStatusType.error: case QuoteStatusType.expired: return theme.colorScheme.errorContainer; - case QuoteStatusType.active: - return theme.colorScheme.primaryContainer; - case QuoteStatusType.missing: - return theme.colorScheme.surfaceContainerHighest; } } Color _resolveForegroundColor(ThemeData theme, QuoteStatusType status) { switch (status) { + case QuoteStatusType.active: + case QuoteStatusType.missing: case QuoteStatusType.loading: - return theme.colorScheme.onSecondaryContainer; + return theme.colorScheme.onSecondary; case QuoteStatusType.error: case QuoteStatusType.expired: return theme.colorScheme.onErrorContainer; + } + } + + Color _resolveElementColor(ThemeData theme, QuoteStatusType status) { + switch (status) { case QuoteStatusType.active: - return theme.colorScheme.onPrimaryContainer; case QuoteStatusType.missing: - return theme.colorScheme.onSurfaceVariant; + case QuoteStatusType.loading: + return theme.colorScheme.primary; + case QuoteStatusType.error: + case QuoteStatusType.expired: + return theme.colorScheme.onErrorContainer; } } 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 519936fd..df80ca77 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 @@ -1,11 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + import 'package:provider/provider.dart'; import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/provider/recipient/provider.dart'; +import 'package:pweb/app/router/payout_routes.dart'; import 'package:pweb/pages/address_book/page/search.dart'; +import 'package:pweb/pages/payout_page/send/widgets/add_recipient_tile.dart'; import 'package:pweb/pages/dashboard/payouts/single/address_book/long_list/widget.dart'; import 'package:pweb/pages/dashboard/payouts/single/address_book/placeholder.dart'; import 'package:pweb/pages/dashboard/payouts/single/address_book/short_list.dart'; @@ -63,6 +67,10 @@ class _AddressBookPayoutState extends State { final loc = AppLocalizations.of(context)!; final provider = context.watch(); final recipients = provider.recipients; + void onAddRecipient() { + provider.setCurrentObject(null); + context.pushNamed(PayoutRoutes.addRecipient); + } final filteredRecipients = filterRecipients( recipients: recipients, query: _query, @@ -97,7 +105,12 @@ class _AddressBookPayoutState extends State { const SizedBox(height: _spacingBetween), Expanded( child: recipients.isEmpty - ? AddressBookPlaceholder(text: loc.noRecipientsYet) + ? Center( + child: AddRecipientTile( + label: loc.addRecipient, + onTap: onAddRecipient, + ), + ) : _isExpanded && filteredRecipients.isEmpty ? AddressBookPlaceholder(text: loc.noRecipientsFound) : _isExpanded @@ -108,6 +121,10 @@ class _AddressBookPayoutState extends State { : ShortListAddressBookPayout( recipients: recipients, onSelected: widget.onSelected, + trailing: AddRecipientTile( + label: loc.addRecipient, + onTap: onAddRecipient, + ), ), ), ], diff --git a/frontend/pweb/lib/pages/invitations/widgets/form/form.dart b/frontend/pweb/lib/pages/invitations/widgets/form/form.dart index 8139a81c..624573e1 100644 --- a/frontend/pweb/lib/pages/invitations/widgets/form/form.dart +++ b/frontend/pweb/lib/pages/invitations/widgets/form/form.dart @@ -40,7 +40,6 @@ class InvitationsForm extends StatelessWidget { firstNameController: firstNameController, lastNameController: lastNameController, messageController: messageController, - canCreateRoles: canCreateRoles, expiryDays: expiryDays, onExpiryChanged: onExpiryChanged, selectedRoleRef: selectedRoleRef, diff --git a/frontend/pweb/lib/pages/invitations/widgets/form/view.dart b/frontend/pweb/lib/pages/invitations/widgets/form/view.dart index c8f81dde..843d8cea 100644 --- a/frontend/pweb/lib/pages/invitations/widgets/form/view.dart +++ b/frontend/pweb/lib/pages/invitations/widgets/form/view.dart @@ -16,7 +16,6 @@ class InvitationFormView extends StatelessWidget { final TextEditingController firstNameController; final TextEditingController lastNameController; final TextEditingController messageController; - final bool canCreateRoles; final int expiryDays; final ValueChanged onExpiryChanged; final String? selectedRoleRef; @@ -31,7 +30,6 @@ class InvitationFormView extends StatelessWidget { required this.firstNameController, required this.lastNameController, required this.messageController, - required this.canCreateRoles, required this.expiryDays, required this.onExpiryChanged, required this.selectedRoleRef, diff --git a/frontend/pweb/lib/pages/loaders/account.dart b/frontend/pweb/lib/pages/loaders/account.dart index 44454a7c..d49f8d77 100644 --- a/frontend/pweb/lib/pages/loaders/account.dart +++ b/frontend/pweb/lib/pages/loaders/account.dart @@ -1,3 +1,5 @@ +import 'dart:developer' as developer; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -12,7 +14,6 @@ import 'package:pweb/models/account/account_loader.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; - class AccountLoader extends StatefulWidget { final Widget child; const AccountLoader({super.key, required this.child}); @@ -27,7 +28,8 @@ class _AccountLoaderState extends State { @override void initState() { super.initState(); - _controller = AccountLoaderController()..addListener(_handleControllerAction); + _controller = AccountLoaderController() + ..addListener(_handleControllerAction); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; Provider.of(context, listen: false).restoreIfPossible(); @@ -45,6 +47,17 @@ class _AccountLoaderState extends State { switch (action) { case AccountLoaderAction.showErrorAndGoToLogin: final error = _controller.error ?? Exception('Authorization failed'); + assert(() { + developer.log( + 'AccountLoader action=showErrorAndGoToLogin', + name: 'pweb.auth.redirect', + error: error, + ); + developer.debugger( + message: 'AccountLoader: redirecting to login due to auth error', + ); + return true; + }()); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; postNotifyUserOfErrorX( @@ -56,6 +69,17 @@ class _AccountLoaderState extends State { }); break; case AccountLoaderAction.goToLogin: + assert(() { + developer.log( + 'AccountLoader action=goToLogin', + name: 'pweb.auth.redirect', + ); + developer.debugger( + message: + 'AccountLoader: redirecting to login due to empty auth state', + ); + return true; + }()); WidgetsBinding.instance.addPostFrameCallback((_) => goToLogin()); break; } @@ -70,12 +94,14 @@ class _AccountLoaderState extends State { @override Widget build(BuildContext context) { - return Consumer(builder: (context, provider, _) { - _controller.update(provider); - if (provider.authState == AuthState.ready && provider.account != null) { - return widget.child; - } - return const Center(child: CircularProgressIndicator()); - }); + return Consumer( + builder: (context, provider, _) { + _controller.update(provider); + if (provider.authState == AuthState.ready && provider.account != null) { + return widget.child; + } + return const Center(child: CircularProgressIndicator()); + }, + ); } } diff --git a/frontend/pweb/lib/pages/loaders/organization.dart b/frontend/pweb/lib/pages/loaders/organization.dart index 6964827b..1ccce66d 100644 --- a/frontend/pweb/lib/pages/loaders/organization.dart +++ b/frontend/pweb/lib/pages/loaders/organization.dart @@ -1,3 +1,5 @@ +import 'dart:developer' as developer; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -6,34 +8,45 @@ import 'package:pshared/provider/organizations.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; - class OrganizationLoader extends StatelessWidget { final Widget child; const OrganizationLoader({super.key, required this.child}); @override - Widget build(BuildContext context) => Consumer(builder: (context, provider, _) { - if (provider.isLoading) return const Center(child: CircularProgressIndicator()); - if (provider.error != null) { - final loc = AppLocalizations.of(context)!; - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(loc.errorLogin), - const SizedBox(height: 12), - ElevatedButton( - onPressed: provider.load, - child: Text(loc.retry), - ), - ], - ), - ); - } - if ((provider.error == null) && (!provider.isOrganizationSet)) { - return const Center(child: CircularProgressIndicator()); - } - return child; - }); + Widget build(BuildContext context) => Consumer( + builder: (context, provider, _) { + if (provider.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + if (provider.error != null) { + assert(() { + developer.log( + 'OrganizationLoader: provider.error != null', + name: 'pweb.auth.redirect', + error: provider.error, + ); + developer.debugger( + message: 'OrganizationLoader blocked app with error', + ); + return true; + }()); + final loc = AppLocalizations.of(context)!; + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(loc.errorLogin), + const SizedBox(height: 12), + ElevatedButton(onPressed: provider.load, child: Text(loc.retry)), + ], + ), + ); + } + if ((provider.error == null) && (!provider.isOrganizationSet)) { + return const Center(child: CircularProgressIndicator()); + } + return child; + }, + ); } diff --git a/frontend/pweb/lib/pages/loaders/permissions.dart b/frontend/pweb/lib/pages/loaders/permissions.dart index 8fef6891..daa15000 100644 --- a/frontend/pweb/lib/pages/loaders/permissions.dart +++ b/frontend/pweb/lib/pages/loaders/permissions.dart @@ -1,3 +1,5 @@ +import 'dart:developer' as developer; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -7,7 +9,6 @@ import 'package:pshared/provider/permissions.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; - class PermissionsLoader extends StatelessWidget { final Widget child; const PermissionsLoader({super.key, required this.child}); @@ -17,6 +18,17 @@ class PermissionsLoader extends StatelessWidget { return Consumer2( builder: (context, provider, _, _) { if (provider.error != null) { + assert(() { + developer.log( + 'PermissionsLoader: provider.error != null', + name: 'pweb.auth.redirect', + error: provider.error, + ); + developer.debugger( + message: 'PermissionsLoader blocked app with error', + ); + return true; + }()); final loc = AppLocalizations.of(context)!; return Center( child: Column( diff --git a/frontend/pweb/lib/pages/payout_page/send/page_handlers.dart b/frontend/pweb/lib/pages/payout_page/send/page_handlers.dart index 6716b4da..76dac6b4 100644 --- a/frontend/pweb/lib/pages/payout_page/send/page_handlers.dart +++ b/frontend/pweb/lib/pages/payout_page/send/page_handlers.dart @@ -19,7 +19,6 @@ 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'; - void initializePaymentPage( BuildContext context, PaymentType? initialPaymentType, diff --git a/frontend/pweb/lib/pages/payout_page/send/widgets/add_recipient_tile.dart b/frontend/pweb/lib/pages/payout_page/send/widgets/add_recipient_tile.dart index ca91f36d..9465cc46 100644 --- a/frontend/pweb/lib/pages/payout_page/send/widgets/add_recipient_tile.dart +++ b/frontend/pweb/lib/pages/payout_page/send/widgets/add_recipient_tile.dart @@ -18,23 +18,34 @@ class AddRecipientTile extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final buttonBorderColor = theme.colorScheme.primary; + final buttonBackgroundColor = theme.colorScheme.onSecondary; + final buttonIconColor = theme.colorScheme.primary; return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(8), - hoverColor: theme.colorScheme.primaryContainer, + hoverColor: theme.colorScheme.surfaceContainerHighest, child: SizedBox( width: _tileSize, height: _tileSize, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - CircleAvatar( - radius: _avatarRadius, - backgroundColor: theme.colorScheme.primaryContainer, + Container( + width: _avatarRadius * 2, + height: _avatarRadius * 2, + alignment: Alignment.center, + decoration: BoxDecoration( + color: buttonBackgroundColor, + shape: BoxShape.circle, + border: Border.fromBorderSide( + BorderSide(color: buttonBorderColor, width: 1.5), + ), + ), child: Icon( Icons.add, - color: theme.colorScheme.primary, + color: buttonIconColor, size: 20, ), ), diff --git a/frontend/pweb/lib/pages/settings/profile/account/password/toggle_button.dart b/frontend/pweb/lib/pages/settings/profile/account/password/toggle_button.dart deleted file mode 100644 index b90dbb25..00000000 --- a/frontend/pweb/lib/pages/settings/profile/account/password/toggle_button.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter/material.dart'; - - -class PasswordToggleButton extends StatelessWidget { - const PasswordToggleButton({ - super.key, - required this.title, - required this.isExpanded, - required this.isBusy, - required this.onToggle, - }); - - final String title; - final bool isExpanded; - final bool isBusy; - final VoidCallback onToggle; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final iconColor = theme.colorScheme.primary; - - return TextButton.icon( - onPressed: isBusy - ? null - : () { - onToggle(); - }, - icon: Icon( - isExpanded ? Icons.lock_open : Icons.lock_outline, - color: iconColor, - ), - label: Text(title, style: theme.textTheme.bodyMedium), - ); - } -} diff --git a/frontend/pweb/lib/pages/signup/form/buttons.dart b/frontend/pweb/lib/pages/signup/form/buttons.dart deleted file mode 100644 index 754c77f8..00000000 --- a/frontend/pweb/lib/pages/signup/form/buttons.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:flutter/material.dart'; - - -class SignUpBackButton extends StatelessWidget { - const SignUpBackButton({super.key}); - - @override - Widget build(BuildContext context) => Row( - children: [ - IconButton( - onPressed: Navigator.of(context).pop, - icon: const Icon(Icons.arrow_back), - ), - ], - ); -} \ No newline at end of file diff --git a/frontend/pweb/lib/widgets/sidebar/side_menu.dart b/frontend/pweb/lib/widgets/sidebar/side_menu.dart index 948dd8e9..35b0d4cf 100644 --- a/frontend/pweb/lib/widgets/sidebar/side_menu.dart +++ b/frontend/pweb/lib/widgets/sidebar/side_menu.dart @@ -37,8 +37,9 @@ class SideMenuColumn extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 8), children: items.map((item) { final isSelected = item == selected; + final selectedBackground = theme.colorScheme.surfaceContainerHighest; final backgroundColor = isSelected - ? theme.colorScheme.primaryContainer + ? selectedBackground : Colors.transparent; return Padding( @@ -54,7 +55,7 @@ class SideMenuColumn extends StatelessWidget { unawaited(PosthogService.pageOpened(item, uiSource: 'sidebar')); }, borderRadius: BorderRadius.circular(12), - hoverColor: theme.colorScheme.primaryContainer, + hoverColor: selectedBackground, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 12), child: Row( diff --git a/frontend/pweb/lib/widgets/sidebar/sidebar.dart b/frontend/pweb/lib/widgets/sidebar/sidebar.dart index 83e7882e..8efa1fc2 100644 --- a/frontend/pweb/lib/widgets/sidebar/sidebar.dart +++ b/frontend/pweb/lib/widgets/sidebar/sidebar.dart @@ -41,15 +41,15 @@ class PayoutSidebar extends StatelessWidget { final menuItems = items ?? - [ - PayoutDestination.dashboard, - PayoutDestination.recipients, - PayoutDestination.invitations, - PayoutDestination.reports, + [ + PayoutDestination.dashboard, + PayoutDestination.recipients, + PayoutDestination.invitations, + PayoutDestination.reports, // PayoutDestination.methods, // PayoutDestination.organizationSettings, - //TODO Add when ready - ]; + //TODO Add when ready + ]; final theme = Theme.of(context); diff --git a/frontend/pweb/lib/widgets/sidebar/user.dart b/frontend/pweb/lib/widgets/sidebar/user.dart index a2a7ef9a..d75fcb30 100644 --- a/frontend/pweb/lib/widgets/sidebar/user.dart +++ b/frontend/pweb/lib/widgets/sidebar/user.dart @@ -25,8 +25,9 @@ class UserProfileCard extends StatelessWidget { Widget build(BuildContext context) { final loc = AppLocalizations.of(context)!; bool isSelected = selected == PayoutDestination.settings; + final selectedBackground = theme.colorScheme.surfaceContainerHighest; final backgroundColor = isSelected - ? theme.colorScheme.primaryContainer + ? selectedBackground : Colors.transparent; return Material(