LCOV - code coverage report
Current view: top level - lib/services - in_app_purchase_service.dart Coverage Total Hit
Test: lcov.info Lines: 0.0 % 106 0
Test Date: 2025-08-12 03:21:51 Functions: - 0 0

            Line data    Source code
       1              : import 'dart:async';
       2              : import 'dart:convert';
       3              : import 'dart:io';
       4              : import 'package:flutter/material.dart';
       5              : import 'package:http/http.dart' as http;
       6              : import 'package:in_app_purchase/in_app_purchase.dart';
       7              : import 'package:in_app_purchase_android/in_app_purchase_android.dart';
       8              : import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart';
       9              : 
      10              : import '../config/api_config.dart';
      11              : import '../models/subscription_model.dart';
      12              : 
      13              : /// 인앱 결제 서비스
      14              : class InAppPurchaseService extends ChangeNotifier {
      15              :   final InAppPurchase _inAppPurchase = InAppPurchase.instance;
      16              :   StreamSubscription<List<PurchaseDetails>>? _subscription;
      17              :   
      18              :   List<ProductDetails> _products = [];
      19              :   List<PurchaseDetails> _purchases = [];
      20              :   bool _isAvailable = false;
      21              :   bool _isLoading = false;
      22              :   String? _error;
      23              :   
      24              :   // 구매 검증 관련 변수
      25              :   bool _purchaseVerified = false;
      26              :   Map<String, dynamic>? _verifiedPurchase;
      27              :   
      28              :   // 구독 상품 ID (스토어에 등록된 ID와 일치해야 함)
      29              :   final Map<SubscriptionPlanType, String> _subscriptionIds = {
      30              :     SubscriptionPlanType.basic: Platform.isAndroid 
      31              :         ? 'com.bowlingmanager.subscription.basic' 
      32              :         : 'com.bowlingmanager.subscription.basic',
      33              :     SubscriptionPlanType.premium: Platform.isAndroid 
      34              :         ? 'com.bowlingmanager.subscription.premium' 
      35              :         : 'com.bowlingmanager.subscription.premium',
      36              :     SubscriptionPlanType.enterprise: Platform.isAndroid 
      37              :         ? 'com.bowlingmanager.subscription.enterprise' 
      38              :         : 'com.bowlingmanager.subscription.enterprise',
      39              :   };
      40              :   
      41              :   // 게터
      42            0 :   List<ProductDetails> get products => _products;
      43            0 :   List<PurchaseDetails> get purchases => _purchases;
      44            0 :   bool get isAvailable => _isAvailable;
      45            0 :   bool get isLoading => _isLoading;
      46            0 :   String? get error => _error;
      47              :   
      48              :   // 구매 검증 관련 게터/세터
      49            0 :   bool get isPurchaseVerified => _purchaseVerified;
      50            0 :   set isPurchaseVerified(bool value) => _purchaseVerified = value;
      51            0 :   PurchaseDetails? get lastVerifiedPurchase => _purchases.isNotEmpty ? _purchases.last : null;
      52            0 :   set lastVerifiedPurchase(PurchaseDetails? value) {
      53            0 :     if (value != null && !_purchases.contains(value)) {
      54            0 :       _purchases.add(value);
      55              :     }
      56              :   }
      57              :   
      58            0 :   InAppPurchaseService() {
      59            0 :     _initialize();
      60              :   }
      61              :   
      62              :   /// 초기화 및 결제 상태 리스너 설정
      63            0 :   Future<void> _initialize() async {
      64            0 :     _isLoading = true;
      65            0 :     notifyListeners();
      66              :     
      67              :     // 인앱 결제 가능 여부 확인
      68            0 :     _isAvailable = await _inAppPurchase.isAvailable();
      69              :     
      70            0 :     if (!_isAvailable) {
      71            0 :       _error = '인앱 결제를 사용할 수 없습니다.';
      72            0 :       _isLoading = false;
      73            0 :       notifyListeners();
      74              :       return;
      75              :     }
      76              :     
      77              :     // 결제 상태 변경 리스너 설정
      78            0 :     final purchaseUpdated = _inAppPurchase.purchaseStream;
      79            0 :     _subscription = purchaseUpdated.listen(
      80            0 :       _onPurchaseUpdate,
      81            0 :       onDone: _updateStreamOnDone,
      82            0 :       onError: _updateStreamOnError,
      83              :     );
      84              :     
      85              :     // 상품 정보 로드
      86            0 :     await _loadProducts();
      87              :     
      88            0 :     _isLoading = false;
      89            0 :     notifyListeners();
      90              :   }
      91              :   
      92              :   /// 상품 정보 로드
      93            0 :   Future<void> _loadProducts() async {
      94              :     try {
      95            0 :       final Set<String> ids = _subscriptionIds.values.toSet();
      96              :       final ProductDetailsResponse response = 
      97            0 :           await _inAppPurchase.queryProductDetails(ids);
      98              :       
      99            0 :       if (response.notFoundIDs.isNotEmpty) {
     100            0 :         _error = '일부 상품을 찾을 수 없습니다: ${response.notFoundIDs.join(", ")}';
     101              :       }
     102              :       
     103            0 :       _products = response.productDetails;
     104            0 :       notifyListeners();
     105              :     } catch (e) {
     106            0 :       _error = '상품 정보를 불러오는데 실패했습니다: $e';
     107              :     }
     108              :   }
     109              :   
     110              :   /// 구독 플랜 타입에 해당하는 상품 정보 가져오기
     111            0 :   ProductDetails? getProductByPlanType(SubscriptionPlanType planType) {
     112            0 :     final String? id = _subscriptionIds[planType];
     113              :     if (id == null) return null;
     114              :     
     115              :     try {
     116            0 :       return _products.firstWhere((product) => product.id == id);
     117              :     } catch (e) {
     118              :       return null;
     119              :     }
     120              :   }
     121              :   
     122              :   /// 구독 구매 시작
     123            0 :   Future<bool> purchaseSubscription(SubscriptionPlanType planType) async {
     124            0 :     final ProductDetails? product = getProductByPlanType(planType);
     125              :     
     126              :     if (product == null) {
     127            0 :       _error = '해당 구독 상품을 찾을 수 없습니다.';
     128            0 :       notifyListeners();
     129              :       return false;
     130              :     }
     131              :     
     132              :     try {
     133              :       // 구매 요청
     134            0 :       final PurchaseParam purchaseParam = PurchaseParam(
     135              :         productDetails: product,
     136              :         applicationUserName: null,
     137              :       );
     138              :       
     139              :       // 구독 상품 구매 요청
     140            0 :       await _inAppPurchase.buyNonConsumable(purchaseParam: purchaseParam);
     141              :       return true;
     142              :     } catch (e) {
     143            0 :       _error = '구독 구매 요청 중 오류가 발생했습니다: $e';
     144            0 :       notifyListeners();
     145              :       return false;
     146              :     }
     147              :   }
     148              :   
     149              :   /// 구매 상태 업데이트 처리
     150            0 :   void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) async {
     151            0 :     _purchases = purchaseDetailsList;
     152              :     
     153            0 :     for (var purchaseDetails in purchaseDetailsList) {
     154            0 :       if (purchaseDetails.status == PurchaseStatus.pending) {
     155              :         // 결제 대기 중
     156            0 :         _handlePendingPurchase(purchaseDetails);
     157            0 :       } else if (purchaseDetails.status == PurchaseStatus.purchased || 
     158            0 :                  purchaseDetails.status == PurchaseStatus.restored) {
     159              :         // 결제 완료 또는 복원됨
     160            0 :         await _handleSuccessfulPurchase(purchaseDetails);
     161            0 :       } else if (purchaseDetails.status == PurchaseStatus.error) {
     162              :         // 결제 오류
     163            0 :         _handleFailedPurchase(purchaseDetails);
     164            0 :       } else if (purchaseDetails.status == PurchaseStatus.canceled) {
     165              :         // 결제 취소
     166            0 :         _handleCanceledPurchase(purchaseDetails);
     167              :       }
     168              :       
     169              :       // 구매 완료 처리 (iOS에서는 명시적으로 완료 처리 필요)
     170            0 :       if (purchaseDetails.pendingCompletePurchase) {
     171            0 :         await _inAppPurchase.completePurchase(purchaseDetails);
     172              :       }
     173              :     }
     174              :     
     175            0 :     notifyListeners();
     176              :   }
     177              :   
     178              :   /// 결제 대기 중 처리
     179            0 :   void _handlePendingPurchase(PurchaseDetails purchaseDetails) {
     180              :     // 결제 대기 중 UI 업데이트 등 필요한 처리
     181              :   }
     182              :   
     183              :   /// 결제 성공 처리
     184            0 :   Future<void> _handleSuccessfulPurchase(PurchaseDetails purchaseDetails) async {
     185              :     // 서버에 구독 정보 업데이트 요청
     186            0 :     await _verifyAndSavePurchase(purchaseDetails);
     187              :   }
     188              :   
     189              :   /// 결제 실패 처리
     190            0 :   void _handleFailedPurchase(PurchaseDetails purchaseDetails) {
     191            0 :     _error = '결제에 실패했습니다: ${purchaseDetails.error?.message ?? "알 수 없는 오류"}';
     192              :   }
     193              :   
     194              :   /// 결제 취소 처리
     195            0 :   void _handleCanceledPurchase(PurchaseDetails purchaseDetails) {
     196            0 :     _error = '결제가 취소되었습니다.';
     197              :   }
     198              :   
     199              :   /// 구매 검증 및 서버 저장
     200            0 :   Future<bool> _verifyAndSavePurchase(PurchaseDetails purchaseDetails) async {
     201              :     // 구매 영수증 정보 추출
     202              :     String? receiptData;
     203            0 :     String? transactionId = purchaseDetails.purchaseID;
     204            0 :     String? productId = purchaseDetails.productID;
     205            0 :     String platform = Platform.isIOS ? 'ios' : 'android';
     206              :     
     207            0 :     if (Platform.isIOS) {
     208              :       // iOS의 경우 구매 정보에서 직접 가져오기
     209            0 :       receiptData = purchaseDetails.verificationData.serverVerificationData;
     210            0 :     } else if (Platform.isAndroid) {
     211            0 :       if (purchaseDetails is GooglePlayPurchaseDetails) {
     212            0 :         receiptData = purchaseDetails.billingClientPurchase.originalJson;
     213              :       }
     214              :     }
     215              :     
     216              :     if (receiptData == null) {
     217            0 :       _error = '영수증 데이터를 추출할 수 없습니다.';
     218            0 :       notifyListeners();
     219              :       return false;
     220              :     }
     221              :     
     222              :     try {
     223              :       // API 설정 가져오기
     224              :       final String baseUrl = ApiConfig.baseUrl;
     225              :       final String verifyEndpoint = ApiConfig.verifyReceipt;
     226              :       
     227              :       // 서버에 영수증 검증 요청
     228            0 :       final response = await http.post(
     229            0 :         Uri.parse('$baseUrl$verifyEndpoint'),
     230            0 :         headers: {
     231              :           'Content-Type': 'application/json',
     232              :           // 인증 토큰은 SubscriptionService에서 처리
     233              :         },
     234            0 :         body: jsonEncode({
     235              :           'receipt': receiptData,
     236              :           'transactionId': transactionId,
     237              :           'productId': productId,
     238              :           'platform': platform,
     239              :         }),
     240              :       );
     241              :       
     242            0 :       if (response.statusCode == 200) {
     243              :         // 검증 성공
     244            0 :         final data = jsonDecode(response.body);
     245              :         // 성공 이벤트 발생 (SubscriptionService에서 구독 정보 업데이트)
     246            0 :         _purchaseVerified = true;
     247            0 :         _verifiedPurchase = {
     248              :           'transactionId': transactionId,
     249              :           'productId': productId,
     250              :           'verificationData': data,
     251              :         };
     252            0 :         notifyListeners();
     253              :         return true;
     254              :       } else {
     255              :         // 검증 실패
     256            0 :         _error = '영수증 검증에 실패했습니다. (${response.statusCode})';
     257            0 :         notifyListeners();
     258              :         return false;
     259              :       }
     260              :     } catch (e) {
     261            0 :       _error = '영수증 검증 중 오류가 발생했습니다: $e';
     262            0 :       notifyListeners();
     263              :       return false;
     264              :     }
     265              :   }
     266              :   
     267              :   /// 구독 복원
     268            0 :   Future<bool> restorePurchases() async {
     269              :     try {
     270            0 :       await _inAppPurchase.restorePurchases();
     271              :       return true;
     272              :     } catch (e) {
     273            0 :       _error = '구독 복원 중 오류가 발생했습니다: $e';
     274            0 :       notifyListeners();
     275              :       return false;
     276              :     }
     277              :   }
     278              :   
     279            0 :   void _updateStreamOnDone() {
     280            0 :     _subscription?.cancel();
     281              :   }
     282              :   
     283            0 :   void _updateStreamOnError(dynamic error) {
     284            0 :     _error = '결제 스트림 오류: $error';
     285            0 :     notifyListeners();
     286              :   }
     287              :   
     288            0 :   @override
     289              :   void dispose() {
     290            0 :     _subscription?.cancel();
     291            0 :     super.dispose();
     292              :   }
     293              : }
        

Generated by: LCOV version 2.3.1-1