refactoring for recipient addition page

This commit is contained in:
Arseni
2026-01-29 19:22:30 +03:00
parent da8da04ae9
commit efa69b43b2
47 changed files with 1376 additions and 532 deletions

View File

@@ -0,0 +1,121 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/methods/ledger.dart';
import 'package:pweb/utils/text_field_styles.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class LedgerForm extends StatefulWidget {
final void Function(LedgerPaymentMethod) onChanged;
final LedgerPaymentMethod? initialData;
final bool isEditable;
const LedgerForm({
super.key,
required this.onChanged,
this.initialData,
required this.isEditable,
});
@override
State<LedgerForm> createState() => _LedgerFormState();
}
class _LedgerFormState extends State<LedgerForm> {
final _formKey = GlobalKey<FormState>();
late final TextEditingController _ledgerAccountRefController;
late final TextEditingController _contraLedgerAccountRefController;
@override
void initState() {
super.initState();
_ledgerAccountRefController = TextEditingController(
text: widget.initialData?.ledgerAccountRef ?? '',
);
_contraLedgerAccountRefController = TextEditingController(
text: widget.initialData?.contraLedgerAccountRef ?? '',
);
WidgetsBinding.instance.addPostFrameCallback((_) => _emitIfValid());
}
void _emitIfValid() {
if (_formKey.currentState?.validate() ?? false) {
final contraRef = _contraLedgerAccountRefController.text.trim();
widget.onChanged(
LedgerPaymentMethod(
ledgerAccountRef: _ledgerAccountRefController.text.trim(),
contraLedgerAccountRef: contraRef.isEmpty ? null : contraRef,
),
);
}
}
@override
void didUpdateWidget(covariant LedgerForm oldWidget) {
super.didUpdateWidget(oldWidget);
final newData = widget.initialData;
final oldData = oldWidget.initialData;
if (newData == null && oldData != null) {
_ledgerAccountRefController.clear();
_contraLedgerAccountRefController.clear();
return;
}
if (newData != null && newData != oldData) {
final hasLedgerRefChange = newData.ledgerAccountRef != _ledgerAccountRefController.text;
final hasContraRefChange = (newData.contraLedgerAccountRef ?? '') != _contraLedgerAccountRefController.text;
if (hasLedgerRefChange) _ledgerAccountRefController.text = newData.ledgerAccountRef;
if (hasContraRefChange) {
_contraLedgerAccountRefController.text = newData.contraLedgerAccountRef ?? '';
}
if (hasLedgerRefChange || hasContraRefChange) {
WidgetsBinding.instance.addPostFrameCallback((_) => _emitIfValid());
}
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Form(
key: _formKey,
onChanged: _emitIfValid,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
readOnly: !widget.isEditable,
controller: _ledgerAccountRefController,
decoration: getInputDecoration(context, l10n.ledgerAccountRef, widget.isEditable),
style: getTextFieldStyle(context, widget.isEditable),
validator: (val) => (val == null || val.trim().isEmpty)
? l10n.enterLedgerAccountRef
: null,
),
const SizedBox(height: 12),
TextFormField(
readOnly: !widget.isEditable,
controller: _contraLedgerAccountRefController,
decoration: getInputDecoration(context, l10n.contraLedgerAccountRef, widget.isEditable),
style: getTextFieldStyle(context, widget.isEditable),
),
],
),
);
}
@override
void dispose() {
_ledgerAccountRefController.dispose();
_contraLedgerAccountRefController.dispose();
super.dispose();
}
}

View File

