import 'dart:async'; import 'dart:convert'; import 'package:crypto/crypto.dart'; import 'package:flutter/foundation.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; import '../../../../core/logging/logger.dart'; import '../../../../core/network/api_problem.dart'; import '../../../../data/network/api_client.dart'; import '../../../points/data/models/package_info.dart'; import '../apis/apple_payment_api.dart'; import '../models/apple_purchase_models.dart'; enum PurchaseFlowState { idle, purchasing, verifying, success, failed } class AppleIapService with ChangeNotifier { AppleIapService({required ApiClient apiClient, required String userId}) : _paymentApi = ApplePaymentApi(apiClient: apiClient), _inAppPurchase = InAppPurchase.instance, _appAccountToken = _hashUserId(userId); final ApplePaymentApi _paymentApi; final InAppPurchase _inAppPurchase; final String? _appAccountToken; final Logger _logger = getLogger('features.payments.apple_iap_service'); static String? _hashUserId(String userId) { if (userId.isEmpty) return null; final bytes = utf8.encode(userId); final digest = md5.convert(bytes); return digest.toString(); } StreamSubscription>? _subscription; Map _storeKitProducts = {}; PurchaseFlowState _state = PurchaseFlowState.idle; String? _lastError; ApiProblem? _lastApiProblem; PurchaseFlowState get state => _state; String? get lastError => _lastError; ApiProblem? get lastApiProblem => _lastApiProblem; void init() { final Stream> purchaseUpdated; purchaseUpdated = _inAppPurchase.purchaseStream; _subscription = purchaseUpdated.listen(_onPurchaseUpdated); } Future loadStoreKitProducts(List packages) async { final ids = packages .map((p) => p.appStoreProductId) .where((id) => id.isNotEmpty) .toSet(); if (ids.isEmpty) return; final response = await _inAppPurchase.queryProductDetails(ids); if (response.notFoundIDs.isNotEmpty) { _logger.warning( message: 'Some StoreKit products not found', extra: {'notFound': response.notFoundIDs.join(', ')}, ); } final products = {}; for (final detail in response.productDetails) { products[detail.id] = detail; } _storeKitProducts = products; } ProductDetails? getStoreKitProduct(String appStoreProductId) { return _storeKitProducts[appStoreProductId]; } Future purchase(PackageInfo package) async { if (_state == PurchaseFlowState.purchasing || _state == PurchaseFlowState.verifying) { return false; } final product = _storeKitProducts[package.appStoreProductId]; if (product == null) { _logger.warning( message: 'StoreKit product not found for purchase', extra: {'productId': package.appStoreProductId}, ); _setError('Product not available'); return false; } _setState(PurchaseFlowState.purchasing); final purchaseParam = PurchaseParam( productDetails: product, applicationUserName: _appAccountToken, ); final bought = await _inAppPurchase.buyConsumable( purchaseParam: purchaseParam, ); if (!bought) { _setState(PurchaseFlowState.idle); return false; } return true; } void _onPurchaseUpdated(List purchases) { for (final purchase in purchases) { _handlePurchase(purchase); } } Future _handlePurchase(PurchaseDetails purchase) async { try { switch (purchase.status) { case PurchaseStatus.purchased: await _verifyAndComplete(purchase); case PurchaseStatus.canceled: await _completePurchaseSafely(purchase); _setState(PurchaseFlowState.idle); case PurchaseStatus.error: final errorCode = purchase.error?.code; final isUserCancel = errorCode == '2'; if (isUserCancel) { await _completePurchaseSafely(purchase); _setState(PurchaseFlowState.idle); } else { _logger.warning( message: 'Purchase error', extra: { 'errorCode': errorCode, 'errorMessage': purchase.error?.message, }, ); await _completePurchaseSafely(purchase); _setError(purchase.error?.message ?? 'Purchase failed'); } case PurchaseStatus.pending: _setState(PurchaseFlowState.purchasing); case PurchaseStatus.restored: await _completePurchaseSafely(purchase); } } catch (e, stackTrace) { _logger.error( message: 'Failed to handle purchase', error: e, stackTrace: stackTrace, extra: { 'productID': purchase.productID, 'purchaseID': purchase.purchaseID, 'status': purchase.status.name, }, ); _setState(PurchaseFlowState.idle); } } Future _completePurchaseSafely(PurchaseDetails purchase) async { try { await _inAppPurchase.completePurchase(purchase); } catch (e, stackTrace) { _logger.warning( message: 'completePurchase failed (likely null purchaseID): $e', stackTrace: stackTrace, extra: { 'productID': purchase.productID, 'purchaseID': purchase.purchaseID, }, ); } } Future _verifyAndComplete(PurchaseDetails purchase) async { _setState(PurchaseFlowState.verifying); try { final request = VerifyTransactionRequest( productCode: _productCodeFromStoreKitId(purchase.productID), appStoreProductId: purchase.productID, transactionId: purchase.purchaseID ?? '', signedTransactionInfo: purchase.verificationData.serverVerificationData, appAccountToken: _appAccountToken, ); final response = await _paymentApi.verifyTransaction(request); await _inAppPurchase.completePurchase(purchase); _state = PurchaseFlowState.success; _lastError = null; _lastApiProblem = null; _logger.info( message: 'Purchase verified and completed', extra: { 'transactionId': purchase.purchaseID, 'creditsAdded': response.creditsAdded, 'status': response.status.name, }, ); notifyListeners(); } catch (e, stackTrace) { _logger.error( message: 'Purchase verification failed', error: e, stackTrace: stackTrace, extra: { 'transactionId': purchase.purchaseID, 'productID': purchase.productID, 'errorType': e.runtimeType.toString(), 'errorMessage': e.toString(), }, ); if (_isRetryableError(e)) { _setState(PurchaseFlowState.idle); return; } await _inAppPurchase.completePurchase(purchase); if (e is ApiProblem) { _lastApiProblem = e; _lastError = e.detail.isNotEmpty ? e.detail : 'Verification failed'; } else { _lastApiProblem = null; _lastError = _extractErrorMessage(e); } _state = PurchaseFlowState.failed; notifyListeners(); } } bool _isRetryableError(Object error) { if (error is ApiProblem) { return error.status >= 500 || error.status == 0; } return true; } String _extractErrorMessage(Object error) { if (error is ApiProblem) { return error.detail.isNotEmpty ? error.detail : 'Verification failed'; } return 'Verification failed'; } String _productCodeFromStoreKitId(String storeKitId) { return switch (storeKitId) { 'com.meeyao.qianwen.new_user_pack' => 'new_user_pack', 'com.meeyao.qianwen.starter_pack' => 'starter_pack', 'com.meeyao.qianwen.popular_pack' => 'popular_pack', 'com.meeyao.qianwen.premium_pack' => 'premium_pack', _ => storeKitId, }; } void _setState(PurchaseFlowState state) { _state = state; if (state != PurchaseFlowState.failed) { _lastError = null; _lastApiProblem = null; } notifyListeners(); } void _setError(String message) { _state = PurchaseFlowState.failed; _lastError = message; _lastApiProblem = null; _logger.warning(message: 'Purchase flow error', extra: {'error': message}); notifyListeners(); } void resetState() { _state = PurchaseFlowState.idle; _lastError = null; _lastApiProblem = null; notifyListeners(); } @override void dispose() { _subscription?.cancel(); super.dispose(); } }