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,67 @@
import 'package:flutter/material.dart';
import 'package:pshared/provider/payment/amount.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pshared/utils/money.dart';
class PaymentAmountFieldController extends ChangeNotifier {
final TextEditingController textController;
PaymentAmountProvider? _provider;
bool _isSyncingText = false;
PaymentAmountFieldController({required double initialAmount})
: textController = TextEditingController(
text: amountToString(initialAmount),
);
void update(PaymentAmountProvider provider) {
if (identical(_provider, provider)) return;
_provider?.removeListener(_handleProviderChanged);
_provider = provider;
_provider?.addListener(_handleProviderChanged);
_syncTextWithAmount(provider.amount);
}
void handleChanged(String value) {
if (_isSyncingText) return;
final parsed = _parseAmount(value);
if (parsed != null) {
_provider?.setAmount(parsed);
}
}
void _handleProviderChanged() {
final provider = _provider;
if (provider == null) return;
_syncTextWithAmount(provider.amount);
}
double? _parseAmount(String value) {
final parsed = parseMoneyAmount(
value.replaceAll(',', '.'),
fallback: double.nan,
);
return parsed.isNaN ? null : parsed;
}
void _syncTextWithAmount(double amount) {
final parsedText = _parseAmount(textController.text);
if (parsedText != null && parsedText == amount) return;
final nextText = amountToString(amount);
_isSyncingText = true;
textController.value = TextEditingValue(
text: nextText,
selection: TextSelection.collapsed(offset: nextText.length),
);
_isSyncingText = false;
}
@override
void dispose() {
_provider?.removeListener(_handleProviderChanged);
textController.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,62 @@
import 'package:flutter/foundation.dart';
import 'package:pshared/models/payment/payment.dart';
import 'package:pshared/models/payment/status.dart';
import 'package:pshared/provider/payment/payments.dart';
import 'package:pweb/utils/report/payment_mapper.dart';
class PaymentDetailsController extends ChangeNotifier {
PaymentDetailsController({required String paymentId})
: _paymentId = paymentId;
PaymentsProvider? _payments;
String _paymentId;
Payment? _payment;
String get paymentId => _paymentId;
Payment? get payment => _payment;
bool get isLoading => _payments?.isLoading ?? false;
Exception? get error => _payments?.error;
bool get canDownload {
final current = _payment;
if (current == null) return false;
final status = statusFromPayment(current);
final paymentRef = current.paymentRef ?? '';
return status == OperationStatus.success &&
paymentRef.trim().isNotEmpty;
}
void update(PaymentsProvider provider, String paymentId) {
if (_paymentId != paymentId) {
_paymentId = paymentId;
}
if (!identical(_payments, provider)) {
_payments = provider;
}
_rebuild();
}
Future<void> refresh() async {
await _payments?.refresh();
}
void _rebuild() {
_payment = _findPayment(_payments?.payments ?? const [], _paymentId);
notifyListeners();
}
Payment? _findPayment(List<Payment> payments, String paymentId) {
final trimmed = paymentId.trim();
if (trimmed.isEmpty) return null;
for (final payment in payments) {
if (payment.paymentRef == trimmed) return payment;
if (payment.idempotencyKey == trimmed) return payment;
}
return null;
}
}

View File

@@ -0,0 +1,80 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:pshared/provider/payment/flow.dart';
import 'package:pshared/provider/payment/provider.dart';
import 'package:pshared/provider/payment/quotation/quotation.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/services/posthog.dart';
class PaymentPageController extends ChangeNotifier {
PaymentProvider? _payment;
QuotationProvider? _quotation;
PaymentFlowProvider? _flow;
RecipientsProvider? _recipients;
bool _isSending = false;
Exception? _error;
bool get isSending => _isSending;
Exception? get error => _error;
void update(
PaymentProvider payment,
QuotationProvider quotation,
PaymentFlowProvider flow,
RecipientsProvider recipients,
) {
_payment = payment;
_quotation = quotation;
_flow = flow;
_recipients = recipients;
}
Future<bool> sendPayment() async {
if (_isSending) return false;
final payment = _payment;
if (payment == null) {
_setError(StateError('Payment provider is not ready'));
return false;
}
try {
_setSending(true);
_error = null;
final result = await payment.pay();
return result != null && payment.error == null;
} catch (e) {
_setError(e);
return false;
} finally {
_setSending(false);
}
}
void resetAfterSuccess() {
_quotation?.reset();
_payment?.reset();
_flow?.setManualPaymentData(null);
_recipients?.setCurrentObject(null);
}
void handleSuccess() {
unawaited(PosthogService.paymentInitiated(method: _flow?.selectedType));
resetAfterSuccess();
}
void _setSending(bool value) {
if (_isSending == value) return;
_isSending = value;
notifyListeners();
}
void _setError(Object error) {
_error = error is Exception ? error : Exception(error.toString());
notifyListeners();
}
}

View File

@@ -0,0 +1,43 @@
import 'package:flutter/widgets.dart';
import 'package:pweb/models/state/visibility.dart';
class PaymentPageUiController extends ChangeNotifier {
final TextEditingController searchController = TextEditingController();
final FocusNode searchFocusNode = FocusNode();
String _query = '';
VisibilityState _paymentDetailsVisibility = VisibilityState.hidden;
String get query => _query;
VisibilityState get paymentDetailsVisibility => _paymentDetailsVisibility;
void setQuery(String query) {
if (_query == query) return;
_query = query;
notifyListeners();
}
void clearSearch() {
if (searchController.text.isNotEmpty) {
searchController.clear();
}
searchFocusNode.unfocus();
setQuery('');
}
void togglePaymentDetails() {
_paymentDetailsVisibility = _paymentDetailsVisibility == VisibilityState.visible
? VisibilityState.hidden
: VisibilityState.visible;
notifyListeners();
}
@override
void dispose() {
searchController.dispose();
searchFocusNode.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/methods/type.dart';
import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pweb/pages/payment_methods/add/widget.dart';
import 'package:pweb/widgets/dialogs/confirmation_dialog.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentConfigController {
final BuildContext context;
PaymentConfigController(this.context);
Future<void> addMethod() async => showDialog<PaymentMethodData>(
context: context,
builder: (_) => const AddPaymentMethodDialog(),
);
Future<void> editMethod(PaymentMethod method) async {
// TODO: implement edit functionality
}
Future<void> deleteMethod(PaymentMethod method) async {
final methodsProvider = context.read<PaymentMethodsProvider>();
final l10n = AppLocalizations.of(context)!;
final confirmed = await showConfirmationDialog(
context: context,
title: l10n.delete,
message: l10n.deletePaymentConfirmation,
confirmLabel: l10n.delete,
);
if (confirmed) {
methodsProvider.delete(method.id);
}
}
void toggleEnabled(PaymentMethod method, bool value) {
context.read<PaymentMethodsProvider>().setArchivedMethod(method: method, newIsArchived: value);
}
void makeMain(PaymentMethod method) {
context.read<PaymentMethodsProvider>().makeMain(method);
}
void reorder(int oldIndex, int newIndex) {
// TODO: rimplement on top of Indexable
// context.read<PaymentMethodsProvider>().reorderMethods(oldIndex, newIndex);
}
}

View File

@@ -0,0 +1,33 @@
import 'package:flutter/foundation.dart';
import 'package:pshared/models/payment/operation.dart';
import 'package:pshared/provider/payment/payments.dart';
import 'package:pweb/utils/report/operations.dart';
import 'package:pweb/utils/report/payment_mapper.dart';
class RecentPaymentsController extends ChangeNotifier {
PaymentsProvider? _payments;
List<OperationItem> _recent = const [];
List<OperationItem> get recentOperations => _recent;
bool get isLoading => _payments?.isLoading ?? false;
Exception? get error => _payments?.error;
void update(PaymentsProvider provider) {
if (!identical(_payments, provider)) {
_payments = provider;
}
_rebuild();
}
void _rebuild() {
final operations = (_payments?.payments ?? const [])
.map(mapPaymentToOperation)
.toList();
_recent = sortOperations(operations).take(5).toList();
notifyListeners();
}
}