Proved #1 position in app revenue tracking accuracy!Learn more
Apphud
Why Apphud?PricingBlogContactSign inStart for Free
Ren
Ren
June 11, 2021
7 min read

StoreKit 2: What Is It, and How Does It Affect Apphud?

At WWDC 2021, Apple introduced many new APIs and Frameworks for developers — as well as iOS 15. One of the biggest changes is related to In-App Purchases: a new StoreKit 2.

StoreKit 2: What Is It, and How Does It Affect Apphud?

StoreKit 2

The well-received StoreKit gave customers an opportunity to buy digital goods and services without having to do any extra work. It provided a very simple and secure way to do that directly from apps on any Apple platform. Due to this, people are able to start reading e-books, playing video games, and much more right away. Now, StoreKit 2 presents modern Swift-based APIs that allow you to provide your customers with the best and easiest in-app purchase experiences they have ever had.

StoreKit 2 is a brand-new framework, written in pure modern Swift syntax with a new concurrency pattern. Here are the main changes:

  • App Store binary receipts were removed. Yes, the long base64 strings have now disappeared from the framework. Instead of this, Apple lets developers retrieve transaction details on their servers using the new App Store Server API.
  • SKPaymentTransactionObserver was removed. Purchasing is now done using the await command.
  • SKRequestDelegate was removed. Requesting product information is also done using the await command.
  • Many new APIs were added, like managing refunds from the app
  • A subscription API was added, which includes status and renewal info.
  • It’s now possible to retrieve historical transactions and the latest transactions.
  • Purchased transactions are now automatically validated for you!
  • Product and Transaction are now structures instead of classes
  • Each transaction is signed using JWS (JSON Web Signature)

During WWDC 2021, Apple did a demonstration of how simple it is to set up your apps with StoreKit 2 in iOS. During the tutorial, the specialist testedStoreKit using Xcode. The tutorial will give you an opportunity to build and test your store before defining products in App Store Connect.

It has to be mentioned that the new StoreKit 2 APIs are composed of 5 major elements:

• Products

• Purchases

• Transaction information (listener)

• Transaction history

• Subscription status (info)

We will review all of them below. Because products and purchases are the most basic units of StoreKit, we will cover them first.


Products

Product in StoreKit 2Product in StoreKit 2

Product is now a structure and getting product details is done using static method:

public static func request(with identifiers: Set<String>) async throws -> [Product]

In practice it looks like this:

func loadProducts() async {
     do {
         let ids = [ "com.apphud.weekly", "com.apphud.weekly2", "com.apphud.monthly"]
         self.products = try await Product.request(with: Set(ids))
     } catch {
         print("error while loading products: \(error.localizedDescription)")
     }
 }

Much easier, isn’t it?

Also, there are new methods in Product, like, ProductType, which returns the type of in-app purchase:

public static var consumable: Product.ProductType
public static var nonConsumable: Product.ProductType
public static var nonRenewable: Product.ProductType
public static var autoRenewable: Product.ProductType

Or, for example, the formatted display price of the product:

/// A localized string representation of `price`.
public let displayPrice: String

Another cool change is the introduction of two new methods of checking trial/intro eligibility:

/// Whether the user is eligible to have an introductory offer applied to their purchase.
public var isEligibleForIntroOffer: Bool { get async }
public static func isEligibleForIntroOffer(for groupID: String) async -> Bool

The first one is an instance getter for a particular product, and the second one is a static method that takes the subscription group ID as a parameter.


App Account Token

/// A UUID that associates the purchase with an account in your system.
public static func appAccountToken(_ token: UUID) -> Product.PurchaseOption

It is now possible to include an anonymous user identifier in a Transaction, which persists forever, even when using REST API. That means that you can map your user with a transaction from REST API.


Purchase

The payment process has been significantly simplified. SKPaymentTransactionObserver has gone and payment is made with one single line:

let result = try await product.purchase()

The result is enum PurchaseResult:

public enum PurchaseResult {
  /// The purchase succeeded with a `Transaction`.
  case success(VerificationResult<Transaction>)
​
  /// The user cancelled the purchase.
  case userCancelled
​
  /// The purchase is pending some user action.
  ///
  /// These purchases may succeed in the future, and the resulting `Transaction` will be
  /// delivered via `Transaction.listener`
  case pending
}

And if there was any error, then it will be returned in catch. Below is an example of using the purchase method:

func purchase(_ product: Product) async throws -> Transaction? {
        //Begin a purchase.
        let result = try await product.purchase()
​
        switch result {
        case .success(let verification):
            let transaction = try checkVerified(verification)
​
            //Deliver content to the user.
            await updatePurchasedIdentifiers(transaction)
​
            //Always finish a transaction.
            await transaction.finish()
​
            return transaction
        case .userCancelled, .pending:
            return nil
        default:
            return nil
        }
    }
