redesigned payment page + a lot of fixes
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:pshared/models/payment/wallet.dart';
|
||||
import 'package:pshared/provider/payment/wallets.dart';
|
||||
|
||||
@@ -15,6 +13,9 @@ class WalletsController with ChangeNotifier {
|
||||
final Set<String> _maskedBalanceWalletRefs = <String>{};
|
||||
Set<String> _knownWalletRefs = <String>{};
|
||||
|
||||
List<Wallet> _walletsList = <Wallet>[];
|
||||
Map<String, Wallet> _walletsById = <String, Wallet>{};
|
||||
|
||||
String? _selectedWalletRef;
|
||||
|
||||
bool get isLoading => _wallets.isLoading;
|
||||
@@ -33,8 +34,11 @@ class WalletsController with ChangeNotifier {
|
||||
_selectedWalletRef = null;
|
||||
}
|
||||
|
||||
_walletsById = _uniqueWalletsById(wallets.wallets);
|
||||
_walletsList = _walletsById.values.toList(growable: false);
|
||||
|
||||
// Remove ids that no longer exist
|
||||
final ids = wallets.wallets.map((w) => w.id).toSet();
|
||||
final ids = _walletsById.keys.toSet();
|
||||
_maskedBalanceWalletRefs.removeWhere((id) => !ids.contains(id));
|
||||
|
||||
final newIds = ids.difference(_knownWalletRefs);
|
||||
@@ -45,13 +49,13 @@ class WalletsController with ChangeNotifier {
|
||||
|
||||
_selectedWalletRef = _resolveSelectedId(
|
||||
currentRef: _selectedWalletRef,
|
||||
wallets: wallets.wallets,
|
||||
wallets: _walletsList,
|
||||
);
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
List<Wallet> get wallets => _wallets.wallets;
|
||||
List<Wallet> get wallets => _walletsList;
|
||||
|
||||
bool isBalanceMasked(String walletRef) => _maskedBalanceWalletRefs.contains(walletRef);
|
||||
bool isBalanceVisible(String walletRef) => !isBalanceMasked(walletRef);
|
||||
@@ -62,7 +66,7 @@ class WalletsController with ChangeNotifier {
|
||||
Wallet? get selectedWallet {
|
||||
final id = _selectedWalletRef;
|
||||
if (id == null) return null;
|
||||
return wallets.firstWhereOrNull((w) => w.id == id);
|
||||
return _walletsById[id];
|
||||
}
|
||||
|
||||
String? get selectedWalletRef => _selectedWalletRef;
|
||||
@@ -103,4 +107,12 @@ class WalletsController with ChangeNotifier {
|
||||
// Fallback to the first wallet
|
||||
return wallets.first.id;
|
||||
}
|
||||
|
||||
Map<String, Wallet> _uniqueWalletsById(List<Wallet> wallets) {
|
||||
final result = <String, Wallet>{};
|
||||
for (final wallet in wallets) {
|
||||
result.putIfAbsent(wallet.id, () => wallet);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,20 @@ import 'package:pshared/models/permissions/bound.dart';
|
||||
import 'package:pshared/models/storable.dart';
|
||||
|
||||
|
||||
extension PaymentMethodDataJsonMapper on PaymentMethodData {
|
||||
Map<String, dynamic> toJsonMap() => switch (this) {
|
||||
CardPaymentMethod card => card.toDTO().toJson(),
|
||||
CardTokenPaymentMethod cardToken => cardToken.toDTO().toJson(),
|
||||
IbanPaymentMethod iban => iban.toDTO().toJson(),
|
||||
RussianBankAccountPaymentMethod bankAccount => bankAccount.toDTO().toJson(),
|
||||
WalletPaymentMethod wallet => wallet.toDTO().toJson(),
|
||||
CryptoAddressPaymentMethod crypto => crypto.toDTO().toJson(),
|
||||
LedgerPaymentMethod ledger => ledger.toDTO().toJson(),
|
||||
ManagedWalletPaymentMethod managedWallet => managedWallet.toDTO().toJson(),
|
||||
_ => throw UnsupportedError('Unsupported payment method data: $runtimeType'),
|
||||
};
|
||||
}
|
||||
|
||||
extension PaymentMethodMapper on PaymentMethod {
|
||||
PaymentMethodDTO toDTO() => PaymentMethodDTO(
|
||||
id: storable.id,
|
||||
@@ -49,17 +63,7 @@ extension PaymentMethodMapper on PaymentMethod {
|
||||
isMain: isMain,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _dataToJson(PaymentMethodData data) => switch (data) {
|
||||
CardPaymentMethod card => card.toDTO().toJson(),
|
||||
CardTokenPaymentMethod cardToken => cardToken.toDTO().toJson(),
|
||||
IbanPaymentMethod iban => iban.toDTO().toJson(),
|
||||
RussianBankAccountPaymentMethod bankAccount => bankAccount.toDTO().toJson(),
|
||||
WalletPaymentMethod wallet => wallet.toDTO().toJson(),
|
||||
CryptoAddressPaymentMethod crypto => crypto.toDTO().toJson(),
|
||||
LedgerPaymentMethod ledger => ledger.toDTO().toJson(),
|
||||
ManagedWalletPaymentMethod managedWallet => managedWallet.toDTO().toJson(),
|
||||
_ => throw UnsupportedError('Unsupported payment method data: ${data.runtimeType}'),
|
||||
};
|
||||
Map<String, dynamic> _dataToJson(PaymentMethodData data) => data.toJsonMap();
|
||||
}
|
||||
|
||||
extension PaymentMethodDTOMapper on PaymentMethodDTO {
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import 'dart:async';
|
||||
|
||||
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/models/auth/state.dart';
|
||||
import 'package:pshared/provider/account.dart';
|
||||
import 'package:pshared/provider/resource.dart';
|
||||
import 'package:pshared/service/organization.dart';
|
||||
import 'package:pshared/service/secure_storage.dart';
|
||||
@@ -16,6 +22,7 @@ class OrganizationsProvider extends ChangeNotifier {
|
||||
|
||||
List<Organization> get organizations => _resource.data ?? [];
|
||||
String? _currentOrg;
|
||||
AccountProvider? _accountProvider;
|
||||
|
||||
Organization get current => isOrganizationSet ? _current! : throw StateError('Organization is not set');
|
||||
|
||||
@@ -26,6 +33,13 @@ class OrganizationsProvider extends ChangeNotifier {
|
||||
bool get isLoading => _resource.isLoading;
|
||||
Object? get error => _resource.error;
|
||||
|
||||
void updateAccount(AccountProvider accountProvider) {
|
||||
if (!identical(_accountProvider, accountProvider)) {
|
||||
_accountProvider = accountProvider;
|
||||
}
|
||||
_triggerLoadIfNeeded(accountProvider);
|
||||
}
|
||||
|
||||
void _setResource(Resource<List<Organization>> newResource) {
|
||||
_resource = newResource;
|
||||
notifyListeners();
|
||||
@@ -52,6 +66,14 @@ class OrganizationsProvider extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void _triggerLoadIfNeeded(AccountProvider accountProvider) {
|
||||
if (accountProvider.authState != AuthState.ready) return;
|
||||
if (accountProvider.account == null) return;
|
||||
if (isLoading || isOrganizationSet) return;
|
||||
if (error != null) return;
|
||||
unawaited(load());
|
||||
}
|
||||
|
||||
Future<Organization> loadByInvitation(String invitationRef) async {
|
||||
_setResource(_resource.copyWith(isLoading: true, error: null));
|
||||
try {
|
||||
@@ -88,4 +110,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<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
78
frontend/pshared/lib/provider/payment/auto_refresh.dart
Normal file
78
frontend/pshared/lib/provider/payment/auto_refresh.dart
Normal file
@@ -0,0 +1,78 @@
|
||||
import 'dart:async';
|
||||
|
||||
|
||||
class AutoRefreshScheduler {
|
||||
bool _enabled = true;
|
||||
Timer? _timer;
|
||||
DateTime? _scheduledAt;
|
||||
DateTime? _triggeredAt;
|
||||
|
||||
void setEnabled(bool enabled) {
|
||||
if (_enabled == enabled) return;
|
||||
_enabled = enabled;
|
||||
if (!enabled) {
|
||||
_clear();
|
||||
}
|
||||
}
|
||||
|
||||
void sync({
|
||||
required bool isLoading,
|
||||
required bool canRefresh,
|
||||
required DateTime? scheduledAt,
|
||||
required Future<void> Function() onRefresh,
|
||||
}) {
|
||||
if (!_enabled || isLoading || !canRefresh) {
|
||||
_clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (scheduledAt == null) {
|
||||
_clear();
|
||||
return;
|
||||
}
|
||||
|
||||
final delay = scheduledAt.difference(DateTime.now().toUtc());
|
||||
if (delay <= Duration.zero) {
|
||||
if (_triggeredAt != null && _triggeredAt!.isAtSameMomentAs(scheduledAt)) {
|
||||
return;
|
||||
}
|
||||
_triggeredAt = scheduledAt;
|
||||
_clearTimer();
|
||||
onRefresh();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_scheduledAt != null &&
|
||||
_scheduledAt!.isAtSameMomentAs(scheduledAt) &&
|
||||
_timer?.isActive == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
_triggeredAt = null;
|
||||
_clearTimer();
|
||||
_scheduledAt = scheduledAt;
|
||||
_timer = Timer(delay, () {
|
||||
onRefresh();
|
||||
});
|
||||
}
|
||||
|
||||
void reset() {
|
||||
_enabled = false;
|
||||
_clear();
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_clear();
|
||||
}
|
||||
|
||||
void _clear() {
|
||||
_scheduledAt = null;
|
||||
_triggeredAt = null;
|
||||
_clearTimer();
|
||||
}
|
||||
|
||||
void _clearTimer() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
}
|
||||
@@ -7,16 +7,20 @@ import 'package:pshared/data/mapper/payment/intent/payment.dart';
|
||||
import 'package:pshared/models/payment/intent.dart';
|
||||
import 'package:pshared/models/payment/quote/quotes.dart';
|
||||
import 'package:pshared/provider/organizations.dart';
|
||||
import 'package:pshared/provider/payment/auto_refresh.dart';
|
||||
import 'package:pshared/provider/resource.dart';
|
||||
import 'package:pshared/service/payment/multiple.dart';
|
||||
import 'package:pshared/utils/exception.dart';
|
||||
|
||||
|
||||
class MultiQuotationProvider extends ChangeNotifier {
|
||||
static const Duration _autoRefreshLead = Duration(seconds: 5);
|
||||
|
||||
OrganizationsProvider? _organizations;
|
||||
String? _loadedOrganizationRef;
|
||||
|
||||
Resource<PaymentQuotes> _quotation = Resource(data: null);
|
||||
final AutoRefreshScheduler _autoRefresh = AutoRefreshScheduler();
|
||||
|
||||
List<PaymentIntent>? _lastIntents;
|
||||
bool _lastPreviewOnly = false;
|
||||
@@ -125,12 +129,32 @@ class MultiQuotationProvider extends ChangeNotifier {
|
||||
_lastPreviewOnly = false;
|
||||
_lastMetadata = null;
|
||||
_quotation = Resource(data: null);
|
||||
_syncAutoRefresh();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _setResource(Resource<PaymentQuotes> quotation) {
|
||||
_quotation = quotation;
|
||||
_syncAutoRefresh();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _syncAutoRefresh() {
|
||||
final scheduledAt = quoteExpiresAt?.subtract(_autoRefreshLead);
|
||||
_autoRefresh.setEnabled(true);
|
||||
_autoRefresh.sync(
|
||||
isLoading: isLoading,
|
||||
canRefresh: canRefresh,
|
||||
scheduledAt: scheduledAt,
|
||||
onRefresh: () async {
|
||||
await refreshQuotation();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_autoRefresh.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import 'package:pshared/utils/exception.dart';
|
||||
|
||||
|
||||
class PaymentsProvider with ChangeNotifier {
|
||||
static const Duration _pendingRefreshInterval = Duration(seconds: 10);
|
||||
|
||||
OrganizationsProvider? _organizations;
|
||||
String? _loadedOrganizationRef;
|
||||
|
||||
@@ -17,15 +19,14 @@ class PaymentsProvider with ChangeNotifier {
|
||||
bool _isLoaded = false;
|
||||
bool _isLoadingMore = false;
|
||||
String? _nextCursor;
|
||||
Timer? _autoRefreshTimer;
|
||||
int _autoRefreshRefs = 0;
|
||||
Duration _autoRefreshInterval = const Duration(seconds: 15);
|
||||
int? _limit;
|
||||
String? _sourceRef;
|
||||
String? _destinationRef;
|
||||
List<String>? _states;
|
||||
|
||||
int _opSeq = 0;
|
||||
Timer? _pendingRefreshTimer;
|
||||
bool _isPendingRefreshInFlight = false;
|
||||
|
||||
Resource<List<Payment>> get resource => _resource;
|
||||
List<Payment> get payments => _resource.data ?? [];
|
||||
@@ -37,23 +38,6 @@ class PaymentsProvider with ChangeNotifier {
|
||||
String? get nextCursor => _nextCursor;
|
||||
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) {
|
||||
_organizations = organizations;
|
||||
if (!organizations.isOrganizationSet) {
|
||||
@@ -61,10 +45,6 @@ class PaymentsProvider with ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_autoRefreshRefs > 0) {
|
||||
_ensureAutoRefreshTimer();
|
||||
}
|
||||
|
||||
final orgRef = organizations.current.id;
|
||||
if (_loadedOrganizationRef != orgRef) {
|
||||
_loadedOrganizationRef = orgRef;
|
||||
@@ -104,6 +84,30 @@ class PaymentsProvider with ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
void mergePayments(List<Payment> incoming) {
|
||||
if (incoming.isEmpty) return;
|
||||
final existing = List<Payment>.from(_resource.data ?? const []);
|
||||
final combined = <Payment>[
|
||||
...incoming,
|
||||
...existing,
|
||||
];
|
||||
final seen = <String>{};
|
||||
final merged = <Payment>[];
|
||||
|
||||
for (final payment in combined) {
|
||||
final key = _paymentKey(payment);
|
||||
if (key == null || key.isEmpty) {
|
||||
merged.add(payment);
|
||||
continue;
|
||||
}
|
||||
if (seen.contains(key)) continue;
|
||||
seen.add(key);
|
||||
merged.add(payment);
|
||||
}
|
||||
|
||||
_applyResource(_resource.copyWith(data: merged), notify: true);
|
||||
}
|
||||
|
||||
Future<void> _refresh({
|
||||
int? limit,
|
||||
String? sourceRef,
|
||||
@@ -224,12 +228,13 @@ class PaymentsProvider with ChangeNotifier {
|
||||
_destinationRef = null;
|
||||
_states = null;
|
||||
_resource = Resource(data: []);
|
||||
_pauseAutoRefreshTimer();
|
||||
_stopPendingRefreshTimer();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _applyResource(Resource<List<Payment>> newResource, {required bool notify}) {
|
||||
_resource = newResource;
|
||||
_syncPendingRefresh();
|
||||
if (notify) notifyListeners();
|
||||
}
|
||||
|
||||
@@ -239,6 +244,12 @@ class PaymentsProvider with ChangeNotifier {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
String? _paymentKey(Payment payment) {
|
||||
final ref = _normalize(payment.paymentRef);
|
||||
if (ref != null) return ref;
|
||||
return _normalize(payment.idempotencyKey);
|
||||
}
|
||||
|
||||
List<String>? _normalizeStates(List<String>? states) {
|
||||
if (states == null || states.isEmpty) return null;
|
||||
final normalized = states
|
||||
@@ -249,31 +260,70 @@ class PaymentsProvider with ChangeNotifier {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
|
||||
void _ensureAutoRefreshTimer() {
|
||||
if (_autoRefreshTimer != null) return;
|
||||
_autoRefreshTimer = Timer.periodic(_autoRefreshInterval, (_) {
|
||||
if (_resource.isLoading || _isLoadingMore) return;
|
||||
unawaited(refreshSilently());
|
||||
});
|
||||
void _syncPendingRefresh() {
|
||||
final hasPending = payments.any(_isPending);
|
||||
if (!hasPending) {
|
||||
_stopPendingRefreshTimer();
|
||||
return;
|
||||
}
|
||||
_ensurePendingRefreshTimer();
|
||||
}
|
||||
|
||||
void _restartAutoRefreshTimer() {
|
||||
if (_autoRefreshTimer == null) return;
|
||||
_autoRefreshTimer?.cancel();
|
||||
_autoRefreshTimer = null;
|
||||
_ensureAutoRefreshTimer();
|
||||
void _ensurePendingRefreshTimer() {
|
||||
if (_pendingRefreshTimer != null) return;
|
||||
_pendingRefreshTimer = Timer.periodic(
|
||||
_pendingRefreshInterval,
|
||||
(_) => _refreshPending(),
|
||||
);
|
||||
_refreshPending();
|
||||
}
|
||||
|
||||
void _stopAutoRefreshTimer() {
|
||||
_autoRefreshTimer?.cancel();
|
||||
_autoRefreshTimer = null;
|
||||
_autoRefreshRefs = 0;
|
||||
_autoRefreshInterval = const Duration(seconds: 15);
|
||||
Future<void> _refreshPending() async {
|
||||
if (_isPendingRefreshInFlight) return;
|
||||
if (isLoading || isLoadingMore) return;
|
||||
|
||||
_isPendingRefreshInFlight = true;
|
||||
try {
|
||||
await refreshSilently(
|
||||
limit: _limit,
|
||||
sourceRef: _sourceRef,
|
||||
destinationRef: _destinationRef,
|
||||
states: _states,
|
||||
);
|
||||
} finally {
|
||||
_isPendingRefreshInFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
void _pauseAutoRefreshTimer() {
|
||||
_autoRefreshTimer?.cancel();
|
||||
_autoRefreshTimer = null;
|
||||
void _stopPendingRefreshTimer() {
|
||||
_pendingRefreshTimer?.cancel();
|
||||
_pendingRefreshTimer = null;
|
||||
_isPendingRefreshInFlight = false;
|
||||
}
|
||||
|
||||
bool _isPending(Payment payment) {
|
||||
final raw = payment.state;
|
||||
final trimmed = (raw ?? '').trim().toUpperCase();
|
||||
final normalized = trimmed.startsWith('PAYMENT_STATE_')
|
||||
? trimmed.substring('PAYMENT_STATE_'.length)
|
||||
: trimmed;
|
||||
|
||||
switch (normalized) {
|
||||
case 'SUCCESS':
|
||||
case 'FAILED':
|
||||
case 'CANCELLED':
|
||||
return false;
|
||||
case 'PROCESSING':
|
||||
return true;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stopPendingRefreshTimer();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,9 +13,11 @@ import 'package:pshared/models/asset.dart';
|
||||
import 'package:pshared/models/payment/intent.dart';
|
||||
import 'package:pshared/models/payment/quote/quote.dart';
|
||||
import 'package:pshared/models/money.dart';
|
||||
import 'package:pshared/models/auto_refresh_mode.dart';
|
||||
import 'package:pshared/provider/organizations.dart';
|
||||
import 'package:pshared/provider/payment/amount.dart';
|
||||
import 'package:pshared/provider/payment/flow.dart';
|
||||
import 'package:pshared/provider/payment/auto_refresh.dart';
|
||||
import 'package:pshared/provider/recipient/provider.dart';
|
||||
import 'package:pshared/provider/recipient/pmethods.dart';
|
||||
import 'package:pshared/provider/resource.dart';
|
||||
@@ -31,6 +33,8 @@ class QuotationProvider extends ChangeNotifier {
|
||||
bool _isLoaded = false;
|
||||
PaymentIntent? _lastIntent;
|
||||
final QuotationIntentBuilder _intentBuilder = QuotationIntentBuilder();
|
||||
final AutoRefreshScheduler _autoRefresh = AutoRefreshScheduler();
|
||||
AutoRefreshMode _autoRefreshMode = AutoRefreshMode.on;
|
||||
|
||||
void update(
|
||||
OrganizationsProvider venue,
|
||||
@@ -59,6 +63,7 @@ class QuotationProvider extends ChangeNotifier {
|
||||
Exception? get error => _quotation.error;
|
||||
bool get canRefresh => _lastIntent != null;
|
||||
bool get isReady => _isLoaded && !_quotation.isLoading && _quotation.error == null;
|
||||
AutoRefreshMode get autoRefreshMode => _autoRefreshMode;
|
||||
|
||||
DateTime? get quoteExpiresAt {
|
||||
final expiresAtUnixMs = quotation?.fxQuote?.expiresAtUnixMs;
|
||||
@@ -78,6 +83,14 @@ class QuotationProvider extends ChangeNotifier {
|
||||
|
||||
void _setResource(Resource<PaymentQuote> quotation) {
|
||||
_quotation = quotation;
|
||||
_syncAutoRefresh();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setAutoRefreshMode(AutoRefreshMode mode) {
|
||||
if (_autoRefreshMode == mode) return;
|
||||
_autoRefreshMode = mode;
|
||||
_syncAutoRefresh();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -122,4 +135,24 @@ class QuotationProvider extends ChangeNotifier {
|
||||
final payload = jsonEncode(intent.toDTO().toJson());
|
||||
return Uuid().v5(Namespace.url.value, 'quote:$payload');
|
||||
}
|
||||
|
||||
void _syncAutoRefresh() {
|
||||
final isEnabled = _autoRefreshMode == AutoRefreshMode.on;
|
||||
_autoRefresh.setEnabled(isEnabled);
|
||||
final canAutoRefresh = isEnabled && canRefresh;
|
||||
_autoRefresh.sync(
|
||||
isLoading: isLoading,
|
||||
canRefresh: canAutoRefresh,
|
||||
scheduledAt: quoteExpiresAt,
|
||||
onRefresh: () async {
|
||||
await refreshQuotation();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_autoRefresh.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
96
frontend/pshared/lib/provider/payment/updates.dart
Normal file
96
frontend/pshared/lib/provider/payment/updates.dart
Normal file
@@ -0,0 +1,96 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:pshared/models/payment/payment.dart';
|
||||
import 'package:pshared/provider/payment/multiple/provider.dart';
|
||||
import 'package:pshared/provider/payment/payments.dart';
|
||||
import 'package:pshared/provider/payment/provider.dart';
|
||||
|
||||
|
||||
class PaymentsUpdatesProvider extends ChangeNotifier {
|
||||
PaymentsProvider? _payments;
|
||||
PaymentProvider? _paymentProvider;
|
||||
MultiPaymentProvider? _multiPaymentProvider;
|
||||
Set<String> _knownKeys = {};
|
||||
|
||||
void update({
|
||||
required PaymentsProvider paymentsProvider,
|
||||
required PaymentProvider paymentProvider,
|
||||
required MultiPaymentProvider multiPaymentProvider,
|
||||
}) {
|
||||
if (!identical(_payments, paymentsProvider)) {
|
||||
_payments = paymentsProvider;
|
||||
_syncKnownKeys();
|
||||
}
|
||||
if (!identical(_paymentProvider, paymentProvider)) {
|
||||
_paymentProvider?.removeListener(_onSinglePaymentChanged);
|
||||
_paymentProvider = paymentProvider;
|
||||
_paymentProvider?.addListener(_onSinglePaymentChanged);
|
||||
}
|
||||
if (!identical(_multiPaymentProvider, multiPaymentProvider)) {
|
||||
_multiPaymentProvider?.removeListener(_onMultiPaymentChanged);
|
||||
_multiPaymentProvider = multiPaymentProvider;
|
||||
_multiPaymentProvider?.addListener(_onMultiPaymentChanged);
|
||||
}
|
||||
_syncKnownKeys();
|
||||
}
|
||||
|
||||
void _syncKnownKeys() {
|
||||
_knownKeys = {
|
||||
for (final payment in _payments?.payments ?? const <Payment>[])
|
||||
..._keyFor(payment),
|
||||
};
|
||||
}
|
||||
|
||||
void _onSinglePaymentChanged() {
|
||||
final payment = _paymentProvider?.payment;
|
||||
if (payment == null) return;
|
||||
final key = _key(payment);
|
||||
if (key != null && _knownKeys.contains(key)) return;
|
||||
_merge([payment]);
|
||||
}
|
||||
|
||||
void _onMultiPaymentChanged() {
|
||||
final payments = _multiPaymentProvider?.payments ?? const <Payment>[];
|
||||
if (payments.isEmpty) return;
|
||||
final incoming = <Payment>[];
|
||||
for (final payment in payments) {
|
||||
final key = _key(payment);
|
||||
if (key == null || !_knownKeys.contains(key)) {
|
||||
incoming.add(payment);
|
||||
}
|
||||
}
|
||||
if (incoming.isEmpty) return;
|
||||
_merge(incoming);
|
||||
}
|
||||
|
||||
void _merge(List<Payment> incoming) {
|
||||
if (incoming.isEmpty) return;
|
||||
_payments?.mergePayments(incoming);
|
||||
for (final payment in incoming) {
|
||||
final key = _key(payment);
|
||||
if (key != null) {
|
||||
_knownKeys.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String? _key(Payment payment) {
|
||||
final ref = payment.paymentRef?.trim();
|
||||
if (ref != null && ref.isNotEmpty) return ref;
|
||||
final idempotency = payment.idempotencyKey?.trim();
|
||||
if (idempotency != null && idempotency.isNotEmpty) return idempotency;
|
||||
return null;
|
||||
}
|
||||
|
||||
Iterable<String> _keyFor(Payment payment) sync* {
|
||||
final key = _key(payment);
|
||||
if (key != null) yield key;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_paymentProvider?.removeListener(_onSinglePaymentChanged);
|
||||
_multiPaymentProvider?.removeListener(_onMultiPaymentChanged);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:share_plus/share_plus.dart';
|
||||
|
||||
import 'package:pshared/api/errors/unauthorized.dart';
|
||||
import 'package:pshared/api/responses/organization.dart';
|
||||
import 'package:pshared/models/describable.dart';
|
||||
import 'package:pshared/models/organization/organization.dart';
|
||||
import 'package:pshared/data/mapper/organization.dart';
|
||||
import 'package:pshared/service/authorization/service.dart';
|
||||
@@ -41,6 +42,40 @@ class OrganizationService {
|
||||
);
|
||||
}
|
||||
|
||||
static Future<Organization> updateSettings(
|
||||
Organization organization, {
|
||||
String? name,
|
||||
String? description,
|
||||
String? timeZone,
|
||||
String? logoUrl,
|
||||
}) async {
|
||||
_logger.fine('Updating organization settings ${organization.id}');
|
||||
final updatedDescribable = (name != null || description != null)
|
||||
? organization.describable.copyWith(
|
||||
name: name,
|
||||
description: description != null ? () => description : null,
|
||||
)
|
||||
: organization.describable;
|
||||
|
||||
final updatedOrg = organization.copyWith(
|
||||
describable: updatedDescribable,
|
||||
timeZone: timeZone,
|
||||
logoUrl: logoUrl != null ? () => logoUrl : null,
|
||||
);
|
||||
|
||||
final updated = await update(updatedOrg);
|
||||
return updated.firstWhere(
|
||||
(org) => org.id == organization.id,
|
||||
orElse: () => updated.first,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<Organization> uploadLogoAndUpdate(Organization organization, XFile logoFile) async {
|
||||
_logger.fine('Uploading logo for organization ${organization.id}');
|
||||
final url = await uploadLogo(organization.id, logoFile);
|
||||
return updateSettings(organization, logoUrl: url);
|
||||
}
|
||||
|
||||
static Future<String> uploadLogo(String organizationRef, XFile logoFile) async {
|
||||
_logger.fine('Uploading logo');
|
||||
return FilesService.uploadImage(_objectType, organizationRef, logoFile);
|
||||
|
||||
Reference in New Issue
Block a user