4가지 인앱 결제 방식
- 인앱 결제란 이름 그대로 앱 내에서 결제하는 것을 말한다.
- 이 글에서는 이 중에서 Non-Consumable 인앱 결제 구현 방법을 알아보겠다.
- 애플 앱스토어의 인앱 결제에는 4가지 종류가 있다.
- Consumable : 한 번 이상 구매가 가능하고 소비될 수 있다.
- Non-Consumable : 한 번만 구매가 가능하고 영구적으로 소유된다.
- Non-Renewing Subscription : 일정 기간 동안만 사용될 수 있다.
- Auto-Renewing Subscription : 반복되는 구독 방식의 인앱 결제.
인앱 결제 기본 작업
- 다음과 같이 인앱 결제를 위한 기본 작업을 해준다.
- 애플 개발자 사이트에서 Apple ID를 만든다.
- 개발자 Agreements를 확인하고 동의한다.
- Feature에서 인앱 결제를 생성한다.
- Users and Access → Sandbox → Testers에서 유저를 생성한다.
Non-Consumable 구현
- In-App Purchase를 도와주는 아래 소스코드를 프로젝트에 추가한다.
import StoreKit
public typealias ProductIdentifier = String
public typealias ProductsRequestCompletionHandler = (_ success: Bool, _ products: [SKProduct]?) -> Void
extension Notification.Name {
static let IAPHelperPurchaseNotification = Notification.Name("IAPHelperPurchaseNotification")
}
open class IAPHelper: NSObject {
private let productIdentifiers: Set<ProductIdentifier>
private var purchasedProductIdentifiers: Set<ProductIdentifier> = []
private var productsRequest: SKProductsRequest?
private var productsRequestCompletionHandler: ProductsRequestCompletionHandler?
public init(productIds: Set<ProductIdentifier>) {
productIdentifiers = productIds
for productIdentifier in productIds {
let purchased = UserDefaults.standard.bool(forKey: productIdentifier)
if purchased {
purchasedProductIdentifiers.insert(productIdentifier)
print("Previously purchased: \(productIdentifier)")
} else {
print("Not purchased: \(productIdentifier)")
}
}
super.init()
SKPaymentQueue.default().add(self)
}
}
extension IAPHelper {
public func requestProducts(_ completionHandler: @escaping ProductsRequestCompletionHandler) {
productsRequest?.cancel()
productsRequestCompletionHandler = completionHandler
productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
productsRequest!.delegate = self
productsRequest!.start()
}
public func buyProduct(_ product: SKProduct) {
print("Buying \(product.productIdentifier)...")
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
public func isProductPurchased(_ productIdentifier: ProductIdentifier) -> Bool {
return purchasedProductIdentifiers.contains(productIdentifier)
}
public class func canMakePayments() -> Bool {
return SKPaymentQueue.canMakePayments()
}
public func restorePurchases() {
SKPaymentQueue.default().restoreCompletedTransactions()
}
}
extension IAPHelper: SKProductsRequestDelegate {
public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print("Loaded list of products...")
let products = response.products
productsRequestCompletionHandler?(true, products)
clearRequestAndHandler()
for p in products {
print("Found product: \(p.productIdentifier) \(p.localizedTitle) \(p.price.floatValue)")
}
}
public func request(_ request: SKRequest, didFailWithError error: Error) {
print("Failed to load list of products.")
print("Error: \(error.localizedDescription)")
productsRequestCompletionHandler?(false, nil)
clearRequestAndHandler()
}
private func clearRequestAndHandler() {
productsRequest = nil
productsRequestCompletionHandler = nil
}
}
extension IAPHelper: SKPaymentTransactionObserver {
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch (transaction.transactionState) {
case .purchased:
complete(transaction: transaction)
break
case .failed:
fail(transaction: transaction)
break
case .restored:
restore(transaction: transaction)
break
case .deferred:
break
case .purchasing:
break
@unknown default:
fatalError()
}
}
}
private func complete(transaction: SKPaymentTransaction) {
print("complete...")
deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func restore(transaction: SKPaymentTransaction) {
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
print("restore... \(productIdentifier)")
deliverPurchaseNotificationFor(identifier: productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func fail(transaction: SKPaymentTransaction) {
print("fail...")
if let transactionError = transaction.error as NSError?,
let localizedDescription = transaction.error?.localizedDescription,
transactionError.code != SKError.paymentCancelled.rawValue {
print("Transaction Error: \(localizedDescription)")
}
SKPaymentQueue.default().finishTransaction(transaction)
}
private func deliverPurchaseNotificationFor(identifier: String?) {
guard let identifier = identifier else { return }
purchasedProductIdentifiers.insert(identifier)
UserDefaults.standard.set(true, forKey: identifier)
NotificationCenter.default.post(name: .IAPHelperPurchaseNotification, object: identifier)
}
}
- 이 IAPHelper 스위프트 파일이 사실 인앱 결제의 대부분을 차지한다.
- 이제 구입을 원하는 Controller에 다음과 같은 구조체를 만든다.
public struct InAppProducts {
public static let product = "(인앱 Product ID)"
private static let productIdentifiers: Set<ProductIdentifier> = [InAppProducts.product]
public static let store = IAPHelper(productIds: InAppProducts.productIdentifiers)
}
- 그리고 원하는 구입 버튼 action에 다음과 같이 코드를 작성한다.
InAppProducts.store.buyProduct(product)
- 이제 결제가 되는지 확인해볼 차례이다.
- 참고로 인앱 결제는 시뮬레이터에서는 작동하지 않는다.
- 아이폰 Settings → iTunes & App Store에서 로그아웃한다.
- 결제 버튼을 눌러보면, 로그인 창이 뜬다.
- 생성했던 Sandbox 유저 로그인 정보를 입력한다.
- Sandbox 유저로 테스트 결제가 완료된다.
- 복원 기능을 사용하기 위해서는 다음과 같이하면 된다.
InAppProducts.store.restorePurchases()