Streaming is available in most browsers,
and in the Developer app.
-
Implement proactive in-app purchase restore
Learn how you can restore someone's in-app purchases access proactively when they first open your app. We'll show you how you can deliver instant access to existing subscriptions using StoreKit or StoreKit 2 and cover best practices for both your client and server implementations. Find out more about how you can determine customer purchase state and create a personalized onboarding experience for your app.
Resources
- App Store Server Notifications
- CloudKit
- Determining service entitlement on the server
- Implementing a store in your app using the StoreKit API
- Introducing StoreKit 2
- Reducing Involuntary Subscriber Churn
Related Videos
WWDC22
WWDC21
Tech Talks
WWDC20
-
Download
♪ instrumental hip hop music ♪ Hi, I’m David Wendland, a Commerce Technical Advocate for the App Store. Today, I’ll show you how your app can deliver a first class experience by proactively identifying a customer's new, current, and past purchases, without the customer taking any action. I’ll cover how to do this with StoreKit 2 and the original StoreKit, so you can optimize your app's onboarding experience for all your customers. Let me start by defining proactive in-app purchase restore. This means that when a customer launches your app, you use the data readily available, on device, to proactively check for the transactions in order to determine if they are a new or existing customer and doing so without requiring any customer action, not even tapping a "Restore Purchases" button or entering a password. This enables you to tailor your app experience to your customer's purchase history and state so your app unlocks products or services for your current customers, or your app merchandises your latest product offering to new customers, or for those past subscribers, you present them subscription offers to win them back. This is what proactive restore is about, using StoreKit to optimize your app's experience for new, existing, and past customers, on all of their devices, automatically. Let’s look at this example. Here we have our Ocean Journal app. This is a common merchandising experience, where the customer has a few different calls to action to choose from. Either I can attempt to buy the in-app purchase and authenticate with biometrics, such as FaceID, or if I’ve created an app account, I could sign in and possibly use Keychain to enter my password, or if I believe I’m an active subscriber, I could use the "Restore Purchases" button. For your active subscribers on a new device, knowing which option to choose isn’t always clear to them. And with the data readily available to your app, this experience can be streamlined with our proactive in-app purchase restore best practice.
So, if I launched this app on a new device but was already an active subscriber, upon launch, the app would proactively restore my service, automatically, without requiring any action from me. So here the app recognized my Pro subscription and loaded my favorite beach, complete with surf conditions and enabled the live cam feature. This experience differentiates your app from the others and I will cover how to do this with StoreKit 2 on iOS 15 and newer. Additionally, if your app supports previous versions of iOS, I will cover how to create this same great experience with original StoreKit and the verifyReceipt endpoint. With that background, here’s what I’ll cover. First, I’ll describe in detail the core customer product states that your app uses to generate personalized experiences based on the customer's in-app purchases with StoreKit. Then I'll review the steps to implement, using StoreKit 2, complete with sample code using the SK Demo app. Let’s look at each in-app purchase type, their core customer product states, and review a few examples of a personalized onboarding experience. To start, the in-app purchase types that apply to proactive restore are non-consumables, non-renewing subscriptions, and auto-renewable subscriptions, as they are all persistent in the customer's transaction history and will always be available with StoreKit. Therefore your app can identify per customer their purchase state for each product or subscription group. For non-renewing and auto-renewable subscriptions, I will use the term "subscriptions" to reference them both as we review the customer product states. Here are the three core states your app can personalize against. Let’s review in-depth new customers. This state represents a signed in App Store Apple ID that does not have any current or past in-app purchase transactions. This state is typically used as the app’s default merchandising experience. Our Ocean Journal app is merchandising its monthly and annual subscription with a one month free trial. Looking at our second core state, we have Purchased and Active Subscriber. In this state, a customer has an active transaction and your app is obligated to grant the customer access to the purchased product or service. Here, our Ocean Journal app immediately presents the customer their preferred beach with the premium live beach cam. No buy buttons are visible, as service was proactively restored. For each purchased product or active subscription, the transaction has a static and unique original transaction ID, which persists for the customer's Apple ID and storefront. To maintain status of the customer's transactions, associate the original transaction IDs with an account on your system. It can be either an anonymous account, or an account that the user created with your system. Knowing the original transaction ID is critical when leveraging the power of App Store Server Notifications, allowing your server to remain current on the transaction status. One scenario to highlight is when a customer's subscription failed to auto-renew, therefore it falls into what we call the billing retry state, where we attempt to recover the subscription for up to 60 days. If you have opted in to the Billing grace period feature in App Store Connect, then subscribers in billing retry with grace period would continue to have access to their subscription service, while we attempt to recover their subscription. And while they have still access to your service, be sure to present them a simple call to action to resolve their payment issue. To learn more about Billing retry and Billing grace period, check out our sessions links and resources about reducing involuntary subscriber loss. The final core state is the inactive purchase or inactive subscriber. This state represents customers who previously made in-app purchases, but are no longer entitled to that product or service, due to expiry or if revoked. These transactions are persistent and contain an original transaction ID, which allows you to maintain status across devices and platforms. For Subscriptions, inactive is determined by the expires date. And for all in-app purchase types, they can be inactive if there is a revocation date. This occurs when a transaction has been refunded or if access granted through Family Sharing has been revoked. For your inactive subscribers, due to expiration or revoked, consider presenting subscription offers to win them back. And for those in the billing retry state, don’t forget to present them that same call to action to resolve their payment details. In review, here are the three core customer product states your app will use to proactively restore in-app purchases and tailor your app's experience to your customers. Let's see how these experiences look side-by-side with our Ocean Journal app.
New customers will see your latest product offering and introductory offers. Your current active customers will have the feeling it just works, as your app has streamlined access to your products and services on all of their devices. And for your inactive subscribers, you can present them your latest win-back offers using offer codes or promotional offers.
Okay, we’ve covered the three core customer product states, and how supporting these states alone is a huge win for your customers. But of course, there are opportunities to take the experience further. Your app could expand or refine the customer experience to fit your product offering, business model, policies, and prioritizations. But here are few things to consider when preparing to implement proactive restore into your app.
If you support multiple products or subscription groups, the customer's state is determined for each product and each subscription group. Therefore, you may need to account for hybrid states or any other dependencies. Consider any off-platform activity and how that factors into your customer's product state. And be sure to check out App Store Server Notifications, as these are critical to maintaining status, server-to-server, for all in-app purchase types. And with Version 2, the new notification types and subtypes support 28 unique events, sent securely to your server in near real time. Learn more about integrating or migrating to Version 2 in the session, "Explore in-app purchase integration and migration." Alex and Gabriel also cover compatibility with StoreKit 2 and the original StoreKit framework, and best practices. We’ve talked through the customer product states to support and what that experience can be for your customers. Now let’s walk through the implementation details. I’ll be using our SK Demo App that we’ve updated with proactive restore using StoreKit 2. Note that the SK Demo app will be available for download with this session. Let's review the SK Demo’s default experience for new customers, those without any active in-app purchases. To view our products, tap the “Shop” button, where up top we have our inventory of available cars as non-consumable in-app purchases. And then we have our navigation service as a monthly auto-renewable subscription, which offers three different levels of service for customers to choose from. And down below, we have a non-renewing subscription option, providing one-time access. This covers our app's new customer experience, when no products have been purchased. Now let's look at how our app is able to determine if the customer has current or past purchases. It requires your app to execute three steps immediately upon app launch. What is most important is that these steps are completed before a "Buy" button is merchandised to the customer.
The first step is your app will need to begin listening for transactions from the App Store. This is an App Store best practice, as transactions can show up at any time from features such as Family Sharing Ask to Buy, code redemptions, subscription auto-renewals, or when a purchase gets interrupted. In addition, your app can receive revoked transactions, where access is lost due to a refund or is no longer shared via Family sharing. This will apply more in subsequent app launches, when access has already been granted and their state is moving from active to inactive. If transactions are found, they are considered unfinished transactions, and need to be validated, delivered to the customer, and marked as finished. This ensures your app won’t miss any transactions and delivers a great customer experience. Now let’s look at how our SK Demo app listens for transactions in StoreKit 2. Here I’m using the function listenForTransactions. It will return any unfinished transactions or updates to a transaction for the signed-in App Store customer. For any transactions found, here, StoreKit 2 will verify the authenticity of these transactions. And then, after my app delivers the content, grants access, or updates the customer product status, I will then finish the transaction to indicate to the App Store that the purchase has been delivered. Once a transaction is finished, it will no longer be returned to your app, on any device, via StoreKit. That first step is critical for all apps and will occur on every app launch going forward. Step 2 is determining that customer product state, and this is done by proactively requesting the customer's active transactions using currentEntitlements. And specifically for auto-renewable subscriptions, to account for the customer's renewal state, such as cancelled, billing retry, or pending downgrades, you will additionally use Product.SubscriptionInfo.RenewalState. Let’s look at the SK Demo app and see how we accomplish this. This starts with the function, updateCustomerProductStatus, which keeps track of the customer's product states for each of our persistent in-app purchase types. I then loop through each of the purchase types using StoreKit 2’s currentEntitlements method. This returns transactions for products that the customer may be entitled to. And we record these transactions per product type. Here, for our non-consumables products, and here for our non-renewing subscription product. In order to determine if they are an active, or inactive subscriber, I’ve added additional logic to calculate an expiration date for our non-renewing subscription. And lastly, I will check for an active auto-renewable subscription, and apply that state to the subscription group. To account for inactive states such as billing retry, expired, and revoked, our variable subscription group status uses Product.SubscriptionInfo.RenewalState Now that we've retrieved the user's transactions and determined the customer status for each product or subscription group, our app has logic to personalize the app experience for the various use cases. Let’s take a look at the SK Demo app source code. If no active transactions are determined for all three in-app purchase product types, the customer will then see the default new customer experience that we reviewed earlier, where they will have a simple call to action to our "Shop" page. If the customer has any active purchases, then upon app launch, they will see their purchases and update "Buy" buttons on all products accordingly. So here for non-consumables, we present what they’ve purchased and the app either shows their purchased non-consumables, or the app provides a call to action for the customer to visit the shop experience. For active products, here we handle if the customer is an active subscriber of the navigation service for non-renewable subscriptions and auto-renewable subscriptions. And in our last portion, we account for inactive subscribers. Those with subscriptions that have expired, been revoked, or are in the billing retry state. Okay, let's now go to the SK Demo app. We want to simulate an active customer for both a non-consumable and auto-renewable subscription. So if I purchase the race car and subscribe to the pro navigation, the demo app will apply green checkmarks to indicate the app has confirmed those purchases were successful, verified, and has enabled them. With these purchases, my customer product state for the non-consumable is purchased. And for our subscription, I’m an active subscriber. Now, if I install the app on a new device, when I launch the SK Demo app for the first time, it will proactively perform steps one, two, and three. Here you see our demo app has proactively restored access to both of my purchases, without any action from me. As this is a demo app, that is the extent of products being delivered. But in your app, this process would ensure these active customers are not offered products to purchase that they already own, and that those products and services are enabled for them automatically.
For your current customers, this is great. No need to require customers to sign-in or tap "Restore Purchases." It just worked. Your app can use the APIs and data readily available. So we’ve covered the three steps to do this with StoreKit 2. Now I want to discuss how to implement this same experience for your customers on previous versions of iOS where you cannot leverage the power of StoreKit 2.
With original StoreKit, you will perform the same steps as StoreKit 2 to determine the customer product state by proactively restoring in-app purchases on iOS 7 or later. To do this, it will require your server to use the verifyReceipt endpoint to validate and retrieve the latest transactions in order to determine customer's product state. The app receipt is present on-device when an app is installed from App Store. But keep in mind, when testing with Sandbox or TestFlight, the app receipt is only present after an in-app purchase has been completed or restored. If your app finds no app receipt present, this should only occur in Sandbox and your app can consider this scenario the same as a new customer where no in-app purchases are found. An app receipt created in the past is sufficient to retrieve the latest transactions from the App Store. Therefore, no customer actions like a "Restore Purchase," or receiptRefresh are necessary. Just include the shared secret with your request to verifyReceipt in order to receive transactions for non-consumables, non-renewing subscriptions, and auto-renewable subscriptions. Let’s look back at the three implementation steps we reviewed earlier. The difference lies within Step 2, where you identify the customer's product state. How we determine customer product state starts with the app receipt on device, that, in turn, your server validates with the App Store verifyReceipt endpoint. Let’s look at this process. First, we need to retrieve the App Receipt, and be sure you are using the appStoreReceiptURL property, as you can see in this sample from our developer documentation. With the app receipt, let’s see how this is sent from the device to your server and the App Store. Your app on a device is here on the left. it will first, retrieve the app receipt, and send it to your server, then validate it with the App Store verifyReceipt endpoint. From that response, you will determine customer product state, and send those states to your app. To determine customer product state, we used the Entitlement Engine from WWDC2020. It’s updated to support non-consumables and non-renewing subscriptions, and now handles the new customer state when there are no in-app purchases.
To learn more about using our Entitlement Engine, I encourage you to check out the "Architecting for subscriptions" session and download the sample project. You can find links to this session and more with this video’s resources.
That completes Step 2, where your app will receive the customer product state from your server.
Now your app will personalize the app experience immediately on launch using the StoreKit 2 and original StoreKit frameworks. I want to share some final best practices. First, continue providing a "Restore Purchases" button within your app. While not used often, it does give customers an opportunity to force an app to restore their Apple ID’s transactions in case of an issue or if the customer uses a different Apple ID. When your app first proactively restores a customer's in-app purchases on a device, it’s recommended to optimize your app and store data securely to assist in determining customer product state. CloudKit is a feature to consider with its flexibility, security, and ability to sync across a customer's devices. Testing your implementation is critical when using StoreKit. And with StoreKit 2, you can test your proactive restore implementation with Sandbox, TestFlight and Xcode StoreKit testing. And if you are using original StoreKit, it’s important to remember an app receipt may not be present when testing in Sandbox and TestFlight, while it is always present when the app is installed from the App Store. If an app receipt isn’t present, it is suggested your app uses its default new customer experience, and ensure you have a Restore Purchases button readily available. In conclusion, update your app to proactively check for purchases without any customer action, no taps or authentication. Allow your app to tailor the customer's experience immediately at launch to fit your new, active, and inactive customers' product states. Maintain status on all your customer's transactions, server to server, for all in-app purchase types, by implementing App Store Server Notifications Version 2. This enables your backend to know in near real-time any change that has occurred with a transaction, such as refunds, or revoked transactions, or subscription renewals, billing retry, and expirations.
Thank you for watching, and be sure to check out this additional session, "What's new with in-app purchases”, where Dani and Ian will tell you about all the great updates to StoreKit, the Server API, and Server Notifications Version 2.
Thank you. Take care. ♪ ♪
-
-
11:16 - Transaction Listener at app launch
//Transaction Listener at app launch func listenForTransactions() -> Task<Void, Error> { return Task.detached { //Iterate through any transactions which didn't come from a direct call to `purchase()`. for await result in Transaction.updates { do { let transaction = try self.checkVerified(result) //Deliver products to the user. await self.updateCustomerProductStatus() //Always finish a transaction await transaction.finish() } catch { //StoreKit transaction failed verification, don't deliver content to user. print("Transaction failed verification") } } } }
-
12:27 - Determine customer product state
//Determine customer product state func updateCustomerProductStatus() async { var purchasedCars: [Product] = [] var purchasedSubscriptions: [Product] = [] var purchasedNonRenewableSubscriptions: [Product] = [] //Iterate through all of the user's purchased products. for await result in Transaction.currentEntitlements { do { //First check if the transaction is verified. If the transaction is not verified //we'll catch the `failedVerification` error. let transaction = try checkVerified(result) //Check the `productType` of the transaction and get the corresponding product from the store. switch transaction.productType { case .nonConsumable: if let car = cars.first(where: { $0.id == transaction.productID }) { purchasedCars.append(car) } //..
-
12:56 - Determine customer product state
//Determine customer product state case .nonRenewable: if let nonRenewable = nonRenewables.first(where: { $0.id == transaction.productID }), transaction.productID == "nonRenewing.standard" { //Non-renewing subscriptions have no inherent expiration. let currentDate = Date() let expirationDate = Calendar(identifier: .gregorian).date(byAdding: DateComponents(year: 1), to: transaction.purchaseDate)! if currentDate < expirationDate { purchasedNonRenewableSubscriptions.append(nonRenewable) } } //..
-
13:09 - Determine customer product state
//Determine customer product state case .autoRenewable: if let subscription = subscriptions.first(where: { $0.id == transaction.productID }) { purchasedSubscriptions.append(subscription) } default: break } } catch { print() } } //Update the Store information with the purchased products. self.purchasedCars = purchasedCars self.purchasedNonRenewableSubscriptions = purchasedNonRenewableSubscriptions self.purchasedSubscriptions = purchasedSubscriptions //Check subscriptionGroupStatus to learn auto-renewable subscription state subscriptionGroupStatus = try? await subscriptions.first?.subscription?.status.first?.state }
-
13:45 - Updating my car view at app launch
//Updating my car view at app launch if store.purchasedCars.isEmpty && store.purchasedNonRenewableSubscriptions.isEmpty && store.purchasedSubscriptions.isEmpty { VStack { Text("SK Demo App") .bold() .font(.system(size: 50)) .padding(.bottom, 20) Text("🏎💨") .font(.system(size: 120)) .padding(.bottom, 20) Text("Head over to the shop to get started!") .font(.headline) NavigationLink { StoreView() } //… } } }
-
13:59 - Updating my car view at app launch
//Updating my car view at app launch else { List { Section("My Cars") { if !store.purchasedCars.isEmpty { ForEach(store.purchasedCars) { product in NavigationLink { ProductDetailView(product: product) } label: { ListCellView(product: product, purchasingEnabled: false) } } } else { Text("You don't own any car products. \nHead over to the shop to get started!") } } //…
-
14:20 - Updating my car view at app launch
//Updating my car view at app launch Section("Navigation Service") { if !store.purchasedNonRenewableSubscriptions.isEmpty || !store.purchasedSubscriptions.isEmpty { ForEach(store.purchasedNonRenewableSubscriptions) { product in NavigationLink { ProductDetailView(product: product) } label: { ListCellView(product: product, purchasingEnabled: false) } } ForEach(store.purchasedSubscriptions) { product in NavigationLink { ProductDetailView(product: product) } label: { ListCellView(product: product, purchasingEnabled: false) } } }
-
14:30 - Updating my car view at app launch
//Updating my car view at app launch else { if let subscriptionGroupStatus = store.subscriptionGroupStatus { if subscriptionGroupStatus == .expired || subscriptionGroupStatus == .revoked { Text("Welcome Back! \nHead over to the shop to get started!") } else if subscriptionGroupStatus == .inBillingRetryPeriod { //Provide a deep link from your app to https://apps.apple.com/account/billing. Text("Please verify your billing details.") } } else { Text("You don't own any subscriptions. \nHead over to the shop to get started!") } } }
-
17:42 - Fetch App Receipt Data
//Fetch App Receipt Data public func getReceipt() { if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: appStoreReceiptURL.path) { do { let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped) print(receiptData) let receiptString = receiptData.base64EncodedString(options: []) print("receipt send it to your server: \(receiptString)") // Read receiptData } catch { print("Couldn't read receipt data with error: " + error.localizedDescription) } } }
-
-
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.