redesigned payment page + a lot of fixes
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/payment/methods/type.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
import 'package:pshared/models/recipient/payment_method_draft.dart';
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
import 'package:pshared/provider/recipient/methods_cache.dart';
|
||||
|
||||
import 'package:pweb/models/recipient/method_snapshot.dart';
|
||||
import 'package:pweb/models/state/seed_state.dart';
|
||||
import 'package:pweb/providers/address_book_recipient_form.dart';
|
||||
import 'package:pweb/utils/payment/label.dart';
|
||||
import 'package:pweb/widgets/dialogs/confirmation_dialog.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class AddressBookRecipientFormController extends ChangeNotifier {
|
||||
static final ListEquality<RecipientMethodSnapshot> _listEquality =
|
||||
ListEquality<RecipientMethodSnapshot>();
|
||||
|
||||
final List<PaymentType> _supportedTypes;
|
||||
final Map<PaymentType, List<RecipientMethodDraft>> _methods;
|
||||
|
||||
Recipient? _recipient;
|
||||
RecipientMethodsCacheProvider? _methodsCache;
|
||||
|
||||
String _initialName = '';
|
||||
String _initialEmail = '';
|
||||
List<RecipientMethodSnapshot> _initialMethods = const [];
|
||||
SeedState _snapshotState = SeedState.idle;
|
||||
SeedState _seedState = SeedState.idle;
|
||||
|
||||
AddressBookRecipientFormController({
|
||||
required List<PaymentType> supportedTypes,
|
||||
}) : _supportedTypes = List.unmodifiable(supportedTypes),
|
||||
_methods = {
|
||||
for (final type in supportedTypes) type: <RecipientMethodDraft>[],
|
||||
};
|
||||
|
||||
List<PaymentType> get supportedTypes => _supportedTypes;
|
||||
Map<PaymentType, List<RecipientMethodDraft>> get methods => {
|
||||
for (final entry in _methods.entries)
|
||||
entry.key: List<RecipientMethodDraft>.unmodifiable(entry.value),
|
||||
};
|
||||
PaymentType? get preferredType =>
|
||||
_supportedTypes.firstWhere(
|
||||
(type) => _methods[type]?.isNotEmpty == true,
|
||||
orElse: () => _supportedTypes.first,
|
||||
);
|
||||
|
||||
bool get hasAnyMethod => _methods.values.any(
|
||||
(entries) => entries.any((entry) => entry.data != null || entry.existing != null),
|
||||
);
|
||||
|
||||
Future<void> saveForm({
|
||||
required BuildContext context,
|
||||
required GlobalKey<FormState> formKey,
|
||||
required AddressBookRecipientFormProvider formState,
|
||||
required String name,
|
||||
required String email,
|
||||
ValueChanged<Recipient?>? onSaved,
|
||||
}) async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
if (!formKey.currentState!.validate() || !hasAnyMethod) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.recipientFormRule)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final saved = await formState.save(
|
||||
name: name,
|
||||
email: email,
|
||||
methodNames: _methodNames(context),
|
||||
methodDrafts: allDrafts(),
|
||||
);
|
||||
onSaved?.call(saved);
|
||||
} catch (_) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.notificationError(l10n.noErrorInformation))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleBack({
|
||||
required BuildContext context,
|
||||
required GlobalKey<FormState> formKey,
|
||||
required AddressBookRecipientFormProvider formState,
|
||||
required String name,
|
||||
required String email,
|
||||
ValueChanged<Recipient?>? onSaved,
|
||||
}) async {
|
||||
if (!context.mounted) return;
|
||||
if (!hasUnsavedChanges(name: name, email: email)) {
|
||||
onSaved?.call(null);
|
||||
return;
|
||||
}
|
||||
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final shouldSave = await showConfirmationDialog(
|
||||
context: context,
|
||||
title: l10n.unsavedChangesTitle,
|
||||
message: l10n.unsavedChangesMessage,
|
||||
confirmLabel: l10n.save,
|
||||
cancelLabel: l10n.discard,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (shouldSave) {
|
||||
await saveForm(
|
||||
context: context,
|
||||
formKey: formKey,
|
||||
formState: formState,
|
||||
name: name,
|
||||
email: email,
|
||||
onSaved: onSaved,
|
||||
);
|
||||
} else {
|
||||
onSaved?.call(null);
|
||||
}
|
||||
}
|
||||
|
||||
void update({
|
||||
required Recipient? recipient,
|
||||
required RecipientMethodsCacheProvider methodsCache,
|
||||
}) {
|
||||
if (!identical(_methodsCache, methodsCache)) {
|
||||
_methodsCache?.removeListener(_handleCacheChange);
|
||||
_methodsCache = methodsCache;
|
||||
_methodsCache?.addListener(_handleCacheChange);
|
||||
}
|
||||
|
||||
if (_recipient?.id != recipient?.id) {
|
||||
_reset(recipient);
|
||||
}
|
||||
|
||||
_maybeSeedFromCache();
|
||||
}
|
||||
|
||||
bool hasUnsavedChanges({
|
||||
required String name,
|
||||
required String email,
|
||||
}) {
|
||||
if (_recipient == null) return false;
|
||||
final methodsCache = _methodsCache;
|
||||
if (methodsCache == null) return false;
|
||||
|
||||
_captureIfReady();
|
||||
final nameChanged = name.trim() != _initialName.trim();
|
||||
final emailChanged = email.trim() != _initialEmail.trim();
|
||||
if (_snapshotState != SeedState.seeded) {
|
||||
return nameChanged || emailChanged;
|
||||
}
|
||||
final current = _snapshotFrom();
|
||||
final methodsChanged = !_listEquality.equals(_initialMethods, current);
|
||||
|
||||
return nameChanged || emailChanged || methodsChanged;
|
||||
}
|
||||
|
||||
List<RecipientMethodDraft> allDrafts() =>
|
||||
_methods.values.expand((entries) => entries).toList();
|
||||
|
||||
int? addMethod(PaymentType type) {
|
||||
final entries = _methods[type];
|
||||
if (entries == null) return null;
|
||||
entries.add(RecipientMethodDraft(type: type));
|
||||
notifyListeners();
|
||||
return entries.length - 1;
|
||||
}
|
||||
|
||||
void removeMethod(PaymentType type, int index) {
|
||||
final entries = _methods[type];
|
||||
if (entries == null) return;
|
||||
if (index < 0 || index >= entries.length) return;
|
||||
entries.removeAt(index);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateMethod(PaymentType type, int index, PaymentMethodData data) {
|
||||
final entries = _methods[type];
|
||||
if (entries == null) return;
|
||||
if (index < 0 || index >= entries.length) return;
|
||||
entries[index].data = data;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _handleCacheChange() {
|
||||
_maybeSeedFromCache();
|
||||
}
|
||||
|
||||
void _reset(Recipient? recipient) {
|
||||
_recipient = recipient;
|
||||
_initialName = recipient?.name ?? '';
|
||||
_initialEmail = recipient?.email ?? '';
|
||||
_initialMethods = const [];
|
||||
_snapshotState = SeedState.idle;
|
||||
_seedState = SeedState.idle;
|
||||
_resetMethods();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _resetMethods() {
|
||||
for (final entries in _methods.values) {
|
||||
entries.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void _maybeSeedFromCache() {
|
||||
final recipient = _recipient;
|
||||
final methodsCache = _methodsCache;
|
||||
if (recipient == null || methodsCache == null) return;
|
||||
if (_seedState == SeedState.seeded) return;
|
||||
if (!methodsCache.hasMethodsFor(recipient.id)) return;
|
||||
_seedState = SeedState.seeded;
|
||||
_seedMethodsFromExisting(methodsCache.methodsForRecipient(recipient.id));
|
||||
}
|
||||
|
||||
void _seedMethodsFromExisting(List<PaymentMethod> existing) {
|
||||
_resetMethods();
|
||||
for (final method in existing) {
|
||||
final type = method.type;
|
||||
final entries = _methods[type];
|
||||
if (entries == null) continue;
|
||||
entries.add(
|
||||
RecipientMethodDraft(
|
||||
type: type,
|
||||
existing: method,
|
||||
data: method.data,
|
||||
),
|
||||
);
|
||||
}
|
||||
_initialMethods = _snapshotFrom();
|
||||
_snapshotState = SeedState.seeded;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _captureIfReady() {
|
||||
if (_snapshotState == SeedState.seeded) return;
|
||||
final recipient = _recipient;
|
||||
final methodsCache = _methodsCache;
|
||||
if (recipient == null || methodsCache == null) return;
|
||||
if (!methodsCache.hasMethodsFor(recipient.id)) return;
|
||||
|
||||
_initialMethods = _snapshotFrom();
|
||||
_snapshotState = SeedState.seeded;
|
||||
}
|
||||
|
||||
List<RecipientMethodSnapshot> _snapshotFrom() {
|
||||
final snapshots = <RecipientMethodSnapshot>[];
|
||||
for (final type in _supportedTypes) {
|
||||
final entries = _methods[type] ?? const <RecipientMethodDraft>[];
|
||||
for (final entry in entries) {
|
||||
snapshots.add(RecipientMethodSnapshot.fromDraft(entry));
|
||||
}
|
||||
}
|
||||
return snapshots;
|
||||
}
|
||||
|
||||
Map<PaymentType, String> _methodNames(BuildContext context) => {
|
||||
for (final type in _supportedTypes) type: getPaymentTypeLabel(context, type),
|
||||
};
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_methodsCache?.removeListener(_handleCacheChange);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
|
||||
import 'package:pweb/controllers/organization/address_book_recipient_form.dart';
|
||||
|
||||
|
||||
class AddressBookRecipientFormSelectionController extends ChangeNotifier {
|
||||
AddressBookRecipientFormController? _formController;
|
||||
PaymentType? _selectedType;
|
||||
int? _selectedIndex;
|
||||
|
||||
PaymentType? get selectedType => _selectedType;
|
||||
int? get selectedIndex => _selectedIndex;
|
||||
|
||||
void update(AddressBookRecipientFormController formController) {
|
||||
if (identical(_formController, formController)) return;
|
||||
_formController?.removeListener(_handleFormChanged);
|
||||
_formController = formController;
|
||||
_formController?.addListener(_handleFormChanged);
|
||||
_reconcileSelection();
|
||||
}
|
||||
|
||||
void select(PaymentType type, int index) {
|
||||
if (_selectedType == type && _selectedIndex == index) return;
|
||||
_selectedType = type;
|
||||
_selectedIndex = index;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void selectAfterAdd(PaymentType type, int? index) {
|
||||
if (_selectedType == type && _selectedIndex == index) return;
|
||||
_selectedType = type;
|
||||
_selectedIndex = index;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _handleFormChanged() {
|
||||
_reconcileSelection();
|
||||
}
|
||||
|
||||
void _reconcileSelection() {
|
||||
final form = _formController;
|
||||
if (form == null) return;
|
||||
final types = form.supportedTypes;
|
||||
if (types.isEmpty) return;
|
||||
|
||||
var nextType = _selectedType;
|
||||
var nextIndex = _selectedIndex;
|
||||
|
||||
if (nextType == null || !types.contains(nextType)) {
|
||||
nextType = form.preferredType ?? types.first;
|
||||
nextIndex = null;
|
||||
}
|
||||
|
||||
final entries = form.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;
|
||||
_selectedType = nextType;
|
||||
_selectedIndex = nextIndex;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_formController?.removeListener(_handleFormChanged);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user