Join us at Adapty Conf in Warsaw on June 13, 2024.
Secure your spot for free!

In-app purchases initialization for Swift with SKProduct

Vitaly Davydov

Updated: March 30, 2023

Content

612ce37cfadfe123ac4312e5 ios tutorial 2 initialization and purchases processing min

We continue our series of articles dedicated to iOS in-app purchases. In the previous part, we have discussed the process of creating in-app purchases and their configuration. You can read all the tutorials following the links:

  1. iOS in-app purchases, part 1: configuration and adding to the project.
  2. iOS in-app purchases, part 2: initialization and purchases processing.
  3. iOS in-app purchases, part 3: testing purchases in XCode.
  4. iOS in-app purchases, part 4: server-side receipt validation.
  5. iOS in-app purchases, part 5: the list of SKError codes and how to handle them.

Now we are going to show you how to create a simple paywall (payment screen) as well as how to initialize and process purchases that we have configured.

Creating an in-app subscription screen

Any app that uses in-app purchases has a paywall. There are some guidelines from Apple that determine must-have elements. At this step, we’ll implement just some of them to give you a sense of that to do next.

13123

So, our screen will have the following:

  • Header with premium description;
  • Subscription activation buttons with local currency and title;
  • Restore purchases button. This element is necessary for all the applications that use subscriptions or non-consumable purchases.

I’ve used Interface Builder Storyboard for layout, but you can use just coding. I’ve also moved UIActivityIndicatorView to ViewController to present the payment process.

Show purchase information using SKProduct

After quickly familiarizing oneself with Apple’s documentation on SKProduct, let’s build a core of our ViewController. It doesn’t have any logic and we’ll add it later.

import StoreKit
import UIKit

class ViewController: UIViewController {
    // 1:
    @IBOutlet private weak var purchaseButtonA: UIButton!
    @IBOutlet private weak var purchaseButtonB: UIButton!
    @IBOutlet private weak var activityIndicator: UIActivityIndicatorView!
    override func viewDidLoad() {
        super.viewDidLoad()
        activityIndicator.hidesWhenStopped = true
        // 2:
        showSpinner()
        Purchases.default.initialize {
            [weak self] result in
            guard let self = self else {
                return
            }
            self.hideSpinner()
            switch result {
                case let .success(products):
                DispatchQueue.main.async {
                    self.updateInterface(products: products)
                }
                default:
                break
            }
        }
    }

    // 3:
    private func updateInterface(products: [SKProduct]) {
        updateButton(purchaseButtonA, with: products[0])
        updateButton(purchaseButtonB, with: products[1])
    }

    // 4:
    @IBAction func purchaseAPressed(_ sender: UIButton) {
    }

    @IBAction func purchaseBPressed(_ sender: UIButton) {
    }

    @IBAction func restorePressed(_ sender: UIButton) {
    }
}

Let’s see what’s inside:

  1. Property class fields to link UI elements with our code.
  2. Launching asynchronous initialization process of purchases unit in viewDidLoad(). To make this process independent from the UI layer, it’s better to call it just right after the app launch. In this article, to make it easier, let’s do it just right in the ViewController.
  3. Before launching the initialization, we’ll show the loading bar and we’ll remove it after the initialization finishes. I’ve written some helper functions for it, check them below.
  4. updateInterface() function that updates UI with purchase data such as trial duration and pricing.
  5. Buttons handlers for initialization and purchase restoration.

Helpers:

extension ViewController {
    // 1:
    func updateButton(_ button: UIButton, with product: SKProduct) {
       let title = "\(product.title ?? product.productIdentifier) for \(product.localizedPrice)"
       button.setTitle(title, for: .normal)
    }

    func showSpinner() {
        DispatchQueue.main.async {
            self.activityIndicator.startAnimating()
            self.activityIndicator.isHidden = false
        }
    }

    func hideSpinner() {
        DispatchQueue.main.async {
            self.activityIndicator.stopAnimating()
        }
    }
}

Pay attention to the usage (1) of SKProduct objects. We make an extension to easily extract the information. This implementation is more flexible than using SKProduct props directly:

extension SKProduct {
    var localizedPrice: String {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        formatter.locale = priceLocale
        return formatter.string(from: price)!
    }

    var title: String? {
        switch productIdentifier {
        case "barcode_month_subscription":
            return "Monthly Subscription"
        case "barcode_year_subscription":
            return "Annual Subscription"
        default:
            return nil
        }
    }
}

Purchases unit

I’ve slightly refined the Purchases class to make it possible to show the result of an asynchronous operation in the interface. I also cached SKProduct objects for further usage.

typealias RequestProductsResult = Result<[SKProduct], Error>
typealias PurchaseProductResult = Result<Bool, Error>

typealias RequestProductsCompletion = (RequestProductsResult) -> Void
typealias PurchaseProductCompletion = (PurchaseProductResult) -> Void

class Purchases: NSObject {
    static let `default` = Purchases()

    private let productIdentifiers = Set(
       arrayLiteral: "barcode_month_subscription", "barcode_year_subscription"
    )

    private var products: [String: SKProduct]?
    private var productRequest: SKProductsRequest?

    func initialize(completion: @escaping RequestProductsCompletion) {
        requestProducts(completion: completion)
    }

    private var productsRequestCallbacks = [RequestProductsCompletion]()

    private func requestProducts(completion: @escaping RequestProductsCompletion) {
        guard productsRequestCallbacks.isEmpty else {
            productsRequestCallbacks.append(completion)
            return
        }

        productsRequestCallbacks.append(completion)

        let productRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
        productRequest.delegate = self
        productRequest.start()

        self.productRequest = productRequest
    }
}

