Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

2022 App Store, Distribution & Marketing

WWDC22 · 21 min · App Store, Distribution & Marketing

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.

Watch at developer.apple.com ↗

Transcript all transcripts

Code shown on screen · 9 snippets

Transaction Listener at app launch swift · at 11:16 ↗
//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")
            }
        }
    }
}
Determine customer product state swift · at 12:27 ↗
//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)
             }
         //..
Determine customer product state swift · at 12:56 ↗
//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)

      }
   }
//..
Determine customer product state swift · at 13:09 ↗
//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

}
Updating my car view at app launch swift · at 13:45 ↗
//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()
                    }
            //…

     }
   }
}
Updating my car view at app launch swift · at 13:59 ↗
//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!")

      }
   }
//…
Updating my car view at app launch swift · at 14:20 ↗
//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)
              }
        }
    }
Updating my car view at app launch swift · at 14:30 ↗
//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!")
     }
   }         
}
Fetch App Receipt Data swift · at 17:42 ↗
//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) 
        }
    }
}

Resources