​
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
        //Check if the transaction passes StoreKit verification.
        switch result {
        case .unverified:
            //StoreKit has parsed the JWS but failed verification. Don't deliver content to the user.
            throw StoreError.failedVerification
        case .verified(let safe):
            //If the transaction is verified, unwrap and return it.
            return safe
        }
}

To run asynchronous code inside synchronous code, just wrap it to async {}

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  let product = products[indexPath.row]
    async {
      await self.purchaseAsync(product: product)
    }
}

TransactionListener

Instead of Transaction Observer, Apple added Transaction Listener, which returns transactions that didn’t come through a direct call to purchase().

// Start a transaction listener as close to app launch as possible so you don't miss any transactions.
​
taskHandle = listenForTransactions()
​
func listenForTransactions() -> Task.Handle<Void, Error> {
        return detach {
            //Iterate through any transactions which didn't come from a direct call to `purchase()`.
            for await result in Transaction.listener {
                do {
                    let transaction = try self.checkVerified(result)
​
                    //Deliver content to the user.
                    await self.updatePurchasedIdentifiers(transaction)
​
                    //Always finish a transaction.
                    await transaction.finish()
                } catch {
                    //StoreKit has a receipt it can read but it failed verification. Don't deliver content to the user.
                    print("Transaction failed verification")
                }
            }
        }
    }

The TransactionListener can be used for “Ask To Buy” or “Strong Customer Authentication” cases when the transaction enters the pending state.


Transaction History

StoreKit 2 can also return transaction history using TransactionSequence struct. You can get all transactions or just the latest one. Or, you can even get entitlements – a set of transactions, one per each entitlement, which is a subscription or a single purchase. In other words, you get the latest transaction for a user’s subscription and all transactions for non-consumable purchases, if available.

Transaction history in StoreKit 2Transaction history in StoreKit 2
/// A sequence of every transaction for this user and app.
public static var all: Transaction.TransactionSequence { get }
​
/// Returns all transactions for products the user is currently entitled to
///
/// i.e. all currently-subscribed transactions, and all purchased (and not refunded) non-consumables
public static var currentEntitlements: Transaction.TransactionSequence { get }
​
/// Get the transaction that entitles the user to a product.
/// - Parameter productID: Identifies the product to check entitlements for.
/// - Returns: A transaction if the user is entitled to the product, or `nil` if they are not.
public static func currentEntitlement(for productID: String) async -> VerificationResult<Transaction>?
​
/// The user's latest transaction for a product.
/// - Parameter productID: Identifies the product to check entitlements for.
/// - Returns: A verified transaction, or `nil` if the user has never purchased this product.
public static func latest(for productID: String) async -> VerificationResult<Transaction>?

For example, if your application has both a subscription and a non-consumable purchase, and the user has purchased both products, then currentEntitlements will return the last active transaction for the subscription and the transaction for the non-consumable purchase.


Subscription Info

Another great improvement in StoreKit 2 is Subscriptions API. Now you can get subscription info right from the app!

What’s available:

  • Renewal State. It’s simply subscription status: subscribedexpiredinBillingRetryPeriodinGracePeriodrevoked.
  • Renewal Info. Get all main subscription information, like, autoRenewalStatusgracePeriodExpirationDate. This info is similar to pending_renewal_info from verifyReceipt endpoint’s JSON.
  • Latest transaction. Get the most recent transaction. Developer is responsible to check whether transaction is not revoked and not upgraded.
Latest transaction in&nbsp;StoreKit 2Latest transaction in&nbsp;StoreKit 2

All transactions are available upon app download and automatically sync on each device. There is still the sync() method in StoreKit 2 API, which forces transactions to sync. However, in most cases, it won’t be needed. You can remove your Restore Purchases button from the UI, as it will because useless.

The full video from WWDC can be viewed here.


StoreKit 2 + Apphud = ❤️

StoreKit 2 certainly makes life much easier for app developers to determine subscription status, which has a positive effect on Apphud.

StoreKit 2 makes life easier for developers, as it can validate transactions and update subscription statuses out of the box. It is positive news for Apphud:

  • SDK (finally) can get rid of sending large receipts to the backend, which will lower outcoming Internet traffic and increase the server response time.
  • The simpler the API, the fewer bugs may occur in SDK and transaction handling.
  • iOS will now be responsible for subscription status updating and restoring, which will allow Apphud to focus on more important parts of the service.

Unfortunately, StoreKit 2 is only available to devices running iOS 15. It may take a few years before the new framework becomes mainstream.

For the lower iOS versions, you still have to use the old API.

Stay tuned!