async / await
도입
import StoreKit
SKProduct
: 상품 정보 (price, name, ...)SKPayment
: 지불 정보 (product id, request data)SKPaymentRequest
: 결제 요청 시 사용 타입 (delegate로 성공/실패 알려줌)SKPaymentQueue.default()
: 데이터 가져오기에 사용ProductRequestCompletionHandler
// completion 정의. (외부에서 정의하고, 내부에서 실행)
typealias ProductRequestCompletionHandler = (_ success: Bool, _ products: [SKProduct]?) -> Void
protocol IAPServiceType
외부에서 필요한 메서드 명시
getProducts
: products 항목 (상품 정보) 가져오기buyProduct
: product 구입isProductPurchased
: 구입했는지 확인restorePurchased
: 구입한 목록 확인// 프로토콜 -> 외부에서 필요한 메서드 명시
protocol IAPServiceType {
var canMakePayments: Bool { get }
func getProducts(completion: @escaping ProductRequestCompletionHandler)
func buyProduct(_ product: SKProduct)
func isProductPurchased(_ productID: String) -> Bool
func restorePurchases()
}
class IAPService
NSObject
상속 (StoreKit 사용), IAPServiceType
채택class IAPService: NSObject, IAPServiceType {
필요한 프로퍼티 선언 및 init
productIDs
: 앱스토어 커넥트에 등록된 productIDpurchaseProductIDs
: 구매한 productIDproductRequest
: productID로 부가 정보 조회하기 위한 인스턴스productsCompletionHandler
: 사용하는 쪽에서 성공/실패 했을 때 completion을 통해 값을 넘겨줄 수 있다.// 필요한 프로퍼티
private let productIdentifiers: Set<String>
private var purchasedProducts: Set<String> = []
private var productRequest: SKProductsRequest?
private var productsCompletionHandler: ProductsRequestCompletionHandler?
// 상품 정보 받아서 초기화 및 SKPaymentQueue 연결
init(productIdentifiers: Set<String>) {
// "com.blogPost.App.~~"
self.productIdentifiers = productIdentifiers
self.purchasedProducts = productIdentifiers.filter {
// UserDefaults에 저장해둔 구매 여부
UserDefaults.standard.bool(forKey: $0) == true
}
super.init()
SKPaymentQueue.default().add(self)
// App Store와 지불정보를 동기화하기 위한 Observer
// -> SKPaymentTransactionObserver 프로토콜 채택
}
canMakePayment
: 사용자의 디바이스가 현재 결제가 가능한지 확인var canMakePayments: Bool {
return SKPaymentQueue.canMakePayments()
}
프로토콜 메서드 정의
// 상품 정보 조회
func getProducts(completion: @escaping ProductsRequestCompletionHandler) {
self.productRequest?.cancel()
self.productsCompletionHandler = completion
self.productRequest = SKProductsRequest(productIdentifiers: self.productIdentifiers)
self.productRequest?.delegate = self
// -> SKProductsRequestDelegate 채택
self.productRequest?.start() // 인앱 상품 조회 시작
// -> delegate 함수 실행 (SKProductsRequestDelegate)
}
// 상품 구입
func buyProduct(_ product: SKProduct) {
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
// -> paymentQueue() 함수 실행
}
// 구입 확인
func isProductPurchased(_ productID: String) -> Bool {
return self.purchasedProducts.contains(productID)
}
// 구입 내역 복원
func restorePurchases() {
SKPaymentQueue.default().restoreCompletedTransactions()
}
extension IAPService: SKProductsRequestDelegate
getProducts
에서 completionHandler를 캡쳐해두고, 여기서 실행한다
extension IAPService: SKProductsRequestDelegate {
// App Store Connect에서 상품 정보 조회
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
let products = response.products
self.productsCompletionHandler?(true, products)
self.clearRequestAndHandler()
// 출력
products.forEach { item in
print("Found Product : \(item.productIdentifier) \(item.localizedTitle)")
}
}
// fail
func request(_ request: SKRequest, didFailWithError error: Error) {
self.productsCompletionHandler?(false, nil)
self.clearRequestAndHandler()
// 출력
print("Error : \(error.localizedDescription)")
}
// 초기화
private func clearRequestAndHandler() {
self.productRequest = nil
self.productsCompletionHandler = nil
}
}
extension IAPService: SKPaymentTransactionObserver
SKPaymentQueue
에서 처리되는 일
purchased
, failed
, restored
, deferred
, purchasing
extension IAPService: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
print(#function)
for transaction in transactions {
let state = transaction.transactionState
switch state {
case .purchasing:
print("구매하는 중")
case .purchased:
print("구매 완료")
completePurchase(transaction: transaction)
case .failed:
print("구매 실패")
failPurchase(transaction: transaction)
case .restored:
print("구매 복원")
restorePurchase(transaction: transaction)
case .deferred:
print("deferred")
@unknown default:
fatalError()
}
}
}
// 구매 완료 (성공)
private func completePurchase(transaction: SKPaymentTransaction) {
print(#function)
guard let id = transaction.original?.payment.productIdentifier else { return }
deliverPurchaseNotificationFor(id: id)
SKPaymentQueue.default().finishTransaction(transaction)
}
// 구매 실패
private func failPurchase(transaction: SKPaymentTransaction) {
print(#function)
if let transactionError = transaction.error as NSError?,
transactionError.code != SKError.paymentCancelled.rawValue {
print("TransactionError : \(transactionError.localizedDescription)")
}
SKPaymentQueue.default().finishTransaction(transaction)
}
// 구매 복원 성공
private func restorePurchase(transaction: SKPaymentTransaction) {
print(#function)
guard let id = transaction.original?.payment.productIdentifier else { return }
deliverPurchaseNotificationFor(id: id)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func deliverPurchaseNotificationFor(id: String?) {
guard let id else { return }
self.purchasedProducts.insert(id)
UserDefaults.standard.set(true, forKey: id)
NotificationCenter.default.post(
name: .iapServicePurchaseNotification, // 곧 정의
object: id
)
}
func paymentQueue(_ queue: SKPaymentQueue, removedTransactions transactions: [SKPaymentTransaction]) {
print(#function)
}
}
extension Notification.Name
extension Notification.Name {
static let iapServicePurchaseNotification = Notification.Name("IAPServicePurchaseNotification")
}
enum MyProducts
Product ID를 가지고 있는 Wrapping 타입. (IAPService를 wrapping한다)
enum MyProducts {
static let productID = "com.blogPost.s_sub.heart100"
static let iapService: IAPServiceType = IAPService(productIdentifiers: Set<String>([productID]))
static func getResourceProductName(_ id: String) -> String? {
id.components(separatedBy: ".").last
}
}
class IAPTestViewController
class FinalIAPTestViewController: UIViewController {
private let restoreButton = UIButton()
private let buyButton = UIButton()
private let productLabel = UILabel()
private var products = [SKProduct]()
func setting() {...}
override func viewDidLoad() {
super.viewDidLoad()
setting()
getProducts()
}
// 상품 정보 조회
func getProducts() {
MyProducts.iapService.getProducts { [weak self] success, products in
if success,
let products = products {
DispatchQueue.main.async {
self?.products = products
self?.productLabel.text = "\(products.first?.productIdentifier ?? "")\n \(products.first?.localizedTitle ?? "")"
}
}
}
}
// 구매 버튼 클릭
@objc func buyButtonClicked() {
MyProducts.iapService.buyProduct(products.first!)
}
// 복구 버튼 클릭 (구입 목록 조회)
@objc func restoreButtonClicked() {
MyProducts.iapService.restorePurchases()
}
// 노티 세팅
func setNotification() {
NotificationCenter.default.addObserver(
self,
selector: #selector(handlePurchaseNoti(_:)),
name: .iapServicePurchaseNotification,
object: nil
)
}
@objc private func handlePurchaseNoti(_ notification: Notification) {
// 노티 핸들 (뷰 업데이트)
}
}
SKPaymentQueue.default().finishTransaction(transaction)
func getReceiptData(_ productIdentifier: String) -> String? {
if let receiptURL = Bundle.main.appStoreReceiptURL,
FileManager.default.fileExists(atPath: receiptFileURL.path) {
do {
let receiptData = try Data(contensOf: receiptFileURL)
let receiptString = receiptData.base64EncodedString(
options: NSData.Base64EncodingOptions(rawValue: 0)
)
return receiptString
} catch {
print("구매 이력 영수증 데이터 변환 과정에서 에러 : \(error.localizedDescription)")
return nil
}
}
print("구매 이력 영수증 URL 에러")
return nil
}
현재 구매 복원 관련해서 심사 거부를 당해서 담당자에게 서버에서 결제 정보를 수신한다고 했는데 그럼에도 구매 복원 버튼이 필요하다고 하더라구요.
마지막 구매복원 부분에서 "서버가 존재해서 구매 복원에 대한 기능을 서버에서 자체적으로 해주고 있다면, 앱 내에서 구현하지 않아도 된다." 라는 내용은 어디서 찾으신 건지 알 수 있을까요? 해당 자료를 토대로 문의를 해보고싶습니다!