iOS

iOS) StoreKit (3) - In App Purchase API

kangwook 2022. 10. 22. 20:37

In-App Purchase를 위한 Original API

Original In-App Purchase API를 사용하여 사용자에게 추가 컨텐츠 및 서비스를 제공

In-App Purchase를 통해 사용자는 인앱 콘텐츠 및 기능을 구매할 수 있는 기회를 제공할 수 있음

  • 앱 내에서 또는 AppStore에서 직접 구매할 수 있음

StoreKit은 앱을 대신하여 AppStore에 연결해 지불을 요청하고 안전하게 처리

  • 그런 다음 구입한 제품을 제공하는 앱에 notify
  • 구매를 확인하려면 서버에서 AppStore 또는 디바이스로 영수증을 확인할 수 있음
  • 자동 갱신 구독의 경우, AppStore는 주요 구독 이벤트를 서버에 알려줄 수도 있음

AppStore Connect에서 In-App Purchase 설정

In-App Purchase를 사용하려면 먼저 AppStore Connect에서 제품을 구성해야 함

 

https://help.apple.com/app-store-connect/#/devb57be10e7

To see this page, you must enable JavaScript. Pour afficher cette page, vous devez activer JavaScript. Zur Anzeige dieser Seite müssen Sie JavaScript aktivieren. このページを表示するには、JavaScript を有効にする必要があります。

help.apple.com

제품 타입 이해하기

4가지 Product Type

  • 소모품(Consumable)
    • 한 번 사용 후 고갈되는 유형
    • 고객이 여러 번 구매 가능
    • ex) 게임에서 라이프, 캐시, 캐시 아이템 등
  • 비소모품(Non-Consumable)
    • 고객이 한 번 구매하는 유형
    • 기한이 만료되지 않음(유통기한이 없음)
    • ex) 앱내 광고 해제, 잠금해제 등
  • 자동 갱신형 구독(Auto-Renewing Subscription)
    • 고객이 한 번 구매하고 고객이 취소를 결정할 때까지 반복적으로 자동 갱신하는 유형
  • 비갱신 구독(Non-Renewing Subscription)
    • 제한된 기간 동안 액세스를 제공하며 자동으로 갱신되지 않음
    • 고객은 다시 구매할 수 있음

StoreKit을 사용하여 디바이스 전체에서 비소모품 및 자동 갱신형 구독을 동기화하고 복원할 수 있음

사용자가 자동 갱신형 또는 비갱신형 구독을 구입하는 경우, 앱은 모든 사용자의 디바이스에서 사용할 수 있도록 하고 사용자가 이전 구매를 복원할 수 있도록 해야하는 책임이 있음

Payment Queue에 대한 Transaction Observer 설정

Observer를 추가하여 앱을 통해 트랜잭션을 수신하고 처리할 수 있음

앱에서 트랜잭션을 처리하려면 Payment Queue에 Observer를 만들고 추가해야 함

Observer 객체는 새로운 트랜잭션에 응답하고 보류 중인 트랜잭션의 큐를 AppStore와 동기화하며, Payment Queue는 사용자가 지불을 승인하도록 프롬프트함

앱을 시작할 때 트랜잭션 Observer를 추가하여 앱이 가능한 한 빨리 결제 대기열 알림을 받을 수 있도록 해야함.

Observer 생성

class StoreObserver: NSObject, SKPaymentTransactionObserver {
// ...
    // Store Observer 초기화
    override init() {
        super.init()
    }
 
    // Transaction 업데이트 Observing
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        // Transaction handling
    }
}
 
let iapObserver = StoreObserver();

Observer 추가

SKPaymentQueue.default().add(iapObserver)
class AppDelegate: UIResponder, UIApplicationDelegate {
    // ...
    // Attach an observer to the paymentQueue
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        SKPaymentQueue.default().add(iapObserver)  
        return true
    }
 
    // Called when the application is about to terminate
    func applicationWillTerminate(_ application: UIApplication) {
        SKPaymentQueue.default().remove(iapObserver)
    }
    // ...
}

In-App Purchase 제공, 완료, 복원

앱에서 트랜잭션을 가져오고 완료하고 복원하기

  • 해당 Sample Code는 In-App Purchase를 검색, 표시 및 복원하는 방법을 보여줌
  • 먼저 앱을 설정해서 시작 시 단일 트랜잭션 옵저버를 등록하고 사용
  • 트랜잭션 queue 옵저버는 모든 지불 트랜잭션을 관리하고 모든 트랜잭션 상태를 처리
  • SKPaymentTransactionObserver 프로토콜을 준수하는 Class의 Shared Instance인지 확인해야 함
  • 앱이 종료될 때는 트랜잭션 옵저버를 제거해야함

