-
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.
Recursos
- Implementing a store in your app using the StoreKit API
- Introducing StoreKit 2
- App Store Server Notifications
- Determining service entitlement on the server
- Reducing Involuntary Subscriber Churn
- CloudKit
Vídeos relacionados
WWDC22
WWDC21
Tech Talks
WWDC20
-
Buscar neste vídeo...
-
-
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) } } }
-