Multiple Wallet support, history of each wallet and updated payment page

This commit is contained in:
Arseni
2025-11-21 19:22:23 +03:00
parent 4c64a8d6e6
commit 87636a7ec3
68 changed files with 2154 additions and 701 deletions

View File

@@ -1,32 +0,0 @@
import 'package:flutter/material.dart';
import 'package:pweb/services/balance.dart';
class BalanceProvider with ChangeNotifier {
final BalanceService _service;
BalanceProvider(this._service);
double? _balance;
String? _walletName;
String? _walletId;
bool _isHidden = true;
double? get balance => _balance;
String? get walletName => _walletName;
String? get walletId => _walletId;
bool get isHidden => _isHidden;
Future<void> loadData() async {
_balance = await _service.getBalance();
_walletName = await _service.getWalletName();
_walletId = await _service.getWalletId();
notifyListeners();
}
void toggleVisibility() {
_isHidden = !_isHidden;
notifyListeners();
}
}

View File

@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/operation.dart';
import 'package:pshared/models/payment/status.dart';
import 'package:pweb/services/operations.dart';
class OperationProvider extends ChangeNotifier {
final OperationService _service;
OperationProvider(this._service);
List<OperationItem> _allOperations = [];
List<OperationItem> _filteredOperations = [];
DateTimeRange? _dateRange;
final Set<String> _selectedStatuses = {};
bool _isLoading = false;
String? _error;
// Getters
List<OperationItem> get allOperations => _allOperations;
List<OperationItem> get filteredOperations => _filteredOperations;
DateTimeRange? get dateRange => _dateRange;
Set<String> get selectedStatuses => _selectedStatuses;
bool get isLoading => _isLoading;
String? get error => _error;
bool get hasFileName => _allOperations.any((op) => op.fileName != null);
Future<void> loadOperations() async {
_isLoading = true;
_error = null;
notifyListeners();
try {
_allOperations = await _service.fetchOperations();
_filteredOperations = List.from(_allOperations);
_isLoading = false;
notifyListeners();
} catch (e) {
_error = e.toString();
_isLoading = false;
notifyListeners();
}
}
void setDateRange(DateTimeRange? range) {
_dateRange = range;
notifyListeners();
}
void toggleStatus(String status) {
if (_selectedStatuses.contains(status)) {
_selectedStatuses.remove(status);
} else {
_selectedStatuses.add(status);
}
notifyListeners();
}
void applyFilters(BuildContext context) {
_filteredOperations = _allOperations.where((op) {
final statusMatch = _selectedStatuses.isEmpty ||
_selectedStatuses.contains(op.status.localized(context));
final dateMatch = _dateRange == null ||
(op.date.isAfter(_dateRange!.start.subtract(const Duration(seconds: 1))) &&
op.date.isBefore(_dateRange!.end.add(const Duration(seconds: 1))));
return statusMatch && dateMatch;
}).toList();
notifyListeners();
}
void resetFilters() {
_dateRange = null;
_selectedStatuses.clear();
_filteredOperations = List.from(_allOperations);
notifyListeners();
}
}

View File