Sample Project 사전 세팅

샘플 코드 :https://developer.apple.com/documentation/storekit/in-app_purchase/original_api_for_in-app_purchase/offering_completing_and_restoring_in-app_purchases

 

Apple Developer Documentation

 

developer.apple.com

테스트 참조 : Testing In-App Purchases with Sandbox

  1. AppStore Connect에서 In-App Purchase를 지원하고 일부 In-App Purchase가 구성되어 있는 완성된 앱으로 시작
  2. AppStore Connect에서 Sandbox 테스트 사용자 계정 생성
  3. Sample Project의 Target에서 In-App Purchase 지원 및 프로비저닝 프로필 설정
  4. ProductIds.plist에서 Bundle ID를 업데이트
  5. iOS 및 tvOS의 경우 샘플이 In-App Purchase를 빌드하는데 사용하는 In-App Purchase 및 In-App Purchase(tvOS)  타겟을 각각 빌드하고 실행
  6. macOS의 경우 In-App purchases(macOS)를 구축하기 전에 Mac Appstore에서 로그아웃

현지화된 가격으로 판매 가능한 제품 표시

Sample Code는 판매용 제품을 제시하기 전에 사용자가 기기에서 결제할 수 있는 권한이 있는지 확인하도록 앱을 구성

var isAuthorizedForPayments: Bool {
    return SKPaymentQueue.canMakePayments()
}

앱이 권한을 확인하면 AppStore에 제품 요청을 보내, AppStore에서 현지화된 제품 정보를 가져옴

AppStore를 조회하면 앱에서 사용자에게 구매할 수 있는 제품만 제공

이 앱은 UI에서 판매하고자 하는 제품과 관련된 제품 식별자 목록으로 제품 요청을 초기화 함

제품 요청 객체에 대해 strong reference를 유지해야 함

fileprivate func fetchProducts(matchingIdentifiers identifiers: [String]) {
    // 제품 식별자에 대한 Set 생성
    let productIdentifiers = Set(identifiers)
     
    // 위 식별자들을 가지고 제품 요청 초기화
    productRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
    productRequest.delegate = self
 
    // AppStore로 요청 전송
    productRequest.start()
}

AppStore는 SKProductsResponse 객체로 제품 요청에 응답

제품 속성에는 실제로 AppStore에서 구매할 수 있는 모든 제품에 대한 정보가 포함되어 있음

앱은 이 속성을 이용하여 UI를 업데이트

응답의 invalidProductIdentifiers에는 AppStore에서 인식하지 못한 모든 제품 식별자가 포함되어 있음

// 제품에 AppStore에서 식별자를 인식한 제품이 포함되어 있을 경우
// 구매할 수 있음
if !response.products.isEmpty {
    availableProducts = response.products
}
 
// invalidProductIdentifiers에는 AppStore가 인식하지 못하는 모든 제품 식별자가 포함
if !response.invalidProductIdentifiers.isEmpty {
    invalidProductIdentifiers = response.invalidIdentifiers
}

제품의 가격을 UI에 표시하려면 AppStore에서 반환한 Locale과 통화를 사용하면됨

예를 들어 프랑스 앱스토어에 로그인하고 디바이스가 미국 Locale을 사용하는 User

  • 제품을 구매하려고 할 때 제품의 가격을 유로로 표시할 것
  • UI에서 디바이스의 Locale과 일치하도록 미국 달러를 변환하는 것은 잘못된 것
extension SKProduct {
    /// - returns: 현지 통화로 포맷팅된 제품의 가격
    var regularPrice: String? {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        formatter.locale = self.priceLocale
        return formatter.string(from: self.price)
    }
}

앱과 상호 작용하여 제품 구매

UI에서 판매 가능한 모든 제품을 눌러 구입

이 앱을 통해 사용자는 소모품이 아닌 제품 및 자동 갱신 구독을 복원할 수 있음

  • 복원 버튼, 설정 > 복원 가능한 모든 구매를 사용하여 앱의 iOS 및 tvOS 버전에서 이 기능을 각각 구현
  • macOS버전에서 구매를 복원하려면 저장 > 복원 항목 선택

