import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:in_app_purchase_android/in_app_purchase_android.dart'; import '../config/api_config.dart'; import '../models/subscription_model.dart'; /// 인앱 결제 서비스 class InAppPurchaseService extends ChangeNotifier { final InAppPurchase _inAppPurchase = InAppPurchase.instance; StreamSubscription>? _subscription; List _products = []; List _purchases = []; bool _isAvailable = false; bool _isLoading = false; String? _error; // 구매 검증 관련 변수 bool _purchaseVerified = false; // 구독 상품 ID (스토어에 등록된 ID와 일치해야 함) final Map _subscriptionIds = { SubscriptionPlanType.basic: Platform.isAndroid ? 'com.bowlingmanager.subscription.basic' : 'com.bowlingmanager.subscription.basic', SubscriptionPlanType.premium: Platform.isAndroid ? 'com.bowlingmanager.subscription.premium' : 'com.bowlingmanager.subscription.premium', SubscriptionPlanType.enterprise: Platform.isAndroid ? 'com.bowlingmanager.subscription.enterprise' : 'com.bowlingmanager.subscription.enterprise', }; // 게터 List get products => _products; List get purchases => _purchases; bool get isAvailable => _isAvailable; bool get isLoading => _isLoading; String? get error => _error; // 구매 검증 관련 게터/세터 bool get isPurchaseVerified => _purchaseVerified; set isPurchaseVerified(bool value) => _purchaseVerified = value; PurchaseDetails? get lastVerifiedPurchase => _purchases.isNotEmpty ? _purchases.last : null; set lastVerifiedPurchase(PurchaseDetails? value) { if (value != null && !_purchases.contains(value)) { _purchases.add(value); } } InAppPurchaseService() { _initialize(); } /// 초기화 및 결제 상태 리스너 설정 Future _initialize() async { _isLoading = true; notifyListeners(); // 인앱 결제 가능 여부 확인 _isAvailable = await _inAppPurchase.isAvailable(); if (!_isAvailable) { _error = '인앱 결제를 사용할 수 없습니다.'; _isLoading = false; notifyListeners(); return; } // 결제 상태 변경 리스너 설정 final purchaseUpdated = _inAppPurchase.purchaseStream; _subscription = purchaseUpdated.listen( _onPurchaseUpdate, onDone: _updateStreamOnDone, onError: _updateStreamOnError, ); // 상품 정보 로드 await _loadProducts(); _isLoading = false; notifyListeners(); } /// 상품 정보 로드 Future _loadProducts() async { try { final Set ids = _subscriptionIds.values.toSet(); final ProductDetailsResponse response = await _inAppPurchase.queryProductDetails(ids); if (response.notFoundIDs.isNotEmpty) { _error = '일부 상품을 찾을 수 없습니다: ${response.notFoundIDs.join(", ")}'; } _products = response.productDetails; notifyListeners(); } catch (e) { _error = '상품 정보를 불러오는데 실패했습니다: $e'; } } /// 구독 플랜 타입에 해당하는 상품 정보 가져오기 ProductDetails? getProductByPlanType(SubscriptionPlanType planType) { final String? id = _subscriptionIds[planType]; if (id == null) return null; try { return _products.firstWhere((product) => product.id == id); } catch (e) { return null; } } /// 구독 구매 시작 Future purchaseSubscription(SubscriptionPlanType planType) async { final ProductDetails? product = getProductByPlanType(planType); if (product == null) { _error = '해당 구독 상품을 찾을 수 없습니다.'; notifyListeners(); return false; } try { // 구매 요청 final PurchaseParam purchaseParam = PurchaseParam( productDetails: product, applicationUserName: null, ); // 구독 상품 구매 요청 await _inAppPurchase.buyNonConsumable(purchaseParam: purchaseParam); return true; } catch (e) { _error = '구독 구매 요청 중 오류가 발생했습니다: $e'; notifyListeners(); return false; } } /// 구매 상태 업데이트 처리 void _onPurchaseUpdate(List purchaseDetailsList) async { _purchases = purchaseDetailsList; for (var purchaseDetails in purchaseDetailsList) { if (purchaseDetails.status == PurchaseStatus.pending) { // 결제 대기 중 _handlePendingPurchase(purchaseDetails); } else if (purchaseDetails.status == PurchaseStatus.purchased || purchaseDetails.status == PurchaseStatus.restored) { // 결제 완료 또는 복원됨 await _handleSuccessfulPurchase(purchaseDetails); } else if (purchaseDetails.status == PurchaseStatus.error) { // 결제 오류 _handleFailedPurchase(purchaseDetails); } else if (purchaseDetails.status == PurchaseStatus.canceled) { // 결제 취소 _handleCanceledPurchase(purchaseDetails); } // 구매 완료 처리 (iOS에서는 명시적으로 완료 처리 필요) if (purchaseDetails.pendingCompletePurchase) { await _inAppPurchase.completePurchase(purchaseDetails); } } notifyListeners(); } /// 결제 대기 중 처리 void _handlePendingPurchase(PurchaseDetails purchaseDetails) { // 결제 대기 중 UI 업데이트 등 필요한 처리 } /// 결제 성공 처리 Future _handleSuccessfulPurchase(PurchaseDetails purchaseDetails) async { // 서버에 구독 정보 업데이트 요청 await _verifyAndSavePurchase(purchaseDetails); } /// 결제 실패 처리 void _handleFailedPurchase(PurchaseDetails purchaseDetails) { _error = '결제에 실패했습니다: ${purchaseDetails.error?.message ?? "알 수 없는 오류"}'; } /// 결제 취소 처리 void _handleCanceledPurchase(PurchaseDetails purchaseDetails) { _error = '결제가 취소되었습니다.'; } /// 구매 검증 및 서버 저장 Future _verifyAndSavePurchase(PurchaseDetails purchaseDetails) async { // 구매 영수증 정보 추출 String? receiptData; String? transactionId = purchaseDetails.purchaseID; String? productId = purchaseDetails.productID; String platform = Platform.isIOS ? 'ios' : 'android'; if (Platform.isIOS) { // iOS의 경우 구매 정보에서 직접 가져오기 receiptData = purchaseDetails.verificationData.serverVerificationData; } else if (Platform.isAndroid) { if (purchaseDetails is GooglePlayPurchaseDetails) { receiptData = purchaseDetails.billingClientPurchase.originalJson; } } if (receiptData == null) { _error = '영수증 데이터를 추출할 수 없습니다.'; notifyListeners(); return false; } try { // API 설정 가져오기 final String baseUrl = ApiConfig.baseUrl; final String verifyEndpoint = ApiConfig.verifyReceipt; // 서버에 영수증 검증 요청 final response = await http.post( Uri.parse('$baseUrl$verifyEndpoint'), headers: { 'Content-Type': 'application/json', // 인증 토큰은 SubscriptionService에서 처리 }, body: jsonEncode({ 'receipt': receiptData, 'transactionId': transactionId, 'productId': productId, 'platform': platform, }), ); if (response.statusCode == 200) { // 검증 성공 jsonDecode(response.body); // 성공 이벤트 발생 (SubscriptionService에서 구독 정보 업데이트) _purchaseVerified = true; notifyListeners(); return true; } else { // 검증 실패 _error = '영수증 검증에 실패했습니다. (${response.statusCode})'; notifyListeners(); return false; } } catch (e) { _error = '영수증 검증 중 오류가 발생했습니다: $e'; notifyListeners(); return false; } } /// 구독 복원 Future restorePurchases() async { try { await _inAppPurchase.restorePurchases(); return true; } catch (e) { _error = '구독 복원 중 오류가 발생했습니다: $e'; notifyListeners(); return false; } } void _updateStreamOnDone() { _subscription?.cancel(); } void _updateStreamOnError(dynamic error) { _error = '결제 스트림 오류: $error'; notifyListeners(); } @override void dispose() { _subscription?.cancel(); super.dispose(); } }