From 0a1e04d0d6ba3e1634cc7ab395f620bad4f240bc Mon Sep 17 00:00:00 2001 From: Arseni Date: Wed, 11 Feb 2026 15:26:11 +0300 Subject: [PATCH] small fixes --- .../interface/api/sresponse/login_pending.go | 6 +- .../lib/api/responses/login_pending.dart | 1 - .../lib/provider/email_verification.dart | 16 +---- .../pshared/lib/provider/organizations.dart | 53 ++++++++++++++ .../provider/payment/multiple/quotation.dart | 36 ---------- .../pweb/lib/app/router/payout_shell.dart | 9 +++ .../pweb/lib/controllers/multi_quotation.dart | 70 +++++++++++++++++++ 7 files changed, 138 insertions(+), 53 deletions(-) create mode 100644 frontend/pweb/lib/controllers/multi_quotation.dart diff --git a/api/server/interface/api/sresponse/login_pending.go b/api/server/interface/api/sresponse/login_pending.go index d77cebc1..1d045e39 100644 --- a/api/server/interface/api/sresponse/login_pending.go +++ b/api/server/interface/api/sresponse/login_pending.go @@ -11,10 +11,10 @@ import ( type pendingLoginResponse struct { Account accountResponse `json:"account"` 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( logger, &pendingLoginResponse{ @@ -23,7 +23,7 @@ func LoginPending(logger mlogger.Logger, account *model.Account, pendingToken *T authResponse: authResponse{}, }, PendingToken: *pendingToken, - Destination: destination, + Target: target, }, ) } diff --git a/frontend/pshared/lib/api/responses/login_pending.dart b/frontend/pshared/lib/api/responses/login_pending.dart index 1805a29a..99e08874 100644 --- a/frontend/pshared/lib/api/responses/login_pending.dart +++ b/frontend/pshared/lib/api/responses/login_pending.dart @@ -10,7 +10,6 @@ part 'login_pending.g.dart'; class PendingLoginResponse { final AccountResponse account; final TokenData pendingToken; - @JsonKey(name: 'destination') final String target; const PendingLoginResponse({ diff --git a/frontend/pshared/lib/provider/email_verification.dart b/frontend/pshared/lib/provider/email_verification.dart index e2fe47f3..9317c458 100644 --- a/frontend/pshared/lib/provider/email_verification.dart +++ b/frontend/pshared/lib/provider/email_verification.dart @@ -15,19 +15,9 @@ class EmailVerificationProvider extends ChangeNotifier { Exception? get error => _resource.error; ErrorResponse? get errorResponse => _resource.error is ErrorResponse ? _resource.error as ErrorResponse : null; - bool get canResendVerification { - final err = errorResponse; - if (err == null) return false; - switch (err.error) { - case 'not_found': - case 'token_expired': - case 'data_conflict': - case 'internal_error': - return true; - default: - return false; - } - } + int? get errorCode => errorResponse?.code; + bool get canResendVerification => + errorCode == 400 || errorCode == 410 || errorCode == 500; Future verify(String token) async { final trimmed = token.trim(); diff --git a/frontend/pshared/lib/provider/organizations.dart b/frontend/pshared/lib/provider/organizations.dart index 1c8bd816..ddaa45dc 100644 --- a/frontend/pshared/lib/provider/organizations.dart +++ b/frontend/pshared/lib/provider/organizations.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; +import 'package:share_plus/share_plus.dart'; + import 'package:pshared/config/constants.dart'; import 'package:pshared/models/organization/organization.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. await SecureStorageService.delete(Constants.currentOrgKey); } + + Future 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 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; + } + } } diff --git a/frontend/pshared/lib/provider/payment/multiple/quotation.dart b/frontend/pshared/lib/provider/payment/multiple/quotation.dart index e02347a7..53ed3880 100644 --- a/frontend/pshared/lib/provider/payment/multiple/quotation.dart +++ b/frontend/pshared/lib/provider/payment/multiple/quotation.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:flutter/foundation.dart'; import 'package:uuid/uuid.dart'; @@ -23,9 +21,6 @@ class MultiQuotationProvider extends ChangeNotifier { List? _lastIntents; bool _lastPreviewOnly = false; Map? _lastMetadata; - Timer? _autoRefreshTimer; - - static const Duration _autoRefreshLead = Duration(seconds: 5); Resource get resource => _quotation; PaymentQuotes? get quotation => _quotation.data; @@ -86,7 +81,6 @@ class MultiQuotationProvider extends ChangeNotifier { ? null : Map.from(metadata); - _cancelAutoRefresh(); _setResource(_quotation.copyWith(isLoading: true, error: null)); try { final response = await MultiplePaymentsService.getQuotation( @@ -102,7 +96,6 @@ class MultiQuotationProvider extends ChangeNotifier { _setResource( _quotation.copyWith(data: response, isLoading: false, error: null), ); - _scheduleAutoRefresh(); } catch (e) { _setResource( _quotation.copyWith( @@ -131,7 +124,6 @@ class MultiQuotationProvider extends ChangeNotifier { _lastIntents = null; _lastPreviewOnly = false; _lastMetadata = null; - _cancelAutoRefresh(); _quotation = Resource(data: null); notifyListeners(); } @@ -141,36 +133,8 @@ class MultiQuotationProvider extends ChangeNotifier { 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 _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 void dispose() { - _cancelAutoRefresh(); super.dispose(); } } diff --git a/frontend/pweb/lib/app/router/payout_shell.dart b/frontend/pweb/lib/app/router/payout_shell.dart index cbaad12f..282a6440 100644 --- a/frontend/pweb/lib/app/router/payout_shell.dart +++ b/frontend/pweb/lib/app/router/payout_shell.dart @@ -24,6 +24,7 @@ import 'package:pweb/app/router/payout_routes.dart'; import 'package:pweb/controllers/multiple_payouts.dart'; import 'package:pweb/controllers/payment_page.dart'; import 'package:pweb/providers/multiple_payouts.dart'; +import 'package:pweb/controllers/multi_quotation.dart'; import 'package:pweb/providers/quotation/quotation.dart'; import 'package:pshared/models/payment/wallet.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'; + RouteBase payoutShellRoute() => ShellRoute( builder: (context, state, child) => MultiProvider( providers: [ @@ -138,6 +140,13 @@ RouteBase payoutShellRoute() => ShellRoute( update: (context, organization, provider) => provider!..update(organization), ), + ChangeNotifierProxyProvider< + MultiQuotationProvider, + MultiQuotationController + >( + create: (_) => MultiQuotationController(), + update: (_, quotation, controller) => controller!..update(quotation), + ), ChangeNotifierProxyProvider2< OrganizationsProvider, MultiQuotationProvider, diff --git a/frontend/pweb/lib/controllers/multi_quotation.dart b/frontend/pweb/lib/controllers/multi_quotation.dart new file mode 100644 index 00000000..ec37b242 --- /dev/null +++ b/frontend/pweb/lib/controllers/multi_quotation.dart @@ -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 _refreshQuotation() async { + await _quotation?.refreshQuotation(); + } + + @override + void dispose() { + _quotation?.removeListener(_handleQuotationChanged); + _autoRefreshController.dispose(); + super.dispose(); + } +}