티스토리 뷰

iOS/IAP

[ IAP ] 1. Essentials

은조공주 2019. 12. 11. 23:27
반응형

안녕하세요 은조공주🥰 입니다. ^__^

Overview 글을 쓰고 한달만에... 1편을 올릴 수 있게 되었습니다.

( 2021-03-16 기준 Apple document 업데이트 내용 반영되었습니다.)

 

[ 1.Setting Up the Transaction Observer and Payment Queue ]

앱에 Observer를 추가함으로써 transaction을 받고, 처리할 수 있도록 한다.

 

앱 내 트랜잭션 수행을 위해서, 옵저버를 생성해 payment queue에 추가해야 합니다.

(여기서 트랜잭션은, 한번의 "거래" 와 같은 느낌입니다. 상품을 사고 파는, 거래 하나하나..? Payment Queue - 결제 대기열 에 그 "거래 -transaction"들이 줄지어있고, 자기 자신이 완료되기 위해서 "결제 - payment" 가 이루어지길 기다리고 있는 느낌입니다. 그리고 옵저버는 그 대기줄을 관리하고 있는 감시자 같은 존재..?)

옵저버 객체가 새로운 트랜잭션을 감지하고, pending transaction 이 있는 queue를 App Store 와 동기화해줍니다.

그리고 payment queue는 유저에게 결제 authorization 을 요청합니다. (상품 정보와 함께 지문인식을 요구하는 그 뷰...)

Payment Queue 노티피케이션을 최대한 빨리 받을 수 있기 위해서, App launch 와 함께 이 옵저버가 큐에 추가되어야 합니다.

(Single observer ( -> shared instance) 를 사용하는 것이 권장된다고 합니다.)

 

 Create an Observer 

Payment queue 의 변화를 핸들링할, 커스텀 옵저버 클래스를 생성해야 합니다.

class StoreObserver: NSObject, SKPaymentTransactionObserver {
                ....
    //Initialize the store observer.
    override init() {
        super.init()
        //Other initialization here.
    }

    //Observe transaction updates.
    func paymentQueue(_ queue: SKPaymentQueue,updatedTransactions transactions: [SKPaymentTransaction]) {
        //Handle transaction states here.
    }
                ....
}

옵저버 인스턴스를 생성합니다.

let iapObserver = StoreObserver()

 

Add an Observer

SKPaymentQueue.default().add(observer) 가 불리면, StoreKit 이 이 옵저버를 큐에 Attach 합니다.

(대기줄에 감시자를 한명 붙이는 느낌입니다.)

SKPaymentQueue.default().add(iapObserver)

StoreKit은 앱이 다시 시작되거나(resume), 혹은 실행되는 동안 payment queue 의 내용이 변경되면 이를 옵저버 인스턴스에 자동으로 알려줍니다.

import UIKit
import StoreKit

class AppDelegate: UIResponder, UIApplicationDelegate {
                ....
    // Attach an observer to the payment queue.
    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) {
        // Remove the observer.
        SKPaymentQueue.default().remove(iapObserver)
    }
                ....
}

application(_:didFinishLaunchingWithOptions:) 에서 옵저버를 추가해야 앱이 항상 모든 payment queue 노티피케이션을 "영구적으로" 수신할 수 있습니다. (이것은 중요합니다!!!!) 앱이 백그라운드로 보내지더라도 옵저버는 메모리에서 해제되지 않고 영구적으로 존재해야 합니다. 옵저버가 영구적으로 계속 살아있어야만 앱이 백그라운드에 있을 때 발생할 수 있는 이벤트 ( 예를 들면 ARS 의 갱신 트랜잭션 등 ) 를 감지할 수 있습니다.

 

Remove an Observer

SKPaymentQueue.default().remove(observer) 가 불리면, StoreKit 이 이 옵저버를 큐에서 제거합니다.

SKPaymentQueue.default().remove(iapObserver)

옵저버가 더이상 존재하지 않는데 payment queue 에서 제거되지 않은 경우에, Store Kit 이 이 옵저버에게 변경 통지를 시도하여 앱 크래시를 일으킬 수 있습니다. Payment queue 에서 옵저버를 제거할 때는, 큐에서 변경된 내용을 감지할 수 없고, 필요에 따라 컨텐츠를 전달해줄 수 없을 수도 있다는 것을 명심해야 합니다.

 

