multiple payout page and small fixes #464
@@ -11,10 +11,10 @@ import (
|
|||||||
type pendingLoginResponse struct {
|
type pendingLoginResponse struct {
|
||||||
Account accountResponse `json:"account"`
|
Account accountResponse `json:"account"`
|
||||||
PendingToken TokenData `json:"pendingToken"`
|
PendingToken TokenData `json:"pendingToken"`
|
||||||
Destination string `json:"destination"`
|
Target string `json:"target"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoginPending(logger mlogger.Logger, account *model.Account, pendingToken *TokenData, destination string) http.HandlerFunc {
|
func LoginPending(logger mlogger.Logger, account *model.Account, pendingToken *TokenData, target string) http.HandlerFunc {
|
||||||
return response.Accepted(
|
return response.Accepted(
|
||||||
logger,
|
logger,
|
||||||
&pendingLoginResponse{
|
&pendingLoginResponse{
|
||||||
@@ -23,7 +23,7 @@ func LoginPending(logger mlogger.Logger, account *model.Account, pendingToken *T
|
|||||||
authResponse: authResponse{},
|
authResponse: authResponse{},
|
||||||
},
|
},
|
||||||
PendingToken: *pendingToken,
|
PendingToken: *pendingToken,
|
||||||
Destination: destination,
|
Target: target,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ part 'login_pending.g.dart';
|
|||||||
class PendingLoginResponse {
|
class PendingLoginResponse {
|
||||||
final AccountResponse account;
|
final AccountResponse account;
|
||||||
final TokenData pendingToken;
|
final TokenData pendingToken;
|
||||||
@JsonKey(name: 'destination')
|
|
||||||
final String target;
|
final String target;
|
||||||
|
|
|||||||
|
|
||||||
const PendingLoginResponse({
|
const PendingLoginResponse({
|
||||||
|
|||||||
@@ -15,19 +15,9 @@ class EmailVerificationProvider extends ChangeNotifier {
|
|||||||
Exception? get error => _resource.error;
|
Exception? get error => _resource.error;
|
||||||
ErrorResponse? get errorResponse =>
|
ErrorResponse? get errorResponse =>
|
||||||
_resource.error is ErrorResponse ? _resource.error as ErrorResponse : null;
|
_resource.error is ErrorResponse ? _resource.error as ErrorResponse : null;
|
||||||
|
tech
commented
есть же хелпер toException? есть же хелпер toException?
protuberanets
commented
toException тут не помогает, он же приводит Object к Exception, но не к ErrorResponse toException тут не помогает, он же приводит Object к Exception, но не к ErrorResponse
|
|||||||
bool get canResendVerification {
|
int? get errorCode => errorResponse?.code;
|
||||||
final err = errorResponse;
|
bool get canResendVerification =>
|
||||||
if (err == null) return false;
|
errorCode == 400 || errorCode == 410 || errorCode == 500;
|
||||||
switch (err.error) {
|
|
||||||
case 'not_found':
|
|
||||||
case 'token_expired':
|
|
||||||
case 'data_conflict':
|
|
||||||
case 'internal_error':
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> verify(String token) async {
|
Future<void> verify(String token) async {
|
||||||
final trimmed = token.trim();
|
final trimmed = token.trim();
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
import 'package:pshared/config/constants.dart';
|
import 'package:pshared/config/constants.dart';
|
||||||
import 'package:pshared/models/organization/organization.dart';
|
import 'package:pshared/models/organization/organization.dart';
|
||||||
import 'package:pshared/provider/resource.dart';
|
import 'package:pshared/provider/resource.dart';
|
||||||
@@ -88,4 +90,55 @@ class OrganizationsProvider extends ChangeNotifier {
|
|||||||
// Best-effort cleanup of stored selection to avoid using stale org on next login.
|
// Best-effort cleanup of stored selection to avoid using stale org on next login.
|
||||||
await SecureStorageService.delete(Constants.currentOrgKey);
|
await SecureStorageService.delete(Constants.currentOrgKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Organization> uploadLogo(XFile logoFile) async {
|
||||||
|
if (!isOrganizationSet) {
|
||||||
|
throw StateError('Organization is not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
_setResource(_resource.copyWith(isLoading: true, error: null));
|
||||||
|
try {
|
||||||
|
final updated = await OrganizationService.uploadLogoAndUpdate(current, logoFile);
|
||||||
|
final updatedList = organizations
|
||||||
|
.map((org) => org.id == updated.id ? updated : org)
|
||||||
|
.toList(growable: false);
|
||||||
|
_setResource(Resource(data: updatedList, isLoading: false));
|
||||||
|
_currentOrg = updated.id;
|
||||||
|
return updated;
|
||||||
|
} catch (e) {
|
||||||
|
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Organization> updateCurrent({
|
||||||
|
String? name,
|
||||||
|
String? description,
|
||||||
|
String? timeZone,
|
||||||
|
String? logoUrl,
|
||||||
|
}) async {
|
||||||
|
if (!isOrganizationSet) {
|
||||||
|
throw StateError('Organization is not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
_setResource(_resource.copyWith(isLoading: true, error: null));
|
||||||
|
try {
|
||||||
|
final updated = await OrganizationService.updateSettings(
|
||||||
|
current,
|
||||||
|
name: name,
|
||||||
|
description: description,
|
||||||
|
timeZone: timeZone,
|
||||||
|
logoUrl: logoUrl,
|
||||||
|
);
|
||||||
|
final updatedList = organizations
|
||||||
|
.map((org) => org.id == updated.id ? updated : org)
|
||||||
|
.toList(growable: false);
|
||||||
|
_setResource(Resource(data: updatedList, isLoading: false));
|
||||||
|
_currentOrg = updated.id;
|
||||||
|
return updated;
|
||||||
|
} catch (e) {
|
||||||
|
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
@@ -23,9 +21,6 @@ class MultiQuotationProvider extends ChangeNotifier {
|
|||||||
List<PaymentIntent>? _lastIntents;
|
List<PaymentIntent>? _lastIntents;
|
||||||
bool _lastPreviewOnly = false;
|
bool _lastPreviewOnly = false;
|
||||||
Map<String, String>? _lastMetadata;
|
Map<String, String>? _lastMetadata;
|
||||||
Timer? _autoRefreshTimer;
|
|
||||||
|
|
||||||
static const Duration _autoRefreshLead = Duration(seconds: 5);
|
|
||||||
|
|
||||||
Resource<PaymentQuotes> get resource => _quotation;
|
Resource<PaymentQuotes> get resource => _quotation;
|
||||||
PaymentQuotes? get quotation => _quotation.data;
|
PaymentQuotes? get quotation => _quotation.data;
|
||||||
@@ -86,7 +81,6 @@ class MultiQuotationProvider extends ChangeNotifier {
|
|||||||
? null
|
? null
|
||||||
: Map<String, String>.from(metadata);
|
: Map<String, String>.from(metadata);
|
||||||
|
|
||||||
_cancelAutoRefresh();
|
|
||||||
_setResource(_quotation.copyWith(isLoading: true, error: null));
|
_setResource(_quotation.copyWith(isLoading: true, error: null));
|
||||||
try {
|
try {
|
||||||
final response = await MultiplePaymentsService.getQuotation(
|
final response = await MultiplePaymentsService.getQuotation(
|
||||||
@@ -102,7 +96,6 @@ class MultiQuotationProvider extends ChangeNotifier {
|
|||||||
_setResource(
|
_setResource(
|
||||||
_quotation.copyWith(data: response, isLoading: false, error: null),
|
_quotation.copyWith(data: response, isLoading: false, error: null),
|
||||||
);
|
);
|
||||||
_scheduleAutoRefresh();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_setResource(
|
_setResource(
|
||||||
_quotation.copyWith(
|
_quotation.copyWith(
|
||||||
@@ -131,7 +124,6 @@ class MultiQuotationProvider extends ChangeNotifier {
|
|||||||
_lastIntents = null;
|
_lastIntents = null;
|
||||||
_lastPreviewOnly = false;
|
_lastPreviewOnly = false;
|
||||||
_lastMetadata = null;
|
_lastMetadata = null;
|
||||||
_cancelAutoRefresh();
|
|
||||||
_quotation = Resource(data: null);
|
_quotation = Resource(data: null);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -141,36 +133,8 @@ class MultiQuotationProvider extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _scheduleAutoRefresh() {
|
|
||||||
_autoRefreshTimer?.cancel();
|
|
||||||
final expiresAt = quoteExpiresAt;
|
|
||||||
if (expiresAt == null) return;
|
|
||||||
|
|
||||||
final now = DateTime.now().toUtc();
|
|
||||||
var delay = expiresAt.difference(now) - _autoRefreshLead;
|
|
||||||
if (delay.isNegative) delay = Duration.zero;
|
|
||||||
_autoRefreshTimer = Timer(delay, _triggerAutoRefresh);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _triggerAutoRefresh() async {
|
|
||||||
if (_quotation.isLoading) return;
|
|
||||||
final intents = _lastIntents;
|
|
||||||
if (intents == null || intents.isEmpty) return;
|
|
||||||
await quotePayments(
|
|
||||||
intents,
|
|
||||||
previewOnly: _lastPreviewOnly,
|
|
||||||
metadata: _lastMetadata,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _cancelAutoRefresh() {
|
|
||||||
_autoRefreshTimer?.cancel();
|
|
||||||
_autoRefreshTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_cancelAutoRefresh();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ 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/providers/multiple_payouts.dart';
|
import 'package:pweb/providers/multiple_payouts.dart';
|
||||||
|
import 'package:pweb/controllers/multi_quotation.dart';
|
||||||
import 'package:pweb/providers/quotation/quotation.dart';
|
import 'package:pweb/providers/quotation/quotation.dart';
|
||||||
import 'package:pshared/models/payment/wallet.dart';
|
import 'package:pshared/models/payment/wallet.dart';
|
||||||
import 'package:pweb/pages/address_book/form/page.dart';
|
import 'package:pweb/pages/address_book/form/page.dart';
|
||||||
@@ -44,6 +45,7 @@ import 'package:pweb/services/payments/csv_input.dart';
|
|||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
RouteBase payoutShellRoute() => ShellRoute(
|
RouteBase payoutShellRoute() => ShellRoute(
|
||||||
builder: (context, state, child) => MultiProvider(
|
builder: (context, state, child) => MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
@@ -138,6 +140,13 @@ RouteBase payoutShellRoute() => ShellRoute(
|
|||||||
update: (context, organization, provider) =>
|
update: (context, organization, provider) =>
|
||||||
provider!..update(organization),
|
provider!..update(organization),
|
||||||
),
|
),
|
||||||
|
ChangeNotifierProxyProvider<
|
||||||
|
MultiQuotationProvider,
|
||||||
|
MultiQuotationController
|
||||||
|
>(
|
||||||
|
create: (_) => MultiQuotationController(),
|
||||||
|
update: (_, quotation, controller) => controller!..update(quotation),
|
||||||
|
),
|
||||||
ChangeNotifierProxyProvider2<
|
ChangeNotifierProxyProvider2<
|
||||||
OrganizationsProvider,
|
OrganizationsProvider,
|
||||||
MultiQuotationProvider,
|
MultiQuotationProvider,
|
||||||
|
|||||||
70
frontend/pweb/lib/controllers/multi_quotation.dart
Normal file
70
frontend/pweb/lib/controllers/multi_quotation.dart
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/provider/payment/multiple/quotation.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/providers/quotation/auto_refresh.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class MultiQuotationController extends ChangeNotifier {
|
||||||
|
static const Duration _autoRefreshLead = Duration(seconds: 5);
|
||||||
|
|
||||||
|
MultiQuotationProvider? _quotation;
|
||||||
|
final QuotationAutoRefreshController _autoRefreshController =
|
||||||
|
QuotationAutoRefreshController();
|
||||||
|
|
||||||
|
void update(MultiQuotationProvider quotation) {
|
||||||
|
if (identical(_quotation, quotation)) return;
|
||||||
|
_quotation?.removeListener(_handleQuotationChanged);
|
||||||
|
_quotation = quotation;
|
||||||
|
_quotation?.addListener(_handleQuotationChanged);
|
||||||
|
_handleQuotationChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isLoading => _quotation?.isLoading ?? false;
|
||||||
|
Exception? get error => _quotation?.error;
|
||||||
|
bool get canRefresh => _quotation?.canRefresh ?? false;
|
||||||
|
bool get isReady => _quotation?.isReady ?? false;
|
||||||
|
|
||||||
|
DateTime? get quoteExpiresAt => _quotation?.quoteExpiresAt;
|
||||||
|
|
||||||
|
void refreshQuotation() {
|
||||||
|
_quotation?.refreshQuotation();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleQuotationChanged() {
|
||||||
|
_syncAutoRefresh();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _syncAutoRefresh() {
|
||||||
|
final quotation = _quotation;
|
||||||
|
if (quotation == null) {
|
||||||
|
_autoRefreshController.reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final expiresAt = quoteExpiresAt;
|
||||||
|
final scheduledAt = expiresAt == null
|
||||||
|
? null
|
||||||
|
: expiresAt.subtract(_autoRefreshLead);
|
||||||
|
|
||||||
|
_autoRefreshController.setEnabled(true);
|
||||||
|
_autoRefreshController.sync(
|
||||||
|
isLoading: quotation.isLoading,
|
||||||
|
canRefresh: quotation.canRefresh,
|
||||||
|
expiresAt: scheduledAt,
|
||||||
|
onRefresh: _refreshQuotation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _refreshQuotation() async {
|
||||||
|
await _quotation?.refreshQuotation();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_quotation?.removeListener(_handleQuotationChanged);
|
||||||
|
_autoRefreshController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user
Поправь, пожалуйста, здесь имя переменной и атавизм в бэке на Target тоже, чтобы не расходилось.