StoreKit 2 delivers powerful, Swift-native APIs for in-app purchases and auto-renewable subscriptions. Learn how you can easily implement in-app purchases and subscriptions, and discover APIs for retrieving product information, handling transactions, determining product entitlements and customer status, as well as comprehensive testing support in Xcode.
♪ Bass music playing ♪ ♪ Ross LeBeau: Hi, and welcome to WWDC21. My name is Ross LeBeau, and I'm an engineer on the StoreKit team. Today, we're going to be talking about StoreKit, and this is actually one of three sessions designed to help you implement client-side code, build out your server for in-app purchase, and support your customers and handle refunds. This session is Meet StoreKit 2, and you can find the other two sessions here at WWDC21. This session will focus on client-side features and implementation. So let's get started! Since StoreKit was introduced in iOS 3, it's created great opportunities for you and your businesses. Today, it exists on four Apple platforms and supports everything from games to news apps, indie titles to international hits. Over the years, we've introduced great features such as offer codes, Family Sharing, and StoreKit testing in Xcode. But this year, we decided to go back to the beginning. Today, I'm excited to introduce you to StoreKit 2! StoreKit 2 is a brand new set of modern and flexible Swift APIs for working with In-App Purchase across iOS, macOS, tvOS, and watchOS. We've taken a new look at StoreKit with a Swift-first mindset. We embraced some of the newest language features, like Swift concurrency using the async/await pattern, to create simple yet powerful APIs. We've also made some huge updates to the in-app purchase transactions, making them much easier to work with, while also providing more information and high security. And we've added more powerful APIs specifically for subscriptions in order to give you deeper insight that you can use to grow your business. StoreKit 2 APIs live inside the same StoreKit framework that exists today and focus on the core in-app purchase experience rather than replacing every API. The new StoreKit 2 APIs are comprised of five major areas: Products, Purchases, Transaction info, Transaction history, and subscription status. Today I'm going to give you an overview of each of these areas, and my colleague Jakob will be showing you how to use the corresponding StoreKit 2 APIs in real code. So first, let's start with the building blocks of StoreKit: products and purchases. The StoreKit 2 product struct is a supercharged version of the StoreKit product object you're used to. To start, we've added additional data to it, such as the product type and extended subscription information. With StoreKit 2, we're making it easier to do things like find out if the customer is eligible for your introductory offer. We're also making StoreKit 2 products forward-compatible with new features. We've accomplished this by adding a wrapping type called BackingValue that allows you to retrieve data contained in the product by subscripting the products directly. This means that if we add data to products in the future, you'll always be able to access them in StoreKit 2, even on SDKs and devices running operating systems that have older versions of StoreKit 2. This means that you can use the latest features to provide new functionality to a larger section of your customer base. With StoreKit 2, you request products by calling a static function on the product type itself. This requests product metadata from the App Store just like the existing SKProductsRequest does. But thanks to the new Swift concurrency async/await pattern, the StoreKit 2 product request requires just one line of code. Similarly, purchasing a product in StoreKit 2 is another simple, one-line task. Purchase is now an instance method on the product type, meaning you can take those products you just retrieved and call purchase directly on them. Since the purchase method also uses async/await, you get the result of your purchase inline in your code. Now we know that not every purchase is the same. If you want to modify purchase behavior beyond the default settings, StoreKit 2 has Purchase options. A Purchase option is an item that describes a single property of a purchase. You can compose Purchase options into a set which you pass into the purchase method. StoreKit 2 includes purchase options for things such as quantity and promotional offers. And in StoreKit 2, we're adding a new option called App account token. The App account token is a way for you to keep track of which of your app's user accounts began and completed a transaction. It’s an opaque token that you generate which you can link to accounts that your app owns. It's easy to generate an App account token, because the only requirement is that it conforms to the UUID format. You send up an App account token on the purchase as a Purchase option, and this token is returned in the transaction info for that purchase. The App account token stays in the transaction info forever, even across devices. If your app supports its own account system, this can help you keep track of which purchases each in-app account has made, regardless of the Apple ID or device that was used to purchase them. So, we've talked about getting products from the App Store and initiating a purchase; now what happens when that purchase completes? As you might expect, StoreKit is going to return a successful transaction to you along with cryptographically signed information. Sounds familiar, right? Well, StoreKit 2 is bringing the biggest update to in-app purchase transactions ever. To start, StoreKit 2 will be providing an individually signed object for every transaction. Not only that, but starting in StoreKit 2, in-app purchase transaction info will now be provided in a very common, easy-to-work-with format: JSON. And since we know that secure cryptographic signing is an important part of StoreKit purchases, we're now using a common standard used across the web known as JSON Web Signature. Plus, all the information contained in the signed object will now be available through native StoreKit APIs, making it easy to work with this data in your app's code. In fact, we'll show you just how easy it is. Here's Jakob with a demonstration of these APIs in real code. Jakob Swank: Hi, I'm Jakob. I'm an engineer on the StoreKit team. I'm really excited today to be showing you how easy it is to get up and running with StoreKit 2 in your app. On the right, I have an app I'm building called Pocket Cars. You can download the sample code for this app in the resources section of this session and follow along. The app has two main views. There's a view for my collected cars and a view for my store. Let's go over to the store. Currently, my store is empty because I have no products available for sale. I'm going to go ahead and implement those now. To get up and running quickly, I'm using StoreKit testing in Xcode. This allows me to build and test my store before defining products in App Store Connect. In my Xcode project, I've already created a StoreKit configuration file defining the products I want to sell. This is the same configuration file I was using for StoreKit; I don't need to change or migrate anything. Here, I also have a plist containing all my product identifiers. It's included as a resource file built into my app so I can use it at runtime. To display these products in my store, I first need to make a product request using a set of the product identifiers I want to display. With StoreKit 2, I can do this simply by calling a static method on the Product struct. After I receive the products from the App Store, I want to separate them by type. I can do this easily with StoreKit 2 since the Product type now provides a property for the type as it's defined on the App Store server. In my app, I'm selling three types of products: fuel, cars, and a navigation package. Fuel is a consumable product -- once I use it, it's gone -- so I'll put all the consumables in to the fuel array. Cars are a non-consumable. Once I purchase a car, I own it forever. So I'll put all the non-consumables into the cars array. The navigation package is a subscription product with three levels of service. My customers can subscribe to one level of service at a time, and they are billed periodically. Also, they can upgrade or downgrade at any time if they want to change their level of service. The App Store will return a Product for each level of service, so I'll put all the auto-renewable subscriptions into the subscriptions array. I also want to sort my products within each type by price, lowest to highest. Let's run my app and check out what I've done so far.
Now I'll navigate to the store. Wow! Before, my store was empty, but now it's looking pretty great with all my products displayed. With just one line of code, I was able to request my app's products from the App Store, then I was able to group and sort those products based only on the metadata I received which made it easy to build my store UI. Now my products are looking great, but if I tap a buy button, nothing happens. That's because the purchase method in my store doesn't do anything. It should initiate the purchase with StoreKit. We can do this simply by calling the purchase method on the product. As Ross mentioned, StoreKit 2 was built from the ground up to use the new concurrency capabilities of Swift. This allows my app to keep the code for purchasing and processing the result of that purchase all within the same context in order to keep my code easy to read. When a purchase completes, a PurchaseResult is returned. This PurchaseResult lets me know whether the purchase was successful or if it completed in some other non-error state, such as the user cancelled the purchase or the purchase needs some extra bank validation or approval from a parent. To handle each case, I'll just switch over them. If the PurchaseResult is in the success state, I also get a verification result. A verification result contains two cases: verified and unverified. In StoreKit 2, the transaction type contains the JWS payload which represents the signed transaction. Every time my app receives a transaction from StoreKit 2, the transaction has passed through a verification process to confirm whether the payload is signed by the App Store for my app for this device. You heard that right. StoreKit 2 does transaction verification for you. Of course, how I choose to handle the verification result is entirely up to me and the needs of my business. For my app, I'm going to make sure this transaction I received from StoreKit has been verified. Here in my store, I'll create a checkVerified method I can use for any VerificationResult. If the result is unverified, I throw my own failedVerification error to alert other parts of my app. If the result is verified, I unwrap the transaction and return it to the caller. Now I can use this checkVerified method on the results of the purchase. Finally, with my transaction verified, I deliver the content to my user. After the user has the content, I need to make sure I tell StoreKit to finish the transaction. Then I need to return it so my UI can get updated. My app has an account database I maintain. I want to include my app's current logged in user with the StoreKit purchase so this information is always available to my app when it gets the App Store signed transaction. I can do this by creating an appAccountToken purchase option using a tokenized version of my logged-in account and passing that option to the purchase method. OK. We're all set with my purchase method implementation. Let's run my app again.
Now we're back in my store, and I'm feeling pretty adventurous. So I'm going to purchase a motorcycle because I've always wanted one. There's the payment sheet from StoreKit showing me the purchase has been initiated properly. I'll tap to confirm the purchase. StoreKit then displays an alert showing the purchase was successful. After I dismiss that alert, my buy button changes to a green checkmark showing my app trusts the transaction and my motorcycle has been delivered. There's one more important thing I want to note here. As I said previously, sometimes a customer will have to do some extra verification on their account or they'll need a parent's approval before a purchase will complete. In these cases, the purchase result I receive from product.purchase() will be in a pending state. This means after the customer completes the account verification or their parent gives approval, my app should update the UI to reflect the completed purchase. To listen for these transaction updates, I need to iterate over a static property on the transaction type.
This property is an infinite async sequence. That means it will continue iterating over transaction updates as they come in from StoreKit until I choose to cancel or break out of the for loop. Here I'm creating a detach task which will return a task handle I can use to explicitly cancel my update listener when the store is deallocated. Just like all transactions I receive from StoreKit 2, I want to check if the verification result is verified before delivering content to the user. I can use my previously defined checkVerified method. And, just like with a purchase response, once I have my verified transaction, I need to deliver the content to my user. And of course, I always need to finish my transactions. It's very important I start my transaction update listener as soon as my app launches so I don't miss a single one. I'm going to do this as soon as my store is created, which happens right around app launch. In order to test my update listener, I'm going to enable Ask To Buy in my Xcode test environment to simulate a purchase response in a pending state. To do this, I select my StoreKit configuration file and in the Editor menu, I select Enable Ask To Buy. Let's run my app again and do a purchase.
This time, after I confirm on the payment sheet, I see a new alert from StoreKit saying I need to ask permission to complete the purchase. I'll go ahead and tap Ask. The purchase response is returned to my app in a pending state. To approve the purchase, I'm going to open the StoreKit testing in Xcode transaction manager and click the Approve button in the top-right corner.
Great! Immediately after I approved the transaction, my update listener received the verification result and the UI immediately changed to show the approved purchase. Now I have a brand-new, standard 5-seater to cruise around in. I just showed you how easy it is to request products, start a purchase, react to different purchase results, verify the integrity of a transaction, and receive updates from the App Store for pending transactions, all using StoreKit 2. Now let's go back to Ross for an introduction on working with your user's transaction history and subscription status. Ross: Wow! It’s pretty amazing to see these new APIs in action. And automatic validation, what more could you want? What's that? You love cryptography and you still want to validate the data yourself? Not to worry. StoreKit 2's automatic validation raises the bar for security, but it isn't meant to completely replace your own validation. As always, security lies on a spectrum of strength, time, and complexity. I'll give you the scoop on validation a bit later. First, if you're as excited about StoreKit 2 transactions as I am, you'll love to hear that we're giving you lots of new ways to work with them. We're adding a new set of APIs for querying completed transactions in the user's transaction history. In StoreKit 2, you can access all of the user's past transactions with a single API call. You can also access the latest transaction for a product. So if you want to see just the most recent renewal of a subscription, you can. And we know that the number one thing you need to know is what products the user has paid for access to right now. So we've distilled that information into a single function called CurrentEntitlements. Current entitlements contains all of the non-consumables in the user's transaction history, as well as all of the subscription transactions that are currently active. With this, you have all of the information you need to unlock everything the user has paid for in your app. And since this represents only things that the user should have access to right now, any transactions that have been revoked are not included in the response. Consumables are also not included, since they’re not a persistent part of the transaction history. Now, you must be thinking, "I can't wait! When can I start calling these in my app?" Well, with StoreKit 2, every transaction a user has ever completed is available to your app as soon as you ask for it. This means that when a user installs your app on a new device, you'll be able to tell which products they are entitled to get access to on the very first time your app is opened. Furthermore, transaction history will automatically update across users' devices. When your customer makes a purchase on one device, your app will be able to see the purchase on every other device it's installed on. In fact, if your app is running when a purchase is made on another device, you'll be notified about the new transaction. Jakob mentioned that it's important to listen for transactions as soon as your app starts, and this is just one more reason that's true. So, all of this means that users won't need to restore completed transactions when your app is reinstalled or downloaded on a new device. Everything should automatically be fetched by StoreKit and stay up to date. But people use their Apple devices in millions of ways in millions of places. In the rare case that a user thinks they should have a transaction but you don't see it, you can use the App Store sync API. This immediately resynchronizes all StoreKit 2 transactions. This is a replacement for the restoreCompletedTransactions API, and you should provide UI in your app that allows users to initiate the sync. However, thanks to StoreKit 2's automatic synchronization, it should be very rare that a user needs to initiate a sync manually. Automatic synchronization should cover the majority of cases. If a user does need to initiate a manual sync, they will be required to authenticate their account. For this reason, you should only use this API in response to user input. Finally, all transactions made using StoreKit 2 APIs are available in the original StoreKit APIs, and vice versa. So if your app has existing transactions, you will be able to see them in the StoreKit 2 APIs as soon as you start using them. New purchases made with the original StoreKit APIs will be available via StoreKit 2 APIs immediately, and purchases made with StoreKit 2 will also be available inside the unified receipt when it is refreshed. In addition to transaction history, StoreKit 2 is also adding ways for you to get detailed information about a user's subscription status. The subscription status has three parts. The first is the latest transaction. This conveniently lets you access the last transaction that occurred for this subscription, and it's the same as if you called the latest transaction we talked about at earlier. The second is the renewal state. This is an enumeration that tells you the current state of the subscription. If you just want to know what's going on with the subscription right now, just look at this value. It'll tell you if it's currently subscribed, expired, in the grace period, and more. We've designed it to give you a single place to look in order to make it easy to base app logic off this value. And the final part of the subscription status is the renewal info. This is where you can see all of the details about a user's subscription. It contains all kinds of information that isn't in the transaction info because this data can actually change without a transaction. For example, in the renewal info, you can find the auto-renew status, which tells you if the user has auto-renew turned on or off for this subscription. You can even see the product ID that their auto-renewal is set for. So if a user has recently downgraded their subscription, you can see that right here, and maybe use that as an opportunity to present them with a winback offer to stay at the higher tier. If the subscription is already expired, you can use the renewal info to see the expiration reason. And the full renewal info has all this data and more, plus another critical feature. That's right, all you cryptography buffs, the renewal info is signed using JWS! Just like the transaction info, the renewal info is a critical part of unlocking service and making marketing decisions. So we're giving you the confidence to know that it's valid and directly from Apple. And, to answer the question that I'm sure is running through your head right now, yes, StoreKit 2 will automatically validate the renewal info for you. One final thing to know about the subscription status APIs is that they return an array of statuses. This is because, in some cases, users can have multiple subscriptions to the same product. For example, a user may have subscribed to your product, and then also receive a subscription through Family Sharing. You should check the array to see what the highest level of service they're entitled to is. Now I'll hand it back to Jakob to show you what it looks like to work with these transaction history and subscription status APIs in your app code. Jakob: Thanks, Ross. Let's go back to the app I've been working on and update it to use the new transaction history and subscription status APIs Ross just talked about. You'll notice the motorcycle I previously purchased doesn't have a green check, and after I navigate away from my store view, then back, the standard 5-seater doesn't have a green check either. As a user, I can't tell what I've already purchased. This is a problem easily solved with StoreKit 2. At any time, my app can query StoreKit for which products have been purchased so I can keep my app's UI always up to date. Over in my Store.swift file, the isPurchased method currently returns only false. Let's fix that with a simple call to Transaction.latest(for:). Then pass in my product identifier to get the most recent transaction. This StoreKit method returns another verification result telling me the transaction has passed through StoreKit 2's verification check. I'll confirm the transaction is verified and unwrap it using the checkVerified method I wrote previously. Then, I'll make sure my app doesn't deliver content for a transaction that has been refunded by checking revocationDate equals nil. Also, subscriptions where my customer has upgraded to a higher level of service in the middle of the period will have the isUpgraded flag set to true. I want to make sure my app is delivering the highest level of service my customer has subscribed to, so the isPurchased method should ignore any upgraded transactions. For a subscription product, the transaction type tells only part of the story. In addition to the date of the transaction and the expiration date of the subscription, I also want to know when the next renewal date is, and whether my customer has turned off the auto-renewal for their subscription or whether their next renewal period will change the level of service they're subscribed to. To get all this information, StoreKit 2 offers a subscription status API. In my SubscriptionsView.swift file, the updateSubscriptionStatus method is responsible for getting the subscription status from StoreKit and displaying it to the user. Because all of my subscription products belong to the same group, I can use any of them to get the current status for the group. I'll just choose the first subscription product from the store. Once I have a product, I can get the status property from the subscription. It's that easy. As Ross mentioned, it's possible for a user to be paying for their own personal subscription while they also have a subscription being shared by a family member. So the status property will return an array containing all the statuses for each subscription. Now, they could have a standard tier shared with them while they've personally subscribed to the pro tier. I want to make sure my user is able to get the highest level of service they have access to, so I'll iterate through each status. Next, I'll check whether the status has a state of expired or revoked. I want to ignore these cases and not display anything to the user. For all other cases, I'll get the renewalInfo and make sure it's verified using the checkVerified method on my store. Once I confirm the renewalInfo is verified, I'll compare the level of service to the previous products. This check will get the corresponding product for the subscription status, then it will compare with any previous product, and if it's a higher tier, we set highestStatus and highestProduct to the new subscription. Once I've checked all my statuses and I've determined the highest level of service, I'll set the status and current subscription of my view. Let's build and run now.
Over in my store view, the products I previously purchased now show a green checkmark to indicate I already own them and I don't need to buy them again. Let's see what happens when I purchase one of my subscription products.
After I confirm the purchase, the status is displayed right in my store. I can let my user know what they're subscribed to and when their subscription will renew all using APIs built into StoreKit 2. Now how about this My Cars view? It should show all of my purchased products, but currently it's empty. To fill this in, I could iterate through all my products, then get the latest transaction for each, check the transaction's expiration date and whether it has been refunded, but that sounds like a lot. Thankfully, I can use the power of StoreKit 2 and a new, simple, and convenient API to get all my user's valid transactions called currentEntitlements. Over in the My Cars view, I have this method to refresh my purchased products when the view is loaded. Just like with transaction updates, I iterate over the current entitlements. But unlike transaction updates, the current entitlement's async sequence is finite, so it won't wait in the for loop forever, delivering new entitlements as the user makes more purchases. For each entitlement, I want to check the verification result as I do with every other transaction. Once I know they're verified, I'll filter the entitlements into different arrays by switching over the productType property, just like I did with my original product request. Current entitlements will only return transactions for non-consumable and auto-renewable products. I can ignore any other product type to complete my switch statement and keep the Swift compiler happy. Once I have the transaction, I need to get the associated product to display in my UI. For non-consumable transactions, I'll search the cars product array for the product identifier matching this transaction. And likewise, I'll search the subscriptions product array to match any auto-renewable transactions. Let's run again and check out my UI.
Now, when I go into the My Cars view, I see everything I've purchased. All my cars are grouped together at the top and my subscription is below. Now my app has a full working store and it looks awesome! And that's how you can use the transaction history and subscription status APIs to make informed decisions in your app about what UI your users see. Now let's go back to Ross who will talk more in depth on the JSON Web Signature object. Ross: Thanks again for the fantastic demo, Jakob. You can really see how the new transaction and subscription APIs come in handy. Now that we've seen the two ways that StoreKit 2 uses JWS for security, I promised we'd take a closer look at it and how you can do your own validation. JSON Web Signature is comprised of three parts. The first is the header, which contains metadata about the object. This contains crucial information such as which algorithm is used for signing and where to find the certificate used to validate the signature. StoreKit 2 currently uses an ECDSA algorithm, which is supported natively in Swift with CryptoKit. For the certificate, StoreKit 2 uses the x5c header, which indicates that the entire certificate chain is included in the JWS data. This means that no internet connection is necessary in order to validate these JWS signatures. The next part of the JWS data is the payload. This is the main information such as transaction ID, product ID, purchase date, and so on. Once you've validated the signature, this is where you go to read all the data you want to know about the transaction or subscription. And the final part of the JWS data is the signature itself. This is generated using both the header and the payload. Validating the JWS signature is a well-documented part of the standard, so I recommend going straight to the original source if you're interested in writing your own implementation. I've included a link to this document in the resources associated with this session. Once you've validated the signature from the JWS data, there are just a couple more things you should do to make sure the signed info is valid for your app and the current device. First, you should make sure that the bundle ID present in the signed info payload matches the bundle ID of your app. We recommend that for added security, you embed your app's bundle ID somewhere in the app, rather than relying on an API call, and use that value to compare it to the bundle ID in the payload. And the final thing you should do is perform a device validation check. This ensures that the signed info was actually generated for the device it currently on. Use the StoreKit 2 API AppStore.deviceVerificationID to retrieve the current device verification identifier. Then, take the device verification nonce from the signed info and append the device verification identifier you just got from StoreKit. Perform a SHA384 hash on this value and compare the result to the device verification field from the signed info. If they match, then the signed info was generated for this device and your signed info validation is complete. One final thing to note is that these new JWS objects are for in-app purchase only. So if you need to validate the app receipt, you should use the existing API and process for that. And of course, we are offering new App Store server APIs for these new JWS objects, so you can retrieve and validate them directly on your server. Well, I hope you were as excited to meet StoreKit 2 as we were to introduce it to you today. StoreKit 2 is making in-app purchase even better with new APIs that give you more information and are easier to use than ever. This includes new JSON-based info objects for each transaction, as well as APIs to give you transaction details and historical transaction data in your native code. Combine that with the new subscription status APIs, and StoreKit 2 unlocks a wealth of possibilities for your in-app purchases. To learn even more about in-app purchase, I encourage you to watch these other sessions for server-side coding and supporting customers. Jakob and I are thrilled we could introduce you to StoreKit 2. Thanks for joining us at WWDC21! ♪
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.