회원목록
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
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;
|
||||
|
||||
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) {
|
||||
final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition =
|
||||
_inAppPurchase.getPlatformAddition<InAppPurchaseStoreKitPlatformAddition>();
|
||||
receiptData = await iosPlatformAddition.retrieveReceiptData();
|
||||
} 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user