@@ -9,11 +9,15 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentMethodTypeSelector extends StatelessWidget {
final PaymentType? value;
final List<PaymentType> types;
final Set<PaymentType> disabledTypes;
final ValueChanged<PaymentType?> onChanged;
const PaymentMethodTypeSelector({
super.key,
required this.value,
required this.types,
this.disabledTypes = const {},
required this.onChanged,
});
@@ -24,12 +28,18 @@ class PaymentMethodTypeSelector extends StatelessWidget {
return DropdownButtonFormField<PaymentType>(
initialValue: value,
decoration: InputDecoration(labelText: l10n.paymentType),
items: PaymentType.values.map((type) {
items: types.map((type) {
final label = getPaymentTypeLabel(context, type);
return DropdownMenuItem(value: type, child: Text(label));
final isDisabled = disabledTypes.contains(type);
final effectiveLabel = isDisabled ? '$label - ${l10n.paymentMethodComingSoon}' : label;
return DropdownMenuItem(
value: type,
enabled: !isDisabled,
child: Text(effectiveLabel),
);
}).toList(),
onChanged: onChanged,
validator: (val) => val == null ? l10n.selectPaymentType : null,
);
}
}
}

View File

@@ -4,6 +4,7 @@ import 'package:pshared/models/payment/type.dart';
import 'package:pweb/pages/payment_methods/add/method_selector.dart';
import 'package:pweb/pages/payment_methods/form.dart';
import 'package:pweb/utils/payment/availability.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -46,6 +47,8 @@ class _AddPaymentMethodDialogState extends State<AddPaymentMethodDialog> {
children: [
PaymentMethodTypeSelector(
value: _selectedType,
types: visiblePaymentTypes,
disabledTypes: disabledPaymentTypes,
onChanged: (val) => setState(() {
_selectedType = val;
_currentMethod = null;
@@ -73,4 +76,4 @@ class _AddPaymentMethodDialogState extends State<AddPaymentMethodDialog> {
],
);
}
}
}

View File

@@ -4,6 +4,7 @@ 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/ledger.dart';
import 'package:pshared/models/payment/methods/russian_bank.dart';
import 'package:pshared/models/payment/methods/wallet.dart';
import 'package:pshared/models/payment/type.dart';
@@ -11,6 +12,7 @@ import 'package:pshared/models/payment/type.dart';
import 'package:pweb/pages/payment_methods/add/card.dart';
import 'package:pweb/pages/payment_methods/add/crypto_address.dart';
import 'package:pweb/pages/payment_methods/add/iban.dart';
import 'package:pweb/pages/payment_methods/add/ledger.dart';
import 'package:pweb/pages/payment_methods/add/russian_bank.dart';
import 'package:pweb/pages/payment_methods/add/wallet.dart';
@@ -57,6 +59,11 @@ class PaymentMethodForm extends StatelessWidget {
initialData: initialData as CryptoAddressPaymentMethod?,
isEditable: isEditable,
),
PaymentType.ledger => LedgerForm(
onChanged: onChanged,
initialData: initialData as LedgerPaymentMethod?,
isEditable: isEditable,
),
_ => const SizedBox.shrink(),
};
}

View File