구입한 항목을 탭하면 제품 식별자, 트랜잭션 식별자 및 트랜잭션 날짜와 같은 구매 정보가 표시됨

  • 구매된 아이템이 hosted product인 경우 구매 정보는 컨텐츠 식별자, 컨텐츠 버전, 컨텐츠 길이를 추가로 포함

구매가 복원된 경우 구매 정보에는 기존 트랜잭션의 식별자와 날짜도 포함

지불 트랜잭션(Payment Transaction) 상태 핸들링

트랜잭션이 payment queue에 대기 중일때 StoreKit은 해당 paymentQueue(_:updatedTransactions:) 메서드를 호출하여 앱의 트랜잭션 옵저버에게 알림

5가지 트랜잭션 상태

  • SKPaymentTransactionState.purchasing
  • SKPaymentTransactionState.purchased
  • SKPaymentTransactionState.failed
  • SKPaymentTransactionState.restored
  • SKPaymentTransactionState.deferred

앱은 옵저버의 paymentQueue(_:updatedTransactions:)가 언제든지 이러한 상태에 응답할 수 있도록 해야 함

Apple에서 호스팅하는 제품을 제공할 때 옵저버에 paymentQueue(_:updatedDownloads:) 메서드를 구현해야 함

func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
    for transaction in transactions {
        switch transaction.transactionState {
        case .purchasing: break
        // Do not block the UI. Allow the user to continue using the app.
        case .deferred: print(Messages.deferred)
        // The purchase was successful.
        case .purchased: handlePurchased(transaction)
        // The transaction failed.
        case .failed: handleFailed(transaction)
        // There're restored products.
        case .restored: handleRestored(transaction)
        @unknown default: fatalError(Messages.unknownPaymentTransaction)
        }
    }
}

트랜잭션이 실패하면 오류 속성을 검사하여 어떤 일이 발생했는지 확인

  • 사용자가 트랜잭션을 연기할 때 앱은 StoreKit이 트랜잭션
// 사용자가 구매를 취소할 때 알림을 보내지 말아야 함
if (transaction.error as? SKError)?.code != .paymentCancelled {
    DispatchQueue.main.async {
        self.delegate?.storeObserverDidReceiveMessage(message)
    }
}

완료된 구매를 복원하기

사용자가 비소모품, 자동 갱신형 구독 또는 비갱신형 구독을 구입할 때 모든 디바이스에서 무제한을 사용할 수 있을 것으로 기대

  • SKPaymentQueue restoreCompletedTransactions()를 사용하여 비소모품 및 자동 갱신형 구독을 복원
  • StoreKit은 각 복원된 트랜잭션에 대해 SKPaymentTransactionState.restored 상태로 paymentQueue(_:updatedTransactions:)를 호출하여 앱의 트랜잭션 옵저버에게 알려줌
@IBAction func restore(_ sender: UIBarButtonItem) {
    // Calls StoreObserver to restore all restorable purchases.
    StoreObserver.shared.restore()
}

내용 제공 및 트랜잭션 완료

앱은 SKPaymentTransactionState.purchased 또는 SKPaymentTransactionState.restored 상태의 트랜잭션을 수신한 후 콘텐츠를 전달하거나 구입한 기능의 잠금을 해제해야 함

  • AppStore가 사용자로부터 제품에 대한 지불을 받았음을 나타냄
  • 구입한 제품에 AppStore로부터 hosted content가 포함되어 있으면 SKPaymentQueue start(_:)를 호출하여 콘텐츠를 다운로드

완료되지 않은 트랜잭션은 payment queue에 남아있음

  • StoreKit은 앱이 이러한 트랜잭션을 완료할 때까지 백그라운드에서 실행하거나, 재개(resume)할 때마다 앱의 영구적인 observer의 paymentQueue(_:updatedTransactions:)를 호출
    • 이에 따라 AppStore는 사용자에게 구매 인증을 반복적으로 요청하거나 앱에서 제품을 구매하지 못하게 할 수 있음

상태가 

SKPaymentTransactionState.failed, 

SKPaymentTransactionState.purchased, 

SKPaymentTransactionState.restored인 트랜잭션에 대해 finishTransaction(_:)을 호출하여 대기열에서 제거

완료된 트랜잭션은 복구할 수 없으므로, 앱은 구매한 콘텐츠를 제공하거나, 제품의 모든 Apple 호스팅 컨텐츠를 다운로드하거나, 트랜잭션을 완료하기 전에 구매 프로세스를 완료해야 함

// Finish the successful transaction.
SKPaymentQueue.default().finishTransaction(transaction)