/

/

A Deep Dive into StoreKit 2 Capabilities

Suren

Jun 30, 2021

Infrastructure

A Deep Dive into StoreKit 2 Capabilities

During WWDC21, Apple announced many exciting product features, and StoreKit 2 is one of the most significant updates. In-app subscriptions have become the most popular monetization method that generates billions of dollars in revenue for Apple and thousands of mobile app developers. Developers have earned $230 billion through the App Store since its launch, with the biggest chunk of that revenue coming in the last few years.

That makes in-app purchases a particular focus of Apple and Google. Just last year WWDC20 presented StoreKitTransactionManager with a .storekit file that simplifies the process of in-app purchases testing. We have been waiting for this update since iOS3. And the next updates were not long in coming.

This article will cover what’s new in StoreKit 2 and discuss the difference between the updated framework and its previous version – StoreKit. 

Before we jump into StoreKit 2 overview, let’s briefly cover what was wrong with the previous StoreKit.

StoreKit 2 Capabilities Deep Dive

Key limitations of StoreKit

What’s new in StoreKit 2?

We have already covered a list of new features in our previous article, but today we would like to highlight the most significant two: Swift-first design and API update

Swift-first design

StoreKit 2 takes advantage of advancements in Swift. To be more precise, Concurrency with async and await makes a software engineer’s life much easier. It solves (fully or partially – based on one’s needs) many issues with unnecessarily complicated architecture. 

Fetching product information 

Previously, you had to create a SKProductsRequest, attach a delegate, initialize the request, and be sure to keep a strong reference to the request to ensure task completion. It looked like that:

let productsRequest = SKProductsRequest(productIdentifiers: identifiers) productsRequest.delegate = self productsRequest.start() self.productsRequest = productsRequestCopy

And then receive the response:

