In-App Purchases StarterKit

How to add subscriptions to your iOS app

In 2021 Apple announced StoreKit 2, a new in-app purchase API that brings significant improvements for managing subscriptions and in-app purchases in iOS apps. In this week's newsletter, we'll take a look at StoreKit 2 and how to use it in your apps.

Key Features

Some of the features of StoreKit 2 include:

  • Simplified code: The API has been completely redesigned to be simpler and more Swifty. Managing in-app purchases now takes a lot less code.

  • Async/await: StoreKit 2 takes full advantage of async/await in Swift. This makes the code for working with in-app purchases much more readable and concise.

  • Real-time updates: Apps can now get real-time updates when a user's subscription status changes. This eliminates the need to manually check statuses.

  • Secure verification: Improved transaction verification helps ensure users get access to content they paid for. Apps can detect fraudulent transactions.

  • Subscription groups: Subscriptions can be grouped together, so a single purchase unlocks access to multiple different subscriptions.

If you aren’t already taking weekly programming deep dives with me, subscribe below!

Code Example

To begin, we need to create our store object

Typealiases

typealias RenewalInfo = StoreKit.Product.SubscriptionInfo.RenewalInfo

typealias RenewalState = StoreKit.Product.SubscriptionInfo.RenewalState

Two type aliases are defined here to make the code more readable and easier to manage.

Class Properties

@Published private(set) var subscriptions: [Product] = []
@Published private(set) var purchasedSubscriptions: [Product] = []
@Published private(set) var subscriptionGroupStatus: RenewalState?
private let productIds: [String] = [<product_ids>]
var updateListenerTask : Task<Void, Error>? = nil
  • subscriptions: A list of available subscriptions.

  • purchasedSubscriptions: A list of purchased subscriptions.

  • subscriptionGroupStatus: The current renewal state of a subscription.

  • productIds: A hardcoded list of product IDs which would normally come from App Store Connect.

  • updateListenerTask: Holds the transaction listening task.

Initializer

init() {

    updateListenerTask = listenForTransactions()
    Task {

        await requestProducts()
        await updateCustomerProductStatus()

    }
}

In the initializer, a transaction listener is set up, and it also requests available products and updates the customer's product status.

Deinitializer

deinit {
    updateListenerTask?.cancel()
}

This cancels the transaction listening task when the object is deallocated.

Transaction Listener

func listenForTransactions() -> Task<Void, Error> {
    return Task.detached {

                    //Iterate through any transactions that don't come from a direct call to `purchase()`.
            for await result in Transaction.updates {
                do {
                    let transaction = try self.checkVerified(result)
                    // deliver products to the user
                    await self.updateCustomerProductStatus()
                    
                    await transaction.finish()
                } catch {
                    print("transaction failed verification")
                }
            }


    }
}

This function listens for transaction updates that come from the App Store. It verifies these transactions, delivers the products, and then finishes the transaction.

Requesting Products

@MainActor
func requestProducts() async {

            do {
            // request from the app store using the product ids (hardcoded)
            subscriptions = try await Product.products(for: productIds)
            print(subscriptions)
        } catch {
            print("Failed product request from app store server: \(error)")
        }


}

requestProducts() requests the list of available products from the App Store using the product IDs.

Making a Purchase

func purchase(_ product: Product) async throws -> Transaction? {

            let result = try await product.purchase()
        
        switch result {
        case .success(let verification):
            //Check whether the transaction is verified. If it isn't,
            //this function rethrows the verification error.
            let transaction = try checkVerified(verification)
            
            //The transaction is verified. Deliver content to the user.
            await updateCustomerProductStatus()
            
            //Always finish a transaction.
            await transaction.finish()

            return transaction
        case .userCancelled, .pending:
            return nil
        default:
            return nil
        }


}

purchase() attempts to purchase a product. If successful, it updates the customer's product status and finishes the transaction.

If you aren’t already taking weekly deep dives with me, subscribe below!

Checking Verification

func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {

            //Check whether the JWS passes StoreKit verification.
        switch result {
        case .unverified:
            //StoreKit parses the JWS, but it fails verification.
            throw StoreError.failedVerification
        case .verified(let safe):
            //The result is verified. Return the unwrapped value.
            return safe
        }
}

checkVerified checks whether a transaction is verified. It's a utility function used by other functions to ensure the transaction has gone through Apple's verification process.

Update Customer Product Status

@MainActor
func updateCustomerProductStatus() async {

            for await result in Transaction.currentEntitlements {
            do {
                //Check whether the transaction is verified. If it isn’t, catch `failedVerification` error.
                let transaction = try checkVerified(result)
                
                switch transaction.productType {
                    case .autoRenewable:
                        if let subscription = subscriptions.first(where: {$0.id == transaction.productID}) {
                            purchasedSubscriptions.append(subscription)
                        }
                    default:
                        break
                }
                //Always finish a transaction.
                await transaction.finish()
            } catch {
                print("failed updating products")
            }
        }


}

This function updates the list of purchased subscriptions for the customer by going through the current entitlements.

Each function in this class serves a specific purpose in the purchase and management of in-app subscriptions. The class as a whole provides a ViewModel in the MVVM design pattern, abstracting away the StoreKit 2 details from the rest of the application.

Using it, you’re able to get all of your subscription products to feed into your SwiftUI Views.

struct SubscriptionOptions: View {
    @EnvironmentObject var storeVM: StoreVM
    @Binding var selectedProductIndex: Int
    
    var body: some View {
        Group {
            Section(header: Text("")){
                HStack(spacing: 15) {
                    ForEach(Array(storeVM.subscriptions.enumerated()), id: \.offset) { index, product in
                        SubscriptionButton(index: index, isSelected: selectedProductIndex == index, action: {
                            selectedProductIndex = index
                        })
                    }
                }
                .padding(.vertical)
            }
        }
    }
}

And thats all there is to it! StoreKit 2 is a pretty extensive library, with many more options for in-app purchases than subscriptions, but subscriptions are a good start if you’re new to making money off of your apps.

[ Zach Coriarty ]