@@ -1,9 +1,13 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/methods/type.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pweb/models/wallet.dart';
import 'package:pweb/providers/payment_methods.dart';
import 'package:pweb/providers/wallets.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
import 'package:pweb/services/amplitude.dart';
@@ -15,13 +19,29 @@ class PageSelectorProvider extends ChangeNotifier {
PaymentType? _type;
bool _cameFromRecipientList = false;
final RecipientProvider? recipientProvider;
final WalletsProvider? walletsProvider;
RecipientProvider? recipientProvider;
WalletsProvider? walletsProvider;
PaymentMethodsProvider? methodsProvider;
PayoutDestination get selected => _selected;
PaymentType? get type => _type;
bool get cameFromRecipientList => _cameFromRecipientList;
PageSelectorProvider({this.recipientProvider, this.walletsProvider});
PageSelectorProvider({
this.recipientProvider,
this.walletsProvider,
this.methodsProvider,
});
void update(
RecipientProvider recipientProv,
WalletsProvider walletsProv,
PaymentMethodsProvider methodsProv,
) {
recipientProvider = recipientProv;
walletsProvider = walletsProv;
methodsProvider = methodsProv;
}
void selectPage(PayoutDestination dest) {
_selected = dest;
@@ -90,7 +110,64 @@ class PageSelectorProvider extends ChangeNotifier {
_selected = PayoutDestination.editwallet;
notifyListeners();
} else {
debugPrint("RecipientProvider is null — cannot select wallet");
debugPrint("WalletsProvider is null — cannot select wallet");
}
}
void startPaymentFromWallet(Wallet wallet) {
_type = PaymentType.wallet;
_cameFromRecipientList = true;
_selected = PayoutDestination.payment;
notifyListeners();
}
PaymentMethod? getPaymentMethodForWallet(Wallet wallet) {
if (methodsProvider == null || methodsProvider!.methods.isEmpty) {
return null;
}
return methodsProvider!.methods.firstWhereOrNull(
(method) => method.type == PaymentType.wallet &&
method.details.contains(wallet.walletUserID)
);
}
Map<PaymentType, Object> getAvailablePaymentTypes() {
final recipient = selectedRecipient;
if (recipient == null) return {};
return {
if (recipient.card != null) PaymentType.card: recipient.card!,
if (recipient.iban != null) PaymentType.iban: recipient.iban!,
if (recipient.wallet != null) PaymentType.wallet: recipient.wallet!,
if (recipient.bank != null) PaymentType.bankAccount: recipient.bank!,
};
}
PaymentType getDefaultPaymentType() {
final availableTypes = getAvailablePaymentTypes();
final currentType = _type ?? PaymentType.bankAccount;
if (availableTypes.containsKey(currentType)) {
return currentType;
} else if (availableTypes.isNotEmpty) {
return availableTypes.keys.first;
} else {
return PaymentType.bankAccount;
}
}
bool shouldShowPaymentForm() {
return selectedRecipient == null;
}
void handleWalletAutoSelection() {
if (selectedWallet != null && methodsProvider != null) {
final wallet = selectedWallet!;
final matchingMethod = getPaymentMethodForWallet(wallet);
if (matchingMethod != null) {
methodsProvider!.selectMethod(matchingMethod);
}
}
}

View File

@@ -0,0 +1,78 @@
import 'package:flutter/foundation.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pweb/providers/page_selector.dart';
class PaymentFlowProvider extends ChangeNotifier {
PaymentType _selectedType;
Object? _manualPaymentData;
PaymentFlowProvider({
required PaymentType initialType,
}) : _selectedType = initialType;
PaymentType get selectedType => _selectedType;
Object? get manualPaymentData => _manualPaymentData;
void syncWithSelector(PageSelectorProvider selector) {
final recipient = selector.selectedRecipient;
final resolvedType = _resolveSelectedType(selector, recipient);
var hasChanges = false;
if (resolvedType != _selectedType) {
_selectedType = resolvedType;
hasChanges = true;
}
if (recipient != null && _manualPaymentData != null) {
_manualPaymentData = null;
hasChanges = true;
}
if (hasChanges) notifyListeners();
}
void reset(PageSelectorProvider selector) {
_selectedType = selector.getDefaultPaymentType();
_manualPaymentData = null;
notifyListeners();
}
void selectType(PaymentType type, {bool resetManualData = false}) {
if (_selectedType == type && (!resetManualData || _manualPaymentData == null)) {
return;
}
_selectedType = type;
if (resetManualData) {
_manualPaymentData = null;
}
notifyListeners();
}
void setManualPaymentData(Object? data) {
_manualPaymentData = data;
notifyListeners();
}
PaymentType _resolveSelectedType(
PageSelectorProvider selector,
Recipient? recipient,
) {
final available = selector.getAvailablePaymentTypes();
final current = _selectedType;
if (recipient == null) {
return current;
}
if (available.keys.contains(current)) {
return current;
}
return selector.getDefaultPaymentType();
}
}

View File

@@ -59,7 +59,9 @@ class PaymentMethodsProvider extends ChangeNotifier {
}
void makeMain(PaymentMethod method) {
for (final m in _methods) m.isMain = false;
for (final m in _methods) {
m.isMain = false;
}
method.isMain = true;
selectMethod(method);
}

View File

@@ -0,0 +1,104 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/status.dart';
import 'package:pweb/models/wallet_transaction.dart';
import 'package:pweb/services/wallet_transactions.dart';
class WalletTransactionsProvider extends ChangeNotifier {
final WalletTransactionsService _service;
WalletTransactionsProvider(this._service);
List<WalletTransaction> _transactions = [];
List<WalletTransaction> _filteredTransactions = [];
DateTimeRange? _dateRange;
final Set<OperationStatus> _selectedStatuses = {};
final Set<WalletTransactionType> _selectedTypes = {};
String? _walletId;
bool _isLoading = false;
String? _error;
List<WalletTransaction> get transactions => _transactions;
List<WalletTransaction> get filteredTransactions => _filteredTransactions;
DateTimeRange? get dateRange => _dateRange;
Set<OperationStatus> get selectedStatuses => _selectedStatuses;
Set<WalletTransactionType> get selectedTypes => _selectedTypes;
String? get walletId => _walletId;
bool get isLoading => _isLoading;
String? get error => _error;
bool get hasFilters =>
_dateRange != null ||
_selectedStatuses.isNotEmpty ||
_selectedTypes.isNotEmpty;
Future<void> load({String? walletId}) async {
_isLoading = true;
_error = null;
notifyListeners();
try {
_walletId = walletId ?? _walletId;
_transactions = await _service.fetchHistory(walletId: _walletId);
_applyFilters(notify: false);
_isLoading = false;
notifyListeners();
} catch (e) {
_error = e.toString();
_isLoading = false;
notifyListeners();
}
}
void setWallet(String walletId) {
_walletId = walletId;
_applyFilters();
}
void setDateRange(DateTimeRange? range) {
_dateRange = range;
_applyFilters();
}
void toggleStatus(OperationStatus status) {
if (_selectedStatuses.contains(status)) {
_selectedStatuses.remove(status);
} else {
_selectedStatuses.add(status);
}
_applyFilters();
}
void toggleType(WalletTransactionType type) {
if (_selectedTypes.contains(type)) {
_selectedTypes.remove(type);
} else {
_selectedTypes.add(type);
}
_applyFilters();
}
void resetFilters() {
_dateRange = null;
_selectedStatuses.clear();
_selectedTypes.clear();
_applyFilters();
}
void _applyFilters({bool notify = true}) {
_filteredTransactions = _transactions.where((tx) {
final walletMatch = _walletId == null || tx.walletId == _walletId;
final statusMatch =
_selectedStatuses.isEmpty || _selectedStatuses.contains(tx.status);
final typeMatch =
_selectedTypes.isEmpty || _selectedTypes.contains(tx.type);
final dateMatch = _dateRange == null ||
(tx.date.isAfter(_dateRange!.start.subtract(const Duration(seconds: 1))) &&
tx.date.isBefore(_dateRange!.end.add(const Duration(seconds: 1))));
return walletMatch && statusMatch && typeMatch && dateMatch;
}).toList();
if (notify) notifyListeners();
}
}

View File

@@ -13,7 +13,7 @@ class WalletsProvider with ChangeNotifier {
bool _isLoading = false;
String? _error;
Wallet? _selectedWallet;
bool _isHidden = true;
final bool _isHidden = true;
List<Wallet>? get wallets => _wallets;
bool get isLoading => _isLoading;
@@ -120,6 +120,11 @@ class WalletsProvider with ChangeNotifier {
if (index != null && index >= 0) {
final wallet = _wallets![index];
_wallets![index] = wallet.copyWith(isHidden: !wallet.isHidden);
if (_selectedWallet?.id == walletId) {
_selectedWallet = _wallets![index];
}
notifyListeners();
}
}