verification before payment and email fixes
This commit is contained in:
44
frontend/pweb/lib/controllers/email.dart
Normal file
44
frontend/pweb/lib/controllers/email.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
77
frontend/pweb/lib/controllers/payment_details.dart
Normal file
77
frontend/pweb/lib/controllers/payment_details.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
184
frontend/pweb/lib/controllers/payout_verification.dart
Normal file
184
frontend/pweb/lib/controllers/payout_verification.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user