redesigned payment page + a lot of fixes
This commit is contained in:
@@ -1,12 +1,28 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:pshared/models/account/account.dart';
|
||||
import 'package:pshared/models/auth/login_outcome.dart';
|
||||
import 'package:pshared/provider/account.dart';
|
||||
|
||||
import 'package:pweb/services/posthog.dart';
|
||||
|
||||
|
||||
class PwebAccountProvider extends AccountProvider {
|
||||
@override
|
||||
Future<LoginOutcome> login({
|
||||
required String email,
|
||||
required String password,
|
||||
required String locale,
|
||||
}) async {
|
||||
final outcome = await super.login(
|
||||
email: email,
|
||||
password: password,
|
||||
locale: locale,
|
||||
);
|
||||
unawaited(PosthogService.login(pending: outcome.isPending));
|
||||
return outcome;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onAccountChanged(Account? previous, Account? current) {
|
||||
if (current != null) {
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/provider/account.dart';
|
||||
|
||||
import 'package:pweb/models/edit_state.dart';
|
||||
|
||||
|
||||
class AccountNameState extends ChangeNotifier {
|
||||
AccountNameState({
|
||||
required this.initialFirstName,
|
||||
required this.initialLastName,
|
||||
required this.errorMessage,
|
||||
required AccountProvider accountProvider,
|
||||
}) : _accountProvider = accountProvider {
|
||||
_firstNameController = TextEditingController(text: initialFirstName);
|
||||
_lastNameController = TextEditingController(text: initialLastName);
|
||||
}
|
||||
|
||||
final AccountProvider _accountProvider;
|
||||
final String initialFirstName;
|
||||
final String initialLastName;
|
||||
final String errorMessage;
|
||||
|
||||
late final TextEditingController _firstNameController;
|
||||
late final TextEditingController _lastNameController;
|
||||
EditState _editState = EditState.view;
|
||||
String _errorText = '';
|
||||
bool _disposed = false;
|
||||
|
||||
TextEditingController get firstNameController => _firstNameController;
|
||||
TextEditingController get lastNameController => _lastNameController;
|
||||
EditState get editState => _editState;
|
||||
String get errorText => _errorText;
|
||||
bool get isEditing => _editState != EditState.view;
|
||||
bool get isSaving => _editState == EditState.saving;
|
||||
bool get isBusy => _accountProvider.isLoading || isSaving;
|
||||
String get currentFirstName => _accountProvider.account?.name ?? initialFirstName;
|
||||
String get currentLastName => _accountProvider.account?.lastName ?? initialLastName;
|
||||
String get currentFullName {
|
||||
final first = currentFirstName.trim();
|
||||
final last = currentLastName.trim();
|
||||
if (first.isEmpty && last.isEmpty) return '';
|
||||
if (first.isEmpty) return last;
|
||||
if (last.isEmpty) return first;
|
||||
return '$first $last';
|
||||
}
|
||||
|
||||
void startEditing() => _setState(EditState.edit);
|
||||
|
||||
void cancelEditing() {
|
||||
_firstNameController.text = currentFirstName;
|
||||
_lastNameController.text = currentLastName;
|
||||
_setError('');
|
||||
_setState(EditState.view);
|
||||
}
|
||||
|
||||
void syncNames(String latestFirstName, String latestLastName) {
|
||||
if (isEditing) return;
|
||||
if (_firstNameController.text != latestFirstName) {
|
||||
_firstNameController.text = latestFirstName;
|
||||
}
|
||||
if (_lastNameController.text != latestLastName) {
|
||||
_lastNameController.text = latestLastName;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> save() async {
|
||||
final newFirstName = _firstNameController.text.trim();
|
||||
final newLastName = _lastNameController.text.trim();
|
||||
final currentFirst = currentFirstName;
|
||||
final currentLast = currentLastName;
|
||||
|
||||
if (newFirstName.isEmpty || (newFirstName == currentFirst && newLastName == currentLast)) {
|
||||
cancelEditing();
|
||||
return false;
|
||||
}
|
||||
|
||||
_setError('');
|
||||
_setState(EditState.saving);
|
||||
|
||||
try {
|
||||
await _accountProvider.resetUsername(newFirstName, lastName: newLastName);
|
||||
_setState(EditState.view);
|
||||
return true;
|
||||
} catch (_) {
|
||||
_setError(errorMessage);
|
||||
_setState(EditState.edit);
|
||||
return false;
|
||||
} finally {
|
||||
if (_editState == EditState.saving) {
|
||||
_setState(EditState.edit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _setState(EditState value) {
|
||||
if (_disposed || _editState == value) return;
|
||||
_editState = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _setError(String value) {
|
||||
if (_disposed) return;
|
||||
_errorText = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_disposed = true;
|
||||
_firstNameController.dispose();
|
||||
_lastNameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -2,127 +2,35 @@ import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:pshared/data/mapper/recipient/recipient.dart';
|
||||
import 'package:pshared/models/describable.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:pshared/provider/recipient/provider.dart';
|
||||
|
||||
import 'package:pweb/models/seed_state.dart';
|
||||
|
||||
|
||||
class AddressBookRecipientFormProvider extends ChangeNotifier {
|
||||
RecipientMethodsCacheProvider? _methodsCache;
|
||||
RecipientsProvider? _recipientsProvider;
|
||||
final Recipient? _recipient;
|
||||
final List<PaymentType> _supportedTypes;
|
||||
|
||||
final Map<PaymentType, List<RecipientMethodDraft>> _methods;
|
||||
SeedState _seedState = SeedState.idle;
|
||||
|
||||
AddressBookRecipientFormProvider({
|
||||
required List<PaymentType> supportedTypes,
|
||||
Recipient? recipient,
|
||||
}) : _recipient = recipient,
|
||||
_supportedTypes = supportedTypes,
|
||||
_methods = {
|
||||
for (final type in supportedTypes) type: <RecipientMethodDraft>[],
|
||||
};
|
||||
}) : _recipient = recipient;
|
||||
|
||||
void updateProviders({
|
||||
required RecipientMethodsCacheProvider methodsCache,
|
||||
required RecipientsProvider recipientsProvider,
|
||||
}) {
|
||||
_recipientsProvider = recipientsProvider;
|
||||
if (identical(_methodsCache, methodsCache)) return;
|
||||
_methodsCache?.removeListener(_handleCacheChange);
|
||||
_methodsCache = methodsCache;
|
||||
_methodsCache?.addListener(_handleCacheChange);
|
||||
_maybeSeedFromCache();
|
||||
}
|
||||
|
||||
List<PaymentType> get supportedTypes => List.unmodifiable(_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),
|
||||
);
|
||||
|
||||
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 _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) {
|
||||
if (existing.isEmpty) return;
|
||||
final next = <PaymentType, List<RecipientMethodDraft>>{
|
||||
for (final type in _supportedTypes) type: <RecipientMethodDraft>[],
|
||||
};
|
||||
for (final method in existing) {
|
||||
final type = method.type;
|
||||
final entries = next[type];
|
||||
if (entries == null) continue;
|
||||
entries.add(
|
||||
RecipientMethodDraft(
|
||||
type: type,
|
||||
existing: method,
|
||||
data: method.data,
|
||||
),
|
||||
);
|
||||
}
|
||||
_methods
|
||||
..clear()
|
||||
..addAll(next);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<Recipient> save({
|
||||
required String name,
|
||||
required String email,
|
||||
required Map<PaymentType, String> methodNames,
|
||||
required List<RecipientMethodDraft> methodDrafts,
|
||||
}) async {
|
||||
final recipientsProvider = _recipientsProvider;
|
||||
final methodsCache = _methodsCache;
|
||||
@@ -140,7 +48,7 @@ class AddressBookRecipientFormProvider extends ChangeNotifier {
|
||||
);
|
||||
await methodsCache.syncRecipientMethods(
|
||||
recipientId: created.id,
|
||||
methods: allDrafts(),
|
||||
methods: methodDrafts,
|
||||
names: methodNames,
|
||||
);
|
||||
return created;
|
||||
@@ -156,15 +64,9 @@ class AddressBookRecipientFormProvider extends ChangeNotifier {
|
||||
await recipientsProvider.update(updated.toDTO().toJson());
|
||||
await methodsCache.syncRecipientMethods(
|
||||
recipientId: updated.id,
|
||||
methods: allDrafts(),
|
||||
methods: methodDrafts,
|
||||
names: methodNames,
|
||||
);
|
||||
return updated;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_methodsCache?.removeListener(_handleCacheChange);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
19
frontend/pweb/lib/providers/locale.dart
Normal file
19
frontend/pweb/lib/providers/locale.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/provider/locale.dart';
|
||||
|
||||
import 'package:pweb/services/posthog.dart';
|
||||
|
||||
|
||||
class PwebLocaleProvider extends LocaleProvider {
|
||||
PwebLocaleProvider(super.localeCode);
|
||||
|
||||
@override
|
||||
void setLocale(Locale locale) {
|
||||
if (this.locale == locale) return;
|
||||
super.setLocale(locale);
|
||||
unawaited(PosthogService.localeChanged(locale));
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,11 @@ import 'package:pshared/models/payment/quote/status_type.dart';
|
||||
import 'package:pshared/models/payment/wallet.dart';
|
||||
import 'package:pshared/provider/payment/multiple/provider.dart';
|
||||
import 'package:pshared/provider/payment/multiple/quotation.dart';
|
||||
import 'package:pshared/provider/payment/payments.dart';
|
||||
import 'package:pshared/utils/currency.dart';
|
||||
import 'package:pshared/utils/money.dart';
|
||||
|
||||
import 'package:pweb/models/multiple_payouts/csv_row.dart';
|
||||
import 'package:pweb/models/multiple_payouts/state.dart';
|
||||
import 'package:pweb/models/payment/multiple_payouts/csv_row.dart';
|
||||
import 'package:pweb/models/payment/multiple_payouts/state.dart';
|
||||
import 'package:pweb/utils/payment/multiple_csv_parser.dart';
|
||||
import 'package:pweb/utils/payment/multiple_intent_builder.dart';
|
||||
|
||||
@@ -22,7 +21,6 @@ class MultiplePayoutsProvider extends ChangeNotifier {
|
||||
|
||||
MultiQuotationProvider? _quotation;
|
||||
MultiPaymentProvider? _payment;
|
||||
PaymentsProvider? _payments;
|
||||
|
||||
MultiplePayoutsState _state = MultiplePayoutsState.idle;
|
||||
String? _selectedFileName;
|
||||
@@ -39,11 +37,9 @@ class MultiplePayoutsProvider extends ChangeNotifier {
|
||||
void update(
|
||||
MultiQuotationProvider quotation,
|
||||
MultiPaymentProvider payment,
|
||||
PaymentsProvider payments,
|
||||
) {
|
||||
_bindQuotation(quotation);
|
||||
_payment = payment;
|
||||
_payments = payments;
|
||||
}
|
||||
|
||||
MultiplePayoutsState get state => _state;
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/provider/account.dart';
|
||||
|
||||
import 'package:pweb/models/edit_state.dart';
|
||||
import 'package:pshared/api/responses/error/server.dart';
|
||||
import 'package:pweb/models/password_field_type.dart';
|
||||
import 'package:pweb/models/visibility.dart';
|
||||
|
||||
|
||||
class PasswordFormProvider extends ChangeNotifier {
|
||||
final formKey = GlobalKey<FormState>();
|
||||
final oldPasswordController = TextEditingController();
|
||||
final newPasswordController = TextEditingController();
|
||||
final confirmPasswordController = TextEditingController();
|
||||
|
||||
final Map<PasswordFieldType, VisibilityState> _visibility = {
|
||||
PasswordFieldType.old: VisibilityState.hidden,
|
||||
PasswordFieldType.newPassword: VisibilityState.hidden,
|
||||
PasswordFieldType.confirmPassword: VisibilityState.hidden,
|
||||
};
|
||||
EditState _state = EditState.view;
|
||||
String _errorText = '';
|
||||
bool _disposed = false;
|
||||
|
||||
bool get isExpanded => _state != EditState.view;
|
||||
bool get isSaving => _state == EditState.saving;
|
||||
String get errorText => _errorText;
|
||||
EditState get state => _state;
|
||||
bool isPasswordVisible(PasswordFieldType type) =>
|
||||
_visibility[type] == VisibilityState.visible;
|
||||
|
||||
void toggleExpanded() {
|
||||
if (_state == EditState.saving) return;
|
||||
_setState(_state == EditState.view ? EditState.edit : EditState.view);
|
||||
_setError('');
|
||||
}
|
||||
|
||||
void togglePasswordVisibility(PasswordFieldType type) {
|
||||
final current = _visibility[type];
|
||||
if (current == null) return;
|
||||
_visibility[type] = current == VisibilityState.hidden
|
||||
? VisibilityState.visible
|
||||
: VisibilityState.hidden;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<bool> submit({
|
||||
required AccountProvider accountProvider,
|
||||
required String errorText,
|
||||
}) async {
|
||||
final currentForm = formKey.currentState;
|
||||
if (currentForm == null || !currentForm.validate()) return false;
|
||||
|
||||
_setState(EditState.saving);
|
||||
_setError('');
|
||||
|
||||
try {
|
||||
await accountProvider.changePassword(
|
||||
oldPasswordController.text,
|
||||
newPasswordController.text,
|
||||
);
|
||||
|
||||
oldPasswordController.clear();
|
||||
newPasswordController.clear();
|
||||
confirmPasswordController.clear();
|
||||
_setState(EditState.view);
|
||||
return true;
|
||||
} catch (e) {
|
||||
_setError(_errorMessageForException(e, errorText));
|
||||
_setState(EditState.edit);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
String _errorMessageForException(Object exception, String fallback) {
|
||||
if (exception is ErrorResponse && exception.details.isNotEmpty) {
|
||||
return exception.details;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
void _setState(EditState value) {
|
||||
if (_state == value || _disposed) return;
|
||||
_state = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _setError(String value) {
|
||||
if (_disposed) return;
|
||||
_errorText = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_disposed = true;
|
||||
oldPasswordController.dispose();
|
||||
newPasswordController.dispose();
|
||||
confirmPasswordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
|
||||
class QuotationAutoRefreshController {
|
||||
bool _enabled = true;
|
||||
Timer? _timer;
|
||||
DateTime? _scheduledAt;
|
||||
DateTime? _triggeredAt;
|
||||
|
||||
void setEnabled(bool enabled) {
|
||||
if (_enabled == enabled) return;
|
||||
_enabled = enabled;
|
||||
}
|
||||
|
||||
void sync({
|
||||
required bool isLoading,
|
||||
required bool canRefresh,
|
||||
required DateTime? expiresAt,
|
||||
required Future<void> Function() onRefresh,
|
||||
}) {
|
||||
if (!_enabled || isLoading || !canRefresh) {
|
||||
_clearTimer();
|
||||
_scheduledAt = null;
|
||||
_triggeredAt = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (expiresAt == null) {
|
||||
_clearTimer();
|
||||
_scheduledAt = null;
|
||||
_triggeredAt = null;
|
||||
return;
|
||||
}
|
||||
|
||||
final delay = expiresAt.difference(DateTime.now().toUtc());
|
||||
if (delay <= Duration.zero) {
|
||||
if (_triggeredAt != null && _triggeredAt!.isAtSameMomentAs(expiresAt)) {
|
||||
return;
|
||||
}
|
||||
_triggeredAt = expiresAt;
|
||||
_clearTimer();
|
||||
onRefresh();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_scheduledAt != null &&
|
||||
_scheduledAt!.isAtSameMomentAs(expiresAt) &&
|
||||
_timer?.isActive == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
_triggeredAt = null;
|
||||
_clearTimer();
|
||||
_scheduledAt = expiresAt;
|
||||
_timer = Timer(delay, () {
|
||||
onRefresh();
|
||||
});
|
||||
}
|
||||
|
||||
void reset() {
|
||||
_enabled = false;
|
||||
_scheduledAt = null;
|
||||
_triggeredAt = null;
|
||||
_clearTimer();
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_clearTimer();
|
||||
}
|
||||
|
||||
void _clearTimer() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:pshared/models/auto_refresh_mode.dart';
|
||||
import 'package:pshared/models/payment/quote/status_type.dart';
|
||||
import 'package:pshared/provider/payment/quotation/quotation.dart';
|
||||
|
||||
import 'package:pweb/providers/quotation/auto_refresh.dart';
|
||||
|
||||
|
||||
class QuotationController extends ChangeNotifier {
|
||||
QuotationProvider? _quotation;
|
||||
AutoRefreshMode _autoRefreshMode = AutoRefreshMode.on;
|
||||
final QuotationAutoRefreshController _autoRefreshController =
|
||||
QuotationAutoRefreshController();
|
||||
Timer? _ticker;
|
||||
|
||||
void update(QuotationProvider quotation) {
|
||||
if (identical(_quotation, quotation)) return;
|
||||
_quotation?.removeListener(_handleQuotationChanged);
|
||||
_quotation = quotation;
|
||||
_quotation?.addListener(_handleQuotationChanged);
|
||||
_handleQuotationChanged();
|
||||
}
|
||||
|
||||
bool get isLoading => _quotation?.isLoading ?? false;
|
||||
Exception? get error => _quotation?.error;
|
||||
bool get canRefresh => _quotation?.canRefresh ?? false;
|
||||
bool get isReady => _quotation?.isReady ?? false;
|
||||
AutoRefreshMode get autoRefreshMode => _autoRefreshMode;
|
||||
|
||||
DateTime? get quoteExpiresAt => _quotation?.quoteExpiresAt;
|
||||
|
||||
Duration? get timeLeft {
|
||||
final expiresAt = quoteExpiresAt;
|
||||
if (expiresAt == null) return null;
|
||||
return expiresAt.difference(DateTime.now().toUtc());
|
||||
}
|
||||
|
||||
bool get isExpired {
|
||||
final remaining = timeLeft;
|
||||
if (remaining == null) return false;
|
||||
return remaining <= Duration.zero;
|
||||
}
|
||||
|
||||
QuoteStatusType get quoteStatus {
|
||||
if (isLoading) return QuoteStatusType.loading;
|
||||
if (error != null) return QuoteStatusType.error;
|
||||
if (_quotation?.quotation == null) return QuoteStatusType.missing;
|
||||
if (isExpired) return QuoteStatusType.expired;
|
||||
return QuoteStatusType.active;
|
||||
}
|
||||
|
||||
bool get hasLiveQuote => isReady && _quotation?.quotation != null && !isExpired;
|
||||
|
||||
void setAutoRefreshMode(AutoRefreshMode mode) {
|
||||
if (_autoRefreshMode == mode) return;
|
||||
_autoRefreshMode = mode;
|
||||
_syncAutoRefresh();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void refreshQuotation() {
|
||||
_quotation?.refreshQuotation();
|
||||
}
|
||||
|
||||
void _handleQuotationChanged() {
|
||||
_syncAutoRefresh();
|
||||
_syncTicker();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _syncTicker() {
|
||||
final expiresAt = quoteExpiresAt;
|
||||
if (expiresAt == null) {
|
||||
_stopTicker();
|
||||
return;
|
||||
}
|
||||
|
||||
final remaining = expiresAt.difference(DateTime.now().toUtc());
|
||||
if (remaining <= Duration.zero) {
|
||||
_stopTicker();
|
||||
return;
|
||||
}
|
||||
|
||||
_ticker ??= Timer.periodic(const Duration(seconds: 1), (_) {
|
||||
final expiresAt = quoteExpiresAt;
|
||||
if (expiresAt == null) {
|
||||
_stopTicker();
|
||||
return;
|
||||
}
|
||||
final remaining = expiresAt.difference(DateTime.now().toUtc());
|
||||
if (remaining <= Duration.zero) {
|
||||
_stopTicker();
|
||||
}
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
void _stopTicker() {
|
||||
_ticker?.cancel();
|
||||
_ticker = null;
|
||||
}
|
||||
|
||||
void _syncAutoRefresh() {
|
||||
final quotation = _quotation;
|
||||
if (quotation == null) {
|
||||
_autoRefreshController.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
final isAutoRefreshEnabled = _autoRefreshMode == AutoRefreshMode.on;
|
||||
_autoRefreshController.setEnabled(isAutoRefreshEnabled);
|
||||
final canAutoRefresh = isAutoRefreshEnabled && quotation.canRefresh;
|
||||
_autoRefreshController.sync(
|
||||
isLoading: quotation.isLoading,
|
||||
canRefresh: canAutoRefresh,
|
||||
expiresAt: quoteExpiresAt,
|
||||
onRefresh: _refreshQuotation,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _refreshQuotation() async {
|
||||
await _quotation?.refreshQuotation();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_quotation?.removeListener(_handleQuotationChanged);
|
||||
_autoRefreshController.dispose();
|
||||
_stopTicker();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
91
frontend/pweb/lib/providers/signup_confirmation.dart
Normal file
91
frontend/pweb/lib/providers/signup_confirmation.dart
Normal file
@@ -0,0 +1,91 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:pshared/models/auth/probe_result.dart';
|
||||
import 'package:pshared/provider/account.dart';
|
||||
|
||||
|
||||
class SignupConfirmationProvider extends ChangeNotifier {
|
||||
SignupConfirmationProvider({
|
||||
required AccountProvider accountProvider,
|
||||
Duration pollInterval = const Duration(seconds: 10),
|
||||
}) : _accountProvider = accountProvider,
|
||||
_pollInterval = pollInterval;
|
||||
|
||||
final AccountProvider _accountProvider;
|
||||
final Duration _pollInterval;
|
||||
|
||||
Timer? _pollTimer;
|
||||
bool _isChecking = false;
|
||||
bool _isAuthorized = false;
|
||||
|
||||
String? _email;
|
||||
String? _password;
|
||||
String? _locale;
|
||||
|
||||
bool get isAuthorized => _isAuthorized;
|
||||
bool get isChecking => _isChecking;
|
||||
|
||||
void startPolling({
|
||||
required String email,
|
||||
required String password,
|
||||
required String locale,
|
||||
}) {
|
||||
final trimmedEmail = email.trim();
|
||||
final trimmedPassword = password.trim();
|
||||
final trimmedLocale = locale.trim();
|
||||
if (trimmedEmail.isEmpty || trimmedPassword.isEmpty || trimmedLocale.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
_email = trimmedEmail;
|
||||
_password = trimmedPassword;
|
||||
_locale = trimmedLocale;
|
||||
|
||||
_pollTimer?.cancel();
|
||||
_pollTimer = Timer.periodic(_pollInterval, (_) => _probeAuthorization());
|
||||
_probeAuthorization();
|
||||
}
|
||||
|
||||
void stopPolling() {
|
||||
_pollTimer?.cancel();
|
||||
_pollTimer = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pollTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _probeAuthorization() async {
|
||||
if (_isChecking || _isAuthorized) return;
|
||||
final email = _email;
|
||||
final password = _password;
|
||||
final locale = _locale;
|
||||
if (email == null || password == null || locale == null) return;
|
||||
|
||||
_setChecking(true);
|
||||
try {
|
||||
final result = await _accountProvider.probeAuthorization(
|
||||
email: email,
|
||||
password: password,
|
||||
locale: locale,
|
||||
);
|
||||
if (result == AuthProbeResult.authorized) {
|
||||
_isAuthorized = true;
|
||||
stopPolling();
|
||||
notifyListeners();
|
||||
}
|
||||
} finally {
|
||||
_setChecking(false);
|
||||
}
|
||||
}
|
||||
|
||||
void _setChecking(bool value) {
|
||||
if (_isChecking == value) return;
|
||||
_isChecking = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import 'package:pshared/models/auth/pending_login.dart';
|
||||
import 'package:pshared/provider/account.dart';
|
||||
import 'package:pshared/service/verification.dart';
|
||||
|
||||
import 'package:pweb/models/flow_status.dart';
|
||||
import 'package:pweb/models/state/flow_status.dart';
|
||||
|
||||
|
||||
class TwoFactorProvider extends ChangeNotifier {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pshared/models/payment/status.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:pweb/models/wallet_transaction.dart';
|
||||
import 'package:pweb/models/wallet/wallet_transaction.dart';
|
||||
import 'package:pweb/services/wallet_transactions.dart';
|
||||
|
||||
|
||||
@@ -10,27 +9,15 @@ class WalletTransactionsProvider extends ChangeNotifier {
|
||||
|
||||
WalletTransactionsProvider(this._service);
|
||||
|
||||
List<WalletTransaction> _transactions = [];
|
||||
List<WalletTransaction> _filteredTransactions = [];
|
||||
DateTimeRange? _dateRange;
|
||||
final Set<OperationStatus> _selectedStatuses = {};
|
||||
final Set<WalletTransactionType> _selectedTypes = {};
|
||||
String? _walletId;
|
||||
List<WalletTransaction> _transactions = const [];
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
String? _walletId;
|
||||
|
||||
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;
|
||||
List<WalletTransaction> get transactions => List.unmodifiable(_transactions);
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
bool get hasFilters =>
|
||||
_dateRange != null ||
|
||||
_selectedStatuses.isNotEmpty ||
|
||||
_selectedTypes.isNotEmpty;
|
||||
String? get walletId => _walletId;
|
||||
|
||||
Future<void> load({String? walletId}) async {
|
||||
_isLoading = true;
|
||||
@@ -40,65 +27,11 @@ class WalletTransactionsProvider extends ChangeNotifier {
|
||||
try {
|
||||
_walletId = walletId ?? _walletId;
|
||||
_transactions = await _service.fetchHistory(walletId: _walletId);
|
||||
_applyFilters(notify: false);
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
} finally {
|
||||
_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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user