@@ -11,6 +11,7 @@ import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/pages/payment_methods/payment_page/body.dart';
import 'package:pweb/utils/recipient/filtering.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
import 'package:pweb/services/posthog.dart';
import 'package:pweb/widgets/dialogs/payment_status_dialog.dart';
@@ -35,6 +36,8 @@ class PaymentPage extends StatefulWidget {
class _PaymentPageState extends State<PaymentPage> {
late final TextEditingController _searchController;
late final FocusNode _searchFocusNode;
Recipient? _previousRecipient;
String _query = '';
@override
void initState() {
@@ -58,17 +61,25 @@ class _PaymentPageState extends State<PaymentPage> {
}
void _handleSearchChanged(String query) {
context.read<RecipientsProvider>().setQuery(query);
setState(() {
_query = query;
});
}
void _handleRecipientSelected(Recipient recipient) {
final recipientProvider = context.read<RecipientsProvider>();
setState(() {
_previousRecipient = recipientProvider.currentObject;
});
recipientProvider.setCurrentObject(recipient.id);
_clearSearchField();
}
void _handleRecipientCleared() {
final recipientProvider = context.read<RecipientsProvider>();
setState(() {
_previousRecipient = recipientProvider.currentObject;
});
recipientProvider.setCurrentObject(null);
_clearSearchField();
}
@@ -76,7 +87,9 @@ class _PaymentPageState extends State<PaymentPage> {
void _clearSearchField() {
_searchController.clear();
_searchFocusNode.unfocus();
context.read<RecipientsProvider>().setQuery('');
setState(() {
_query = '';
});
}
void _handleSendPayment() {
@@ -97,16 +110,21 @@ class _PaymentPageState extends State<PaymentPage> {
@override
Widget build(BuildContext context) {
final methodsProvider = context.watch<PaymentMethodsProvider>();
final recipientProvider = context.read<RecipientsProvider>();
final recipient = context.select<RecipientsProvider, Recipient?>(
(provider) => provider.currentObject,
final recipientProvider = context.watch<RecipientsProvider>();
final recipient = recipientProvider.currentObject;
final filteredRecipients = filterRecipients(
recipients: recipientProvider.recipients,
query: _query,
);
return PaymentPageBody(
onBack: widget.onBack,
fallbackDestination: widget.fallbackDestination,
recipient: recipient,
previousRecipient: _previousRecipient,
recipientProvider: recipientProvider,
searchQuery: _query,
filteredRecipients: filteredRecipients,
methodsProvider: methodsProvider,
onWalletSelected: context.read<WalletsController>().selectWallet,
searchController: _searchController,

View File

@@ -15,7 +15,10 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentPageBody extends StatelessWidget {
final ValueChanged<Recipient?>? onBack;
final Recipient? recipient;
final Recipient? previousRecipient;
final RecipientsProvider recipientProvider;
final String searchQuery;
final List<Recipient> filteredRecipients;
final PaymentMethodsProvider methodsProvider;
final ValueChanged<Wallet> onWalletSelected;
final PayoutDestination fallbackDestination;
@@ -30,7 +33,10 @@ class PaymentPageBody extends StatelessWidget {
super.key,
required this.onBack,
required this.recipient,
required this.previousRecipient,
required this.recipientProvider,
required this.searchQuery,
required this.filteredRecipients,
required this.methodsProvider,
required this.onWalletSelected,
required this.fallbackDestination,
@@ -59,7 +65,10 @@ class PaymentPageBody extends StatelessWidget {
return PaymentPageContent(
onBack: onBack,
recipient: recipient,
previousRecipient: previousRecipient,
recipientProvider: recipientProvider,
searchQuery: searchQuery,
filteredRecipients: filteredRecipients,
onWalletSelected: onWalletSelected,
fallbackDestination: fallbackDestination,
searchController: searchController,

View File

@@ -25,7 +25,10 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentPageContent extends StatelessWidget {
final ValueChanged<Recipient?>? onBack;
final Recipient? recipient;
final Recipient? previousRecipient;
final RecipientsProvider recipientProvider;
final String searchQuery;
final List<Recipient> filteredRecipients;
final ValueChanged<Wallet> onWalletSelected;
final PayoutDestination fallbackDestination;
final TextEditingController searchController;
@@ -39,7 +42,10 @@ class PaymentPageContent extends StatelessWidget {
super.key,
required this.onBack,
required this.recipient,
required this.previousRecipient,
required this.recipientProvider,
required this.searchQuery,
required this.filteredRecipients,
required this.onWalletSelected,
required this.fallbackDestination,
required this.searchController,
@@ -99,8 +105,11 @@ class PaymentPageContent extends StatelessWidget {
SizedBox(height: dimensions.paddingXLarge),
RecipientSection(
recipient: recipient,
previousRecipient: previousRecipient,
dimensions: dimensions,
recipientProvider: recipientProvider,
searchQuery: searchQuery,
filteredRecipients: filteredRecipients,
searchController: searchController,
searchFocusNode: searchFocusNode,
onSearchChanged: onSearchChanged,

View File

@@ -21,7 +21,10 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentPageContent extends StatelessWidget {
final ValueChanged<Recipient?>? onBack;
final Recipient? recipient;
final Recipient? previousRecipient;
final RecipientsProvider recipientProvider;
final String searchQuery;
final List<Recipient> filteredRecipients;
final ValueChanged<Wallet> onWalletSelected;
final PayoutDestination fallbackDestination;
final TextEditingController searchController;
@@ -35,7 +38,10 @@ class PaymentPageContent extends StatelessWidget {
super.key,
required this.onBack,
required this.recipient,
required this.previousRecipient,
required this.recipientProvider,
required this.searchQuery,
required this.filteredRecipients,
required this.onWalletSelected,
required this.fallbackDestination,
required this.searchController,
@@ -82,8 +88,11 @@ class PaymentPageContent extends StatelessWidget {
SizedBox(height: dimensions.paddingXLarge),
RecipientSection(
recipient: recipient,
previousRecipient: previousRecipient,
dimensions: dimensions,
recipientProvider: recipientProvider,
searchQuery: searchQuery,
filteredRecipients: filteredRecipients,
searchController: searchController,
searchFocusNode: searchFocusNode,
onSearchChanged: onSearchChanged,

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:pweb/models/button_state.dart';
import 'package:pweb/models/control_state.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -8,12 +8,12 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
class SendButton extends StatelessWidget {
final VoidCallback onPressed;
final ButtonState state;
final ControlState state;
const SendButton({
super.key,
required this.onPressed,
this.state = ButtonState.enabled,
this.state = ControlState.enabled,
});
@override
@@ -21,8 +21,8 @@ class SendButton extends StatelessWidget {
final theme = Theme.of(context);
final dimensions = AppDimensions();
final isEnabled = state == ButtonState.enabled;
final isLoading = state == ButtonState.loading;
final isEnabled = state == ControlState.enabled;
final isLoading = state == ControlState.loading;
final backgroundColor = isEnabled || isLoading
? theme.colorScheme.primary
: theme.colorScheme.onSurface.withValues(alpha: 0.12);

View File

@@ -8,6 +8,7 @@ import 'package:pshared/provider/payment/flow.dart';
import 'package:pweb/pages/payment_methods/form.dart';
import 'package:pweb/pages/payment_methods/widgets/section_title.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/utils/payment/availability.dart';
import 'package:pweb/utils/payment/selector_type.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -26,7 +27,10 @@ class PaymentInfoSection extends StatelessWidget {
final loc = AppLocalizations.of(context)!;
final flowProvider = context.watch<PaymentFlowProvider>();
final hasRecipient = flowProvider.hasRecipient;
final MethodMap resolvedAvailableTypes = flowProvider.availableTypes;
final MethodMap resolvedAvailableTypes = filterVisiblePaymentTypes(flowProvider.availableTypes);
final disabledTypesForSelection = hasRecipient
? disabledPaymentTypes.difference(resolvedAvailableTypes.keys.toSet())
: disabledPaymentTypes;
if (hasRecipient && resolvedAvailableTypes.isEmpty) {
return Text(loc.recipientNoPaymentDetails);
@@ -42,6 +46,7 @@ class PaymentInfoSection extends StatelessWidget {
PaymentTypeSelector(
availableTypes: resolvedAvailableTypes,
selectedType: selectedType,
disabledTypes: disabledTypesForSelection,
onSelected: (type) => flowProvider.selectType(
type,
resetManualData: !hasRecipient,

View File

@@ -14,8 +14,11 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
class RecipientSection extends StatelessWidget {
final Recipient? recipient;
final Recipient? previousRecipient;
final AppDimensions dimensions;
final RecipientsProvider recipientProvider;
final String searchQuery;
final List<Recipient> filteredRecipients;
final TextEditingController searchController;
final FocusNode searchFocusNode;
final ValueChanged<String> onSearchChanged;
@@ -25,8 +28,11 @@ class RecipientSection extends StatelessWidget {
const RecipientSection({
super.key,
required this.recipient,
required this.previousRecipient,
required this.dimensions,
required this.recipientProvider,
required this.searchQuery,
required this.filteredRecipients,
required this.searchController,
required this.searchFocusNode,
required this.onSearchChanged,
@@ -48,8 +54,8 @@ class RecipientSection extends StatelessWidget {
return AnimatedBuilder(
animation: recipientProvider,
builder: (context, _) {
final previousRecipient = recipientProvider.previousRecipient;
final hasQuery = recipientProvider.query.isNotEmpty;
final hasQuery = searchQuery.isNotEmpty;
final prev = previousRecipient;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -61,15 +67,15 @@ class RecipientSection extends StatelessWidget {
onChanged: onSearchChanged,
focusNode: searchFocusNode,
),
if (previousRecipient != null) ...[
if (prev != null) ...[
SizedBox(height: dimensions.paddingSmall),
ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.undo),
title: Text(loc.back),
subtitle: Text(previousRecipient.name),
onTap: () => onRecipientSelected(previousRecipient),
subtitle: Text(prev.name),
onTap: () => onRecipientSelected(prev),
),
],
if (hasQuery) ...[
@@ -77,6 +83,7 @@ class RecipientSection extends StatelessWidget {
RecipientSearchResults(
dimensions: dimensions,
recipientProvider: recipientProvider,
results: filteredRecipients,
onRecipientSelected: onRecipientSelected,
),
],

View File

@@ -11,12 +11,14 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
class RecipientSearchResults extends StatelessWidget {
final AppDimensions dimensions;
final RecipientsProvider recipientProvider;
final List<Recipient> results;
final ValueChanged<Recipient> onRecipientSelected;
const RecipientSearchResults({
super.key,
required this.dimensions,
required this.recipientProvider,
required this.results,
required this.onRecipientSelected,
});
@@ -38,8 +40,6 @@ class RecipientSearchResults extends StatelessWidget {
return Text(loc.noRecipientsYet);
}
final results = recipientProvider.filteredRecipients;
if (results.isEmpty) {
return Text(loc.noRecipientsFound);
}