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