In-App Purchase Tutorial: Getting Started
Learn how to grow app revenue in this in-app purchase tutorial by allowing users to purchase or unlock content or features. By Pietro Rea.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
In-App Purchase Tutorial: Getting Started
30 mins
- Getting Started
- Creating an App ID
- Checking Your Agreements
- Creating an App in iTunes Connect
- Creating In-App Purchase Products
- Creating a Sandbox User
- Project Configuration
- Listing In-App Purchases
- Purchased Items
- Making Purchases (Show Me The Money!)
- Making a Sandbox Purchase
- Restoring Purchases
- Payment Permissions
- Where To Go From Here?
Listing In-App Purchases
The store
property of RazeFaceProducts
is an instance of IAPHelper
. As mentioned earlier, this object interacts with the StoreKit API to list and perform purchases. Your first task is to update IAPHelper
to retrieve a list of IAPs — there’s only one so far — from Apple’s servers.
Open IAPHelper.swift. At the top of the class, add the following private property:
private let productIdentifiers: Set<ProductIdentifier>
Next, add the following to init(productIds:)
before the call to super.init()
:
productIdentifiers = productIds
An IAPHelper
instance is created by passing in a set of product identifiers. This is how RazeFaceProducts
creates its store
instance.
Next, add these other private properties just under the one you added a moment ago:
private var purchasedProductIdentifiers: Set<ProductIdentifier> = []
private var productsRequest: SKProductsRequest?
private var productsRequestCompletionHandler: ProductsRequestCompletionHandler?
purchasedProductIdentifiers
tracks which items have been purchased. The other two properties are used by the SKProductsRequest
delegate to perform requests to Apple servers.
Next, still in IAPHelper.swift replace the implementation of requestProducts(_:)
with the following:
public func requestProducts(completionHandler: @escaping ProductsRequestCompletionHandler) {
productsRequest?.cancel()
productsRequestCompletionHandler = completionHandler
productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
productsRequest!.delegate = self
productsRequest!.start()
}
This code saves the user’s completion handler for future execution. It then creates and initiates a request to Apple via an SKProductsRequest
object. There’s one problem: the code declares IAPHelper
as the request’s delegate, but it doesn’t yet conform to the SKProductsRequestDelegate
protocol.
To fix this, add the following extension to the very end of IAPHelper.swift, after the last curly brace:
// 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
}
}
This extension is used to get a list of products, their titles, descriptions and prices from Apple’s servers by implementing the two methods required by the SKProductsRequestDelegate
protocol.
productsRequest(_:didReceive:)
is called when the list is succesfully retrieved. It receives an array of SKProduct
objects and passes them to the previously saved completion handler. The handler reloads the table with new data. If a problem occurs, request(_:didFailWithError:)
is called. In either case, when the request finishes, both the request and completion handler are cleared with clearRequestAndHandler()
.
Build and run. Hooray! A list of products (only one so far) is displayed in the table view! It took some work, but you got there in the end.
Still stuck? As you can see, there’s a lot of setting up to do for IAP. Try this tutorial’s comments for a discussion with other readers.
- Does the project’s Bundle ID match the App ID from the iOS Development Center?
- Is the full product ID being used when making an
SKProductRequest
? (Check theproductIdentifiers
property ofRazeFaceProducts
.) - Is the Paid Applications Contract in effect on iTunes Connect? It can take hours to days for them to go from pending to accepted from them moment you submit them.
- Have you waited several hours since adding your product to App Store Connect? Product additions may be active immediately or may take some time.
- Check Apple Developer System Status. Alternatively, try this link. If it doesn’t respond with a status value, then the iTunes sandbox may be down. The status codes are explained in Apple’s Validating Receipts With the App Store documentation.
- Have IAPs been enabled for the App ID? (Did you select Cleared for Sale earlier?)
- Have you tried deleting the app from your device and reinstalling it?
Purchased Items
You want to be able to determine which items are already purchased. To do this, you’ll use the purchasedProductIdentifiers
property added earlier. If a product identifier is contained in this set, the user has purchased the item. The method for checking this is straightforward.
In IAPHelper.swift, replace the return
statement in isProductPurchased(_:)
with the following:
return purchasedProductIdentifiers.contains(productIdentifier)
Saving purchase status locally alleviates the need to request such data to Apple’s servers every time the app starts. purchasedProductIdentifiers
are saved using UserDefaults
.
Still in IAPHelper.swift, replace init(productIds:)
with the following:
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()
}
For each product identifier, you check whether the value is stored in UserDefaults
. If it is, then the identifier is inserted into the purchasedProductIdentifiers
set. Later, you’ll add an identifier to the set following a purchase.
UserDefaults
plist, and modify it to ‘unlock’ purchases. If this sort of thing concerns you, then it’s worth checking out Apple’s documentation on Validating App Store Receipts — this allows you to verify that a user has made a particular purchase.Making Purchases (Show Me The Money!)
Knowing what a user has purchased is great, but you still need to be able to make the purchases in the first place! Implementing purchase capability is the logical next step.
Still in IAPHelper.swift, replace buyProduct(_:)
with the following:
public func buyProduct(_ product: SKProduct) {
print("Buying \(product.productIdentifier)...")
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
This creates a payment object using an SKProduct
(retrieved from the Apple server) to add to a payment queue. The code utilizes a singleton SKPaymentQueue
object called default()
. Boom! Money in the bank. Or is it? How do you know if the payment went through?
Payment verification is achieved by having the IAPHelper
observe transactions happening on the SKPaymentQueue
. Before setting up IAPHelper
as an SKPaymentQueue
transactions observer, the class must conform to the SKPaymentTransactionObserver
protocol.
Go to the very bottom (after the last curly brace) of IAPHelper.swift and add the following extension:
// 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
}
}
}
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)
}
}
That’s a lot of code! A detailed review is in order. Fortunately, each method is quite short.
paymentQueue(_:updatedTransactions:)
is the only method actually required by the protocol. It gets called when one or more transaction states change. This method evaluates the state of each transaction in an array of updated transactions and calls the relevant helper method: complete(transaction:)
, restore(transaction:)
or fail(transaction:)
.
If the transaction was completed or restored, it adds to the set of purchases and saves the identifier in UserDefaults
. It also posts a notification with that transaction so that any interested object in the app can listen for it to do things like update the user interface. Finally, in both the case of success or failure, it marks the transaction as finished.
All that’s left is to hook up IAPHelper
as a payment transaction observer. Still in IAPHelper.swift, go back to init(productIds:)
and add the following line right after super.init()
.
SKPaymentQueue.default().add(self)