refactoring for recipient addition page
This commit is contained in:
120
frontend/pweb/lib/pages/address_book/form/body.dart
Normal file
120
frontend/pweb/lib/pages/address_book/form/body.dart
Normal file
@@ -0,0 +1,120 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
|
||||
import 'package:pweb/pages/address_book/form/view.dart';
|
||||
import 'package:pweb/providers/address_book_recipient_form.dart';
|
||||
import 'package:pweb/utils/payment/availability.dart';
|
||||
|
||||
|
||||
class AddressBookRecipientFormBody extends StatefulWidget {
|
||||
final GlobalKey<FormState> formKey;
|
||||
final TextEditingController nameCtrl;
|
||||
final TextEditingController emailCtrl;
|
||||
final bool isEditing;
|
||||
final Future<void> Function(AddressBookRecipientFormProvider) onSave;
|
||||
final VoidCallback onBack;
|
||||
|
||||
const AddressBookRecipientFormBody({
|
||||
required this.formKey,
|
||||
required this.nameCtrl,
|
||||
required this.emailCtrl,
|
||||
required this.isEditing,
|
||||
required this.onSave,
|
||||
required this.onBack,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AddressBookRecipientFormBody> createState() => _AddressBookRecipientFormBodyState();
|
||||
}
|
||||
|
||||
class _AddressBookRecipientFormBodyState extends State<AddressBookRecipientFormBody> {
|
||||
PaymentType? _selectedType;
|
||||
int? _selectedIndex;
|
||||
|
||||
void _reconcileSelection(AddressBookRecipientFormProvider formState) {
|
||||
final types = formState.supportedTypes;
|
||||
if (types.isEmpty) return;
|
||||
|
||||
var nextType = _selectedType;
|
||||
var nextIndex = _selectedIndex;
|
||||
|
||||
if (nextType == null || !types.contains(nextType)) {
|
||||
nextType = formState.preferredType ?? types.first;
|
||||
nextIndex = null;
|
||||
}
|
||||
|
||||
final entries = formState.methods[nextType] ?? const [];
|
||||
if (entries.isEmpty) {
|
||||
nextIndex = null;
|
||||
} else if (nextIndex == null || nextIndex < 0 || nextIndex >= entries.length) {
|
||||
nextIndex = 0;
|
||||
}
|
||||
|
||||
if (nextType == _selectedType && nextIndex == _selectedIndex) return;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_selectedType = nextType;
|
||||
_selectedIndex = nextIndex;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _onMethodSelected(PaymentType type, int index) {
|
||||
setState(() {
|
||||
_selectedType = type;
|
||||
_selectedIndex = index;
|
||||
});
|
||||
}
|
||||
|
||||
void _onMethodAdd(AddressBookRecipientFormProvider formState, PaymentType type) {
|
||||
final newIndex = formState.addMethod(type);
|
||||
setState(() {
|
||||
_selectedType = type;
|
||||
_selectedIndex = newIndex;
|
||||
});
|
||||
}
|
||||
|
||||
void _onMethodRemove(AddressBookRecipientFormProvider formState, int index) {
|
||||
final type = _selectedType ?? formState.supportedTypes.first;
|
||||
formState.removeMethod(type, index);
|
||||
}
|
||||
|
||||
void _onMethodChanged(
|
||||
AddressBookRecipientFormProvider formState,
|
||||
int index,
|
||||
PaymentMethodData data,
|
||||
) {
|
||||
final type = _selectedType ?? formState.supportedTypes.first;
|
||||
formState.updateMethod(type, index, data);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final formState = context.watch<AddressBookRecipientFormProvider>();
|
||||
_reconcileSelection(formState);
|
||||
|
||||
final selectedType = _selectedType ?? formState.supportedTypes.first;
|
||||
return FormView(
|
||||
formKey: widget.formKey,
|
||||
nameCtrl: widget.nameCtrl,
|
||||
emailCtrl: widget.emailCtrl,
|
||||
types: formState.supportedTypes,
|
||||
selectedType: selectedType,
|
||||
selectedIndex: _selectedIndex,
|
||||
methods: formState.methods,
|
||||
onMethodSelected: _onMethodSelected,
|
||||
onMethodAdd: (type) => _onMethodAdd(formState, type),
|
||||
disabledTypes: disabledPaymentTypes,
|
||||
onMethodRemove: (index) => _onMethodRemove(formState, index),
|
||||
onMethodChanged: (index, data) => _onMethodChanged(formState, index, data),
|
||||
onSave: () => widget.onSave(formState),
|
||||
isEditing: widget.isEditing,
|
||||
onBack: widget.onBack,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/payment/type.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/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class AddressBookPaymentMethodTile extends StatefulWidget {
|
||||
final PaymentType type;
|
||||
final String title;
|
||||
final MethodMap methods;
|
||||
final ValueChanged<PaymentMethodData?> onChanged;
|
||||
|
||||
final double spacingM;
|
||||
final double spacingS;
|
||||
final double sizeM;
|
||||
final TextStyle? titleTextStyle;
|
||||
|
||||
const AddressBookPaymentMethodTile({
|
||||
super.key,
|
||||
required this.type,
|
||||
required this.title,
|
||||
required this.methods,
|
||||
required this.onChanged,
|
||||
this.spacingM = 12,
|
||||
this.spacingS = 8,
|
||||
this.sizeM = 20,
|
||||
this.titleTextStyle,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AddressBookPaymentMethodTile> createState() => _AddressBookPaymentMethodTileState();
|
||||
}
|
||||
|
||||
class _AddressBookPaymentMethodTileState extends State<AddressBookPaymentMethodTile> {
|
||||
Future<void> _confirmDelete() async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final confirmed = await showConfirmationDialog(
|
||||
context: context,
|
||||
title: l10n.delete,
|
||||
message: l10n.deletePaymentConfirmation,
|
||||
confirmLabel: l10n.delete,
|
||||
);
|
||||
if (confirmed) {
|
||||
widget.onChanged(null);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final isAdded = widget.methods.containsKey(widget.type);
|
||||
|
||||
return ExpansionTile(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
iconForPaymentType(widget.type),
|
||||
size: widget.sizeM,
|
||||
color: isAdded
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.onSurface,
|
||||
),
|
||||
SizedBox(width: widget.spacingS),
|
||||
Text(
|
||||
widget.title,
|
||||
style: widget.titleTextStyle ??
|
||||
theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isAdded ? theme.colorScheme.primary : null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isAdded)
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete, color: theme.colorScheme.error),
|
||||
onPressed: _confirmDelete,
|
||||
),
|
||||
Icon(
|
||||
isAdded ? Icons.check_circle : Icons.add_circle_outline,
|
||||
color: isAdded ? theme.colorScheme.primary : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
children: [
|
||||
PaymentMethodForm(
|
||||
key: ValueKey(widget.type),
|
||||
selectedType: widget.type,
|
||||
initialData: widget.methods[widget.type],
|
||||
onChanged: widget.onChanged,
|
||||
),
|
||||
SizedBox(height: widget.spacingM),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,16 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
import 'package:pshared/models/recipient/status.dart';
|
||||
import 'package:pshared/models/recipient/type.dart';
|
||||
import 'package:pshared/provider/recipient/methods_cache.dart';
|
||||
import 'package:pshared/provider/recipient/provider.dart';
|
||||
|
||||
import 'package:pweb/pages/address_book/form/view.dart';
|
||||
import 'package:pweb/services/posthog.dart';
|
||||
import 'package:pweb/utils/error/snackbar.dart';
|
||||
import 'package:pweb/pages/address_book/form/body.dart';
|
||||
import 'package:pweb/providers/address_book_recipient_form.dart';
|
||||
import 'package:pweb/utils/payment/availability.dart';
|
||||
import 'package:pweb/utils/payment/label.dart';
|
||||
import 'package:pweb/utils/snackbar.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
@@ -35,11 +29,8 @@ class _AddressBookRecipientFormState extends State<AddressBookRecipientForm> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late TextEditingController _nameCtrl;
|
||||
late TextEditingController _emailCtrl;
|
||||
RecipientType _type = RecipientType.internal;
|
||||
RecipientStatus _status = RecipientStatus.ready;
|
||||
final MethodMap _methods = {};
|
||||
late RecipientMethodsCacheProvider _methodsCacheProvider;
|
||||
bool _hasInitializedMethods = false;
|
||||
|
||||
static const List<PaymentType> _supportedTypes = visiblePaymentTypes;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -47,129 +38,60 @@ class _AddressBookRecipientFormState extends State<AddressBookRecipientForm> {
|
||||
final r = widget.recipient;
|
||||
_nameCtrl = TextEditingController(text: r?.name ?? '');
|
||||
_emailCtrl = TextEditingController(text: r?.email ?? '');
|
||||
_type = r?.type ?? RecipientType.internal;
|
||||
_status = r?.status ?? RecipientStatus.ready;
|
||||
_methodsCacheProvider = context.read<RecipientMethodsCacheProvider>()
|
||||
..addListener(_onProviderChanged);
|
||||
if (r != null) {
|
||||
_methodsCacheProvider.refreshRecipient(r.id);
|
||||
_syncMethodsFromCache();
|
||||
}
|
||||
}
|
||||
|
||||
Future<Recipient?> _doSave() async {
|
||||
final recipients = context.read<RecipientsProvider>();
|
||||
final recipient = widget.recipient == null
|
||||
? await recipients.create(
|
||||
name: _nameCtrl.text,
|
||||
email: _emailCtrl.text,
|
||||
)
|
||||
: widget.recipient!;
|
||||
recipients.setCurrentObject(recipient.id);
|
||||
final methods = <PaymentType, PaymentMethodData>{};
|
||||
final names = <PaymentType, String>{};
|
||||
for (final entry in _methods.entries) {
|
||||
final data = entry.value;
|
||||
if (data == null) continue;
|
||||
methods[entry.key] = data;
|
||||
names[entry.key] = getPaymentTypeLabel(context, entry.key);
|
||||
}
|
||||
await _methodsCacheProvider.syncRecipientMethods(
|
||||
recipientId: recipient.id,
|
||||
methods: methods,
|
||||
names: names,
|
||||
);
|
||||
return recipient;
|
||||
}
|
||||
Map<PaymentType, String> _methodNames(BuildContext context) => {
|
||||
for (final type in _supportedTypes) type: getPaymentTypeLabel(context, type),
|
||||
};
|
||||
|
||||
Future<void> _save() async {
|
||||
Future<void> _save(AddressBookRecipientFormProvider formState) async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
notifyUser(context, l10n.recipientFormValidationError);
|
||||
if (!_formKey.currentState!.validate() || !formState.hasAnyMethod) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.recipientFormRule)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_methods.isEmpty) {
|
||||
notifyUser(context, l10n.recipientFormRule);
|
||||
return;
|
||||
try {
|
||||
final saved = await formState.save(
|
||||
name: _nameCtrl.text,
|
||||
email: _emailCtrl.text,
|
||||
methodNames: _methodNames(context),
|
||||
);
|
||||
widget.onSaved?.call(saved);
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.notificationError(l10n.noErrorInformation))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
unawaited(PosthogService.recipientAddCompleted(
|
||||
_type,
|
||||
_status,
|
||||
_methods.keys.toSet(),
|
||||
));
|
||||
final recipient = await executeActionWithNotification(
|
||||
context: context,
|
||||
action: _doSave,
|
||||
errorMessage: l10n.errorSaveRecipient,
|
||||
successMessage: l10n.recipientSavedSuccessfully,
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProxyProvider2<
|
||||
RecipientsProvider,
|
||||
RecipientMethodsCacheProvider,
|
||||
AddressBookRecipientFormProvider
|
||||
>(
|
||||
create: (_) => AddressBookRecipientFormProvider(
|
||||
recipient: widget.recipient,
|
||||
supportedTypes: _supportedTypes,
|
||||
),
|
||||
update: (_, recipientsProvider, methodsCache, formProvider) =>
|
||||
formProvider!..updateProviders(
|
||||
recipientsProvider: recipientsProvider,
|
||||
methodsCache: methodsCache,
|
||||
),
|
||||
child: AddressBookRecipientFormBody(
|
||||
formKey: _formKey,
|
||||
nameCtrl: _nameCtrl,
|
||||
emailCtrl: _emailCtrl,
|
||||
isEditing: widget.recipient != null,
|
||||
onSave: _save,
|
||||
onBack: () => widget.onSaved?.call(null),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
widget.onSaved?.call(recipient);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_methodsCacheProvider.removeListener(_onProviderChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onProviderChanged() => _syncMethodsFromCache();
|
||||
|
||||
void _syncMethodsFromCache() {
|
||||
final recipient = widget.recipient;
|
||||
if (recipient == null || _hasInitializedMethods) return;
|
||||
if (!_methodsCacheProvider.hasMethodsFor(recipient.id)) return;
|
||||
final list = _methodsCacheProvider.methodsForRecipient(recipient.id);
|
||||
if (list.isEmpty) {
|
||||
_hasInitializedMethods = true;
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_methods
|
||||
..clear()
|
||||
..addEntries(list.map((m) {
|
||||
final data = switch (m.type) {
|
||||
PaymentType.card => m.cardData,
|
||||
PaymentType.iban => m.ibanData,
|
||||
PaymentType.wallet => m.walletData,
|
||||
PaymentType.bankAccount => m.bankAccountData,
|
||||
PaymentType.externalChain => m.cryptoAddressData,
|
||||
//TODO: support new payment methods
|
||||
_ => throw UnimplementedError('Payment method ${m.type} is not supported yet'),
|
||||
};
|
||||
return MapEntry(m.type, data);
|
||||
}));
|
||||
_hasInitializedMethods = true;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => FormView(
|
||||
formKey: _formKey,
|
||||
nameCtrl: _nameCtrl,
|
||||
emailCtrl: _emailCtrl,
|
||||
type: _type,
|
||||
status: _status,
|
||||
methods: _methods,
|
||||
onTypeChanged: (t) => setState(() => _type = t),
|
||||
onStatusChanged: (s) => setState(() => _status = s),
|
||||
onMethodsChanged: (type, data) {
|
||||
setState(() {
|
||||
if (data != null) {
|
||||
_methods[type] = data;
|
||||
} else {
|
||||
_methods.remove(type);
|
||||
}
|
||||
});
|
||||
},
|
||||
onSave: _save,
|
||||
isEditing: widget.recipient != null,
|
||||
onBack: () {
|
||||
widget.onSaved?.call(null);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
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/status.dart';
|
||||
import 'package:pshared/models/recipient/type.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/method_tile.dart';
|
||||
import 'package:pweb/pages/address_book/form/widgets/button.dart';
|
||||
import 'package:pweb/pages/address_book/form/widgets/email_field.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/name_field.dart';
|
||||
import 'package:pweb/utils/payment/label.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/selector_row.dart';
|
||||
import 'package:pweb/pages/address_book/form/widgets/save_button.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
@@ -19,12 +18,15 @@ class FormView extends StatelessWidget {
|
||||
final GlobalKey<FormState> formKey;
|
||||
final TextEditingController nameCtrl;
|
||||
final TextEditingController emailCtrl;
|
||||
final RecipientType type;
|
||||
final RecipientStatus status;
|
||||
final MethodMap methods;
|
||||
final ValueChanged<RecipientType> onTypeChanged;
|
||||
final ValueChanged<RecipientStatus> onStatusChanged;
|
||||
final void Function(PaymentType, PaymentMethodData?) onMethodsChanged;
|
||||
final List<PaymentType> types;
|
||||
final PaymentType selectedType;
|
||||
final int? selectedIndex;
|
||||
final Map<PaymentType, List<RecipientMethodDraft>> methods;
|
||||
final void Function(PaymentType type, int index) onMethodSelected;
|
||||
final ValueChanged<PaymentType> onMethodAdd;
|
||||
final Set<PaymentType> disabledTypes;
|
||||
final ValueChanged<int> onMethodRemove;
|
||||
final void Function(int, PaymentMethodData) onMethodChanged;
|
||||
final VoidCallback onSave;
|
||||
final bool isEditing;
|
||||
final VoidCallback onBack;
|
||||
@@ -45,16 +47,19 @@ class FormView extends StatelessWidget {
|
||||
required this.formKey,
|
||||
required this.nameCtrl,
|
||||
required this.emailCtrl,
|
||||
required this.type,
|
||||
required this.status,
|
||||
required this.types,
|
||||
required this.selectedType,
|
||||
required this.selectedIndex,
|
||||
required this.methods,
|
||||
required this.onTypeChanged,
|
||||
required this.onStatusChanged,
|
||||
required this.onMethodsChanged,
|
||||
required this.onMethodSelected,
|
||||
required this.onMethodAdd,
|
||||
this.disabledTypes = const {},
|
||||
required this.onMethodRemove,
|
||||
required this.onMethodChanged,
|
||||
required this.onSave,
|
||||
required this.isEditing,
|
||||
required this.onBack,
|
||||
this.maxWidth = 500,
|
||||
this.maxWidth = 800,
|
||||
this.elevation = 4,
|
||||
this.borderRadius = 16,
|
||||
this.padding = const EdgeInsets.all(20),
|
||||
@@ -69,6 +74,10 @@ class FormView extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final entries = methods[selectedType] ?? const <RecipientMethodDraft>[];
|
||||
final hasSelection = selectedIndex != null &&
|
||||
selectedIndex! >= 0 &&
|
||||
selectedIndex! < entries.length;
|
||||
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
@@ -102,14 +111,25 @@ class FormView extends StatelessWidget {
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
SizedBox(height: spacingFields),
|
||||
...PaymentType.values.map(
|
||||
(p) => AddressBookPaymentMethodTile(
|
||||
type: p,
|
||||
title: getPaymentTypeLabel(context, p),
|
||||
methods: methods,
|
||||
onChanged: (data) => onMethodsChanged(p, data),
|
||||
),
|
||||
PaymentMethodSelectorRow(
|
||||
types: types,
|
||||
selectedType: selectedType,
|
||||
selectedIndex: selectedIndex,
|
||||
methods: methods,
|
||||
onSelected: onMethodSelected,
|
||||
onAdd: onMethodAdd,
|
||||
disabledTypes: disabledTypes,
|
||||
),
|
||||
if (hasSelection) ...[
|
||||
SizedBox(height: spacingFields),
|
||||
PaymentMethodPanel(
|
||||
selectedType: selectedType,
|
||||
selectedIndex: selectedIndex!,
|
||||
entries: entries,
|
||||
onRemove: onMethodRemove,
|
||||
onChanged: onMethodChanged,
|
||||
),
|
||||
],
|
||||
SizedBox(height: spacingSave),
|
||||
SaveButton(onSave: onSave),
|
||||
SizedBox(height: spacingBottom),
|
||||
@@ -122,4 +142,4 @@ class FormView extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class ChoiceChips<T> extends StatelessWidget {
|
||||
final String label;
|
||||
final List<T> values;
|
||||
final T selected;
|
||||
final ValueChanged<T> onChanged;
|
||||
|
||||
final double spacing;
|
||||
final double runSpacing;
|
||||
final double labelSpacing;
|
||||
|
||||
const ChoiceChips({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.values,
|
||||
required this.selected,
|
||||
required this.onChanged,
|
||||
this.spacing = 8,
|
||||
this.runSpacing = 8,
|
||||
this.labelSpacing = 8,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.titleMedium!.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
SizedBox(height: labelSpacing),
|
||||
Wrap(
|
||||
spacing: spacing,
|
||||
runSpacing: runSpacing,
|
||||
children: values.map((v) {
|
||||
final isSelected = v == selected;
|
||||
return ChoiceChip(
|
||||
selectedColor: theme.colorScheme.primary,
|
||||
backgroundColor: theme.colorScheme.onSecondary,
|
||||
showCheckmark: false,
|
||||
label: Text(
|
||||
v.toString().split('.').last,
|
||||
style: TextStyle(
|
||||
color: isSelected
|
||||
? theme.colorScheme.onSecondary
|
||||
: theme.colorScheme.inverseSurface,
|
||||
),
|
||||
),
|
||||
selected: isSelected,
|
||||
onSelected: (_) => onChanged(v),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
import 'package:pweb/pages/address_book/form/widgets/feilds/recipient_text_field.dart';
|
||||
|
||||
|
||||
class EmailField extends StatelessWidget {
|
||||
@@ -20,17 +21,16 @@ class EmailField extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
|
||||
return TextFormField(
|
||||
return RecipientTextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
labelText: loc.username,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
),
|
||||
contentPadding: contentPadding,
|
||||
),
|
||||
validator: (v) =>
|
||||
v == null || v.isEmpty ? loc.usernameErrorInvalid : null,
|
||||
labelText: loc.username,
|
||||
hintText: loc.usernameHint,
|
||||
icon: Icons.alternate_email,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
autofillHints: const [AutofillHints.email],
|
||||
validator: (v) => v == null || v.isEmpty ? loc.usernameErrorInvalid : null,
|
||||
borderRadius: borderRadius,
|
||||
contentPadding: contentPadding,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
import 'package:pweb/pages/address_book/form/widgets/feilds/recipient_text_field.dart';
|
||||
|
||||
|
||||
class NameField extends StatelessWidget {
|
||||
@@ -20,16 +21,16 @@ class NameField extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
|
||||
return TextFormField(
|
||||
return RecipientTextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
labelText: loc.recipientName,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
),
|
||||
contentPadding: contentPadding,
|
||||
),
|
||||
labelText: loc.recipientName,
|
||||
hintText: loc.recipientNameHint,
|
||||
icon: Icons.person_outline,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
autofillHints: const [AutofillHints.name],
|
||||
validator: (v) => v == null || v.isEmpty ? loc.enterRecipientName : null,
|
||||
borderRadius: borderRadius,
|
||||
contentPadding: contentPadding,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class RecipientTextField extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
final String labelText;
|
||||
final String hintText;
|
||||
final String? helperText;
|
||||
final IconData icon;
|
||||
final TextInputType keyboardType;
|
||||
final TextInputAction textInputAction;
|
||||
final TextCapitalization textCapitalization;
|
||||
final Iterable<String>? autofillHints;
|
||||
final FormFieldValidator<String>? validator;
|
||||
final double borderRadius;
|
||||
final EdgeInsetsGeometry contentPadding;
|
||||
|
||||
const RecipientTextField({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.labelText,
|
||||
required this.hintText,
|
||||
required this.icon,
|
||||
this.helperText,
|
||||
this.keyboardType = TextInputType.text,
|
||||
this.textInputAction = TextInputAction.next,
|
||||
this.textCapitalization = TextCapitalization.none,
|
||||
this.autofillHints,
|
||||
this.validator,
|
||||
this.borderRadius = 12,
|
||||
this.contentPadding = const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: controller,
|
||||
builder: (context, value, _) {
|
||||
final isEmpty = value.text.trim().isEmpty;
|
||||
final errorColor = theme.colorScheme.error;
|
||||
final neutralBorder = theme.colorScheme.onSurface.withAlpha(40);
|
||||
final borderColor = isEmpty ? errorColor : neutralBorder;
|
||||
final focusedColor = isEmpty ? errorColor : theme.colorScheme.primary;
|
||||
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
keyboardType: keyboardType,
|
||||
textInputAction: textInputAction,
|
||||
textCapitalization: textCapitalization,
|
||||
autofillHints: autofillHints,
|
||||
decoration: InputDecoration(
|
||||
labelText: labelText,
|
||||
hintText: hintText,
|
||||
helperText: helperText,
|
||||
prefixIcon: Icon(icon, color: isEmpty ? errorColor : null),
|
||||
filled: true,
|
||||
fillColor: theme.colorScheme.onSecondary,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
borderSide: BorderSide(color: borderColor),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
borderSide: BorderSide(color: focusedColor, width: 1.4),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
borderSide: BorderSide(color: errorColor),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
borderSide: BorderSide(color: errorColor, width: 1.4),
|
||||
),
|
||||
contentPadding: contentPadding,
|
||||
),
|
||||
validator: validator,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
|
||||
import 'package:pweb/pages/payment_methods/icon.dart';
|
||||
import 'package:pweb/utils/payment/label.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class AddPaymentMethodButton extends StatelessWidget {
|
||||
final List<PaymentType> types;
|
||||
final Set<PaymentType> disabledTypes;
|
||||
final ValueChanged<PaymentType> onAdd;
|
||||
|
||||
static const double _borderRadius = 14;
|
||||
static const double _iconSize = 18;
|
||||
static const double _iconTextSpacing = 8;
|
||||
static const double _menuIconSize = 18;
|
||||
static const double _menuIconTextSpacing = 8;
|
||||
static const double _buttonHeight = 70;
|
||||
static const EdgeInsets _buttonPadding = EdgeInsets.symmetric(horizontal: 14, vertical: 10);
|
||||
static const FontWeight _labelWeight = FontWeight.w600;
|
||||
|
||||
const AddPaymentMethodButton({
|
||||
required this.types,
|
||||
required this.disabledTypes,
|
||||
required this.onAdd,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final hasEnabled = types.any((type) => !disabledTypes.contains(type));
|
||||
final borderColor = hasEnabled
|
||||
? theme.colorScheme.primary.withValues(alpha: 0.5)
|
||||
: theme.colorScheme.onSurface.withValues(alpha: 0.2);
|
||||
final textColor = hasEnabled
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.onSurface.withValues(alpha: 0.4);
|
||||
|
||||
return PopupMenuButton<PaymentType>(
|
||||
enabled: hasEnabled,
|
||||
onSelected: onAdd,
|
||||
itemBuilder: (context) => types
|
||||
.map((type) {
|
||||
final isDisabled = disabledTypes.contains(type);
|
||||
return PopupMenuItem<PaymentType>(
|
||||
value: type,
|
||||
enabled: !isDisabled,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
iconForPaymentType(type),
|
||||
size: _menuIconSize,
|
||||
color: isDisabled
|
||||
? theme.colorScheme.onSurface.withValues(alpha: 0.4)
|
||||
: theme.colorScheme.onSurface,
|
||||
),
|
||||
const SizedBox(width: _menuIconTextSpacing),
|
||||
Text(getPaymentTypeLabel(context, type)),
|
||||
],
|
||||
),
|
||||
);
|
||||
})
|
||||
.toList(),
|
||||
child: Container(
|
||||
height: _buttonHeight,
|
||||
padding: _buttonPadding,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(_borderRadius),
|
||||
border: Border.all(color: borderColor),
|
||||
color: theme.colorScheme.onSecondary,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.add, size: _iconSize, color: textColor),
|
||||
const SizedBox(width: _iconTextSpacing),
|
||||
Text(
|
||||
l10n.addPaymentMethod,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
color: textColor,
|
||||
fontWeight: _labelWeight,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
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/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class PaymentMethodPanel extends StatelessWidget {
|
||||
final PaymentType selectedType;
|
||||
final int selectedIndex;
|
||||
final List<RecipientMethodDraft> entries;
|
||||
final ValueChanged<int> onRemove;
|
||||
final void Function(int, PaymentMethodData) onChanged;
|
||||
|
||||
final double padding;
|
||||
|
||||
const PaymentMethodPanel({
|
||||
super.key,
|
||||
required this.selectedType,
|
||||
required this.selectedIndex,
|
||||
required this.entries,
|
||||
required this.onRemove,
|
||||
required this.onChanged,
|
||||
this.padding = 16,
|
||||
});
|
||||
|
||||
Future<void> _confirmDelete(BuildContext context, VoidCallback onConfirmed) async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final confirmed = await showConfirmationDialog(
|
||||
context: context,
|
||||
title: l10n.delete,
|
||||
message: l10n.deletePaymentConfirmation,
|
||||
confirmLabel: l10n.delete,
|
||||
);
|
||||
if (confirmed) {
|
||||
onConfirmed();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final label = l10n.paymentMethodDetails;
|
||||
final entry = selectedIndex >= 0 && selectedIndex < entries.length
|
||||
? entries[selectedIndex]
|
||||
: null;
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 3000),
|
||||
padding: EdgeInsets.all(padding),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.onSecondary,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: theme.colorScheme.onSurface.withAlpha(30)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
iconForPaymentType(selectedType),
|
||||
size: 18,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (entry != null)
|
||||
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,
|
||||
onChanged: (data) {
|
||||
if (data == null) return;
|
||||
onChanged(selectedIndex, data);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
import 'package:pshared/models/recipient/payment_method_draft.dart';
|
||||
|
||||
import 'package:pweb/models/payment_method_tile/availability.dart';
|
||||
import 'package:pweb/models/payment_method_tile/selection.dart';
|
||||
import 'package:pweb/pages/address_book/form/widgets/payment_methods/add_button.dart';
|
||||
import 'package:pweb/pages/address_book/form/widgets/payment_methods/tile.dart';
|
||||
|
||||
|
||||
class PaymentMethodSelectorRow extends StatelessWidget {
|
||||
final List<PaymentType> types;
|
||||
final PaymentType selectedType;
|
||||
final int? selectedIndex;
|
||||
final Map<PaymentType, List<RecipientMethodDraft>> methods;
|
||||
final void Function(PaymentType type, int index) onSelected;
|
||||
final ValueChanged<PaymentType> onAdd;
|
||||
final Set<PaymentType> disabledTypes;
|
||||
|
||||
final double spacing;
|
||||
final double tilePadding;
|
||||
final double runSpacing;
|
||||
|
||||
const PaymentMethodSelectorRow({
|
||||
super.key,
|
||||
required this.types,
|
||||
required this.selectedType,
|
||||
required this.selectedIndex,
|
||||
required this.methods,
|
||||
required this.onSelected,
|
||||
required this.onAdd,
|
||||
this.disabledTypes = const {},
|
||||
this.spacing = 12,
|
||||
this.tilePadding = 10,
|
||||
this.runSpacing = 12,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tiles = <Widget>[];
|
||||
for (final type in types) {
|
||||
final entries = methods[type] ?? const <RecipientMethodDraft>[];
|
||||
for (var index = 0; index < entries.length; index += 1) {
|
||||
final entry = entries[index];
|
||||
final isSelected = type == selectedType && selectedIndex == index;
|
||||
final selection = isSelected
|
||||
? PaymentMethodTileSelection.selected
|
||||
: PaymentMethodTileSelection.idle;
|
||||
final isAdded = entry.data != null || entry.existing != null;
|
||||
final availability = isAdded
|
||||
? PaymentMethodTileAvailability.added
|
||||
: PaymentMethodTileAvailability.available;
|
||||
tiles.add(
|
||||
PaymentMethodTile(
|
||||
type: type,
|
||||
selection: selection,
|
||||
availability: availability,
|
||||
padding: tilePadding,
|
||||
onTap: () => onSelected(type, index),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
tiles.add(
|
||||
AddPaymentMethodButton(
|
||||
types: types,
|
||||
disabledTypes: disabledTypes,
|
||||
onAdd: onAdd,
|
||||
),
|
||||
);
|
||||
|
||||
return Wrap(
|
||||
spacing: spacing,
|
||||
runSpacing: runSpacing,
|
||||
alignment: WrapAlignment.start,
|
||||
children: tiles,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
|
||||
import 'package:pweb/models/payment_method_tile/availability.dart';
|
||||
import 'package:pweb/models/payment_method_tile/selection.dart';
|
||||
import 'package:pweb/pages/payment_methods/icon.dart';
|
||||
import 'package:pweb/utils/payment/label.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class PaymentMethodTile extends StatelessWidget {
|
||||
final PaymentType type;
|
||||
final PaymentMethodTileSelection selection;
|
||||
final PaymentMethodTileAvailability availability;
|
||||
final double padding;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const PaymentMethodTile({
|
||||
required this.type,
|
||||
required this.selection,
|
||||
required this.availability,
|
||||
required this.padding,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final label = getPaymentTypeLabel(context, type);
|
||||
final badgeLabel = switch (availability) {
|
||||
PaymentMethodTileAvailability.added => l10n.paymentMethodAdded,
|
||||
PaymentMethodTileAvailability.comingSoon => l10n.paymentMethodComingSoon,
|
||||
PaymentMethodTileAvailability.available => l10n.paymentMethodNotAdded,
|
||||
};
|
||||
final isSelected = selection == PaymentMethodTileSelection.selected;
|
||||
final isDisabled = availability == PaymentMethodTileAvailability.comingSoon;
|
||||
final disabledOpacity = isDisabled ? 0.55 : 1.0;
|
||||
final badgeColor = availability == PaymentMethodTileAvailability.added
|
||||
? theme.colorScheme.primary.withValues(alpha: 0.12)
|
||||
: theme.colorScheme.onSurface.withValues(alpha: isDisabled ? 0.06 : 0.08);
|
||||
final badgeTextColor = availability == PaymentMethodTileAvailability.added
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.onSurface.withValues(alpha: isDisabled ? 0.6 : 1.0);
|
||||
final borderColor = isSelected
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.onSurface.withValues(alpha: 0.12);
|
||||
final backgroundColor = isSelected
|
||||
? theme.colorScheme.primary.withValues(alpha: 0.08)
|
||||
: theme.colorScheme.onSecondary;
|
||||
|
||||
return IntrinsicWidth(
|
||||
child: Opacity(
|
||||
opacity: disabledOpacity,
|
||||
child: Material(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 160),
|
||||
padding: EdgeInsets.all(padding),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: borderColor),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
iconForPaymentType(type),
|
||||
size: 20,
|
||||
color: isSelected ? theme.colorScheme.primary : theme.colorScheme.onSurface,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isSelected ? theme.colorScheme.primary : theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: badgeColor,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(
|
||||
badgeLabel,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: badgeTextColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -42,10 +42,10 @@ class SaveButton extends StatelessWidget {
|
||||
child: Text(
|
||||
text ?? AppLocalizations.of(context)!.saveRecipient,
|
||||
style: textStyle ??
|
||||
theme.textTheme.labelLarge?.copyWith(
|
||||
color: theme.colorScheme.onPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
theme.textTheme.labelLarge?.copyWith(
|
||||
color: theme.colorScheme.onPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -11,6 +11,7 @@ import 'package:pweb/pages/address_book/page/filter_button.dart';
|
||||
import 'package:pweb/pages/address_book/page/header.dart';
|
||||
import 'package:pweb/pages/address_book/page/list.dart';
|
||||
import 'package:pweb/pages/address_book/page/search.dart';
|
||||
import 'package:pweb/utils/recipient/filtering.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
@@ -41,12 +42,13 @@ class RecipientAddressBookPage extends StatefulWidget {
|
||||
class _RecipientAddressBookPageState extends State<RecipientAddressBookPage> {
|
||||
late final TextEditingController _searchController;
|
||||
late final FocusNode _searchFocusNode;
|
||||
RecipientFilter _selectedFilter = RecipientFilter.all;
|
||||
String _query = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final provider = context.read<RecipientsProvider>();
|
||||
_searchController = TextEditingController(text: provider.query);
|
||||
_searchController = TextEditingController();
|
||||
_searchFocusNode = FocusNode();
|
||||
}
|
||||
|
||||
@@ -57,23 +59,27 @@ class _RecipientAddressBookPageState extends State<RecipientAddressBookPage> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _syncSearchField(RecipientsProvider provider) {
|
||||
final query = provider.query;
|
||||
if (_searchController.text == query) return;
|
||||
void _setQuery(String query) {
|
||||
setState(() {
|
||||
_query = query;
|
||||
});
|
||||
}
|
||||
|
||||
_searchController.value = TextEditingValue(
|
||||
text: query,
|
||||
selection: TextSelection.collapsed(offset: query.length),
|
||||
);
|
||||
void _setFilter(RecipientFilter filter) {
|
||||
setState(() {
|
||||
_selectedFilter = filter;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final provider = context.watch<RecipientsProvider>();
|
||||
_syncSearchField(provider);
|
||||
final filteredRecipients = provider.filteredRecipients;
|
||||
final filteredRecipients = filterRecipients(
|
||||
recipients: provider.recipients,
|
||||
filter: _selectedFilter,
|
||||
query: _query,
|
||||
);
|
||||
|
||||
if (provider.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
@@ -91,7 +97,7 @@ class _RecipientAddressBookPageState extends State<RecipientAddressBookPage> {
|
||||
RecipientSearchField(
|
||||
controller: _searchController,
|
||||
focusNode: _searchFocusNode,
|
||||
onChanged: provider.setQuery,
|
||||
onChanged: _setQuery,
|
||||
),
|
||||
const SizedBox(height: RecipientAddressBookPage._bigBox),
|
||||
Row(
|
||||
@@ -99,26 +105,26 @@ class _RecipientAddressBookPageState extends State<RecipientAddressBookPage> {
|
||||
RecipientFilterButton(
|
||||
text: loc.allStatus,
|
||||
filter: RecipientFilter.all,
|
||||
selected: provider.selectedFilter,
|
||||
onTap: provider.setFilter,
|
||||
selected: _selectedFilter,
|
||||
onTap: _setFilter,
|
||||
),
|
||||
RecipientFilterButton(
|
||||
text: loc.readyStatus,
|
||||
filter: RecipientFilter.ready,
|
||||
selected: provider.selectedFilter,
|
||||
onTap: provider.setFilter,
|
||||
selected: _selectedFilter,
|
||||
onTap: _setFilter,
|
||||
),
|
||||
RecipientFilterButton(
|
||||
text: loc.registeredStatus,
|
||||
filter: RecipientFilter.registered,
|
||||
selected: provider.selectedFilter,
|
||||
onTap: provider.setFilter,
|
||||
selected: _selectedFilter,
|
||||
onTap: _setFilter,
|
||||
),
|
||||
RecipientFilterButton(
|
||||
text: loc.notRegisteredStatus,
|
||||
filter: RecipientFilter.notRegistered,
|
||||
selected: provider.selectedFilter,
|
||||
onTap: provider.setFilter,
|
||||
selected: _selectedFilter,
|
||||
onTap: _setFilter,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -145,4 +151,4 @@ class _RecipientAddressBookPageState extends State<RecipientAddressBookPage> {
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,13 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
import 'package:pshared/provider/organizations.dart';
|
||||
import 'package:pshared/provider/recipient/pmethods.dart';
|
||||
import 'package:pshared/provider/recipient/methods_cache.dart';
|
||||
|
||||
import 'package:pweb/pages/address_book/page/recipient/info_row.dart';
|
||||
import 'package:pweb/utils/payment/label.dart';
|
||||
|
||||
|
||||
class RecipientPaymentRow extends StatefulWidget {
|
||||
class RecipientPaymentRow extends StatelessWidget {
|
||||
final Recipient recipient;
|
||||
final double spacing;
|
||||
|
||||
@@ -20,40 +19,19 @@ class RecipientPaymentRow extends StatefulWidget {
|
||||
this.spacing = 18
|
||||
});
|
||||
|
||||
@override
|
||||
State<RecipientPaymentRow> createState() => _RecipientPaymentRowState();
|
||||
}
|
||||
|
||||
class _RecipientPaymentRowState extends State<RecipientPaymentRow> {
|
||||
late final PaymentMethodsProvider _methodsProvider;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_methodsProvider = PaymentMethodsProvider()
|
||||
..addListener(_onProviderChanged)
|
||||
..loadMethods(
|
||||
context.read<OrganizationsProvider>(),
|
||||
widget.recipient.id,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_methodsProvider.removeListener(_onProviderChanged);
|
||||
_methodsProvider.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onProviderChanged() => setState(() {});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_methodsProvider.isReady) return const Center(child: CircularProgressIndicator());
|
||||
final cacheProvider = context.watch<RecipientMethodsCacheProvider>();
|
||||
final recipientId = recipient.id;
|
||||
final isLoading = cacheProvider.isLoadingFor(recipientId);
|
||||
|
||||
if (isLoading && !cacheProvider.hasMethodsFor(recipientId)) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return Row(
|
||||
spacing: widget.spacing,
|
||||
children: _methodsProvider.methods.map((m) => RecipientAddressBookInfoRow(
|
||||
spacing: spacing,
|
||||
children: cacheProvider.methodsForRecipient(recipientId).map((m) => RecipientAddressBookInfoRow(
|
||||
type: m.type,
|
||||
value: getPaymentTypeDescription(context, m),
|
||||
)).toList(),
|
||||
|
||||
Reference in New Issue
Block a user