* SKPaymentTransactionState

  • purchasing : transaction이 App Store에 의해 프로세싱되고 있음
  • purchased : transaction이 성공적으로 프로세싱되었음
  • failed : 실패한 transaction
  • restored : 사용자가 이전에 구매한 컨텐츠를 복원하는 transaction
  • deferred : 대기큐에 있긴 하지만, 최종 상태에서 구매요청 등의 외부 작업을 보류 중인 transaction

 

[ 2.Offering, Completing, and Restoring In-App Purchases ]

Fetch / Complete / Restore

 

이 장에서는 샘플 코드를 사용해서, StoreKit 을 사용해 상품을 노출하고 / 제공하고 / 복구하는 방법을 설명합니다.

(샘플코드는 https://developer.apple.com/documentation/storekit/in-app_purchase/offering_completing_and_restoring_in-app_purchases 에서 다운로드 하실 수 있습니다.)

 

먼저, 앱에서는 앱 런칭부터 모든 payment 트랜잭션을 관리하고, 트랜잭션 상태들을 핸들링하기 위해 단일 트랜잭션 큐 옵저버를 사용하고 있어야 합니다. (SKPaymentTransactionObserver 프로토콜을 따르는 커스텀 클래스의 shared instance 합니다.) 그리고 앱 종료시에는 이 옵저버를 remove 해야 합니다.

앱은, 앱스토어에서 현지화된 상품 정보를 가져오기 전에 먼저 해당 유저가 해당 기기에서 결제를 할 수 있는지 확인해야 합니다. 그리고 앱의 UI 에는 유저가 실제로 구매할 수 있는 상품만 노출해야 합니다.

만약 앱에서 non-consumable 상품이나 auto-renewable subscription, 그리고 non-renewing subscription 상품을 판매할 경우, 유저에게 구매 복원 기능을 제공해야 합니다. (restore) Payment transaction 을 finish 하기 전에, 구매한/복원한 컨텐츠를 제공했다는 것을 확실히 해야합니다.

 

이 포스팅에서는 샘플 코드 프로젝트에 대해서는 자세히 다루지 않을 예정입니다. 따라서 가이드에서 제공된 프로젝트에 국한되는, 자세한 설명은 스킵하도록 하겠습니다. ^_^

 

Display Available Products for Sale with Localized Pricing

앱 내에 상품을 노출하기 전에, 유저가 해당 단말에서 결제를 할 수 있는 상황인지 먼저 체크해야 합니다.

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

만약 유저가 해당 단말에서 구매 가능한 상태라면, 앱스토어로 product request 를 보내야 합니다. 앱스토어를 Querying 하면, 앱에서 유저가 구매할 수 있는 제품만 제공할 수 있습니다.

먼저, 앱에서 판매하려고 하는 제품들의 product identifier 를 가지고 product request 를 초기화해야합니다.(참고 : Product ID ) 이 product request 객체에 대해서는 강한 참조를 유지해야 합니다. (Strong reference) 그렇지 않으면 리퀘스트가 완료되기 전에 해당 객체가 릴리즈될 수 있습니다.

fileprivate func fetchProducts(matchingIdentifiers identifiers: [String]) {
	// Create a set for the product identifiers.
	let productIdentifiers = Set(identifiers)

	// Initialize the product request with the above identifiers.
	productRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
	productRequest.delegate = self

	// Send the request to the App Store.
	productRequest.start()
}

앱스토어에서는 이 product request 에 SKResponse 객체를 돌려줍니다. 이 SKResponse 객체 내의 products 프로퍼티에는 앱스토어에서 실제로 구매 가능한 모든 상품의 정보가 들어있습니다. 이 정보를 가지고 앱 UI 를 업데이트 하면 됩니다.

SKResponse 객체의 invalidProductIdentifiers 프로퍼티는 앱스토어에서 인식하지 못한 모든 product identifier 를 가지고 있습니다.  (앱스토어에서 invalid product identifier 로 인식하는 다양한 이유는 invalidProductIdentifiers 에서 참고하세요.)

// products contains products whose identifiers have been recognized by the App Store. As such, they can be purchased.
if !response.products.isEmpty {
	availableProducts = response.products
	storeResponse.append(Section(type: .availableProducts, elements: availableProducts))
}

// invalidProductIdentifiers contains all product identifiers not recognized by the App Store.
if !response.invalidProductIdentifiers.isEmpty {
	invalidProductIdentifiers = response.invalidProductIdentifiers
	storeResponse.append(Section(type: .invalidProductIdentifiers, elements: invalidProductIdentifiers))
}

앱의 UI 에 상품 가격을 노출하려면, 앱스토어에서 리턴해주는 locale 과 currency 정보를 사용해야 합니다. 예를 들어서, 디바이스의 언어는 US 이면서 프랑스 앱스토어에 로그인해서 상품을 구매하려는 유저에게, 앱스토어는 상품 가격을 유로로 노출합니다. 따라서, 앱에서 자체적으로 디바이스 로케일을 가지고 상품 가격을 US 달러로 환산하여 보여주는 것은 부정확할 수 있겠죠.

extension SKProduct {
	/// - returns: The cost of the product formatted in the local currency.
	var regularPrice: String? {
		let formatter = NumberFormatter()
		formatter.numberStyle = .currency
		formatter.locale = self.priceLocale
		return formatter.string(from: self.price)
	}
}

 

Handle Payment Transaction States

Payment Queue 에 트랜잭션이 대기하고 있을 때, StoreKit 이 paymentQueue(_:updatedTransactions:) 델리게이트 메소드를 통하여 앱 내의 트랜잭션 옵저버에게 이를 알려줍니다. 모든 트랜잭션은 다섯개의 상태를 가집니다. -> .purchasing, .purchased, .failed, .restored, .deferred (SKPaymentTransactionState)

앱 내 옵저버의 paymentQueue(_:updatedTransactions:) 가 이 모든 다섯개의 상태에 대해 항상 대응할 수 있어야 합니다.

앱이 애플에서 호스팅하는 상품을 제공하는 경우, 옵저버에 paymentQueue(_:updatedDownloads:) 메소드가 구현되어 있어야 합니다.

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

트랜잭션이 실패하면, error 프로퍼티를 검사하여 에러 핸들링을 하되 error code 가 .paymentCancelled 가 아닐 때에만 에러를 표시합니다.

if let error = transaction.error {
	message += "\n\(Messages.error) \(error.localizedDescription)"
	print("\(Messages.error) \(error.localizedDescription)")
}

// Do not send any notifications when the user cancels the purchase.
if (transaction.error as? SKError)?.code != .paymentCancelled {
	DispatchQueue.main.async {
		self.delegate?.storeObserverDidReceiveMessage(message)
	}
}

유저가 트랜잭션을 defer 하면, 트랜잭션이 업데이트 되기를 기다리는 동안 유저가 계속해서 앱을 사용할 수 있도록 앱의 UI 를 차단하지 않아야 합니다.

 

Restore Completed Purchases

앱 내에서 non-consumable, auto-renewable subscription, 혹은 non-renewing subscription 상품을 판매한다면, 해당 상품이 복구될 수 있는 UI 를 반드시 제공해야 합니다. 유저는 그들의 모든 디바이스에서 무한정으로 앱 내 구입한 컨텐츠를 이용할 수 있어야 합니다.

/// Called when tapping the "Restore" button in the UI.
@IBAction func restore(_ sender: UIBarButtonItem) {
	// Calls StoreObserver to restore all restorable purchases.
	StoreObserver.shared.restore()
}

SKPaymentQueue 의 restoreCompletedTransactions() 를 사용하여, 앱 내의 non-consumable, auto-renewable subscription 을 복구해야 합니다. StoreKit 은 앱의 트랜잭션 옵저버의 paymentQueue(_:updatedTransactions:) 에 “restored” state 를 전달해줍니다.

(만약 복구가 실패한다면, https://developer.apple.com/documentation/storekit/skpaymentqueue/1506123-restorecompletedtransactions 참조) Non-renewing subscriptions 를 복구하는 것은 이 샘플 코드에는 포함되어 있지 않습니다.

 

Provide Content and Finish the Transaction

앱에서 state가 .purchased 이거나 .restored 인 트랜잭션을 받으면 컨텐츠를 제공해주거나, 구입한 기능을 언락해줘야 합니다. 이 상태값들은, 결제가 완료되었고 이제는 유저에게 상품을 제공해야한다는 뜻입니다.

만약 구입한 상품에 앱스토어에 호스팅된 컨텐츠가 포함되었다면, SKPaymentQueue 의 start(_:) 를 꼭 호출해줘야 한다.

트랜잭션은 payment queue 에 머물러 있을 것이고, Queue 에서 제거될 때까지 Storekit 은 앱이 켜지거나, BG에서 FG 로 올라올 때마다 앱 옵저버의 paymentQueue(_:updatedTransactions:) 를 호출할 것입니다. 결과적으로 유저에게 구매 인증을 반복적으로 요청하거나, 상품 구매가 막힐 수도 있습니다.

.failed, .purchased, .restored 인 트랜잭션에 대해서 finishTransaction(_:) 를 호출해야 해서 Queue 에서 제거해야 합니다. (이것은 아주 중요한 일입니다.) 그리고 이 동작은 복구 불가능합니다. (Finished transactions are not recoverable) 그러므로, 트랜잭션을 피니시 처리 하기 전에 유저에게 상품을 확실히 지급 했는지 확인해야 합니다. 만약 유저가 상품을 지급받지 못한 채로 트랜잭션이 finish 됐다면, 이 트랜잭션을 다시 열 방법이 없기 때문입니다.

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

 

[ 3.SKPaymentQueue ]

앱스토어에 의해 처리되는 결제 트랜잭션 대기열

class SKPaymentQueue : NSObject

Payment Queue 는 앱스토어와 통신하고, 유저가 결제를 진행할 수 있도록 UI 를 제공합니다. 앱의 런칭 사이사이에도 Queue 의 내용물은 영구적으로 유지됩니다.

결제를 처리하기 위해, 먼저 하나 이상의 옵저버 객체(SKPaymentTransactionObserver -> add(_:))를 큐에 추가해야합니다. 그리고, 유저가 구매하기를 원하는 상품의 payment 객체(SKPayment)를 추가해야 합니다. Payment 객체(SKPaymentTransaction)를 추가할 때마다, queue 는 트랜잭션 객체를 생성하여 해당 payment 를 처리하도록 합니다. 결제가 완료되면, queue는 트랜잭션 객체를 업데이트하고, 옵저버 객체에 그 업데이트된 트랜잭션을 갖다줍니다. 옵저버는 트랜잭션을 처리하고 이를 queue에서 제거해야 합니다.

 

지불이 완료된(?), 결제 처리가 왼료된(...?) 트랜잭션을 처리하는 정확한 매커니즘은, 앱의 설계나 상품 구조에 따라 달라집니다.

예를 들어 -

  • 상품이 이미 앱에 내장된 기능인 경우, 앱에서 기능을 언락해주고 트랜잭션을 처리할 수 있습니다.
  • 앱스토어에 호스팅된 컨텐츠일 경우, 앱은 SKDownload 객체를 가져와서 payment queue 에 해당 객체를 다운로드하도록 요청해야 합니다.
  • 상품이 자체 서버에서 제공하는 다운로드 가능한 컨텐츠인 경우, 앱은 자체 서버에 대해서 컨텐츠 언락을 요청해야 합니다.

Determining Whether the User Can Make Payments

class func canMakePayments() -> Bool // 유저가 결제를 할 수 있는 상황인지

Determining Store Content

var storefront: SKStorefront? // Payment queue를 위한 현재 앱스토어 스토어프론트

Getting the Queue

class func `default`() -> Self // 디폴트 payment queue 인스턴스

Adding and Removing the Observer

func add(SKPaymentTransactionObserver) // Payment queue 에 옵저버를 추가

func remove(SKPaymentTransactionObserver) // Payment queue 에서 옵저버를 제거

Managing Transactions

var delegate: SKPaymentQueueDelegate? // 트랜잭션을 처리하기 위해 필요한 정보를 제공하는 delegate

var transactions: [SKPaymentTransaction] // Pending transaction들의 배열

func add(SKPayment) // Queue 에 payment request 를 추가

func finishTransaction(SKPaymentTransaction) // Pending transaction 을 완료 처리 (피니시처리)

Restoring Purchases

func restoreCompletedTransactions() // Payment queue 에 이전에 완료 처리된 구입(거래..?)을 복구하도록 요청func restoreCompletedTransactions(withApplicationUsername: String?) // Payment queue 에 이전에 완료 처리된 구입(거래..?)을 복구하도록, 유저 계정의 identifier와 함께 요청

Showing Price Consent 

func showPriceConsentIfNeeded() // 사용자가 아직 구독 가격 인상에 응답하지 않은 경우 시스템에 가격 동의서를 표시하도록 요청

Redeeming Codes

func presentCodeRedemptionSheet() // 리딤 코드를 입력할 수 있는 sheet view 를 노출하도록 요청

Downloading Content

func start([SKDownload]) // 다운로드 리스트에 다운로드 세트를 추가

func cancel([SKDownload]) // 다운로드 리스트에서 다운로드 세트를 제거

func pause([SKDownload]) // 다운로드를 일시정지

func resume([SKDownload]) // 다운로드 재개

 

[ 4.SKPaymentQueueDelegate ]

트랜잭션을 완료하는 데에 필요한 정보를 제공하기 위해 구현된 프로토콜

protocol SKPaymentQueueDelegate

이 프로토콜에는 유저의 앱 스토어 -스토어프론트 가 변경되었을 경우 해당 앱에서 트랜잭션을 계속할지 말지 - 여부를 결정할 수 있는 메소드들이 포함되어 있습니다.

Continuing Transactions

func paymentQueue(SKPaymentQueue, shouldContinue: SKPaymentTransaction, in: SKStorefront) -> Bool // 트랜잭션 도중에 앱스토어의 storefront 가 편경된 경우, 트랜잭션을 계속 해야하는지 delegate 에 묻기

 

Showing Price Consent

func paymentQueueShouldShowPriceConsent(SKPaymentQueue) -> Bool // 새로운 가격에 관한 동의 sheet 를 즉시 보여줄지를 delegate 에 묻기

 

[ 5.SKPaymentTransactionObserver ]

트랜잭션을 처리하고, 구입한 기능을 언락하고, 인앱결제 프로모션을 계속하기 위한 메소드들의 집합

protocol SKPaymentTransactionObserver

이 프로토콜의 메소드들은 SKPaymentQueue 의 옵저버에 의해 구현됩니다.

Payment queue 에 의해 트랜잭션이 업데이트되거나, queue 에서 제거될 때 옵저버가 호출됩니다.

옵저버는 모든 성공한 트랜잭션을 처리하고 유저가 구입한 기능을 언락해주어야 하며 finishTransaction(_:) 을 호출해서 트랜잭션을 완료 처리 해야 합니다. (finish transaction 은, 유저에게 상품이 지급되었다는 것이 보장된 이후에 호출해야 합니다.)

 

Handling Transactions

func paymentQueue(SKPaymentQueue, updatedTransactions: [SKPaymentTransaction]) // 하나 이상의 트랜잭션이 업데이트되었음을 알려줌

func paymentQueue(SKPaymentQueue, removedTransactions: [SKPaymentTransaction])// 하나 이상의 트랜잭션이 큐에서 제거되었음을 알려줌

Handling Restored Transactions

func paymentQueue(SKPaymentQueue, restoreCompletedTransactionsFailedWithError: Error) // 트랜잭션을 restore 중 오류가 발생했음을 알려줌

func paymentQueueRestoreCompletedTransactionsFinished(SKPaymentQueue) // 트랜잭션 restore 가 완료되었음을 알림

Handling Download Actions

func paymentQueue(SKPaymentQueue, updatedDownloads: [SKDownload]) // 하나 이상의 다운로드 객체가 업데이트 되었음을 알려줌

Handling Purchases

func paymentQueue(SKPaymentQueue, shouldAddStorePayment: SKPayment, for: SKProduct) -> Bool // 유저가 앱스토어에서 IAP 를 시작했다고 알려줌

Revoking Entitlements

func paymentQueue(SKPaymentQueue, didRevokeEntitlementsForProductIdentifiers: [String]) // 유저가 더이상 가족 공유 구매를 사용할 수 없다고 알림

Handling Changes to the Storefront

func paymentQueueDidChangeStorefront(SKPaymentQueue) // Payment queue 의 storefront 가 변경되었음을 알려줌

 

 

[ 6. SKRequest ]

앱스토어로의 리퀘스트 클래스

class SKRequest : NSObject

 

리퀘스트를 생성하기 위해서, SKProductsRequest SKReceiptRefreshRequest 등의 SKRequest 의 하위클래스를 초기화해서, delegate  를 설정하고 start() 메소드를 호출합니다.

 

Controlling the Request

func start() // 리퀘스트를 앱스토어로 전송

func cancel() // 이전에 시작된 리퀘스트를 취소

Accessing the Delegate

var delegate: SKRequestDelegate? // 리퀘스트 객체의 델리게이트

protocol SKRequestDelegate

 

[ 참고 문서 ]

https://developer.apple.com/documentation/storekit/in-app_purchase/setting_up_the_transaction_observer_and_payment_queue https://developer.apple.com/documentation/storekit/in-app_purchase/offering_completing_and_restoring_in-app_purchases

https://developer.apple.com/documentation/storekit/skpaymentqueue

https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate

https://developer.apple.com/documentation/storekit/skpaymenttransactionobserver

https://developer.apple.com/documentation/storekit/skrequest

반응형

'iOS > IAP' 카테고리의 다른 글

[ IAP ] 5. Purchase Validation  (7) 2019.12.16
[ IAP ] 4. Purchases  (3) 2019.12.12
[ IAP ] 3. Storefronts  (0) 2019.12.12
[ IAP ] 2. Product Information  (1) 2019.12.12
[ IAP ] Overview  (0) 2019.11.12
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함