verification before payment and email fixes

This commit is contained in:
Arseni
2026-02-18 18:15:38 +03:00
parent 4dc182bfa2
commit e901ac3eb6
35 changed files with 1023 additions and 192 deletions

View File

@@ -17,7 +17,9 @@ class PaymentsProvider with ChangeNotifier {
bool _isLoaded = false;
bool _isLoadingMore = false;
String? _nextCursor;
Timer? _autoRefreshTimer;
int _autoRefreshRefs = 0;
Duration _autoRefreshInterval = const Duration(seconds: 15);
int? _limit;
String? _sourceRef;
String? _destinationRef;
@@ -35,6 +37,23 @@ class PaymentsProvider with ChangeNotifier {
String? get nextCursor => _nextCursor;
bool get canLoadMore => _nextCursor != null && _nextCursor!.isNotEmpty;
void beginAutoRefresh({Duration interval = const Duration(seconds: 15)}) {
_autoRefreshRefs += 1;
if (interval < _autoRefreshInterval) {
_autoRefreshInterval = interval;
_restartAutoRefreshTimer();
}
_ensureAutoRefreshTimer();
}
void endAutoRefresh() {
if (_autoRefreshRefs == 0) return;
_autoRefreshRefs -= 1;
if (_autoRefreshRefs == 0) {
_stopAutoRefreshTimer();
}
}
void update(OrganizationsProvider organizations) {
_organizations = organizations;
if (!organizations.isOrganizationSet) {
@@ -42,6 +61,10 @@ class PaymentsProvider with ChangeNotifier {
return;
}
if (_autoRefreshRefs > 0) {
_ensureAutoRefreshTimer();
}
final orgRef = organizations.current.id;
if (_loadedOrganizationRef != orgRef) {
_loadedOrganizationRef = orgRef;
@@ -54,6 +77,40 @@ class PaymentsProvider with ChangeNotifier {
String? sourceRef,
String? destinationRef,
List<String>? states,
}) async {
await _refresh(
limit: limit,
sourceRef: sourceRef,
destinationRef: destinationRef,
states: states,
showLoading: true,
updateError: true,
);
}
Future<void> refreshSilently({
int? limit,
String? sourceRef,
String? destinationRef,
List<String>? states,
}) async {
await _refresh(
limit: limit,
sourceRef: sourceRef,
destinationRef: destinationRef,
states: states,
showLoading: false,
updateError: false,
);
}
Future<void> _refresh({
int? limit,
String? sourceRef,
String? destinationRef,
List<String>? states,
required bool showLoading,
required bool updateError,
}) async {
final org = _organizations;
if (org == null || !org.isOrganizationSet) return;
@@ -67,7 +124,9 @@ class PaymentsProvider with ChangeNotifier {
final seq = ++_opSeq;
_applyResource(_resource.copyWith(isLoading: true, error: null), notify: true);
if (showLoading) {
_applyResource(_resource.copyWith(isLoading: true, error: null), notify: true);
}
try {
final page = await PaymentService.listPage(
@@ -84,16 +143,26 @@ class PaymentsProvider with ChangeNotifier {
_isLoaded = true;
_nextCursor = _normalize(page.nextCursor);
_applyResource(
Resource(data: page.items, isLoading: false, error: null),
Resource(
data: page.items,
isLoading: false,
error: null,
),
notify: true,
);
} catch (e) {
if (seq != _opSeq) return;
_applyResource(
_resource.copyWith(isLoading: false, error: toException(e)),
notify: true,
);
if (updateError) {
_applyResource(
_resource.copyWith(isLoading: false, error: toException(e)),
notify: true,
);
} else if (showLoading) {
_applyResource(
_resource.copyWith(isLoading: false),
notify: true,
);
}
}
}
@@ -155,31 +224,10 @@ class PaymentsProvider with ChangeNotifier {
_destinationRef = null;
_states = null;
_resource = Resource(data: []);
_pauseAutoRefreshTimer();
notifyListeners();
}
void addPayments(List<Payment> items, {bool prepend = true}) {
if (items.isEmpty) return;
final current = List<Payment>.from(payments);
final existingRefs = <String>{};
for (final payment in current) {
final ref = payment.paymentRef;
if (ref != null && ref.isNotEmpty) {
existingRefs.add(ref);
}
}
final newItems = items.where((payment) {
final ref = payment.paymentRef;
if (ref == null || ref.isEmpty) return true;
return !existingRefs.contains(ref);
}).toList();
if (newItems.isEmpty) return;
final combined = prepend ? [...newItems, ...current] : [...current, ...newItems];
_applyResource(_resource.copyWith(data: combined, error: null), notify: true);
}
void _applyResource(Resource<List<Payment>> newResource, {required bool notify}) {
_resource = newResource;
if (notify) notifyListeners();
@@ -200,4 +248,32 @@ class PaymentsProvider with ChangeNotifier {
if (normalized.isEmpty) return null;
return normalized;
}
void _ensureAutoRefreshTimer() {
if (_autoRefreshTimer != null) return;
_autoRefreshTimer = Timer.periodic(_autoRefreshInterval, (_) {
if (_resource.isLoading || _isLoadingMore) return;
unawaited(refreshSilently());
});
}
void _restartAutoRefreshTimer() {
if (_autoRefreshTimer == null) return;
_autoRefreshTimer?.cancel();
_autoRefreshTimer = null;
_ensureAutoRefreshTimer();
}
void _stopAutoRefreshTimer() {
_autoRefreshTimer?.cancel();
_autoRefreshTimer = null;
_autoRefreshRefs = 0;
_autoRefreshInterval = const Duration(seconds: 15);
}
void _pauseAutoRefreshTimer() {
_autoRefreshTimer?.cancel();
_autoRefreshTimer = null;
}
}

View File

@@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
import 'package:pshared/api/responses/verification/response.dart';
import 'package:pshared/provider/resource.dart';
import 'package:pshared/service/verification.dart';
import 'package:pshared/utils/exception.dart';
class PayoutVerificationProvider extends ChangeNotifier {
Resource<VerificationResponse?> _resource = Resource(data: null);
DateTime? _cooldownUntil;
VerificationResponse? get response => _resource.data;
bool get isLoading => _resource.isLoading;
Exception? get error => _resource.error;
String get target => _resource.data?.target ?? '';
String? get idempotencyKey => _resource.data?.idempotencyKey;
DateTime? get cooldownUntil => _cooldownUntil;
Future<VerificationResponse> requestCode() async {
_setResource(_resource.copyWith(isLoading: true, error: null));
try {
final response = await VerificationService.requestPayoutCode();
_cooldownUntil = _resolveCooldownUntil(response);
_setResource(_resource.copyWith(
data: response,
isLoading: false,
error: null,
));
return response;
} catch (e) {
_setResource(_resource.copyWith(
isLoading: false,
error: toException(e),
));
rethrow;
}
}
Future<VerificationResponse> resendCode() async {
final currentKey = _resource.data?.idempotencyKey;
if (currentKey == null || currentKey.isEmpty) {
throw StateError('Payout verification is not initialized');
}
_setResource(_resource.copyWith(isLoading: true, error: null));
try {
final response = await VerificationService.resendPayoutCode(
idempotencyKey: currentKey,
);
_cooldownUntil = _resolveCooldownUntil(response);
_setResource(_resource.copyWith(
data: response,
isLoading: false,
error: null,
));
return response;
} catch (e) {
_setResource(_resource.copyWith(
isLoading: false,
error: toException(e),
));
rethrow;
}
}
Future<void> confirmCode(String code) async {
final currentKey = _resource.data?.idempotencyKey;
if (currentKey == null || currentKey.isEmpty) {
throw StateError('Payout verification is not initialized');
}
_setResource(_resource.copyWith(isLoading: true, error: null));
try {
await VerificationService.confirmPayoutCode(
code: code.trim(),
idempotencyKey: currentKey,
);
_setResource(_resource.copyWith(isLoading: false, error: null));
} catch (e) {
_setResource(_resource.copyWith(
isLoading: false,
error: toException(e),
));
rethrow;
}
}
void reset() {
_cooldownUntil = null;
_setResource(Resource(data: null));
}
DateTime? _resolveCooldownUntil(VerificationResponse response) {
if (response.cooldownSeconds <= 0) return null;
return DateTime.now().add(response.cooldownDuration);
}
void _setResource(Resource<VerificationResponse?> resource) {
_resource = resource;
notifyListeners();
}
}