extension StoreKitService: SKProductsRequestDelegate { func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { // handle products here } }Copy

With StoreKit 2 it will look like this:

let storeProducts = try await Product.request(with: identifiers) Copy

And that’s it. The following line will have the product information. 

Purchase 

To process a purchase in StoreKit, you would have to write something like this:

func purchase(_ product: SKProduct) { let payment = SKPayment(product: product) SKPaymentQueue.default().add(payment) }Copy

And to handle the response, you would need to write this:

extension StoreKitService: SKPaymentTransactionObserver { func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { transactions.forEach { transaction in switch transaction.transactionState { case .purchased: // handle purchased case .failed: // handle failed case .deferred: // handle deferred case .restored: // handle restored default: break } } }Copy

In response, you would find an array with one item – your transaction. The same function will be receiving the results from the restoreCompletedTransactions. In the latter case, there are multiple transactions, and that’s why the function returns the array. On top of that, you will also need to remember who called the purchase in order to send the response back to the right place.

One option is to keep completion in the same class that will be returned – you do not have direct access to completion at this point as handling happens in a different unrelated function. If you need some additional data, it would have to be stored somewhere and synchronized later.

Here is how that could be implemented with StoreKit 2:

func purchase(_ product: Product) async throws -> Transaction? { let result = try await product.purchase() switch result { case .success(let verification): // handle success ... return result case .userCancelled, .pending: // handle if needed default: break }Copy

And again – that’s it.

You have the result from the second line and the rest is its handling. Also, note that a new switch case .userCancelled for transactions that correspond to a user aborting purchase process. To get this info in the previous StoreKit, you would have to process transactionState of SKPaymentTransaction and if it has .failed, then check the error code. It’s important to notice that if there was a problem, you should show an error message to the user, but showing it for a canceled purchase would be silly. It’s nice that the new SDK recognizes this situation as an individual outcome in PurchaseResult and doesn’t mix it up with errors. 

You also don’t need to keep track of who requested a purchase nor think of how it needs to be wrapped. You can just return result at the right moment.

Validation 

As you may notice, after a successful purchase, you might see a verification value that confirms that StoreKit validates this purchase. It is all up to you which field you would follow – do you find device validation reliable or check it on a server.

Additional payment options

Several extra options can be set when a purchase is made, but we found the following is pretty interesting.

Tokens

An arbitrary UUID token can be attached to a transaction that will remain forever. This comes in handy if you have your authorization implementation that isn’t based on AppStore accounts. Let’s say you are providing a streaming service where a subscription provides access across all user devices as long as they stay logged in on your service. In this case, you could simply include your internal accounts as UUID tokens in user purchases. It could be something between these lines:

let result = try await product.purchase(options::[.appAccountToken(yourAppToken))])Copy

Other interesting features

  • Updated interface of promotional purchases

  • Ability to buy products in “batches”. For example, if you have an SKU “1000 coins”, then a user can select five of those and receive 5000 coins at once.

One more thing 

There is a lot to say about the interface updates since almost everything has been reworked and adjusted for the new Swift features, but that’s not the main point. We would like to highlight the changes in the interface of SKPaymentTransactionObserver, which was used to process transactions. For example, whether this is a confirmation of a purchase from a parent account, SCA, an auto-renewal of the subscription, or something else. It is now a listener for the Transaction object. And you can “listen” to new transactions like this:

func listenForTransactions() -> Task.Handle<Void, Error> { return detach { for await result in Transaction.listener { do { // handle transaction result here } } } }Copy

The key is to remember to call transaction.finish(). Otherwise, they will keep coming to the listener on every app restart. Although, this behavior isn’t new as it has not changed since the previous version.

Swift-first design summary

As we can see, Swift’s innovations, coupled with the new StoreKit 2 interface, make developers’ lives much easier. In just a couple of code lines, the developers are now able to: 

  • Fetch product info

  • Launch in-apps

  • Process the purchase results

  • Verify purchases

  • Get new transactions

According to Apple, StoreKit 2 significantly improves the security level of purchase verification, but this is still not a replacement for your server-side validation. And not a reason to give it up.

Powerful new APIs

Here Apple added a lot of new and valuable features. We covered almost everything related to Products and Purchases in the first part. Let’s address the rest now.

Many entities and fields have been reworked, improved, and expanded. Now there is more information on products, purchases, and subscription statuses. Apple has added a large amount of data to the StoreKit 2 public API that was previously only available in a receipt. Since the information in it is encrypted, most of the data available in StoreKit 2 public API will also be encrypted using JWS. For example, information on transactions and auto-renewal of a subscription. And yes, StoreKit 2 will automatically validate this data.

Let’s look at the most exciting advancements of API more closely:

  • Transactions 

  • Current entitlements 

  • Subscription information

  • Auto-synchronization of purchases

Transactions

Now you can get a list of all transactions (or just the latest one) for a particular product directly from StoreKit 2. Previously, you had to parse a receipt to do this. Transaction data can be helpful, for example, for some kind of analytics or simply to share the details with the user on their purchase.

Current entitlements

We have discussed above that the previous version required developers to store the history of active purchases to enable users’ access to it in the app. StoreKit 2 will now do it for you.

Note that this data includes only two types of product:

  • Active subscriptions

  • Non-consumable purchases. Consumable purchases such as coins/ammo/gas will not appear on this list. You must process them immediately upon purchase. 

Subscription information

This part of the article will discuss the most interesting stuff that is included in SubscriptionInfo object. As with transactions, SubscriptionInfo was previously only available in a receipt and required some work on your server. 

Intro offer eligibility

In the previous version of StoreKit, there was no way to determine if a user had a purchase in this product group to decide whether to give them a discount. So you had to do this manually somewhere on the server; now, you can simply call one function and get a Bool in the response.

static func isEligibleForIntroOffer(for groupID: String) async -> Bool Copy

Renewal state

There are several states:

  1. subscribed – the user is currently subscribed.

  2. expired – the subscription expired.

  3. inBillingRetryPeriod – the subscription is in a billing retry period. 

  4. inGracePeriod – the subscription is in a billing grace period state. This feature allows you to extend users access to the app if they have a billing issue. The grace period length varies from 6 to 16 days, depending on the duration of the subscription itself.

  5. revoked – the App Store has revoked the user’s access to the subscription group.

Renewal info

This object will display everything related to the auto-renewal of the subscription. The following information could be found there:

  1. willAutoRenew – a Boolean value that indicates whether the subscription will automatically renew in the next period. If the status is negative, then with some degree of probability, the user will churn. So it’s time to think about how to re-engage them.

  2. autoRenewPreference – the product ID of the subscription that will automatically renew. For example, you can check whether a user has downgraded and plans to use a cheaper option in your subscription. In this case, you can offer that user a discount to keep them on the premium version.

  3. expirationReason – the reason the subscription expired.

An important note about subscription statuses

StoreKit 2 returns an array of statuses. It is necessary for the cases where a user has multiple subscriptions to the same product. For example, they bought one subscription themself and got the second one through family sharing. This way, you will validate the entire array and unlock functionality in your app by referencing the subscription with the highest access level.

Auto-synchronization of purchases

Synchronization of purchases across different devices with one Apple ID is another cool feature. If the user made a purchase on their iPhone and then switched to iPad, this purchase would also be available on it. There is no need for developers to perform additional steps like calling restore().

However, there is a caveat from Apple’s side: automatic synchronizations should cover most of all cases, but as millions of people worldwide use Apple devices, there is still a risk for synchronizations to fail. For such cases, Apple suggests an alternative – a manual call for purchase synchronization – AppStore.sync(). It is pretty similar to restore(), but you will need to call it less often.

Apple also warns that AppStore.sync() should only be called in response to a user action (i.e., pressing a button), since the call initiates the App Store authenticate notification, and if you call it somewhere at the start, then the user experience in the app won’t be satisfactory. 

Summary of the new StoreKit 2 API

A lot of data that previously could only be obtained by decrypting a receipt on the server is now available in the StoreKit itself. It drastically simplifies the work for developers. It has become much easier to check the status of subscriptions and synchronize data across the devices.

So, does StoreKit 2 resolve all the problems of the previous version? Let’s revise the issues we discussed at the beginning of the article.

  1. Is complexity solved? Overall, we still think that this is a complex product that requires a lot of pre-work to understand completely. StoreKit 2 removes the questions like “Where can I get this data?” by introducing “out of the box” solutions.

  2. Has the architecture improved? No doubts. Thanks to Swift Concurrency. 

  3. No validation of purchases? Purchase validation was introduced to StoreKit 2, but API validation remains more reliable, even according to Apple. 

  4. No auto-sync across the devices? Purchases are finally available across all devices with the same Apple ID without any hassle.

  5. No data on existing purchases? This may seem like a minor issue, but it was necessary for everyone who has ever implemented in-app purchases. Now it is also available “right out of the box”.

  6. Missing some valuable data? That still depends on your needs. You may want to parse the receipt for additional data. But StoreKit 2 will cover a lot of use cases. 

To sum up, StoreKit 2 is an excellent product that introduced more than we were asking for. Just think about the features of Swift Concurrency as it solves a lot of everyday problems. Migration to StoreKit 2 won’t happen soon, but there is at least some hope for a brighter future.

Qonversion is staying on top of all new developments and will support StoreKit 2 in all of our products and services. If you’re looking for a single place to manage subscribers and grant them access to premium content — look no further. Our Product Center provides a comprehensive in-app purchases infrastructure so that there’s no need for you to build your servers for validating receipts. Automation and In-App messaging tool can help you to send automatic and personalized messages to your users. In addition, mobile subscription analytics and native integration with marketing services give you the complete visibility of your business, and A/B tests allow you to make justified decisions on your price.

Want to learn more? Don’t hesitate to reach out and stay tuned for new updates on our blog.

Suren

Jun 30, 2021

Infrastructure

A Deep Dive into StoreKit 2 Capabilities

During WWDC21, Apple announced many exciting product features, and StoreKit 2 is one of the most significant updates. In-app subscriptions have become the most popular monetization method that generates billions of dollars in revenue for Apple and thousands of mobile app developers. Developers have earned $230 billion through the App Store since its launch, with the biggest chunk of that revenue coming in the last few years.

That makes in-app purchases a particular focus of Apple and Google. Just last year WWDC20 presented StoreKitTransactionManager with a .storekit file that simplifies the process of in-app purchases testing. We have been waiting for this update since iOS3. And the next updates were not long in coming.

This article will cover what’s new in StoreKit 2 and discuss the difference between the updated framework and its previous version – StoreKit. 

Before we jump into StoreKit 2 overview, let’s briefly cover what was wrong with the previous StoreKit.

StoreKit 2 Capabilities Deep Dive

Key limitations of StoreKit

What’s new in StoreKit 2?

We have already covered a list of new features in our previous article, but today we would like to highlight the most significant two: Swift-first design and API update

Swift-first design

StoreKit 2 takes advantage of advancements in Swift. To be more precise, Concurrency with async and await makes a software engineer’s life much easier. It solves (fully or partially – based on one’s needs) many issues with unnecessarily complicated architecture. 

Fetching product information 

Previously, you had to create a SKProductsRequest, attach a delegate, initialize the request, and be sure to keep a strong reference to the request to ensure task completion. It looked like that:

let productsRequest = SKProductsRequest(productIdentifiers: identifiers) productsRequest.delegate = self productsRequest.start() self.productsRequest = productsRequestCopy

And then receive the response:

extension StoreKitService: SKProductsRequestDelegate { func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { // handle products here } }Copy

With StoreKit 2 it will look like this:

let storeProducts = try await Product.request(with: identifiers) Copy

And that’s it. The following line will have the product information. 

Purchase 

To process a purchase in StoreKit, you would have to write something like this:

func purchase(_ product: SKProduct) { let payment = SKPayment(product: product) SKPaymentQueue.default().add(payment) }Copy

And to handle the response, you would need to write this:

extension StoreKitService: SKPaymentTransactionObserver { func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { transactions.forEach { transaction in switch transaction.transactionState { case .purchased: // handle purchased case .failed: // handle failed case .deferred: // handle deferred case .restored: // handle restored default: break } } }Copy

In response, you would find an array with one item – your transaction. The same function will be receiving the results from the restoreCompletedTransactions. In the latter case, there are multiple transactions, and that’s why the function returns the array. On top of that, you will also need to remember who called the purchase in order to send the response back to the right place.

One option is to keep completion in the same class that will be returned – you do not have direct access to completion at this point as handling happens in a different unrelated function. If you need some additional data, it would have to be stored somewhere and synchronized later.

Here is how that could be implemented with StoreKit 2:

func purchase(_ product: Product) async throws -> Transaction? { let result = try await product.purchase() switch result { case .success(let verification): // handle success ... return result case .userCancelled, .pending: // handle if needed default: break }Copy

And again – that’s it.

You have the result from the second line and the rest is its handling. Also, note that a new switch case .userCancelled for transactions that correspond to a user aborting purchase process. To get this info in the previous StoreKit, you would have to process transactionState of SKPaymentTransaction and if it has .failed, then check the error code. It’s important to notice that if there was a problem, you should show an error message to the user, but showing it for a canceled purchase would be silly. It’s nice that the new SDK recognizes this situation as an individual outcome in PurchaseResult and doesn’t mix it up with errors. 

You also don’t need to keep track of who requested a purchase nor think of how it needs to be wrapped. You can just return result at the right moment.

Validation 

As you may notice, after a successful purchase, you might see a verification value that confirms that StoreKit validates this purchase. It is all up to you which field you would follow – do you find device validation reliable or check it on a server.

Additional payment options

Several extra options can be set when a purchase is made, but we found the following is pretty interesting.

Tokens

An arbitrary UUID token can be attached to a transaction that will remain forever. This comes in handy if you have your authorization implementation that isn’t based on AppStore accounts. Let’s say you are providing a streaming service where a subscription provides access across all user devices as long as they stay logged in on your service. In this case, you could simply include your internal accounts as UUID tokens in user purchases. It could be something between these lines:

let result = try await product.purchase(options::[.appAccountToken(yourAppToken))])Copy

Other interesting features

  • Updated interface of promotional purchases

  • Ability to buy products in “batches”. For example, if you have an SKU “1000 coins”, then a user can select five of those and receive 5000 coins at once.

One more thing 

There is a lot to say about the interface updates since almost everything has been reworked and adjusted for the new Swift features, but that’s not the main point. We would like to highlight the changes in the interface of SKPaymentTransactionObserver, which was used to process transactions. For example, whether this is a confirmation of a purchase from a parent account, SCA, an auto-renewal of the subscription, or something else. It is now a listener for the Transaction object. And you can “listen” to new transactions like this:

func listenForTransactions() -> Task.Handle<Void, Error> { return detach { for await result in Transaction.listener { do { // handle transaction result here } } } }Copy

The key is to remember to call transaction.finish(). Otherwise, they will keep coming to the listener on every app restart. Although, this behavior isn’t new as it has not changed since the previous version.

Swift-first design summary

As we can see, Swift’s innovations, coupled with the new StoreKit 2 interface, make developers’ lives much easier. In just a couple of code lines, the developers are now able to: 

  • Fetch product info

  • Launch in-apps

  • Process the purchase results

  • Verify purchases

  • Get new transactions

According to Apple, StoreKit 2 significantly improves the security level of purchase verification, but this is still not a replacement for your server-side validation. And not a reason to give it up.

Powerful new APIs

Here Apple added a lot of new and valuable features. We covered almost everything related to Products and Purchases in the first part. Let’s address the rest now.

Many entities and fields have been reworked, improved, and expanded. Now there is more information on products, purchases, and subscription statuses. Apple has added a large amount of data to the StoreKit 2 public API that was previously only available in a receipt. Since the information in it is encrypted, most of the data available in StoreKit 2 public API will also be encrypted using JWS. For example, information on transactions and auto-renewal of a subscription. And yes, StoreKit 2 will automatically validate this data.

Let’s look at the most exciting advancements of API more closely:

  • Transactions 

  • Current entitlements 

  • Subscription information

  • Auto-synchronization of purchases

Transactions

Now you can get a list of all transactions (or just the latest one) for a particular product directly from StoreKit 2. Previously, you had to parse a receipt to do this. Transaction data can be helpful, for example, for some kind of analytics or simply to share the details with the user on their purchase.

Current entitlements

We have discussed above that the previous version required developers to store the history of active purchases to enable users’ access to it in the app. StoreKit 2 will now do it for you.

Note that this data includes only two types of product:

  • Active subscriptions

  • Non-consumable purchases. Consumable purchases such as coins/ammo/gas will not appear on this list. You must process them immediately upon purchase. 

Subscription information

This part of the article will discuss the most interesting stuff that is included in SubscriptionInfo object. As with transactions, SubscriptionInfo was previously only available in a receipt and required some work on your server. 

Intro offer eligibility

In the previous version of StoreKit, there was no way to determine if a user had a purchase in this product group to decide whether to give them a discount. So you had to do this manually somewhere on the server; now, you can simply call one function and get a Bool in the response.

static func isEligibleForIntroOffer(for groupID: String) async -> Bool Copy

Renewal state

There are several states:

  1. subscribed – the user is currently subscribed.

  2. expired – the subscription expired.

  3. inBillingRetryPeriod – the subscription is in a billing retry period. 

  4. inGracePeriod – the subscription is in a billing grace period state. This feature allows you to extend users access to the app if they have a billing issue. The grace period length varies from 6 to 16 days, depending on the duration of the subscription itself.

  5. revoked – the App Store has revoked the user’s access to the subscription group.

Renewal info

This object will display everything related to the auto-renewal of the subscription. The following information could be found there:

  1. willAutoRenew – a Boolean value that indicates whether the subscription will automatically renew in the next period. If the status is negative, then with some degree of probability, the user will churn. So it’s time to think about how to re-engage them.

  2. autoRenewPreference – the product ID of the subscription that will automatically renew. For example, you can check whether a user has downgraded and plans to use a cheaper option in your subscription. In this case, you can offer that user a discount to keep them on the premium version.

  3. expirationReason – the reason the subscription expired.

An important note about subscription statuses

StoreKit 2 returns an array of statuses. It is necessary for the cases where a user has multiple subscriptions to the same product. For example, they bought one subscription themself and got the second one through family sharing. This way, you will validate the entire array and unlock functionality in your app by referencing the subscription with the highest access level.

Auto-synchronization of purchases

Synchronization of purchases across different devices with one Apple ID is another cool feature. If the user made a purchase on their iPhone and then switched to iPad, this purchase would also be available on it. There is no need for developers to perform additional steps like calling restore().

However, there is a caveat from Apple’s side: automatic synchronizations should cover most of all cases, but as millions of people worldwide use Apple devices, there is still a risk for synchronizations to fail. For such cases, Apple suggests an alternative – a manual call for purchase synchronization – AppStore.sync(). It is pretty similar to restore(), but you will need to call it less often.

Apple also warns that AppStore.sync() should only be called in response to a user action (i.e., pressing a button), since the call initiates the App Store authenticate notification, and if you call it somewhere at the start, then the user experience in the app won’t be satisfactory. 

Summary of the new StoreKit 2 API

A lot of data that previously could only be obtained by decrypting a receipt on the server is now available in the StoreKit itself. It drastically simplifies the work for developers. It has become much easier to check the status of subscriptions and synchronize data across the devices.

So, does StoreKit 2 resolve all the problems of the previous version? Let’s revise the issues we discussed at the beginning of the article.

  1. Is complexity solved? Overall, we still think that this is a complex product that requires a lot of pre-work to understand completely. StoreKit 2 removes the questions like “Where can I get this data?” by introducing “out of the box” solutions.

  2. Has the architecture improved? No doubts. Thanks to Swift Concurrency. 

  3. No validation of purchases? Purchase validation was introduced to StoreKit 2, but API validation remains more reliable, even according to Apple. 

  4. No auto-sync across the devices? Purchases are finally available across all devices with the same Apple ID without any hassle.

  5. No data on existing purchases? This may seem like a minor issue, but it was necessary for everyone who has ever implemented in-app purchases. Now it is also available “right out of the box”.

  6. Missing some valuable data? That still depends on your needs. You may want to parse the receipt for additional data. But StoreKit 2 will cover a lot of use cases. 

To sum up, StoreKit 2 is an excellent product that introduced more than we were asking for. Just think about the features of Swift Concurrency as it solves a lot of everyday problems. Migration to StoreKit 2 won’t happen soon, but there is at least some hope for a brighter future.

Qonversion is staying on top of all new developments and will support StoreKit 2 in all of our products and services. If you’re looking for a single place to manage subscribers and grant them access to premium content — look no further. Our Product Center provides a comprehensive in-app purchases infrastructure so that there’s no need for you to build your servers for validating receipts. Automation and In-App messaging tool can help you to send automatic and personalized messages to your users. In addition, mobile subscription analytics and native integration with marketing services give you the complete visibility of your business, and A/B tests allow you to make justified decisions on your price.

Want to learn more? Don’t hesitate to reach out and stay tuned for new updates on our blog.

Suren

Jun 30, 2021

Infrastructure

A Deep Dive into StoreKit 2 Capabilities

During WWDC21, Apple announced many exciting product features, and StoreKit 2 is one of the most significant updates. In-app subscriptions have become the most popular monetization method that generates billions of dollars in revenue for Apple and thousands of mobile app developers. Developers have earned $230 billion through the App Store since its launch, with the biggest chunk of that revenue coming in the last few years.

That makes in-app purchases a particular focus of Apple and Google. Just last year WWDC20 presented StoreKitTransactionManager with a .storekit file that simplifies the process of in-app purchases testing. We have been waiting for this update since iOS3. And the next updates were not long in coming.

This article will cover what’s new in StoreKit 2 and discuss the difference between the updated framework and its previous version – StoreKit. 

Before we jump into StoreKit 2 overview, let’s briefly cover what was wrong with the previous StoreKit.

StoreKit 2 Capabilities Deep Dive

Key limitations of StoreKit

What’s new in StoreKit 2?

We have already covered a list of new features in our previous article, but today we would like to highlight the most significant two: Swift-first design and API update

Swift-first design

StoreKit 2 takes advantage of advancements in Swift. To be more precise, Concurrency with async and await makes a software engineer’s life much easier. It solves (fully or partially – based on one’s needs) many issues with unnecessarily complicated architecture. 

Fetching product information 

Previously, you had to create a SKProductsRequest, attach a delegate, initialize the request, and be sure to keep a strong reference to the request to ensure task completion. It looked like that:

let productsRequest = SKProductsRequest(productIdentifiers: identifiers) productsRequest.delegate = self productsRequest.start() self.productsRequest = productsRequestCopy

And then receive the response:

extension StoreKitService: SKProductsRequestDelegate { func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { // handle products here } }Copy

With StoreKit 2 it will look like this:

let storeProducts = try await Product.request(with: identifiers) Copy

And that’s it. The following line will have the product information. 

Purchase 

To process a purchase in StoreKit, you would have to write something like this:

func purchase(_ product: SKProduct) { let payment = SKPayment(product: product) SKPaymentQueue.default().add(payment) }Copy

And to handle the response, you would need to write this:

extension StoreKitService: SKPaymentTransactionObserver { func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { transactions.forEach { transaction in switch transaction.transactionState { case .purchased: // handle purchased case .failed: // handle failed case .deferred: // handle deferred case .restored: // handle restored default: break } } }Copy

In response, you would find an array with one item – your transaction. The same function will be receiving the results from the restoreCompletedTransactions. In the latter case, there are multiple transactions, and that’s why the function returns the array. On top of that, you will also need to remember who called the purchase in order to send the response back to the right place.

One option is to keep completion in the same class that will be returned – you do not have direct access to completion at this point as handling happens in a different unrelated function. If you need some additional data, it would have to be stored somewhere and synchronized later.

Here is how that could be implemented with StoreKit 2:

func purchase(_ product: Product) async throws -> Transaction? { let result = try await product.purchase() switch result { case .success(let verification): // handle success ... return result case .userCancelled, .pending: // handle if needed default: break }Copy

And again – that’s it.

You have the result from the second line and the rest is its handling. Also, note that a new switch case .userCancelled for transactions that correspond to a user aborting purchase process. To get this info in the previous StoreKit, you would have to process transactionState of SKPaymentTransaction and if it has .failed, then check the error code. It’s important to notice that if there was a problem, you should show an error message to the user, but showing it for a canceled purchase would be silly. It’s nice that the new SDK recognizes this situation as an individual outcome in PurchaseResult and doesn’t mix it up with errors. 

You also don’t need to keep track of who requested a purchase nor think of how it needs to be wrapped. You can just return result at the right moment.

Validation 

As you may notice, after a successful purchase, you might see a verification value that confirms that StoreKit validates this purchase. It is all up to you which field you would follow – do you find device validation reliable or check it on a server.

Additional payment options

Several extra options can be set when a purchase is made, but we found the following is pretty interesting.

Tokens

An arbitrary UUID token can be attached to a transaction that will remain forever. This comes in handy if you have your authorization implementation that isn’t based on AppStore accounts. Let’s say you are providing a streaming service where a subscription provides access across all user devices as long as they stay logged in on your service. In this case, you could simply include your internal accounts as UUID tokens in user purchases. It could be something between these lines:

let result = try await product.purchase(options::[.appAccountToken(yourAppToken))])Copy

Other interesting features

  • Updated interface of promotional purchases

  • Ability to buy products in “batches”. For example, if you have an SKU “1000 coins”, then a user can select five of those and receive 5000 coins at once.

One more thing 

There is a lot to say about the interface updates since almost everything has been reworked and adjusted for the new Swift features, but that’s not the main point. We would like to highlight the changes in the interface of SKPaymentTransactionObserver, which was used to process transactions. For example, whether this is a confirmation of a purchase from a parent account, SCA, an auto-renewal of the subscription, or something else. It is now a listener for the Transaction object. And you can “listen” to new transactions like this:

func listenForTransactions() -> Task.Handle<Void, Error> { return detach { for await result in Transaction.listener { do { // handle transaction result here } } } }Copy

The key is to remember to call transaction.finish(). Otherwise, they will keep coming to the listener on every app restart. Although, this behavior isn’t new as it has not changed since the previous version.

Swift-first design summary

As we can see, Swift’s innovations, coupled with the new StoreKit 2 interface, make developers’ lives much easier. In just a couple of code lines, the developers are now able to: 

  • Fetch product info

  • Launch in-apps

  • Process the purchase results

  • Verify purchases

  • Get new transactions

According to Apple, StoreKit 2 significantly improves the security level of purchase verification, but this is still not a replacement for your server-side validation. And not a reason to give it up.

Powerful new APIs

Here Apple added a lot of new and valuable features. We covered almost everything related to Products and Purchases in the first part. Let’s address the rest now.

Many entities and fields have been reworked, improved, and expanded. Now there is more information on products, purchases, and subscription statuses. Apple has added a large amount of data to the StoreKit 2 public API that was previously only available in a receipt. Since the information in it is encrypted, most of the data available in StoreKit 2 public API will also be encrypted using JWS. For example, information on transactions and auto-renewal of a subscription. And yes, StoreKit 2 will automatically validate this data.

Let’s look at the most exciting advancements of API more closely:

  • Transactions 

  • Current entitlements 

  • Subscription information

  • Auto-synchronization of purchases

Transactions

Now you can get a list of all transactions (or just the latest one) for a particular product directly from StoreKit 2. Previously, you had to parse a receipt to do this. Transaction data can be helpful, for example, for some kind of analytics or simply to share the details with the user on their purchase.

Current entitlements

We have discussed above that the previous version required developers to store the history of active purchases to enable users’ access to it in the app. StoreKit 2 will now do it for you.

Note that this data includes only two types of product:

  • Active subscriptions

  • Non-consumable purchases. Consumable purchases such as coins/ammo/gas will not appear on this list. You must process them immediately upon purchase. 

Subscription information

This part of the article will discuss the most interesting stuff that is included in SubscriptionInfo object. As with transactions, SubscriptionInfo was previously only available in a receipt and required some work on your server. 

Intro offer eligibility

In the previous version of StoreKit, there was no way to determine if a user had a purchase in this product group to decide whether to give them a discount. So you had to do this manually somewhere on the server; now, you can simply call one function and get a Bool in the response.

static func isEligibleForIntroOffer(for groupID: String) async -> Bool Copy

Renewal state

There are several states:

  1. subscribed – the user is currently subscribed.

  2. expired – the subscription expired.

  3. inBillingRetryPeriod – the subscription is in a billing retry period. 

  4. inGracePeriod – the subscription is in a billing grace period state. This feature allows you to extend users access to the app if they have a billing issue. The grace period length varies from 6 to 16 days, depending on the duration of the subscription itself.

  5. revoked – the App Store has revoked the user’s access to the subscription group.

Renewal info

This object will display everything related to the auto-renewal of the subscription. The following information could be found there:

  1. willAutoRenew – a Boolean value that indicates whether the subscription will automatically renew in the next period. If the status is negative, then with some degree of probability, the user will churn. So it’s time to think about how to re-engage them.

  2. autoRenewPreference – the product ID of the subscription that will automatically renew. For example, you can check whether a user has downgraded and plans to use a cheaper option in your subscription. In this case, you can offer that user a discount to keep them on the premium version.

  3. expirationReason – the reason the subscription expired.

An important note about subscription statuses

StoreKit 2 returns an array of statuses. It is necessary for the cases where a user has multiple subscriptions to the same product. For example, they bought one subscription themself and got the second one through family sharing. This way, you will validate the entire array and unlock functionality in your app by referencing the subscription with the highest access level.

Auto-synchronization of purchases

Synchronization of purchases across different devices with one Apple ID is another cool feature. If the user made a purchase on their iPhone and then switched to iPad, this purchase would also be available on it. There is no need for developers to perform additional steps like calling restore().

However, there is a caveat from Apple’s side: automatic synchronizations should cover most of all cases, but as millions of people worldwide use Apple devices, there is still a risk for synchronizations to fail. For such cases, Apple suggests an alternative – a manual call for purchase synchronization – AppStore.sync(). It is pretty similar to restore(), but you will need to call it less often.

Apple also warns that AppStore.sync() should only be called in response to a user action (i.e., pressing a button), since the call initiates the App Store authenticate notification, and if you call it somewhere at the start, then the user experience in the app won’t be satisfactory. 

Summary of the new StoreKit 2 API

A lot of data that previously could only be obtained by decrypting a receipt on the server is now available in the StoreKit itself. It drastically simplifies the work for developers. It has become much easier to check the status of subscriptions and synchronize data across the devices.

So, does StoreKit 2 resolve all the problems of the previous version? Let’s revise the issues we discussed at the beginning of the article.

  1. Is complexity solved? Overall, we still think that this is a complex product that requires a lot of pre-work to understand completely. StoreKit 2 removes the questions like “Where can I get this data?” by introducing “out of the box” solutions.

  2. Has the architecture improved? No doubts. Thanks to Swift Concurrency. 

  3. No validation of purchases? Purchase validation was introduced to StoreKit 2, but API validation remains more reliable, even according to Apple. 

  4. No auto-sync across the devices? Purchases are finally available across all devices with the same Apple ID without any hassle.

  5. No data on existing purchases? This may seem like a minor issue, but it was necessary for everyone who has ever implemented in-app purchases. Now it is also available “right out of the box”.

  6. Missing some valuable data? That still depends on your needs. You may want to parse the receipt for additional data. But StoreKit 2 will cover a lot of use cases. 

To sum up, StoreKit 2 is an excellent product that introduced more than we were asking for. Just think about the features of Swift Concurrency as it solves a lot of everyday problems. Migration to StoreKit 2 won’t happen soon, but there is at least some hope for a brighter future.

Qonversion is staying on top of all new developments and will support StoreKit 2 in all of our products and services. If you’re looking for a single place to manage subscribers and grant them access to premium content — look no further. Our Product Center provides a comprehensive in-app purchases infrastructure so that there’s no need for you to build your servers for validating receipts. Automation and In-App messaging tool can help you to send automatic and personalized messages to your users. In addition, mobile subscription analytics and native integration with marketing services give you the complete visibility of your business, and A/B tests allow you to make justified decisions on your price.

Want to learn more? Don’t hesitate to reach out and stay tuned for new updates on our blog.

Suren

Jun 30, 2021

Infrastructure

A Deep Dive into StoreKit 2 Capabilities

During WWDC21, Apple announced many exciting product features, and StoreKit 2 is one of the most significant updates. In-app subscriptions have become the most popular monetization method that generates billions of dollars in revenue for Apple and thousands of mobile app developers. Developers have earned $230 billion through the App Store since its launch, with the biggest chunk of that revenue coming in the last few years.

That makes in-app purchases a particular focus of Apple and Google. Just last year WWDC20 presented StoreKitTransactionManager with a .storekit file that simplifies the process of in-app purchases testing. We have been waiting for this update since iOS3. And the next updates were not long in coming.

This article will cover what’s new in StoreKit 2 and discuss the difference between the updated framework and its previous version – StoreKit. 

Before we jump into StoreKit 2 overview, let’s briefly cover what was wrong with the previous StoreKit.

StoreKit 2 Capabilities Deep Dive

Key limitations of StoreKit

What’s new in StoreKit 2?

We have already covered a list of new features in our previous article, but today we would like to highlight the most significant two: Swift-first design and API update

Swift-first design

StoreKit 2 takes advantage of advancements in Swift. To be more precise, Concurrency with async and await makes a software engineer’s life much easier. It solves (fully or partially – based on one’s needs) many issues with unnecessarily complicated architecture. 

Fetching product information 

Previously, you had to create a SKProductsRequest, attach a delegate, initialize the request, and be sure to keep a strong reference to the request to ensure task completion. It looked like that:

let productsRequest = SKProductsRequest(productIdentifiers: identifiers) productsRequest.delegate = self productsRequest.start() self.productsRequest = productsRequestCopy

And then receive the response:

extension StoreKitService: SKProductsRequestDelegate { func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { // handle products here } }Copy

With StoreKit 2 it will look like this:

let storeProducts = try await Product.request(with: identifiers) Copy

And that’s it. The following line will have the product information. 

Purchase 

To process a purchase in StoreKit, you would have to write something like this:

func purchase(_ product: SKProduct) { let payment = SKPayment(product: product) SKPaymentQueue.default().add(payment) }Copy

And to handle the response, you would need to write this:

extension StoreKitService: SKPaymentTransactionObserver { func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { transactions.forEach { transaction in switch transaction.transactionState { case .purchased: // handle purchased case .failed: // handle failed case .deferred: // handle deferred case .restored: // handle restored default: break } } }Copy

In response, you would find an array with one item – your transaction. The same function will be receiving the results from the restoreCompletedTransactions. In the latter case, there are multiple transactions, and that’s why the function returns the array. On top of that, you will also need to remember who called the purchase in order to send the response back to the right place.

One option is to keep completion in the same class that will be returned – you do not have direct access to completion at this point as handling happens in a different unrelated function. If you need some additional data, it would have to be stored somewhere and synchronized later.

Here is how that could be implemented with StoreKit 2:

func purchase(_ product: Product) async throws -> Transaction? { let result = try await product.purchase() switch result { case .success(let verification): // handle success ... return result case .userCancelled, .pending: // handle if needed default: break }Copy

And again – that’s it.

You have the result from the second line and the rest is its handling. Also, note that a new switch case .userCancelled for transactions that correspond to a user aborting purchase process. To get this info in the previous StoreKit, you would have to process transactionState of SKPaymentTransaction and if it has .failed, then check the error code. It’s important to notice that if there was a problem, you should show an error message to the user, but showing it for a canceled purchase would be silly. It’s nice that the new SDK recognizes this situation as an individual outcome in PurchaseResult and doesn’t mix it up with errors. 

You also don’t need to keep track of who requested a purchase nor think of how it needs to be wrapped. You can just return result at the right moment.

Validation 

As you may notice, after a successful purchase, you might see a verification value that confirms that StoreKit validates this purchase. It is all up to you which field you would follow – do you find device validation reliable or check it on a server.

Additional payment options

Several extra options can be set when a purchase is made, but we found the following is pretty interesting.

Tokens

An arbitrary UUID token can be attached to a transaction that will remain forever. This comes in handy if you have your authorization implementation that isn’t based on AppStore accounts. Let’s say you are providing a streaming service where a subscription provides access across all user devices as long as they stay logged in on your service. In this case, you could simply include your internal accounts as UUID tokens in user purchases. It could be something between these lines:

let result = try await product.purchase(options::[.appAccountToken(yourAppToken))])Copy

Other interesting features

  • Updated interface of promotional purchases

  • Ability to buy products in “batches”. For example, if you have an SKU “1000 coins”, then a user can select five of those and receive 5000 coins at once.

One more thing 

There is a lot to say about the interface updates since almost everything has been reworked and adjusted for the new Swift features, but that’s not the main point. We would like to highlight the changes in the interface of SKPaymentTransactionObserver, which was used to process transactions. For example, whether this is a confirmation of a purchase from a parent account, SCA, an auto-renewal of the subscription, or something else. It is now a listener for the Transaction object. And you can “listen” to new transactions like this:

func listenForTransactions() -> Task.Handle<Void, Error> { return detach { for await result in Transaction.listener { do { // handle transaction result here } } } }Copy

The key is to remember to call transaction.finish(). Otherwise, they will keep coming to the listener on every app restart. Although, this behavior isn’t new as it has not changed since the previous version.

Swift-first design summary

As we can see, Swift’s innovations, coupled with the new StoreKit 2 interface, make developers’ lives much easier. In just a couple of code lines, the developers are now able to: 

  • Fetch product info

  • Launch in-apps

  • Process the purchase results

  • Verify purchases

  • Get new transactions

According to Apple, StoreKit 2 significantly improves the security level of purchase verification, but this is still not a replacement for your server-side validation. And not a reason to give it up.

Powerful new APIs

Here Apple added a lot of new and valuable features. We covered almost everything related to Products and Purchases in the first part. Let’s address the rest now.

Many entities and fields have been reworked, improved, and expanded. Now there is more information on products, purchases, and subscription statuses. Apple has added a large amount of data to the StoreKit 2 public API that was previously only available in a receipt. Since the information in it is encrypted, most of the data available in StoreKit 2 public API will also be encrypted using JWS. For example, information on transactions and auto-renewal of a subscription. And yes, StoreKit 2 will automatically validate this data.

Let’s look at the most exciting advancements of API more closely:

  • Transactions 

  • Current entitlements 

  • Subscription information

  • Auto-synchronization of purchases

Transactions

Now you can get a list of all transactions (or just the latest one) for a particular product directly from StoreKit 2. Previously, you had to parse a receipt to do this. Transaction data can be helpful, for example, for some kind of analytics or simply to share the details with the user on their purchase.

Current entitlements

We have discussed above that the previous version required developers to store the history of active purchases to enable users’ access to it in the app. StoreKit 2 will now do it for you.

Note that this data includes only two types of product:

  • Active subscriptions

  • Non-consumable purchases. Consumable purchases such as coins/ammo/gas will not appear on this list. You must process them immediately upon purchase. 

Subscription information

This part of the article will discuss the most interesting stuff that is included in SubscriptionInfo object. As with transactions, SubscriptionInfo was previously only available in a receipt and required some work on your server. 

Intro offer eligibility

In the previous version of StoreKit, there was no way to determine if a user had a purchase in this product group to decide whether to give them a discount. So you had to do this manually somewhere on the server; now, you can simply call one function and get a Bool in the response.

static func isEligibleForIntroOffer(for groupID: String) async -> Bool Copy

Renewal state

There are several states:

  1. subscribed – the user is currently subscribed.

  2. expired – the subscription expired.

  3. inBillingRetryPeriod – the subscription is in a billing retry period. 

  4. inGracePeriod – the subscription is in a billing grace period state. This feature allows you to extend users access to the app if they have a billing issue. The grace period length varies from 6 to 16 days, depending on the duration of the subscription itself.

  5. revoked – the App Store has revoked the user’s access to the subscription group.

Renewal info

This object will display everything related to the auto-renewal of the subscription. The following information could be found there:

  1. willAutoRenew – a Boolean value that indicates whether the subscription will automatically renew in the next period. If the status is negative, then with some degree of probability, the user will churn. So it’s time to think about how to re-engage them.

  2. autoRenewPreference – the product ID of the subscription that will automatically renew. For example, you can check whether a user has downgraded and plans to use a cheaper option in your subscription. In this case, you can offer that user a discount to keep them on the premium version.

  3. expirationReason – the reason the subscription expired.

An important note about subscription statuses

StoreKit 2 returns an array of statuses. It is necessary for the cases where a user has multiple subscriptions to the same product. For example, they bought one subscription themself and got the second one through family sharing. This way, you will validate the entire array and unlock functionality in your app by referencing the subscription with the highest access level.

Auto-synchronization of purchases

Synchronization of purchases across different devices with one Apple ID is another cool feature. If the user made a purchase on their iPhone and then switched to iPad, this purchase would also be available on it. There is no need for developers to perform additional steps like calling restore().

However, there is a caveat from Apple’s side: automatic synchronizations should cover most of all cases, but as millions of people worldwide use Apple devices, there is still a risk for synchronizations to fail. For such cases, Apple suggests an alternative – a manual call for purchase synchronization – AppStore.sync(). It is pretty similar to restore(), but you will need to call it less often.

Apple also warns that AppStore.sync() should only be called in response to a user action (i.e., pressing a button), since the call initiates the App Store authenticate notification, and if you call it somewhere at the start, then the user experience in the app won’t be satisfactory. 

Summary of the new StoreKit 2 API

A lot of data that previously could only be obtained by decrypting a receipt on the server is now available in the StoreKit itself. It drastically simplifies the work for developers. It has become much easier to check the status of subscriptions and synchronize data across the devices.

So, does StoreKit 2 resolve all the problems of the previous version? Let’s revise the issues we discussed at the beginning of the article.

  1. Is complexity solved? Overall, we still think that this is a complex product that requires a lot of pre-work to understand completely. StoreKit 2 removes the questions like “Where can I get this data?” by introducing “out of the box” solutions.

  2. Has the architecture improved? No doubts. Thanks to Swift Concurrency. 

  3. No validation of purchases? Purchase validation was introduced to StoreKit 2, but API validation remains more reliable, even according to Apple. 

  4. No auto-sync across the devices? Purchases are finally available across all devices with the same Apple ID without any hassle.

  5. No data on existing purchases? This may seem like a minor issue, but it was necessary for everyone who has ever implemented in-app purchases. Now it is also available “right out of the box”.

  6. Missing some valuable data? That still depends on your needs. You may want to parse the receipt for additional data. But StoreKit 2 will cover a lot of use cases. 

To sum up, StoreKit 2 is an excellent product that introduced more than we were asking for. Just think about the features of Swift Concurrency as it solves a lot of everyday problems. Migration to StoreKit 2 won’t happen soon, but there is at least some hope for a brighter future.

Qonversion is staying on top of all new developments and will support StoreKit 2 in all of our products and services. If you’re looking for a single place to manage subscribers and grant them access to premium content — look no further. Our Product Center provides a comprehensive in-app purchases infrastructure so that there’s no need for you to build your servers for validating receipts. Automation and In-App messaging tool can help you to send automatic and personalized messages to your users. In addition, mobile subscription analytics and native integration with marketing services give you the complete visibility of your business, and A/B tests allow you to make justified decisions on your price.

Want to learn more? Don’t hesitate to reach out and stay tuned for new updates on our blog.

Start Now for Free

Or book a demo with our team to learn more about Qonversion

Start Now for Free

Or book a demo with our team to learn more about Qonversion

Start Now for Free

Or book a demo with our team to learn more about Qonversion

Start Now for Free

Or book a demo with our team to learn more about Qonversion