// // StoreManager.swift // SodaLive // // Created by klaus on 2023/08/11. // // Ref. // https://www.youtube.com/watch?v=qyKmpr9EjwU // https://nicgoon.tistory.com/205 // https://charlie-choi.tistory.com/241 // https://eunjo-princess.tistory.com/20 // https://developer.apple.com/documentation/storekit/in-app_purchase/original_api_for_in-app_purchase/validating_receipts_with_the_app_store#//apple_ref/doc/uid/TP40010573-CH104-SW2 // https://velog.io/@givepro91/%EC%9D%B8%EC%95%B1%EA%B2%B0%EC%A0%9C-%EC%84%9C%EB%B2%84-%EA%B0%9C%EB%B0%9C // https://twih1203.medium.com/swift-%EC%9D%B8%EC%95%B1-%EA%B2%B0%EC%A0%9C-%EA%B5%AC%ED%98%84-ff4b2d20a260 // import Foundation import StoreKit class StoreManager: NSObject, ObservableObject { @Published var errorMessage = "" @Published var isShowPopup = false @Published var isLoading = false @Published var products = [SKProduct]() var onSuccessPayment: ((String, Int) -> Void)? var chargeId: Int! var request: SKProductsRequest! func getProducts() { isLoading = true products.removeAll() let request = SKProductsRequest(productIdentifiers: [ "\(Bundle.main.bundleIdentifier!).can_100" ]) request.delegate = self request.start() } func payment(product: SKProduct, chargeId: Int) { isLoading = true self.chargeId = chargeId let payment = SKPayment(product: product) SKPaymentQueue.default().add(self) SKPaymentQueue.default().add(payment) } } extension StoreManager: SKProductsRequestDelegate { func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { if !response.products.isEmpty { let products = response.products.sorted { $0.price.compare($1.price) == .orderedAscending } for product in products { DispatchQueue.main.async { self.products.append(product) } } } DispatchQueue.main.async { [unowned self] in self.isLoading = false } self.request = nil } func request(_ request: SKRequest, didFailWithError error: Error) { self.request = nil DEBUG_LOG("상품불러오기 실패: \(error)") DispatchQueue.main.async { [unowned self] in self.isLoading = false errorMessage = "상품을 불러오지 못했습니다.\n다시 시도해 주세요." self.isShowPopup = true } } } extension StoreManager: SKPaymentTransactionObserver { func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { for transaction in transactions { switch(transaction.transactionState) { case .purchasing: DEBUG_LOG("결제 진행 중...") break case .purchased: isLoading = false DEBUG_LOG("결제 완료") complete(transaction: transaction) break case .failed: isLoading = false DEBUG_LOG("결제 실패") fail(transaction: transaction) break case .deferred: isLoading = false DEBUG_LOG("아이폰이 잠김 등의 이유로 결제를 진행하지 못했습니다.") errorMessage = "아이폰이 잠김 등의 이유로 결제를 진행하지 못했습니다." isShowPopup = true SKPaymentQueue.default().finishTransaction(transaction) SKPaymentQueue.default().remove(self) break case .restored: isLoading = false DEBUG_LOG("상품 검증을 하였습니다.") errorMessage = "상품 검증을 하였습니다." isShowPopup = true SKPaymentQueue.default().finishTransaction(transaction) SKPaymentQueue.default().remove(self) break @unknown default: isLoading = false DEBUG_LOG("알 수 없는 오류가 발생했습니다.") errorMessage = "알 수 없는 오류가 발생했습니다." isShowPopup = true SKPaymentQueue.default().finishTransaction(transaction) SKPaymentQueue.default().remove(self) fatalError() } } } private func complete(transaction: SKPaymentTransaction) { if let onSuccessPayment = onSuccessPayment { if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: appStoreReceiptURL.path) { do { let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped) let receiptString = receiptData.base64EncodedString(options: []) onSuccessPayment(receiptString, chargeId) SKPaymentQueue.default().finishTransaction(transaction) SKPaymentQueue.default().remove(self) } catch { DEBUG_LOG("영수증 데이터 읽기 실패: " + error.localizedDescription) fail(transaction: transaction) } } else { DEBUG_LOG("영수증 데이터 읽기 실패") fail(transaction: transaction) } } else { fail(transaction: transaction) } } private func fail(transaction: SKPaymentTransaction) { if let transactionError = transaction.error as NSError?, let localizedDescription = transaction.error?.localizedDescription, transactionError.code != SKError.paymentCancelled.rawValue { DEBUG_LOG("Transaction Error: \(localizedDescription)") } DispatchQueue.main.async { [unowned self] in errorMessage = "결제를 진행하지 못했습니다.\n다시 시도해 주세요." isShowPopup = true } SKPaymentQueue.default().finishTransaction(transaction) SKPaymentQueue.default().remove(self) } }