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

@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:email_validator/email_validator.dart';
class EmailFieldController {
final TextEditingController textController;
final ValueNotifier<bool> isValid;
EmailFieldController({
TextEditingController? controller,
}) : textController = controller ?? TextEditingController(),
isValid = ValueNotifier<bool>(false);
String get text => textController.text;
void setText(String value) {
textController.text = value;
onChanged(value);
}
bool _isValidEmail(String? value) {
final trimmed = value?.trim() ?? '';
if (trimmed.isEmpty) {
return false;
}
return EmailValidator.validate(trimmed);
}
String? validate(String? value, String invalidMessage) {
final result = _isValidEmail(value) ? null : invalidMessage;
isValid.value = result == null;
return result;
}
void onChanged(String? value) {
isValid.value = _isValidEmail(value);
}
void dispose() {
textController.dispose();
isValid.dispose();
}
}

View File

@@ -17,6 +17,7 @@ class MultiplePayoutsController extends ChangeNotifier {
MultiplePayoutsProvider? _provider;
WalletsController? _wallets;
_PickState _pickState = _PickState.idle;
Exception? _uiError;
MultiplePayoutsController({
required CsvInputService csvInput,
@@ -46,7 +47,7 @@ class MultiplePayoutsController extends ChangeNotifier {
String? get selectedFileName => _provider?.selectedFileName;
List<CsvPayoutRow> get rows => _provider?.rows ?? const <CsvPayoutRow>[];
int get sentCount => _provider?.sentCount ?? 0;
Exception? get error => _provider?.error;
Exception? get error => _uiError ?? _provider?.error;
bool get isQuoting => _provider?.isQuoting ?? false;
bool get isSending => _provider?.isSending ?? false;
@@ -71,15 +72,19 @@ class MultiplePayoutsController extends ChangeNotifier {
Future<void> pickAndQuote() async {
if (_pickState == _PickState.picking) return;
final provider = _provider;
if (provider == null) return;
if (provider == null) {
_setUiError(StateError('Multiple payouts provider is not ready'));
return;
}
_clearUiError();
_pickState = _PickState.picking;
try {
final picked = await _csvInput.pickCsv();
if (picked == null) return;
final wallet = _selectedWallet;
if (wallet == null) {
provider.setError(StateError('Select source wallet first'));
_setUiError(StateError('Select source wallet first'));
return;
}
await provider.quoteFromCsv(
@@ -88,7 +93,7 @@ class MultiplePayoutsController extends ChangeNotifier {
sourceWallet: wallet,
);
} catch (e) {
provider.setError(e);
_setUiError(e);
} finally {
_pickState = _PickState.idle;
}
@@ -98,10 +103,16 @@ class MultiplePayoutsController extends ChangeNotifier {
return _provider?.send() ?? const <Payment>[];
}
Future<MultiplePayoutSendOutcome> sendAndStorePayments() async {
final payments =
await _provider?.sendAndStorePayments() ?? const <Payment>[];
final hasError = _provider?.error != null;
Future<MultiplePayoutSendOutcome> sendAndGetOutcome() async {
_clearUiError();
final provider = _provider;
if (provider == null) {
_setUiError(StateError('Multiple payouts provider is not ready'));
return MultiplePayoutSendOutcome.failure;
}
final payments = await provider.send();
final hasError = provider.error != null;
if (hasError || payments.isEmpty) {
return MultiplePayoutSendOutcome.failure;
}
@@ -110,6 +121,7 @@ class MultiplePayoutsController extends ChangeNotifier {
void removeUploadedFile() {
_provider?.removeUploadedFile();
_clearUiError(notify: false);
}
void _onProviderChanged() {
@@ -122,6 +134,19 @@ class MultiplePayoutsController extends ChangeNotifier {
Wallet? get _selectedWallet => _wallets?.selectedWallet;
void _setUiError(Object error) {
_uiError = error is Exception ? error : Exception(error.toString());
notifyListeners();
}
void _clearUiError({bool notify = true}) {
if (_uiError == null) return;
_uiError = null;
if (notify) {
notifyListeners();
}
}
@override
void dispose() {
_provider?.removeListener(_onProviderChanged);

View File

@@ -0,0 +1,77 @@
import 'dart:async';
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?.endAutoRefresh();
_payments = provider;
_payments?.beginAutoRefresh();
if (provider.isReady || provider.isLoading) {
unawaited(_payments?.refreshSilently());
} else {
unawaited(_payments?.refresh());
}
}
_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;
}
@override
void dispose() {
_payments?.endAutoRefresh();
super.dispose();
}
}

View File

@@ -0,0 +1,184 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:pshared/provider/payout_verification.dart';
import 'package:pweb/models/flow_status.dart';
class PayoutVerificationController extends ChangeNotifier {
PayoutVerificationProvider? _provider;
FlowStatus _status = FlowStatus.idle;
Object? _error;
Timer? _cooldownTimer;
int _cooldownRemainingSeconds = 0;
DateTime? _cooldownUntil;
FlowStatus get status => _status;
bool get isSubmitting => _status == FlowStatus.submitting;
bool get isResending => _status == FlowStatus.resending;
bool get hasError => _status == FlowStatus.error;
bool get verificationSuccess => _status == FlowStatus.success;
Object? get error => _error;
String get target => _provider?.target ?? '';
int get cooldownRemainingSeconds => _cooldownRemainingSeconds;
bool get isCooldownActive => _cooldownRemainingSeconds > 0;
void update(PayoutVerificationProvider provider) {
if (identical(_provider, provider)) return;
_provider?.removeListener(_onProviderChanged);
_provider = provider;
_provider?.addListener(_onProviderChanged);
_syncCooldown(provider.cooldownUntil);
}
Future<void> requestCode() async {
final provider = _provider;
if (provider == null) {
throw StateError('Payout verification provider is not ready');
}
_error = null;
_setStatus(FlowStatus.submitting);
try {
await provider.requestCode();
_setStatus(FlowStatus.idle);
} catch (e) {
_error = e;
_setStatus(FlowStatus.error);
rethrow;
}
}
Future<void> submitCode(String code) async {
final provider = _provider;
if (provider == null) {
throw StateError('Payout verification provider is not ready');
}
_error = null;
_setStatus(FlowStatus.submitting);
try {
await provider.confirmCode(code);
_setStatus(FlowStatus.success);
} catch (e) {
_error = e;
_setStatus(FlowStatus.error);
}
}
Future<void> resendCode() async {
final provider = _provider;
if (provider == null) {
throw StateError('Payout verification provider is not ready');
}
if (isResending || isCooldownActive) return;
_error = null;
_setStatus(FlowStatus.resending);
try {
await provider.resendCode();
_setStatus(FlowStatus.idle);
} catch (e) {
_error = e;
_setStatus(FlowStatus.error);
}
}
void reset() {
_error = null;
_setStatus(FlowStatus.idle);
_stopCooldown();
_provider?.reset();
}
void resetStatus() {
_error = null;
_setStatus(FlowStatus.idle);
}
void _onProviderChanged() {
_syncCooldown(_provider?.cooldownUntil);
notifyListeners();
}
void _syncCooldown(DateTime? until) {
if (until == null) {
_stopCooldown(notify: _cooldownRemainingSeconds != 0);
return;
}
if (!_isCooldownActive(until) && _cooldownRemainingSeconds != 0) {
_stopCooldown(notify: true);
return;
}
if (_cooldownUntil == null || _cooldownUntil != until) {
_startCooldownUntil(until);
}
}
void _startCooldownUntil(DateTime until) {
_cooldownTimer?.cancel();
_cooldownUntil = until;
_cooldownRemainingSeconds = _cooldownRemaining();
if (_cooldownRemainingSeconds <= 0) {
_cooldownTimer = null;
_cooldownUntil = null;
notifyListeners();
return;
}
_cooldownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
final remaining = _cooldownRemaining();
if (remaining <= 0) {
_stopCooldown(notify: true);
return;
}
if (remaining != _cooldownRemainingSeconds) {
_cooldownRemainingSeconds = remaining;
notifyListeners();
}
});
notifyListeners();
}
bool _isCooldownActive(DateTime until) => until.isAfter(DateTime.now());
int _cooldownRemaining() {
final until = _cooldownUntil;
if (until == null) return 0;
final remaining = until.difference(DateTime.now()).inSeconds;
return remaining < 0 ? 0 : remaining;
}
void _stopCooldown({bool notify = false}) {
_cooldownTimer?.cancel();
_cooldownTimer = null;
final hadCooldown = _cooldownRemainingSeconds != 0;
_cooldownRemainingSeconds = 0;
_cooldownUntil = null;
if (notify && hadCooldown) {
notifyListeners();
}
}
void _setStatus(FlowStatus status) {
if (_status == status) return;
_status = status;
notifyListeners();
}
@override
void dispose() {
_provider?.removeListener(_onProviderChanged);
_stopCooldown();
super.dispose();
}
}

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:pshared/models/payment/operation.dart';
@@ -17,7 +19,14 @@ class RecentPaymentsController extends ChangeNotifier {
void update(PaymentsProvider provider) {
if (!identical(_payments, provider)) {
_payments?.endAutoRefresh();
_payments = provider;
_payments?.beginAutoRefresh();
if (provider.isReady || provider.isLoading) {
unawaited(_payments?.refreshSilently());
} else {
unawaited(_payments?.refresh());
}
}
_rebuild();
}
@@ -30,4 +39,10 @@ class RecentPaymentsController extends ChangeNotifier {
notifyListeners();
}
@override
void dispose() {
_payments?.endAutoRefresh();
super.dispose();
}
}

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:collection';
import 'package:flutter/material.dart';
@@ -6,6 +7,7 @@ import 'package:pshared/models/payment/operation.dart';
import 'package:pshared/models/payment/status.dart';
import 'package:pshared/provider/payment/payments.dart';
import 'package:pweb/models/load_more_state.dart';
import 'package:pweb/utils/report/operations.dart';
import 'package:pweb/utils/report/payment_mapper.dart';
@@ -25,10 +27,26 @@ class ReportOperationsController extends ChangeNotifier {
bool get isLoading => _payments?.isLoading ?? false;
Exception? get error => _payments?.error;
LoadMoreState get loadMoreState {
if (_payments?.isLoadingMore ?? false) {
return LoadMoreState.loading;
}
if (_payments?.canLoadMore ?? false) {
return LoadMoreState.available;
}
return LoadMoreState.hidden;
}
void update(PaymentsProvider provider) {
if (!identical(_payments, provider)) {
_payments?.endAutoRefresh();
_payments = provider;
_payments?.beginAutoRefresh();
if (provider.isReady || provider.isLoading) {
unawaited(_payments?.refreshSilently());
} else {
unawaited(_payments?.refresh());
}
}
_rebuildOperations();
}
@@ -59,6 +77,10 @@ class ReportOperationsController extends ChangeNotifier {
await _payments?.refresh();
}
Future<void> loadMore() async {
await _payments?.loadMore();
}
void _rebuildOperations() {
final items = _payments?.payments ?? const [];
_operations = items.map(mapPaymentToOperation).toList();
@@ -101,4 +123,10 @@ class ReportOperationsController extends ChangeNotifier {
left.end.isAtSameMomentAs(right.end);
}
@override
void dispose() {
_payments?.endAutoRefresh();
super.dispose();
}
}