Swift 인앱 결제 구현하기

4가지 인앱 결제 방식

  • 인앱 결제란 이름 그대로 앱 내에서 결제하는 것을 말한다.
  • 애플 앱스토어의 인앱 결제에는 4가지 종류가 있다.

    • Consumable : 한 번 이상 구매가 가능하고 소비될 수 있다.
    • Non-Consumable : 한 번만 구매가 가능하고 영구적으로 소유된다.
    • Non-Renewing Subscription : 일정 기간 동안만 사용될 수 있다.
    • Auto-Renewing Subscription : 반복되는 구독 방식의 인앱 결제.
  • 이 글에서는 이 중에서 Non-Consumable 인앱 결제 구현 방법을 알아보겠다.

인앱 결제 기본 작업

  • 다음과 같이 인앱 결제를 위한 기본 작업을 해준다.

    1. 애플 개발자 사이트에서 App ID를 만든다.
    2. 개발자 Agreements를 확인하고 동의한다.
    3. 앱스토어 커넥트에서 앱을 생성한다.
    4. Feature에서 인앱 결제를 생성한다.
    5. 샌드박스 유저를 생성한다.

Non-Consumable 구현

  • In-App Purchase를 도와주는 아래 소스코드를 프로젝트에 추가한다.
IAPHelper.swift
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)
    }
}

// MARK: - StoreKit API

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()
    }
}

// MARK: - SKProductsRequestDelegate

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
    }
}

// MARK: - SKPaymentTransactionObserver

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에서 로그아웃한다.
  • 결제 버튼을 눌러보면, 로그인 창이 뜬다.
  • 생성했던 샌드박스 유저 로그인 정보를 입력한다.
  • 샌드박스 유저로 테스트 결제가 완료된다.
  • 복원 기능을 사용하기 위해서는 다음과 같이하면 된다.
InAppProducts.store.restorePurchases()

관련 글