Delegate:

extension Purchases: SKProductsRequestDelegate {
    func productsRequest(_ request: SKProductsRequest,
                        didReceive response: SKProductsResponse) {
        guard !response.products.isEmpty else {
            print("Found 0 products")

            productsRequestCallbacks.forEach { $0(.success(response.products)) }
            productsRequestCallbacks.removeAll()
            return
        }

        var products = [String: SKProduct]()
        for skProduct in response.products {
            print("Found product: \(skProduct.productIdentifier)")
            products[skProduct.productIdentifier] = skProduct
        }

        self.products = products

        productsRequestCallbacks.forEach { $0(.success(response.products)) }
        productsRequestCallbacks.removeAll()
    }

    func request(_ request: SKRequest, didFailWithError error: Error) {
        print("Failed to load products with error:\n \(error)")

        productsRequestCallbacks.forEach { $0(.failure(error)) }
        productsRequestCallbacks.removeAll()
    }
}

10 ideas
to increase
paywall conversion

Get an ebook with insights
and advice from top experts

Purchase mechanism implementation

Let’s add the Error handling to our code. I created the enum PurchaseError to implement Error protocol (or LocalizedError):

enum PurchasesError: Error {
    case purchaseInProgress
    case productNotFound
    case unknown
}

All errors on the StoreKit level will be caught as a single error, get more information about the error types in the documentation.

purchaseProduct() function launches the purchase process of the selected product, and restorePurchases() function requests the list of the users’ purchases from the system: auto-renewable subscriptions or non-consumable purchases:

fileprivate var productPurchaseCallback: ((PurchaseProductResult) -> Void)?
func purchaseProduct(productId: String, completion: @escaping (PurchaseProductResult) -> Void) {
    // 1:
    guard productPurchaseCallback == nil else {
        completion(.failure(PurchasesError.purchaseInProgress))
        return
    }

    // 2:
    guard let product = products?[productId] else {
        completion(.failure(PurchasesError.productNotFound))
        return
    }

    productPurchaseCallback = completion
    // 3:
    let payment = SKPayment(product: product)
    SKPaymentQueue.default().add(payment)
}

public func restorePurchases(completion: @escaping (PurchaseProductResult) -> Void) {
    guard productPurchaseCallback == nil else {
        completion(.failure(PurchasesError.purchaseInProgress))
        return
    }

    productPurchaseCallback = completion
    // 4:
    SKPaymentQueue.default().restoreCompletedTransactions()
}

So:

  1. Check that there’s the only purchase process launched.
  2. Return the error when trying to purchase with a nonexistent productId.
  3. Add the purchase to SKPaymentQueue.
  4. Make a request to SKPaymentQueue to restore purchases.

To process the result of the purchase, we need to implementSKPaymentTransactionObserver() protocol:

extension Purchases: SKPaymentTransactionObserver {
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        // 1:
        for transaction in transactions {
            switch transaction.transactionState {
            // 2:
            case .purchased, .restored:
                if finishTransaction(transaction) {
                    SKPaymentQueue.default().finishTransaction(transaction)
                    productPurchaseCallback?(.success(true))
                } else {
                    productPurchaseCallback?(.failure(PurchasesError.unknown))
                }
            // 3:
            case .failed:
                productPurchaseCallback?(.failure(transaction.error ?? PurchasesError.unknown))
                SKPaymentQueue.default().finishTransaction(transaction)
            default:
                break
            }
        }
        
        productPurchaseCallback = nil
   }
}

extension Purchases {
    // 4:
    func finishTransaction(_ transaction: SKPaymentTransaction) -> Bool {
        let productId = transaction.payment.productIdentifier
        print("Product \(productId) successfully purchased")
        return true
    }
}
  1. Iterate on the transaction body, processing each one individually
  2. In case the transaction is in the purchased or restored status, we need to do all the necessary actions to make the content/subscription available for the user. Then we need to close the transaction with the finishTransaction method. Important: if you work with consumable purchases it’s crucially important to ensure that the user has gained access to the content and close the transaction right after the check. Otherwise, it’s possible to lose the information about the purchase.
  3. The purchase process may finish with an error due to different reasons, so return this information.
  4. Provide the user with the purchased content in the function that we request in step 2: (for example, we remember the subscription expiry date for UI to interpret the user as premium)
  5. In this case, we have discussed some of the existent transaction statuses. There is also purchasing status (which means the transaction is being processed) and deferred status means the transaction is postponed indefinitely and will be finished later (for example, while waiting for confirmation from parents). These statuses can be shown in UI if necessary.

Public API call in the interface

The core part is done, so we just need to call execute these functions in the ViewController.

@IBAction func purchaseAPressed(_ sender: UIButton) {
    showSpinner()
    Purchases.default.purchaseProduct(productId: "barcode_month_subscription") {
        [weak self] _ in
        self?.hideSpinner()
        // Handle result
    }
}
    
@IBAction func purchaseBPressed(_ sender: Any) {
    showSpinner()
    Purchases.default.purchaseProduct(productId: "barcode_year_subscription") {
        [weak self] _ in
        self?.hideSpinner()
        // Handle result
    }
}
    
@IBAction func restorePressed(_ sender: UIButton) {
    showSpinner()
    Purchases.default.restorePurchases {
        [weak self] _ in
        self?.hideSpinner()
        // Handle result
    }
}

This is it. In the next part, we will discuss the basic ways to test the purchase mechanism, and how to design a paywall to pass the App Store review.

Unlock Industry Insights
Gain the edge with key insights from our State of in-app subscriptions in the US 2023 report. Essential for app professionals!
Get your free report now
Report in app US 2023 (book)

Further reading