verification before payment and email fixes
This commit is contained in:
@@ -216,7 +216,7 @@ func (p *Payments) List(ctx context.Context, filter *model.PaymentFilter) (*mode
|
|||||||
|
|
||||||
if cursor := strings.TrimSpace(filter.Cursor); cursor != "" {
|
if cursor := strings.TrimSpace(filter.Cursor); cursor != "" {
|
||||||
if oid, err := bson.ObjectIDFromHex(cursor); err == nil {
|
if oid, err := bson.ObjectIDFromHex(cursor); err == nil {
|
||||||
query = query.Comparison(repository.IDField(), builder.Gt, oid)
|
query = query.Comparison(repository.IDField(), builder.Lt, oid)
|
||||||
} else {
|
} else {
|
||||||
p.logger.Warn("Ignoring invalid payments cursor", zap.String("cursor", cursor), zap.Error(err))
|
p.logger.Warn("Ignoring invalid payments cursor", zap.String("cursor", cursor), zap.Error(err))
|
||||||
}
|
}
|
||||||
@@ -224,7 +224,7 @@ func (p *Payments) List(ctx context.Context, filter *model.PaymentFilter) (*mode
|
|||||||
|
|
||||||
limit := sanitizePaymentLimit(filter.Limit)
|
limit := sanitizePaymentLimit(filter.Limit)
|
||||||
fetchLimit := limit + 1
|
fetchLimit := limit + 1
|
||||||
query = query.Sort(repository.IDField(), true).Limit(&fetchLimit)
|
query = query.Sort(repository.IDField(), false).Limit(&fetchLimit)
|
||||||
|
|
||||||
payments := make([]*model.Payment, 0, fetchLimit)
|
payments := make([]*model.Payment, 0, fetchLimit)
|
||||||
decoder := func(cur *mongo.Cursor) error {
|
decoder := func(cur *mongo.Cursor) error {
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ class PaymentsProvider with ChangeNotifier {
|
|||||||
bool _isLoaded = false;
|
bool _isLoaded = false;
|
||||||
bool _isLoadingMore = false;
|
bool _isLoadingMore = false;
|
||||||
String? _nextCursor;
|
String? _nextCursor;
|
||||||
|
Timer? _autoRefreshTimer;
|
||||||
|
int _autoRefreshRefs = 0;
|
||||||
|
Duration _autoRefreshInterval = const Duration(seconds: 15);
|
||||||
int? _limit;
|
int? _limit;
|
||||||
String? _sourceRef;
|
String? _sourceRef;
|
||||||
String? _destinationRef;
|
String? _destinationRef;
|
||||||
@@ -35,6 +37,23 @@ class PaymentsProvider with ChangeNotifier {
|
|||||||
String? get nextCursor => _nextCursor;
|
String? get nextCursor => _nextCursor;
|
||||||
bool get canLoadMore => _nextCursor != null && _nextCursor!.isNotEmpty;
|
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) {
|
void update(OrganizationsProvider organizations) {
|
||||||
_organizations = organizations;
|
_organizations = organizations;
|
||||||
if (!organizations.isOrganizationSet) {
|
if (!organizations.isOrganizationSet) {
|
||||||
@@ -42,6 +61,10 @@ class PaymentsProvider with ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_autoRefreshRefs > 0) {
|
||||||
|
_ensureAutoRefreshTimer();
|
||||||
|
}
|
||||||
|
|
||||||
final orgRef = organizations.current.id;
|
final orgRef = organizations.current.id;
|
||||||
if (_loadedOrganizationRef != orgRef) {
|
if (_loadedOrganizationRef != orgRef) {
|
||||||
_loadedOrganizationRef = orgRef;
|
_loadedOrganizationRef = orgRef;
|
||||||
@@ -54,6 +77,40 @@ class PaymentsProvider with ChangeNotifier {
|
|||||||
String? sourceRef,
|
String? sourceRef,
|
||||||
String? destinationRef,
|
String? destinationRef,
|
||||||
List<String>? states,
|
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 {
|
}) async {
|
||||||
final org = _organizations;
|
final org = _organizations;
|
||||||
if (org == null || !org.isOrganizationSet) return;
|
if (org == null || !org.isOrganizationSet) return;
|
||||||
@@ -67,7 +124,9 @@ class PaymentsProvider with ChangeNotifier {
|
|||||||
|
|
||||||
final seq = ++_opSeq;
|
final seq = ++_opSeq;
|
||||||
|
|
||||||
_applyResource(_resource.copyWith(isLoading: true, error: null), notify: true);
|
if (showLoading) {
|
||||||
|
_applyResource(_resource.copyWith(isLoading: true, error: null), notify: true);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final page = await PaymentService.listPage(
|
final page = await PaymentService.listPage(
|
||||||
@@ -84,16 +143,26 @@ class PaymentsProvider with ChangeNotifier {
|
|||||||
_isLoaded = true;
|
_isLoaded = true;
|
||||||
_nextCursor = _normalize(page.nextCursor);
|
_nextCursor = _normalize(page.nextCursor);
|
||||||
_applyResource(
|
_applyResource(
|
||||||
Resource(data: page.items, isLoading: false, error: null),
|
Resource(
|
||||||
|
data: page.items,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
),
|
||||||
notify: true,
|
notify: true,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (seq != _opSeq) return;
|
if (seq != _opSeq) return;
|
||||||
|
if (updateError) {
|
||||||
_applyResource(
|
_applyResource(
|
||||||
_resource.copyWith(isLoading: false, error: toException(e)),
|
_resource.copyWith(isLoading: false, error: toException(e)),
|
||||||
notify: true,
|
notify: true,
|
||||||
);
|
);
|
||||||
|
} else if (showLoading) {
|
||||||
|
_applyResource(
|
||||||
|
_resource.copyWith(isLoading: false),
|
||||||
|
notify: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,31 +224,10 @@ class PaymentsProvider with ChangeNotifier {
|
|||||||
_destinationRef = null;
|
_destinationRef = null;
|
||||||
_states = null;
|
_states = null;
|
||||||
_resource = Resource(data: []);
|
_resource = Resource(data: []);
|
||||||
|
_pauseAutoRefreshTimer();
|
||||||
notifyListeners();
|
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}) {
|
void _applyResource(Resource<List<Payment>> newResource, {required bool notify}) {
|
||||||
_resource = newResource;
|
_resource = newResource;
|
||||||
if (notify) notifyListeners();
|
if (notify) notifyListeners();
|
||||||
@@ -200,4 +248,32 @@ class PaymentsProvider with ChangeNotifier {
|
|||||||
if (normalized.isEmpty) return null;
|
if (normalized.isEmpty) return null;
|
||||||
return normalized;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
103
frontend/pshared/lib/provider/payout_verification.dart
Normal file
103
frontend/pshared/lib/provider/payout_verification.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import 'package:logging/logging.dart';
|
|||||||
|
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/api/requests/tokens/session_identifier.dart';
|
||||||
import 'package:pshared/api/requests/verification/login.dart';
|
import 'package:pshared/api/requests/verification/login.dart';
|
||||||
import 'package:pshared/api/responses/login.dart';
|
import 'package:pshared/api/responses/login.dart';
|
||||||
import 'package:pshared/api/responses/verification/response.dart';
|
import 'package:pshared/api/responses/verification/response.dart';
|
||||||
@@ -9,6 +10,8 @@ import 'package:pshared/data/mapper/account/account.dart';
|
|||||||
import 'package:pshared/data/mapper/session_identifier.dart';
|
import 'package:pshared/data/mapper/session_identifier.dart';
|
||||||
import 'package:pshared/models/account/account.dart';
|
import 'package:pshared/models/account/account.dart';
|
||||||
import 'package:pshared/models/auth/pending_login.dart';
|
import 'package:pshared/models/auth/pending_login.dart';
|
||||||
|
import 'package:pshared/models/verification/purpose.dart';
|
||||||
|
import 'package:pshared/service/authorization/service.dart';
|
||||||
import 'package:pshared/service/authorization/storage.dart';
|
import 'package:pshared/service/authorization/storage.dart';
|
||||||
import 'package:pshared/service/services.dart';
|
import 'package:pshared/service/services.dart';
|
||||||
import 'package:pshared/utils/http/requests.dart';
|
import 'package:pshared/utils/http/requests.dart';
|
||||||
@@ -69,4 +72,60 @@ class VerificationService {
|
|||||||
await AuthorizationStorage.updateRefreshToken(loginResponse.refreshToken);
|
await AuthorizationStorage.updateRefreshToken(loginResponse.refreshToken);
|
||||||
return loginResponse.account.toDomain();
|
return loginResponse.account.toDomain();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<VerificationResponse> requestPayoutCode({
|
||||||
|
String? target,
|
||||||
|
String? idempotencyKey,
|
||||||
|
}) async {
|
||||||
|
_logger.fine('Requesting payout confirmation code');
|
||||||
|
final response = await AuthorizationService.getPOSTResponse(
|
||||||
|
_objectType,
|
||||||
|
'',
|
||||||
|
LoginVerificationRequest(
|
||||||
|
purpose: VerificationPurpose.payout,
|
||||||
|
target: target,
|
||||||
|
idempotencyKey: idempotencyKey ?? Uuid().v4(),
|
||||||
|
).toJson(),
|
||||||
|
);
|
||||||
|
return VerificationResponse.fromJson(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<VerificationResponse> resendPayoutCode({
|
||||||
|
required String idempotencyKey,
|
||||||
|
String? target,
|
||||||
|
}) async {
|
||||||
|
_logger.fine('Resending payout confirmation code');
|
||||||
|
final response = await AuthorizationService.getPOSTResponse(
|
||||||
|
_objectType,
|
||||||
|
'/resend',
|
||||||
|
LoginVerificationRequest(
|
||||||
|
purpose: VerificationPurpose.payout,
|
||||||
|
target: target,
|
||||||
|
idempotencyKey: idempotencyKey,
|
||||||
|
).toJson(),
|
||||||
|
);
|
||||||
|
return VerificationResponse.fromJson(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> confirmPayoutCode({
|
||||||
|
required String code,
|
||||||
|
required String idempotencyKey,
|
||||||
|
String? target,
|
||||||
|
}) async {
|
||||||
|
_logger.fine('Confirming payout code');
|
||||||
|
await AuthorizationService.getPOSTResponse(
|
||||||
|
_objectType,
|
||||||
|
'/verify',
|
||||||
|
LoginCodeVerifyicationRequest(
|
||||||
|
purpose: VerificationPurpose.payout,
|
||||||
|
target: target,
|
||||||
|
idempotencyKey: idempotencyKey,
|
||||||
|
code: code,
|
||||||
|
sessionIdentifier: const SessionIdentifierDTO(
|
||||||
|
clientId: '',
|
||||||
|
deviceId: '',
|
||||||
|
),
|
||||||
|
).toJson(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import 'package:pshared/provider/payment/multiple/quotation.dart';
|
|||||||
import 'package:pshared/provider/payment/payments.dart';
|
import 'package:pshared/provider/payment/payments.dart';
|
||||||
import 'package:pshared/provider/payment/provider.dart';
|
import 'package:pshared/provider/payment/provider.dart';
|
||||||
import 'package:pshared/provider/payment/quotation/quotation.dart';
|
import 'package:pshared/provider/payment/quotation/quotation.dart';
|
||||||
|
import 'package:pshared/provider/payout_verification.dart';
|
||||||
import 'package:pshared/provider/recipient/provider.dart';
|
import 'package:pshared/provider/recipient/provider.dart';
|
||||||
import 'package:pshared/provider/recipient/methods_cache.dart';
|
import 'package:pshared/provider/recipient/methods_cache.dart';
|
||||||
import 'package:pshared/provider/recipient/pmethods.dart';
|
import 'package:pshared/provider/recipient/pmethods.dart';
|
||||||
@@ -23,6 +24,7 @@ import 'package:pweb/app/router/pages.dart';
|
|||||||
import 'package:pweb/app/router/payout_routes.dart';
|
import 'package:pweb/app/router/payout_routes.dart';
|
||||||
import 'package:pweb/controllers/multiple_payouts.dart';
|
import 'package:pweb/controllers/multiple_payouts.dart';
|
||||||
import 'package:pweb/controllers/payment_page.dart';
|
import 'package:pweb/controllers/payment_page.dart';
|
||||||
|
import 'package:pweb/controllers/payout_verification.dart';
|
||||||
import 'package:pweb/providers/multiple_payouts.dart';
|
import 'package:pweb/providers/multiple_payouts.dart';
|
||||||
import 'package:pweb/controllers/multi_quotation.dart';
|
import 'package:pweb/controllers/multi_quotation.dart';
|
||||||
import 'package:pweb/providers/quotation/quotation.dart';
|
import 'package:pweb/providers/quotation/quotation.dart';
|
||||||
@@ -122,6 +124,15 @@ RouteBase payoutShellRoute() => ShellRoute(
|
|||||||
update: (context, organization, quotation, provider) =>
|
update: (context, organization, quotation, provider) =>
|
||||||
provider!..update(organization, quotation),
|
provider!..update(organization, quotation),
|
||||||
),
|
),
|
||||||
|
ChangeNotifierProvider(create: (_) => PayoutVerificationProvider()),
|
||||||
|
ChangeNotifierProxyProvider<
|
||||||
|
PayoutVerificationProvider,
|
||||||
|
PayoutVerificationController
|
||||||
|
>(
|
||||||
|
create: (_) => PayoutVerificationController(),
|
||||||
|
update: (context, verification, controller) =>
|
||||||
|
controller!..update(verification),
|
||||||
|
),
|
||||||
ChangeNotifierProxyProvider4<
|
ChangeNotifierProxyProvider4<
|
||||||
PaymentProvider,
|
PaymentProvider,
|
||||||
QuotationProvider,
|
QuotationProvider,
|
||||||
|
|||||||
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;
|
MultiplePayoutsProvider? _provider;
|
||||||
WalletsController? _wallets;
|
WalletsController? _wallets;
|
||||||
_PickState _pickState = _PickState.idle;
|
_PickState _pickState = _PickState.idle;
|
||||||
|
Exception? _uiError;
|
||||||
|
|
||||||
MultiplePayoutsController({
|
MultiplePayoutsController({
|
||||||
required CsvInputService csvInput,
|
required CsvInputService csvInput,
|
||||||
@@ -46,7 +47,7 @@ class MultiplePayoutsController extends ChangeNotifier {
|
|||||||
String? get selectedFileName => _provider?.selectedFileName;
|
String? get selectedFileName => _provider?.selectedFileName;
|
||||||
List<CsvPayoutRow> get rows => _provider?.rows ?? const <CsvPayoutRow>[];
|
List<CsvPayoutRow> get rows => _provider?.rows ?? const <CsvPayoutRow>[];
|
||||||
int get sentCount => _provider?.sentCount ?? 0;
|
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 isQuoting => _provider?.isQuoting ?? false;
|
||||||
bool get isSending => _provider?.isSending ?? false;
|
bool get isSending => _provider?.isSending ?? false;
|
||||||
@@ -71,15 +72,19 @@ class MultiplePayoutsController extends ChangeNotifier {
|
|||||||
Future<void> pickAndQuote() async {
|
Future<void> pickAndQuote() async {
|
||||||
if (_pickState == _PickState.picking) return;
|
if (_pickState == _PickState.picking) return;
|
||||||
final provider = _provider;
|
final provider = _provider;
|
||||||
if (provider == null) return;
|
if (provider == null) {
|
||||||
|
_setUiError(StateError('Multiple payouts provider is not ready'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_clearUiError();
|
||||||
_pickState = _PickState.picking;
|
_pickState = _PickState.picking;
|
||||||
try {
|
try {
|
||||||
final picked = await _csvInput.pickCsv();
|
final picked = await _csvInput.pickCsv();
|
||||||
if (picked == null) return;
|
if (picked == null) return;
|
||||||
final wallet = _selectedWallet;
|
final wallet = _selectedWallet;
|
||||||
if (wallet == null) {
|
if (wallet == null) {
|
||||||
provider.setError(StateError('Select source wallet first'));
|
_setUiError(StateError('Select source wallet first'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await provider.quoteFromCsv(
|
await provider.quoteFromCsv(
|
||||||
@@ -88,7 +93,7 @@ class MultiplePayoutsController extends ChangeNotifier {
|
|||||||
sourceWallet: wallet,
|
sourceWallet: wallet,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
provider.setError(e);
|
_setUiError(e);
|
||||||
} finally {
|
} finally {
|
||||||
_pickState = _PickState.idle;
|
_pickState = _PickState.idle;
|
||||||
}
|
}
|
||||||
@@ -98,10 +103,16 @@ class MultiplePayoutsController extends ChangeNotifier {
|
|||||||
return _provider?.send() ?? const <Payment>[];
|
return _provider?.send() ?? const <Payment>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<MultiplePayoutSendOutcome> sendAndStorePayments() async {
|
Future<MultiplePayoutSendOutcome> sendAndGetOutcome() async {
|
||||||
final payments =
|
_clearUiError();
|
||||||
await _provider?.sendAndStorePayments() ?? const <Payment>[];
|
final provider = _provider;
|
||||||
final hasError = _provider?.error != null;
|
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) {
|
if (hasError || payments.isEmpty) {
|
||||||
return MultiplePayoutSendOutcome.failure;
|
return MultiplePayoutSendOutcome.failure;
|
||||||
}
|
}
|
||||||
@@ -110,6 +121,7 @@ class MultiplePayoutsController extends ChangeNotifier {
|
|||||||
|
|
||||||
void removeUploadedFile() {
|
void removeUploadedFile() {
|
||||||
_provider?.removeUploadedFile();
|
_provider?.removeUploadedFile();
|
||||||
|
_clearUiError(notify: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onProviderChanged() {
|
void _onProviderChanged() {
|
||||||
@@ -122,6 +134,19 @@ class MultiplePayoutsController extends ChangeNotifier {
|
|||||||
|
|
||||||
Wallet? get _selectedWallet => _wallets?.selectedWallet;
|
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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_provider?.removeListener(_onProviderChanged);
|
_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:flutter/foundation.dart';
|
||||||
|
|
||||||
import 'package:pshared/models/payment/operation.dart';
|
import 'package:pshared/models/payment/operation.dart';
|
||||||
@@ -17,7 +19,14 @@ class RecentPaymentsController extends ChangeNotifier {
|
|||||||
|
|
||||||
void update(PaymentsProvider provider) {
|
void update(PaymentsProvider provider) {
|
||||||
if (!identical(_payments, provider)) {
|
if (!identical(_payments, provider)) {
|
||||||
|
_payments?.endAutoRefresh();
|
||||||
_payments = provider;
|
_payments = provider;
|
||||||
|
_payments?.beginAutoRefresh();
|
||||||
|
if (provider.isReady || provider.isLoading) {
|
||||||
|
unawaited(_payments?.refreshSilently());
|
||||||
|
} else {
|
||||||
|
unawaited(_payments?.refresh());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_rebuild();
|
_rebuild();
|
||||||
}
|
}
|
||||||
@@ -30,4 +39,10 @@ class RecentPaymentsController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_payments?.endAutoRefresh();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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/models/payment/status.dart';
|
||||||
import 'package:pshared/provider/payment/payments.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/operations.dart';
|
||||||
import 'package:pweb/utils/report/payment_mapper.dart';
|
import 'package:pweb/utils/report/payment_mapper.dart';
|
||||||
|
|
||||||
@@ -25,10 +27,26 @@ class ReportOperationsController extends ChangeNotifier {
|
|||||||
|
|
||||||
bool get isLoading => _payments?.isLoading ?? false;
|
bool get isLoading => _payments?.isLoading ?? false;
|
||||||
Exception? get error => _payments?.error;
|
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) {
|
void update(PaymentsProvider provider) {
|
||||||
if (!identical(_payments, provider)) {
|
if (!identical(_payments, provider)) {
|
||||||
|
_payments?.endAutoRefresh();
|
||||||
_payments = provider;
|
_payments = provider;
|
||||||
|
_payments?.beginAutoRefresh();
|
||||||
|
if (provider.isReady || provider.isLoading) {
|
||||||
|
unawaited(_payments?.refreshSilently());
|
||||||
|
} else {
|
||||||
|
unawaited(_payments?.refresh());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_rebuildOperations();
|
_rebuildOperations();
|
||||||
}
|
}
|
||||||
@@ -59,6 +77,10 @@ class ReportOperationsController extends ChangeNotifier {
|
|||||||
await _payments?.refresh();
|
await _payments?.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> loadMore() async {
|
||||||
|
await _payments?.loadMore();
|
||||||
|
}
|
||||||
|
|
||||||
void _rebuildOperations() {
|
void _rebuildOperations() {
|
||||||
final items = _payments?.payments ?? const [];
|
final items = _payments?.payments ?? const [];
|
||||||
_operations = items.map(mapPaymentToOperation).toList();
|
_operations = items.map(mapPaymentToOperation).toList();
|
||||||
@@ -101,4 +123,10 @@ class ReportOperationsController extends ChangeNotifier {
|
|||||||
left.end.isAtSameMomentAs(right.end);
|
left.end.isAtSameMomentAs(right.end);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_payments?.endAutoRefresh();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,13 @@
|
|||||||
"twoFactorResend": "Didn’t receive a code? Resend",
|
"twoFactorResend": "Didn’t receive a code? Resend",
|
||||||
"twoFactorTitle": "Two-Factor Authentication",
|
"twoFactorTitle": "Two-Factor Authentication",
|
||||||
"twoFactorError": "Invalid code. Please try again.",
|
"twoFactorError": "Invalid code. Please try again.",
|
||||||
|
"payoutCooldown": "You can send again in {time}",
|
||||||
|
"@payoutCooldown": {
|
||||||
|
"placeholders": {
|
||||||
|
"time": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"loadMore": "Load more",
|
||||||
"payoutNavDashboard": "Dashboard",
|
"payoutNavDashboard": "Dashboard",
|
||||||
"payoutNavSendPayout": "Send payout",
|
"payoutNavSendPayout": "Send payout",
|
||||||
"payoutNavRecipients": "Recipients",
|
"payoutNavRecipients": "Recipients",
|
||||||
|
|||||||
@@ -129,6 +129,13 @@
|
|||||||
"twoFactorResend": "Не получили код? Отправить снова",
|
"twoFactorResend": "Не получили код? Отправить снова",
|
||||||
"twoFactorTitle": "Двухфакторная аутентификация",
|
"twoFactorTitle": "Двухфакторная аутентификация",
|
||||||
"twoFactorError": "Неверный код. Пожалуйста, попробуйте снова.",
|
"twoFactorError": "Неверный код. Пожалуйста, попробуйте снова.",
|
||||||
|
"payoutCooldown": "Можно отправить через {time}",
|
||||||
|
"@payoutCooldown": {
|
||||||
|
"placeholders": {
|
||||||
|
"time": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"loadMore": "Показать еще",
|
||||||
"payoutNavDashboard": "Дашборд",
|
"payoutNavDashboard": "Дашборд",
|
||||||
"payoutNavSendPayout": "Отправить выплату",
|
"payoutNavSendPayout": "Отправить выплату",
|
||||||
"payoutNavRecipients": "Получатели",
|
"payoutNavRecipients": "Получатели",
|
||||||
|
|||||||
5
frontend/pweb/lib/models/load_more_state.dart
Normal file
5
frontend/pweb/lib/models/load_more_state.dart
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
enum LoadMoreState {
|
||||||
|
hidden,
|
||||||
|
available,
|
||||||
|
loading,
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ class TwoFactorCodePage extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<TwoFactorProvider>(
|
return Consumer<TwoFactorProvider>(
|
||||||
builder: (context, provider, child) {
|
builder: (context, provider, child) {
|
||||||
|
final email = provider.pendingLogin?.target ?? '';
|
||||||
if (provider.verificationSuccess) {
|
if (provider.verificationSuccess) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
onVerificationSuccess();
|
onVerificationSuccess();
|
||||||
@@ -36,7 +37,7 @@ class TwoFactorCodePage extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const TwoFactorPromptText(),
|
TwoFactorPromptText(email: email),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
TwoFactorCodeInput(
|
TwoFactorCodeInput(
|
||||||
onCompleted: (code) => provider.submitCode(code),
|
onCompleted: (code) => provider.submitCode(code),
|
||||||
@@ -45,7 +46,12 @@ class TwoFactorCodePage extends StatelessWidget {
|
|||||||
if (provider.isSubmitting)
|
if (provider.isSubmitting)
|
||||||
const Center(child: CircularProgressIndicator())
|
const Center(child: CircularProgressIndicator())
|
||||||
else
|
else
|
||||||
const ResendCodeButton(),
|
ResendCodeButton(
|
||||||
|
onPressed: provider.resendCode,
|
||||||
|
isCooldownActive: provider.isCooldownActive,
|
||||||
|
isResending: provider.isResending,
|
||||||
|
cooldownRemainingSeconds: provider.cooldownRemainingSeconds,
|
||||||
|
),
|
||||||
if (provider.hasError) ...[
|
if (provider.hasError) ...[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
ErrorMessage(error: AppLocalizations.of(context)!.twoFactorError),
|
ErrorMessage(error: AppLocalizations.of(context)!.twoFactorError),
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/providers/two_factor.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
class TwoFactorPromptText extends StatelessWidget {
|
class TwoFactorPromptText extends StatelessWidget {
|
||||||
const TwoFactorPromptText({super.key});
|
final String email;
|
||||||
|
|
||||||
|
const TwoFactorPromptText({
|
||||||
|
super.key,
|
||||||
|
required this.email,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Text(
|
Widget build(BuildContext context) => Text(
|
||||||
AppLocalizations.of(context)!.twoFactorPrompt(
|
AppLocalizations.of(context)!.twoFactorPrompt(email),
|
||||||
context.watch<TwoFactorProvider>().pendingLogin?.target ?? '',
|
|
||||||
),
|
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/providers/two_factor.dart';
|
|
||||||
import 'package:pweb/utils/cooldown_format.dart';
|
import 'package:pweb/utils/cooldown_format.dart';
|
||||||
import 'package:pweb/widgets/resend_link.dart';
|
import 'package:pweb/widgets/resend_link.dart';
|
||||||
|
|
||||||
@@ -10,23 +7,33 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
|
|||||||
|
|
||||||
|
|
||||||
class ResendCodeButton extends StatelessWidget {
|
class ResendCodeButton extends StatelessWidget {
|
||||||
const ResendCodeButton({super.key});
|
final VoidCallback onPressed;
|
||||||
|
final bool isCooldownActive;
|
||||||
|
final bool isResending;
|
||||||
|
final int cooldownRemainingSeconds;
|
||||||
|
|
||||||
|
const ResendCodeButton({
|
||||||
|
super.key,
|
||||||
|
required this.onPressed,
|
||||||
|
required this.isCooldownActive,
|
||||||
|
required this.isResending,
|
||||||
|
required this.cooldownRemainingSeconds,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final localizations = AppLocalizations.of(context)!;
|
final localizations = AppLocalizations.of(context)!;
|
||||||
final provider = context.watch<TwoFactorProvider>();
|
final isDisabled = isCooldownActive || isResending;
|
||||||
final isDisabled = provider.isCooldownActive || provider.isResending;
|
|
||||||
|
|
||||||
final label = provider.isCooldownActive
|
final label = isCooldownActive
|
||||||
? '${localizations.twoFactorResend} (${formatCooldownSeconds(provider.cooldownRemainingSeconds)})'
|
? '${localizations.twoFactorResend} (${formatCooldownSeconds(cooldownRemainingSeconds)})'
|
||||||
: localizations.twoFactorResend;
|
: localizations.twoFactorResend;
|
||||||
|
|
||||||
return ResendLink(
|
return ResendLink(
|
||||||
label: label,
|
label: label,
|
||||||
onPressed: provider.resendCode,
|
onPressed: onPressed,
|
||||||
isDisabled: isDisabled,
|
isDisabled: isDisabled,
|
||||||
isLoading: provider.isResending,
|
isLoading: isResending,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'package:pweb/controllers/multiple_payouts.dart';
|
import 'package:pweb/controllers/multiple_payouts.dart';
|
||||||
|
import 'package:pweb/controllers/payout_verification.dart';
|
||||||
|
import 'package:pweb/utils/payment/payout_verification_flow.dart';
|
||||||
import 'package:pweb/widgets/dialogs/payment_status_dialog.dart';
|
import 'package:pweb/widgets/dialogs/payment_status_dialog.dart';
|
||||||
|
|
||||||
|
|
||||||
@@ -8,7 +12,14 @@ Future<void> handleMultiplePayoutSend(
|
|||||||
BuildContext context,
|
BuildContext context,
|
||||||
MultiplePayoutsController controller,
|
MultiplePayoutsController controller,
|
||||||
) async {
|
) async {
|
||||||
final outcome = await controller.sendAndStorePayments();
|
final verificationController = context.read<PayoutVerificationController>();
|
||||||
|
final verified = await runPayoutVerification(
|
||||||
|
context: context,
|
||||||
|
controller: verificationController,
|
||||||
|
);
|
||||||
|
if (!verified) return;
|
||||||
|
|
||||||
|
final outcome = await controller.sendAndGetOutcome();
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,17 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:pshared/controllers/balance_mask/wallets.dart';
|
import 'package:pshared/controllers/balance_mask/wallets.dart';
|
||||||
|
|
||||||
import 'package:pweb/controllers/multiple_payouts.dart';
|
import 'package:pweb/controllers/multiple_payouts.dart';
|
||||||
|
import 'package:pweb/controllers/payout_verification.dart';
|
||||||
import 'package:pweb/pages/dashboard/payouts/multiple/actions.dart';
|
import 'package:pweb/pages/dashboard/payouts/multiple/actions.dart';
|
||||||
import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/header.dart';
|
import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/header.dart';
|
||||||
import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/summary.dart';
|
import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/summary.dart';
|
||||||
import 'package:pweb/pages/dashboard/payouts/multiple/widgets/quote_status.dart';
|
import 'package:pweb/pages/dashboard/payouts/multiple/widgets/quote_status.dart';
|
||||||
|
import 'package:pweb/pages/payment_methods/payment_page/send_button.dart';
|
||||||
import 'package:pweb/widgets/payment/source_wallet_selector.dart';
|
import 'package:pweb/widgets/payment/source_wallet_selector.dart';
|
||||||
|
import 'package:pweb/widgets/cooldown_hint.dart';
|
||||||
|
import 'package:pweb/models/control_state.dart';
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
|
||||||
class SourceQuotePanel extends StatelessWidget {
|
class SourceQuotePanel extends StatelessWidget {
|
||||||
@@ -25,7 +29,10 @@ class SourceQuotePanel extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final verificationController =
|
||||||
|
context.watch<PayoutVerificationController>();
|
||||||
|
final isCooldownActive = verificationController.isCooldownActive;
|
||||||
|
final canSend = controller.canSend && !isCooldownActive;
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
@@ -51,22 +58,24 @@ class SourceQuotePanel extends StatelessWidget {
|
|||||||
MultipleQuoteStatusCard(controller: controller),
|
MultipleQuoteStatusCard(controller: controller),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Center(
|
Center(
|
||||||
child: ElevatedButton(
|
child: Column(
|
||||||
onPressed: controller.canSend
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
? () => handleMultiplePayoutSend(context, controller)
|
children: [
|
||||||
: null,
|
SendButton(
|
||||||
style: ElevatedButton.styleFrom(
|
onPressed: () => handleMultiplePayoutSend(context, controller),
|
||||||
backgroundColor: theme.colorScheme.primary,
|
state: controller.isSending
|
||||||
foregroundColor: theme.colorScheme.onPrimary,
|
? ControlState.loading
|
||||||
padding: const EdgeInsets.symmetric(
|
: canSend
|
||||||
horizontal: 32,
|
? ControlState.enabled
|
||||||
vertical: 16,
|
: ControlState.disabled,
|
||||||
),
|
),
|
||||||
textStyle: theme.textTheme.titleSmall?.copyWith(
|
if (isCooldownActive) ...[
|
||||||
fontWeight: FontWeight.w600,
|
const SizedBox(height: 8),
|
||||||
),
|
CooldownHint(
|
||||||
),
|
seconds: verificationController.cooldownRemainingSeconds,
|
||||||
child: Text(l10n.send),
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import 'package:pweb/widgets/password/hint/short.dart';
|
|||||||
import 'package:pweb/widgets/password/password.dart';
|
import 'package:pweb/widgets/password/password.dart';
|
||||||
import 'package:pweb/widgets/username.dart';
|
import 'package:pweb/widgets/username.dart';
|
||||||
import 'package:pweb/widgets/vspacer.dart';
|
import 'package:pweb/widgets/vspacer.dart';
|
||||||
|
import 'package:pweb/controllers/email.dart';
|
||||||
import 'package:pweb/utils/error/snackbar.dart';
|
import 'package:pweb/utils/error/snackbar.dart';
|
||||||
import 'package:pweb/services/posthog.dart';
|
import 'package:pweb/services/posthog.dart';
|
||||||
|
|
||||||
@@ -31,12 +32,11 @@ class LoginForm extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LoginFormState extends State<LoginForm> {
|
class _LoginFormState extends State<LoginForm> {
|
||||||
final TextEditingController _usernameController = TextEditingController();
|
final EmailFieldController _emailController = EmailFieldController();
|
||||||
final TextEditingController _passwordController = TextEditingController();
|
final TextEditingController _passwordController = TextEditingController();
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
// ValueNotifiers for validation state
|
// ValueNotifiers for validation state
|
||||||
final ValueNotifier<bool> _isUsernameAcceptable = ValueNotifier<bool>(false);
|
|
||||||
final ValueNotifier<bool> _isPasswordAcceptable = ValueNotifier<bool>(false);
|
final ValueNotifier<bool> _isPasswordAcceptable = ValueNotifier<bool>(false);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -44,8 +44,7 @@ class _LoginFormState extends State<LoginForm> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
final initialEmail = widget.initialEmail?.trim();
|
final initialEmail = widget.initialEmail?.trim();
|
||||||
if (initialEmail != null && initialEmail.isNotEmpty) {
|
if (initialEmail != null && initialEmail.isNotEmpty) {
|
||||||
_usernameController.text = initialEmail;
|
_emailController.setText(initialEmail);
|
||||||
_isUsernameAcceptable.value = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +53,7 @@ class _LoginFormState extends State<LoginForm> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final outcome = await provider.login(
|
final outcome = await provider.login(
|
||||||
email: _usernameController.text,
|
email: _emailController.text,
|
||||||
password: _passwordController.text,
|
password: _passwordController.text,
|
||||||
locale: context.read<LocaleProvider>().locale.languageCode,
|
locale: context.read<LocaleProvider>().locale.languageCode,
|
||||||
);
|
);
|
||||||
@@ -74,9 +73,8 @@ class _LoginFormState extends State<LoginForm> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_usernameController.dispose();
|
_emailController.dispose();
|
||||||
_passwordController.dispose();
|
_passwordController.dispose();
|
||||||
_isUsernameAcceptable.dispose();
|
|
||||||
_isPasswordAcceptable.dispose();
|
_isPasswordAcceptable.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@@ -93,8 +91,7 @@ class _LoginFormState extends State<LoginForm> {
|
|||||||
const LoginHeader(),
|
const LoginHeader(),
|
||||||
const VSpacer(multiplier: 1.5),
|
const VSpacer(multiplier: 1.5),
|
||||||
UsernameField(
|
UsernameField(
|
||||||
controller: _usernameController,
|
controller: _emailController,
|
||||||
onValid: (isValid) => _isUsernameAcceptable.value = isValid,
|
|
||||||
),
|
),
|
||||||
VSpacer(),
|
VSpacer(),
|
||||||
defaulRulesPasswordField(
|
defaulRulesPasswordField(
|
||||||
@@ -105,7 +102,7 @@ class _LoginFormState extends State<LoginForm> {
|
|||||||
),
|
),
|
||||||
VSpacer(multiplier: 2.0),
|
VSpacer(multiplier: 2.0),
|
||||||
ValueListenableBuilder<bool>(
|
ValueListenableBuilder<bool>(
|
||||||
valueListenable: _isUsernameAcceptable,
|
valueListenable: _emailController.isValid,
|
||||||
builder: (context, isUsernameValid, child) => ValueListenableBuilder<bool>(
|
builder: (context, isUsernameValid, child) => ValueListenableBuilder<bool>(
|
||||||
valueListenable: _isPasswordAcceptable,
|
valueListenable: _isPasswordAcceptable,
|
||||||
builder: (context, isPasswordValid, child) => ButtonsRow(
|
builder: (context, isPasswordValid, child) => ButtonsRow(
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ import 'package:pweb/widgets/sidebar/destinations.dart';
|
|||||||
import 'package:pweb/services/posthog.dart';
|
import 'package:pweb/services/posthog.dart';
|
||||||
import 'package:pweb/widgets/dialogs/payment_status_dialog.dart';
|
import 'package:pweb/widgets/dialogs/payment_status_dialog.dart';
|
||||||
import 'package:pweb/controllers/payment_page.dart';
|
import 'package:pweb/controllers/payment_page.dart';
|
||||||
|
import 'package:pweb/controllers/payout_verification.dart';
|
||||||
|
import 'package:pweb/utils/payment/payout_verification_flow.dart';
|
||||||
|
import 'package:pweb/models/control_state.dart';
|
||||||
|
|
||||||
|
|
||||||
class PaymentPage extends StatefulWidget {
|
class PaymentPage extends StatefulWidget {
|
||||||
@@ -98,8 +101,15 @@ class _PaymentPageState extends State<PaymentPage> {
|
|||||||
final flowProvider = context.read<PaymentFlowProvider>();
|
final flowProvider = context.read<PaymentFlowProvider>();
|
||||||
final paymentProvider = context.read<PaymentProvider>();
|
final paymentProvider = context.read<PaymentProvider>();
|
||||||
final controller = context.read<PaymentPageController>();
|
final controller = context.read<PaymentPageController>();
|
||||||
|
final verificationController = context.read<PayoutVerificationController>();
|
||||||
if (paymentProvider.isLoading) return;
|
if (paymentProvider.isLoading) return;
|
||||||
|
|
||||||
|
final verified = await runPayoutVerification(
|
||||||
|
context: context,
|
||||||
|
controller: verificationController,
|
||||||
|
);
|
||||||
|
if (!verified || !mounted) return;
|
||||||
|
|
||||||
final isSuccess = await controller.sendPayment();
|
final isSuccess = await controller.sendPayment();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
@@ -117,11 +127,16 @@ class _PaymentPageState extends State<PaymentPage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final methodsProvider = context.watch<PaymentMethodsProvider>();
|
final methodsProvider = context.watch<PaymentMethodsProvider>();
|
||||||
final recipientProvider = context.watch<RecipientsProvider>();
|
final recipientProvider = context.watch<RecipientsProvider>();
|
||||||
|
final verificationController =
|
||||||
|
context.watch<PayoutVerificationController>();
|
||||||
final recipient = recipientProvider.currentObject;
|
final recipient = recipientProvider.currentObject;
|
||||||
final filteredRecipients = filterRecipients(
|
final filteredRecipients = filterRecipients(
|
||||||
recipients: recipientProvider.recipients,
|
recipients: recipientProvider.recipients,
|
||||||
query: _query,
|
query: _query,
|
||||||
);
|
);
|
||||||
|
final sendState = verificationController.isCooldownActive
|
||||||
|
? ControlState.disabled
|
||||||
|
: ControlState.enabled;
|
||||||
|
|
||||||
return PaymentPageBody(
|
return PaymentPageBody(
|
||||||
onBack: widget.onBack,
|
onBack: widget.onBack,
|
||||||
@@ -132,6 +147,9 @@ class _PaymentPageState extends State<PaymentPage> {
|
|||||||
searchQuery: _query,
|
searchQuery: _query,
|
||||||
filteredRecipients: filteredRecipients,
|
filteredRecipients: filteredRecipients,
|
||||||
methodsProvider: methodsProvider,
|
methodsProvider: methodsProvider,
|
||||||
|
sendState: sendState,
|
||||||
|
cooldownRemainingSeconds:
|
||||||
|
verificationController.cooldownRemainingSeconds,
|
||||||
onWalletSelected: context.read<WalletsController>().selectWallet,
|
onWalletSelected: context.read<WalletsController>().selectWallet,
|
||||||
searchController: _searchController,
|
searchController: _searchController,
|
||||||
searchFocusNode: _searchFocusNode,
|
searchFocusNode: _searchFocusNode,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:pshared/provider/recipient/provider.dart';
|
|||||||
import 'package:pweb/widgets/sidebar/destinations.dart';
|
import 'package:pweb/widgets/sidebar/destinations.dart';
|
||||||
import 'package:pweb/pages/payment_methods/widgets/state_view.dart';
|
import 'package:pweb/pages/payment_methods/widgets/state_view.dart';
|
||||||
import 'package:pweb/pages/payment_methods/payment_page/page.dart';
|
import 'package:pweb/pages/payment_methods/payment_page/page.dart';
|
||||||
|
import 'package:pweb/models/control_state.dart';
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -20,6 +21,8 @@ class PaymentPageBody extends StatelessWidget {
|
|||||||
final String searchQuery;
|
final String searchQuery;
|
||||||
final List<Recipient> filteredRecipients;
|
final List<Recipient> filteredRecipients;
|
||||||
final PaymentMethodsProvider methodsProvider;
|
final PaymentMethodsProvider methodsProvider;
|
||||||
|
final ControlState sendState;
|
||||||
|
final int cooldownRemainingSeconds;
|
||||||
final ValueChanged<Wallet> onWalletSelected;
|
final ValueChanged<Wallet> onWalletSelected;
|
||||||
final PayoutDestination fallbackDestination;
|
final PayoutDestination fallbackDestination;
|
||||||
final TextEditingController searchController;
|
final TextEditingController searchController;
|
||||||
@@ -38,6 +41,8 @@ class PaymentPageBody extends StatelessWidget {
|
|||||||
required this.searchQuery,
|
required this.searchQuery,
|
||||||
required this.filteredRecipients,
|
required this.filteredRecipients,
|
||||||
required this.methodsProvider,
|
required this.methodsProvider,
|
||||||
|
required this.sendState,
|
||||||
|
required this.cooldownRemainingSeconds,
|
||||||
required this.onWalletSelected,
|
required this.onWalletSelected,
|
||||||
required this.fallbackDestination,
|
required this.fallbackDestination,
|
||||||
required this.searchController,
|
required this.searchController,
|
||||||
@@ -71,6 +76,8 @@ class PaymentPageBody extends StatelessWidget {
|
|||||||
filteredRecipients: filteredRecipients,
|
filteredRecipients: filteredRecipients,
|
||||||
onWalletSelected: onWalletSelected,
|
onWalletSelected: onWalletSelected,
|
||||||
fallbackDestination: fallbackDestination,
|
fallbackDestination: fallbackDestination,
|
||||||
|
sendState: sendState,
|
||||||
|
cooldownRemainingSeconds: cooldownRemainingSeconds,
|
||||||
searchController: searchController,
|
searchController: searchController,
|
||||||
searchFocusNode: searchFocusNode,
|
searchFocusNode: searchFocusNode,
|
||||||
onSearchChanged: onSearchChanged,
|
onSearchChanged: onSearchChanged,
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ import 'package:pweb/pages/payment_methods/widgets/payment_info_section.dart';
|
|||||||
import 'package:pweb/pages/payment_methods/widgets/recipient_section.dart';
|
import 'package:pweb/pages/payment_methods/widgets/recipient_section.dart';
|
||||||
import 'package:pweb/pages/payment_methods/widgets/section_title.dart';
|
import 'package:pweb/pages/payment_methods/widgets/section_title.dart';
|
||||||
import 'package:pweb/utils/dimensions.dart';
|
import 'package:pweb/utils/dimensions.dart';
|
||||||
|
import 'package:pweb/widgets/cooldown_hint.dart';
|
||||||
import 'package:pweb/widgets/sidebar/destinations.dart';
|
import 'package:pweb/widgets/sidebar/destinations.dart';
|
||||||
|
import 'package:pweb/models/control_state.dart';
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -27,6 +29,8 @@ class PaymentPageContent extends StatelessWidget {
|
|||||||
final List<Recipient> filteredRecipients;
|
final List<Recipient> filteredRecipients;
|
||||||
final ValueChanged<Wallet> onWalletSelected;
|
final ValueChanged<Wallet> onWalletSelected;
|
||||||
final PayoutDestination fallbackDestination;
|
final PayoutDestination fallbackDestination;
|
||||||
|
final ControlState sendState;
|
||||||
|
final int cooldownRemainingSeconds;
|
||||||
final TextEditingController searchController;
|
final TextEditingController searchController;
|
||||||
final FocusNode searchFocusNode;
|
final FocusNode searchFocusNode;
|
||||||
final ValueChanged<String> onSearchChanged;
|
final ValueChanged<String> onSearchChanged;
|
||||||
@@ -44,6 +48,8 @@ class PaymentPageContent extends StatelessWidget {
|
|||||||
required this.filteredRecipients,
|
required this.filteredRecipients,
|
||||||
required this.onWalletSelected,
|
required this.onWalletSelected,
|
||||||
required this.fallbackDestination,
|
required this.fallbackDestination,
|
||||||
|
required this.sendState,
|
||||||
|
required this.cooldownRemainingSeconds,
|
||||||
required this.searchController,
|
required this.searchController,
|
||||||
required this.searchFocusNode,
|
required this.searchFocusNode,
|
||||||
required this.onSearchChanged,
|
required this.onSearchChanged,
|
||||||
@@ -104,7 +110,20 @@ class PaymentPageContent extends StatelessWidget {
|
|||||||
SizedBox(height: dimensions.paddingLarge),
|
SizedBox(height: dimensions.paddingLarge),
|
||||||
const PaymentFormWidget(),
|
const PaymentFormWidget(),
|
||||||
SizedBox(height: dimensions.paddingXXXLarge),
|
SizedBox(height: dimensions.paddingXXXLarge),
|
||||||
SendButton(onPressed: onSend),
|
Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SendButton(
|
||||||
|
onPressed: onSend,
|
||||||
|
state: sendState,
|
||||||
|
),
|
||||||
|
if (sendState == ControlState.disabled &&
|
||||||
|
cooldownRemainingSeconds > 0) ...[
|
||||||
|
SizedBox(height: dimensions.paddingSmall),
|
||||||
|
CooldownHint(seconds: cooldownRemainingSeconds),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
SizedBox(height: dimensions.paddingLarge),
|
SizedBox(height: dimensions.paddingLarge),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
64
frontend/pweb/lib/pages/payout_verification/page.dart
Normal file
64
frontend/pweb/lib/pages/payout_verification/page.dart
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/controllers/payout_verification.dart';
|
||||||
|
import 'package:pweb/pages/2fa/error_message.dart';
|
||||||
|
import 'package:pweb/pages/2fa/input.dart';
|
||||||
|
import 'package:pweb/pages/2fa/prompt.dart';
|
||||||
|
import 'package:pweb/pages/2fa/resend.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class PayoutVerificationPage extends StatelessWidget {
|
||||||
|
const PayoutVerificationPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Consumer<PayoutVerificationController>(
|
||||||
|
builder: (context, controller, child) {
|
||||||
|
if (controller.verificationSuccess) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(AppLocalizations.of(context)!.twoFactorTitle),
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
TwoFactorPromptText(email: controller.target),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
TwoFactorCodeInput(
|
||||||
|
onCompleted: controller.submitCode,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
if (controller.isSubmitting)
|
||||||
|
const Center(child: CircularProgressIndicator())
|
||||||
|
else
|
||||||
|
ResendCodeButton(
|
||||||
|
onPressed: controller.resendCode,
|
||||||
|
isCooldownActive: controller.isCooldownActive,
|
||||||
|
isResending: controller.isResending,
|
||||||
|
cooldownRemainingSeconds: controller.cooldownRemainingSeconds,
|
||||||
|
),
|
||||||
|
if (controller.hasError) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
ErrorMessage(
|
||||||
|
error: AppLocalizations.of(context)!.twoFactorError,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:pshared/models/payment/operation.dart';
|
import 'package:pshared/models/payment/operation.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/models/load_more_state.dart';
|
||||||
import 'package:pweb/pages/report/cards/items.dart';
|
import 'package:pweb/pages/report/cards/items.dart';
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
@@ -10,11 +11,15 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
|
|||||||
class OperationsCardsList extends StatelessWidget {
|
class OperationsCardsList extends StatelessWidget {
|
||||||
final List<OperationItem> operations;
|
final List<OperationItem> operations;
|
||||||
final ValueChanged<OperationItem>? onTap;
|
final ValueChanged<OperationItem>? onTap;
|
||||||
|
final LoadMoreState loadMoreState;
|
||||||
|
final VoidCallback? onLoadMore;
|
||||||
|
|
||||||
const OperationsCardsList({
|
const OperationsCardsList({
|
||||||
super.key,
|
super.key,
|
||||||
required this.operations,
|
required this.operations,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
|
this.loadMoreState = LoadMoreState.hidden,
|
||||||
|
this.onLoadMore,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -26,18 +31,42 @@ class OperationsCardsList extends StatelessWidget {
|
|||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (operations.isEmpty) {
|
||||||
|
return Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
loc.reportPaymentsEmpty,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final extraItems = loadMoreState == LoadMoreState.hidden ? 0 : 1;
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: operations.isEmpty
|
child: ListView.builder(
|
||||||
? Center(
|
itemCount: items.length + extraItems,
|
||||||
child: Text(
|
itemBuilder: (context, index) {
|
||||||
loc.reportPaymentsEmpty,
|
if (index < items.length) {
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
return items[index];
|
||||||
|
}
|
||||||
|
if (loadMoreState == LoadMoreState.loading) {
|
||||||
|
return const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 16),
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
child: Center(
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: onLoadMore,
|
||||||
|
child: Text(loc.loadMore),
|
||||||
),
|
),
|
||||||
)
|
|
||||||
: ListView.builder(
|
|
||||||
itemCount: items.length,
|
|
||||||
itemBuilder: (context, index) => items[index],
|
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,16 +4,14 @@ import 'package:go_router/go_router.dart';
|
|||||||
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'package:pshared/models/payment/payment.dart';
|
|
||||||
import 'package:pshared/models/payment/status.dart';
|
|
||||||
import 'package:pshared/provider/payment/payments.dart';
|
import 'package:pshared/provider/payment/payments.dart';
|
||||||
|
|
||||||
import 'package:pweb/app/router/payout_routes.dart';
|
import 'package:pweb/app/router/payout_routes.dart';
|
||||||
|
import 'package:pweb/controllers/payment_details.dart';
|
||||||
import 'package:pweb/pages/report/details/content.dart';
|
import 'package:pweb/pages/report/details/content.dart';
|
||||||
import 'package:pweb/pages/report/details/states/error.dart';
|
import 'package:pweb/pages/report/details/states/error.dart';
|
||||||
import 'package:pweb/pages/report/details/states/not_found.dart';
|
import 'package:pweb/pages/report/details/states/not_found.dart';
|
||||||
import 'package:pweb/utils/report/download_act.dart';
|
import 'package:pweb/utils/report/download_act.dart';
|
||||||
import 'package:pweb/utils/report/payment_mapper.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -26,39 +24,48 @@ class PaymentDetailsPage extends StatelessWidget {
|
|||||||
required this.paymentId,
|
required this.paymentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ChangeNotifierProxyProvider<PaymentsProvider, PaymentDetailsController>(
|
||||||
|
create: (_) => PaymentDetailsController(paymentId: paymentId),
|
||||||
|
update: (_, payments, controller) => controller!
|
||||||
|
..update(payments, paymentId),
|
||||||
|
child: const _PaymentDetailsView(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PaymentDetailsView extends StatelessWidget {
|
||||||
|
const _PaymentDetailsView();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Consumer<PaymentsProvider>(
|
child: Consumer<PaymentDetailsController>(
|
||||||
builder: (context, provider, child) {
|
builder: (context, controller, child) {
|
||||||
final loc = AppLocalizations.of(context)!;
|
final loc = AppLocalizations.of(context)!;
|
||||||
if (provider.isLoading) {
|
if (controller.isLoading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider.error != null) {
|
if (controller.error != null) {
|
||||||
return PaymentDetailsError(
|
return PaymentDetailsError(
|
||||||
message: provider.error?.toString() ?? loc.noErrorInformation,
|
message: controller.error?.toString() ?? loc.noErrorInformation,
|
||||||
onRetry: () => provider.refresh(),
|
onRetry: () => controller.refresh(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final payment = _findPayment(provider.payments, paymentId);
|
final payment = controller.payment;
|
||||||
if (payment == null) {
|
if (payment == null) {
|
||||||
return PaymentDetailsNotFound(onBack: () => _handleBack(context));
|
return PaymentDetailsNotFound(onBack: () => _handleBack(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
final status = statusFromPayment(payment);
|
|
||||||
final paymentRef = payment.paymentRef ?? '';
|
|
||||||
final canDownload = status == OperationStatus.success &&
|
|
||||||
paymentRef.trim().isNotEmpty;
|
|
||||||
|
|
||||||
return PaymentDetailsContent(
|
return PaymentDetailsContent(
|
||||||
payment: payment,
|
payment: payment,
|
||||||
onBack: () => _handleBack(context),
|
onBack: () => _handleBack(context),
|
||||||
onDownloadAct: canDownload
|
onDownloadAct: controller.canDownload
|
||||||
? () => downloadPaymentAct(context, paymentRef)
|
? () => downloadPaymentAct(context, payment.paymentRef ?? '')
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -66,16 +73,6 @@ class PaymentDetailsPage extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleBack(BuildContext context) {
|
void _handleBack(BuildContext context) {
|
||||||
final router = GoRouter.of(context);
|
final router = GoRouter.of(context);
|
||||||
if (router.canPop()) {
|
if (router.canPop()) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import 'package:pweb/controllers/report_operations.dart';
|
|||||||
import 'package:pweb/pages/report/table/filters.dart';
|
import 'package:pweb/pages/report/table/filters.dart';
|
||||||
import 'package:pweb/utils/report/payment_mapper.dart';
|
import 'package:pweb/utils/report/payment_mapper.dart';
|
||||||
import 'package:pweb/app/router/payout_routes.dart';
|
import 'package:pweb/app/router/payout_routes.dart';
|
||||||
|
import 'package:pweb/models/load_more_state.dart';
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -124,6 +125,11 @@ class _OperationHistoryView extends StatelessWidget {
|
|||||||
OperationsCardsList(
|
OperationsCardsList(
|
||||||
operations: filteredOperations,
|
operations: filteredOperations,
|
||||||
onTap: (operation) => _openPaymentDetails(context, operation),
|
onTap: (operation) => _openPaymentDetails(context, operation),
|
||||||
|
loadMoreState: controller.loadMoreState,
|
||||||
|
onLoadMore: controller.loadMoreState ==
|
||||||
|
LoadMoreState.available
|
||||||
|
? () => controller.loadMore()
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/controllers/email.dart';
|
||||||
|
|
||||||
|
|
||||||
class SignUpFormControllers {
|
class SignUpFormControllers {
|
||||||
final TextEditingController companyName = TextEditingController();
|
final TextEditingController companyName = TextEditingController();
|
||||||
final TextEditingController description = TextEditingController();
|
final TextEditingController description = TextEditingController();
|
||||||
final TextEditingController firstName = TextEditingController();
|
final TextEditingController firstName = TextEditingController();
|
||||||
final TextEditingController lastName = TextEditingController();
|
final TextEditingController lastName = TextEditingController();
|
||||||
final TextEditingController email = TextEditingController();
|
final EmailFieldController email = EmailFieldController();
|
||||||
final TextEditingController password = TextEditingController();
|
final TextEditingController password = TextEditingController();
|
||||||
final TextEditingController passwordConfirm = TextEditingController();
|
final TextEditingController passwordConfirm = TextEditingController();
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:email_validator/email_validator.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
|
||||||
|
|
||||||
//TODO: check with /widgets/username.dart
|
|
||||||
class EmailField extends StatelessWidget {
|
|
||||||
final TextEditingController controller;
|
|
||||||
|
|
||||||
const EmailField({super.key, required this.controller});
|
|
||||||
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return TextFormField(
|
|
||||||
controller: controller,
|
|
||||||
keyboardType: TextInputType.emailAddress,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: AppLocalizations.of(context)!.username,
|
|
||||||
hintText: AppLocalizations.of(context)!.usernameHint,
|
|
||||||
),
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || !EmailValidator.validate(value)) {
|
|
||||||
return AppLocalizations.of(context)!.usernameErrorInvalid;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:pweb/pages/signup/form/controllers.dart';
|
import 'package:pweb/pages/signup/form/controllers.dart';
|
||||||
import 'package:pweb/pages/signup/form/description.dart';
|
import 'package:pweb/pages/signup/form/description.dart';
|
||||||
import 'package:pweb/pages/signup/form/email.dart';
|
import 'package:pweb/widgets/username.dart';
|
||||||
import 'package:pweb/pages/signup/form/password_ui_controller.dart';
|
import 'package:pweb/pages/signup/form/password_ui_controller.dart';
|
||||||
import 'package:pweb/pages/signup/header.dart';
|
import 'package:pweb/pages/signup/header.dart';
|
||||||
import 'package:pweb/widgets/password/verify.dart';
|
import 'package:pweb/widgets/password/verify.dart';
|
||||||
@@ -45,7 +45,7 @@ class SignUpFormFields extends StatelessWidget {
|
|||||||
error: AppLocalizations.of(context)!.enterLastName,
|
error: AppLocalizations.of(context)!.enterLastName,
|
||||||
),
|
),
|
||||||
const VSpacer(),
|
const VSpacer(),
|
||||||
EmailField(controller: controllers.email),
|
UsernameField(controller: controllers.email),
|
||||||
const VSpacer(),
|
const VSpacer(),
|
||||||
SignUpPasswordUiController(controller: controllers.password),
|
SignUpPasswordUiController(controller: controllers.password),
|
||||||
const VSpacer(multiplier: 2.0),
|
const VSpacer(multiplier: 2.0),
|
||||||
|
|||||||
@@ -174,10 +174,6 @@ class MultiplePayoutsProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setError(Object error) {
|
|
||||||
_setErrorObject(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<Payment>> send() async {
|
Future<List<Payment>> send() async {
|
||||||
if (isBusy) return const <Payment>[];
|
if (isBusy) return const <Payment>[];
|
||||||
|
|
||||||
@@ -219,12 +215,6 @@ class MultiplePayoutsProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Payment>> sendAndStorePayments() async {
|
|
||||||
final result = await send();
|
|
||||||
_payments?.addPayments(result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeUploadedFile() {
|
void removeUploadedFile() {
|
||||||
if (isBusy) return;
|
if (isBusy) return;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/controllers/payout_verification.dart';
|
||||||
|
import 'package:pweb/pages/payout_verification/page.dart';
|
||||||
|
import 'package:pweb/utils/error/snackbar.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
|
Future<bool> runPayoutVerification({
|
||||||
|
required BuildContext context,
|
||||||
|
required PayoutVerificationController controller,
|
||||||
|
}) async {
|
||||||
|
final localizations = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
if (controller.isCooldownActive) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await controller.requestCode();
|
||||||
|
} catch (e) {
|
||||||
|
await notifyUserOfError(
|
||||||
|
context: context,
|
||||||
|
errorSituation: localizations.verificationFailed,
|
||||||
|
exception: e,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.mounted) return false;
|
||||||
|
|
||||||
|
final verified = await Navigator.of(context).push<bool>(
|
||||||
|
MaterialPageRoute(
|
||||||
|
fullscreenDialog: true,
|
||||||
|
builder: (_) => ChangeNotifierProvider.value(
|
||||||
|
value: controller,
|
||||||
|
child: const PayoutVerificationPage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (verified == true) {
|
||||||
|
controller.reset();
|
||||||
|
} else {
|
||||||
|
controller.resetStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
return verified == true;
|
||||||
|
}
|
||||||
25
frontend/pweb/lib/widgets/cooldown_hint.dart
Normal file
25
frontend/pweb/lib/widgets/cooldown_hint.dart
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/utils/cooldown_format.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class CooldownHint extends StatelessWidget {
|
||||||
|
final int seconds;
|
||||||
|
|
||||||
|
const CooldownHint({super.key, required this.seconds});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
return Text(
|
||||||
|
l10n.payoutCooldown(formatCooldownSeconds(seconds)),
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,43 +1,30 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/controllers/email.dart';
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
class UsernameField extends StatelessWidget {
|
class UsernameField extends StatelessWidget {
|
||||||
final TextEditingController controller;
|
final EmailFieldController controller;
|
||||||
final ValueChanged<bool>? onValid;
|
|
||||||
|
|
||||||
const UsernameField({
|
const UsernameField({
|
||||||
super.key,
|
super.key,
|
||||||
required this.controller,
|
required this.controller,
|
||||||
this.onValid,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
String? _reportResult(String? msg) {
|
|
||||||
onValid?.call(msg == null);
|
|
||||||
return msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => TextFormField(
|
Widget build(BuildContext context) => TextFormField(
|
||||||
controller: controller,
|
controller: controller.textController,
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: AppLocalizations.of(context)!.username,
|
labelText: AppLocalizations.of(context)!.username,
|
||||||
hintText: AppLocalizations.of(context)!.usernameHint,
|
hintText: AppLocalizations.of(context)!.usernameHint,
|
||||||
),
|
),
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
return _reportResult((value?.isNotEmpty ?? false) ? null : AppLocalizations.of(context)!.usernameErrorInvalid);
|
final locs = AppLocalizations.of(context)!;
|
||||||
// bool isValid = value != null && EmailValidator.validate(value);
|
return controller.validate(value, locs.usernameErrorInvalid);
|
||||||
// if (!isValid) {
|
|
||||||
// return _reportResult(AppLocalizations.of(context)!.usernameErrorInvalid);
|
|
||||||
// }
|
|
||||||
// final tld = value.split('.').last;
|
|
||||||
// isValid = tlds.contains(tld);
|
|
||||||
// if (!isValid) {
|
|
||||||
// return _reportResult(AppLocalizations.of(context)!.usernameUnknownTLD(tld));
|
|
||||||
// }
|
|
||||||
// return _reportResult(null);
|
|
||||||
},
|
},
|
||||||
onChanged: (value) => onValid?.call(value.isNotEmpty),
|
onChanged: controller.onChanged,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ dependencies:
|
|||||||
web: ^1.1.0
|
web: ^1.1.0
|
||||||
share_plus: ^12.0.1
|
share_plus: ^12.0.1
|
||||||
collection: ^1.18.0
|
collection: ^1.18.0
|
||||||
icann_tlds: ^1.0.0
|
|
||||||
flutter_timezone: ^5.0.1
|
flutter_timezone: ^5.0.1
|
||||||
json_annotation: ^4.10.0
|
json_annotation: ^4.10.0
|
||||||
go_router: ^17.0.0
|
go_router: ^17.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user