Files
bowlingManager/mobile/lib/services/in_app_purchase_service.dart
T
2025-08-09 03:09:17 +09:00

294 lines
9.2 KiB
Dart

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 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart';
import '../config/api_config.dart';
import '../models/subscription_model.dart';
/// 인앱 결제 서비스
class InAppPurchaseService extends ChangeNotifier {
final InAppPurchase _inAppPurchase = InAppPurchase.instance;
StreamSubscription<List<PurchaseDetails>>? _subscription;
List<ProductDetails> _products = [];
List<PurchaseDetails> _purchases = [];
bool _isAvailable = false;
bool _isLoading = false;
String? _error;
// 구매 검증 관련 변수
bool _purchaseVerified = false;
Map<String, dynamic>? _verifiedPurchase;
// 구독 상품 ID (스토어에 등록된 ID와 일치해야 함)
final Map<SubscriptionPlanType, String> _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<ProductDetails> get products => _products;
List<PurchaseDetails> 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<void> _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<void> _loadProducts() async {
try {
final Set<String> 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<bool> 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<PurchaseDetails> 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<void> _handleSuccessfulPurchase(PurchaseDetails purchaseDetails) async {
// 서버에 구독 정보 업데이트 요청
await _verifyAndSavePurchase(purchaseDetails);
}
/// 결제 실패 처리
void _handleFailedPurchase(PurchaseDetails purchaseDetails) {
_error = '결제에 실패했습니다: ${purchaseDetails.error?.message ?? "알 수 없는 오류"}';
}
/// 결제 취소 처리
void _handleCanceledPurchase(PurchaseDetails purchaseDetails) {
_error = '결제가 취소되었습니다.';
}
/// 구매 검증 및 서버 저장
Future<bool> _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) {
// 검증 성공
final data = jsonDecode(response.body);
// 성공 이벤트 발생 (SubscriptionService에서 구독 정보 업데이트)
_purchaseVerified = true;
_verifiedPurchase = {
'transactionId': transactionId,
'productId': productId,
'verificationData': data,
};
notifyListeners();
return true;
} else {
// 검증 실패
_error = '영수증 검증에 실패했습니다. (${response.statusCode})';
notifyListeners();
return false;
}
} catch (e) {
_error = '영수증 검증 중 오류가 발생했습니다: $e';
notifyListeners();
return false;
}
}
/// 구독 복원
Future<bool> 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();
}
}