import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import 'package:collection/collection.dart'; import 'package:uuid/uuid.dart'; import 'package:pshared/api/requests/payment/quote.dart'; import 'package:pshared/data/mapper/payment/intent/payment.dart'; import 'package:pshared/models/asset.dart'; import 'package:pshared/models/payment/quote/status_type.dart'; import 'package:pshared/models/payment/intent.dart'; import 'package:pshared/models/payment/quote/quote.dart'; import 'package:pshared/models/payment/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/wallets.dart'; import 'package:pshared/provider/recipient/provider.dart'; import 'package:pshared/provider/recipient/pmethods.dart'; import 'package:pshared/provider/resource.dart'; import 'package:pshared/provider/payment/quotation/auto_refresh.dart'; import 'package:pshared/provider/payment/quotation/intent_builder.dart'; import 'package:pshared/service/payment/quotation.dart'; class QuotationProvider extends ChangeNotifier { static final _logger = Logger('provider.payment.quotation'); Resource _quotation = Resource(data: null, isLoading: false, error: null); late OrganizationsProvider _organizations; bool _isLoaded = false; PaymentIntent? _lastIntent; final QuotationIntentBuilder _intentBuilder = QuotationIntentBuilder(); final QuotationAutoRefreshController _autoRefreshController = QuotationAutoRefreshController(); AutoRefreshMode _autoRefreshMode = AutoRefreshMode.on; void update( OrganizationsProvider venue, PaymentAmountProvider payment, WalletsProvider wallets, PaymentFlowProvider flow, RecipientsProvider recipients, PaymentMethodsProvider methods, ) { _organizations = venue; final intent = _intentBuilder.build( payment: payment, wallets: wallets, flow: flow, recipients: recipients, methods: methods, ); if (intent == null) return; final lastIntentDto = _lastIntent?.toDTO().toJson(); final isSameIntent = lastIntentDto != null && const DeepCollectionEquality().equals(intent.toDTO().toJson(), lastIntentDto); if (isSameIntent) return; getQuotation(intent); } PaymentQuote? get quotation => _quotation.data; bool get isLoading => _quotation.isLoading; Exception? get error => _quotation.error; bool get canRefresh => _lastIntent != null; AutoRefreshMode get autoRefreshMode => _autoRefreshMode; bool get isReady => _isLoaded && !_quotation.isLoading && _quotation.error == null; bool get hasLiveQuote => isReady && quotation != null && !isExpired; DateTime? get quoteExpiresAt { final expiresAtUnixMs = quotation?.fxQuote?.expiresAtUnixMs; if (expiresAtUnixMs == null) return null; return DateTime.fromMillisecondsSinceEpoch(expiresAtUnixMs, isUtc: true); } Duration? get quoteTimeLeft { final expiresAt = quoteExpiresAt; if (expiresAt == null) return null; return expiresAt.difference(DateTime.now().toUtc()); } bool get isExpired { final timeLeft = quoteTimeLeft; if (timeLeft == null) return false; return timeLeft <= Duration.zero; } QuoteStatusType get quoteStatusType { if (isLoading) return QuoteStatusType.loading; if (error != null) return QuoteStatusType.error; if (quotation == null) return QuoteStatusType.missing; if (isExpired) return QuoteStatusType.expired; return QuoteStatusType.active; } Asset? get fee => _assetFromMoney(quotation?.expectedFeeTotal); Asset? get total => _assetFromMoney(quotation?.debitAmount); Asset? get recipientGets => _assetFromMoney(quotation?.expectedSettlementAmount); Asset? _assetFromMoney(Money? money) { if (money == null) return null; return createAsset(money.currency, money.amount); } void _setResource(Resource quotation) { _quotation = quotation; _syncAutoRefresh(); notifyListeners(); } void setAutoRefreshMode(AutoRefreshMode mode) { if (_autoRefreshMode == mode) return; _autoRefreshMode = mode; _syncAutoRefresh(); notifyListeners(); } Future refreshQuotation() async { final intent = _lastIntent; if (intent == null) return null; return getQuotation(intent); } Future getQuotation(PaymentIntent intent) async { if (!_organizations.isOrganizationSet) throw StateError('Organization is not set'); _lastIntent = intent; try { _setResource(_quotation.copyWith(isLoading: true, error: null)); final response = await QuotationService.getQuotation( _organizations.current.id, QuotePaymentRequest( idempotencyKey: Uuid().v4(), intent: intent.toDTO(), ), ); _isLoaded = true; _setResource(_quotation.copyWith(data: response, isLoading: false, error: null)); } catch (e, st) { _logger.warning('Failed to get quotation', e, st); _setResource(_quotation.copyWith( data: null, error: e is Exception ? e : Exception(e.toString()), isLoading: false, )); } return _quotation.data; } void reset() { _isLoaded = false; _lastIntent = null; _autoRefreshMode = AutoRefreshMode.off; _autoRefreshController.reset(); _setResource(Resource(data: null, isLoading: false, error: null)); } void _syncAutoRefresh() { final isAutoRefreshEnabled = _autoRefreshMode == AutoRefreshMode.on; _autoRefreshController.setEnabled(isAutoRefreshEnabled); final canAutoRefresh = isAutoRefreshEnabled && canRefresh; _autoRefreshController.sync( isLoading: isLoading, canRefresh: canAutoRefresh, expiresAt: quoteExpiresAt, onRefresh: refreshQuotation, ); } @override void dispose() { _autoRefreshController.dispose(); super.dispose(); } }