redesigned payment page + a lot of fixes

This commit is contained in:
Arseni
2026-02-21 21:55:20 +03:00
parent a68aa2abff
commit 0c6fa03aba
208 changed files with 4062 additions and 2217 deletions

View File

@@ -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();
}
}

View File

@@ -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();
}
}