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:
SKPaymentTransactionObserver
was removed. Purchasing is now done using the await
command.SKRequestDelegate
was removed. Requesting product information is also done using the await
command.Product
and Transaction
are now structures instead of classesDuring 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.
is now a structure and getting product details is done using Product
method:static
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
, like, Product
, which returns the type of in-app purchase:ProductType
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.
/// 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.
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) } }
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.
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.
/// 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.
Another great improvement in StoreKit 2 is Subscriptions API. Now you can get subscription info right from the app!
What’s available:
subscribed
, expired
, inBillingRetryPeriod
, inGracePeriod
, revoked
.autoRenewalStatus
, gracePeriodExpirationDate
. This info is similar to pending_renewal_info
from verifyReceipt
endpoint’s JSON.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 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:
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!