✨ Read how Fotorama reduced app’s subscription refund rate by 40% with Refund Saver

A detailed guide to in-app purchases for your iOS app

Anton Kondrashov

Updated: September 9, 2024

Content

A detailed guide to in app purchases for your iOS app

In the world of mobile apps, in-app purchases have become an increasingly popular way to monetize applications. With the rise of subscription-based models, apps can now offer a recurring revenue stream by providing users with access to premium features or content for a recurring fee. But implementing in-app purchases can be tricky and requires a deep understanding of Apple’s StoreKit framework.

This article provides a comprehensive guide to in-app purchases on iOS, covering everything from creating and configuring products in App Store Connect to using the StoreKit framework in your app code and testing with .storekit files. The article also covers best practices for handling transactions, restoring purchases, and supporting family sharing.

One of the key challenges when implementing in-app purchases is ensuring that users have a smooth and reliable experience. This includes handling renewals, refunds, billing issues, and more. The article offers tips on how to handle these scenarios and ensure that users are always receiving the correct access for their subscriptions.

Whether you’re new to in-app purchases or looking to improve your existing implementation, this article has everything you need to know to make sure your app is monetizing effectively and providing a great user experience.

Are you striving to optimize your app’s monetization and offer a flawless user experience? Start your journey with Adapty! Schedule a free demo call with us to uncover how our cutting-edge solutions can drive your iOS in-app purchases, ensuring a seamless and reliable experience for your users while maximizing your recurring revenue stream.

Integrate in-app purchases with Adapty in 30 minutes
We’d be glad to show you how Adapty works and how you can benefit from it.
Schedule a Demo

Prerequisites

  • macOS 13.2.1
  • Xcode 14.2
  • iOS 16.0+
  • SwiftUI
  • StoreKit 2

Types of Apple In-App Purchases

It’s essential to understand the different types of in-app purchases to come up with the most effective business model for your app. There are four types of in-app purchases:

  • Consumable: These are digital items that can be purchased multiple times and are typically used up or consumed after use. Examples of consumable in-app purchases include in-game currency, temporary power-ups, or virtual goods like clothing or furniture in a virtual world.
  • Non-Consumable: These are digital items that are purchased once and permanently unlocked within the app. Examples of non-consumable in-app purchases include the permanent removal of ads or lifetime access to exclusive content.
  • Auto-Renewing Subscription: This type of in-app purchase allows users to access content or features for a set period of time, with the subscription automatically renewing at the end of each period. Users have the option to cancel at any time. Examples of auto-renewing subscriptions include access to premium content or features in a news app or a music streaming service.
  • Non-Renewing Subscription: This type of in-app purchase allows users to access content or features for a set period of time, but the subscription does not renew automatically. Users must manually renew the subscription to continue accessing the content or features. A good example of it is access to a limited-time course or premium content within an app.

It’s important to note that in-app purchases can only be offered for digital items and not physical goods or services. Make sure to consult Apple’s full documentation on creating in-app purchase products to ensure you’re following their guidelines.

Also, note that additional downloadable content available for purchase can be stored on either the app developer’s server or Apple’s servers.

Step 1. Getting started

To start the exploration of the in-app purchases world, download the starter project from our GitHub.

In the project, you will find some parts of the UI presets including the counters for different in-apps.

Step 2. Configure the project

Now you need to configure the project. First, you need to log into your App Store Connect account in Xcode.

Then select the project file, and select target “AdaptyPurchase”. Go to “Signing & Capabilities” and change the team to your own one. Then you need to update the bundle identifier so that it is unique for your app.

Next, add the In-App Purchase capability. Click on ‘+ Capability’.

11 1024x659 1 jpg

Then type in the search field “in-app” and select “In-App Purchase”.

do djHTWAoaPu CkqQB6 5bjiVdVIYLntX1CSyHbdq2QLsmqAX1DwbAOOxOyWUKcWLqn8QMf6cbUPUDqRlQWY24U5Uto4fV DRxwkwpREme22g8CfcGZ3ygCrbCDTdQIMcvCvsU hUMY7B2DNhCs u0

Step 3. Use a .storekit file for local testing

The .storekit file is a good way to define the available in-app products and their metadata for local testing. It might be useful for testing without a network connection in case you are working on your way to the Canary islands or just want faster debug cycles.

Here is how we create it:

gCxApI4yQUagyibYvsQnnRAyopNM3 5Il4w49XyZUKMXQ1Xvp2rpETcUFLcFsmk7jE60X BIp53GBY8KFyIw0XKgEXQBY7B9vjKYa1u7uF5zTWFMTiEsP3doFaJei8zEyhpA7yigNX5YCk wzfftHSA
  1. In Xcode, navigate to File > New > File or use the keyboard shortcut ⌘N.
  2. In the template selection dialog, choose “StoreKit Configuration File” under the “iOS” section.
  3. Type in the name “SyncedProducts” and do not tick “Sync this file with an app in App Store Connect”.
  4. Click “Next” and then “Create”.

Now let’s add the first product which will represent a non-consumable item in some game.

Open the newly created Products.storekit and click on the “+” symbol in the bottom left corner. Then select “Add Non-Consumable In-App Purchase. In the opened editor window update the Reference Name to “Stone”, and Product ID to “stone”. 

Then click on the only localization twice and change the Display Name to “Indestructible Stone”. Because did you try to break the curling stone?

Right-click on the Products.storekit file. Select Open As > Source Code. It should look similar to the following:

JSON
JSON

{
  "identifier" : "<FooBar>",
  "nonRenewingSubscriptions" : [
  ],
  "products" : [
    {
      "displayPrice" : "0.99",
      "familyShareable" : false,
      "internalID" : "<FooBar>",
      "localizations" : [
        {
          "description" : "",
          "displayName" : "Indestructible Stone",
          "locale" : "en_US"
        }
      ],
      "productID" : "stone",
      "referenceName" : "Stone",
      "type" : "NonConsumable"
    }
  ],
  "settings" : {
  },
  "subscriptionGroups" : [
  ],
  "version" : {
    "major" : 2,
    "minor" : 0
  }
}

As you can see the structure of this file is pretty simple which makes it easier to test in-app purchases. However, .storekit file is not used for the published app hence it is not the default source of in-app purchases. We need to explicitly tell Xcode to use this file during the next app run.

To do that, click on the target icon with your app name in the Xcode status bar. Then click “Edit Scheme”.

22

In the “Options” tab of the “Run” scheme select the Products.storekit file we created.

33

Sandbox environment

Sandbox testing is the older way of testing in-app purchases in a simulated environment that mimics the App Store environment. It is essential to test and verify that in-app purchases work correctly before releasing them to production.

Sandbox testing for in-app purchases is more difficult than with .storekit files because it requires a live connection to the App Store server. The sandbox environment is isolated, but it still requires a live connection to the App Store server to verify transactions.

This means that in order to test in-app purchases in the sandbox environment, you need to have a functioning backend server that can handle requests from the App Store server. This server must be able to communicate with the App Store server to validate and complete transactions.

In contrast, testing with .storekit files can be done offline without requiring a connection to the App Store server. This makes it a simpler and more straightforward process. However, it’s important to note that testing with .storekit files does not fully replicate the App Store environment, so sandbox testing is still necessary to fully verify in-app purchases before releasing them to production.

Step 4. Fetch and display the in-app product

The .storkit file emulates the in-app purchases we will register in the app store later. We need to fetch the products first to display them.

Get in-apps in the app

Let’s do that in our Store component which represents an actual store with goods and subscriptions. Open the Store.swift and add the following lines to it.

Swift
swift

// 1:
import StoreKit
// 2:
class Store: ObservableObject {
    // 3:
    private var productIDs = ["stone"]
    // 4:
    @Published var products = [Product]()
    // 5:
    init() {
        Task {
            await requestProducts()
        }
    }  
    // 6:
    @MainActor
    func requestProducts() async {
        do {
            // 7:
            products = try await Product.products(for: productIDs)
        } catch {
            // 8:
            print(error)
    }
  }
}
  1. First, we need to import StoreKit which allows us to interact with the in-app purchases. It contains both StoreKit 1 and StoreKit 2 APIs.
  2. Store object should conform to an ObservableObject protocol so that our SwiftUI interface could change when the `Store` data changes.
  3. productIDs is an array of IDs that are used to uniquely identify the products we create in the .storekit file or on App Store Connect.
  4. products is an array that stores the available in-app purchases and subscriptions. It is marked @Published to update the user interface as soon as we fetch the data.
  5. We need to start fetching the products as soon as the View that displays them appears on the screen.
  6. Getting products is an async operation because the .storekit file only works for testing purposes and the published version of your app will be getting the products from App Store using the network connection. When the products are received we need to switch to the same thread as UI works on to display them right away. This is the reason to mark the method as @MainActor.
  7. product is the main entity of StoreKit 2 which represents any in-app purchase. The static products method fetches the products with the provided IDs. It is asynchronous and can throw an error.
  8. The error products method might throw is of type SKError which happens for 21 different reasons. If you want to dive deeper into it, we have a guide about handling every one of them.

If we run the app now, nothing will happen in the interface.

Display in-app purchases

Find the “To buy” section in the `ContenView.swift` file. It should look like this:

Swift
 Section(header: Text("To buy")) {
           // Add in-app purchases here...
 }

Remove the comment and put the following code in its place:

Swift
// 1:
ForEach(store.products, id: .id) {
  product in
    HStack {
      // 2:
      Text(product.displayName)
      Spacer()
        // 3:
      Button("(product.displayPrice)") {
        // Here is going to be purchasing action...
    }
  }
}
  1. Even though for now there is only one product, we account for the future when our Store will contain more in-app purchases.
  2. We show the displayName of the product which might be different depending on the user’s locale and the localizations we created.
  3. Users usually prefer to see the price in the currency of their country. That is why we use displayPrice value. The `price` property of the Product is usually used for calculations and never for the user interface.

Now run the app and you should see something like this:

Simulator Screen Shot iPhone 14 Pro 2023 03 24 at 14.01.33

Our in-app product is available for sale! However, if you tap on it, nothing happens. Let’s correct it by implementing the purchase feature.

Step 5. Add the purchase feature

Now that the user sees the available products, let them buy! Now, add the array for the non-consumable products that we’ll sell first.

Swift
@Published var purchasedNonConsumables = [Product]()

Then

Swift
@MainActor
func purchase(_ product: Product) async throws -> Transaction ? {
  // 1:
  let result =
    try await product.purchase()
  switch result {
    // 2:
    case .success(.verified(let transaction)):
      // 3:
      purchasedNonConsumables.append(product)
        // 4:
      await transaction.finish()
      return transaction
    default:
      return nil
  }
}
  1. Initiate an asynchronous call to the purchasing API.
  2. In this step we only handle the successful action path. That means the user really paid in the modal sheet and the transaction was successfully verified.
  3. After the transaction verification we are sure that the purchase is successful and we can provide the content to the user.
  4. Calling the finish method on a transaction tells AppStore that we have successfully delivered the paid content to the user. It’s extremely important for consumable in-app purchases and you’ll see why in Step.8.

Next thing we need to do is call this action from our View when the user taps on the product price. Go to ContentView.swift and find the comment we left earlier.

Swift
Button("(product.displayPrice)") {
 // Here is going to be purchasing action...
 }

Replace the comment with a call to purchasing method.

Swift
Button("(product.displayPrice)") {
 Task {
  try await store.purchase(product)
  }
}

And to see when the item is purchased add

Swift
ProductView(
 icon: "🥌",
 quantity: "(store.purchasedNonConsumables.count)"
 )

Now if you run the app you’ll see the purchasing menu

Ro7SR3jWaQhaTlZtnoqrkyhIuMUCUIpwtC L6aJLJH1GTc2ihEESpKvi1O491OFHqWEPcENy8OuENXW vPJ4viIIVQXtGfrSrU7 c7Og17VUvdDHem1L64xW7ox9bwe5OWU3m62lgrU8A aJb ksJ20

As we use the .storekit file we are in testing mode. Don’t be afraid to tap Purchase, you won’t be actually charged but the purchase will be successful. After you do that, you should see the number of curling stones change from 0 to 1.

VS2G3RoHeI

Now if you close the app and open it again all the numbers will be zeros again. Imagine the face of the user when they see that! Furthermore, it’s not the only problem. Let’s correct all of them ASAP.

Step 6. Listen for transactions

You might’ve noticed a hint of what we are going to do now in a form of a warning in Xcode. It’s displayed on the line where we fetch the products.

N6R6L Ah4u7 o0JBap7ZowKe BfjBGwiNLomU06IsqGqwG2U INCLNAKVSuUHU5iIZQKmeIKK0XttiNoafG91zI6p3yZk f3p KOHo3

What that means is if something goes wrong after the purchase was successfully made, we might still miss the transaction. This can be solved by listening to transaction updates provided by StoreKit in real time. Let’s write the function that would solve the task.

Swift
 func listenForTransactions() -> Task < Void, Error > {
  // 1:
  return Task.detached {
    // 2:
    for await result in Transaction.updates {
      switch result {
        // 3:
        case let.verified(transaction):
          // 4:
          guard
          let product = self.products.first(where: {
            $0.id == transaction.productID
          })
          else {
            continue
          }
          self.purchasedNonConsumables.append(product)
          // 5:
          await transaction.finish()
        default:
          continue
      }
    }
  }
}
  1. The listening mechanism should perform its work in real-time. Meanwhile, the app should be able to perform other work such as processing user interactions. That’s why we detach the task.
  2. Transaction.updates constantly monitors the new transactions and provide the new ones as they appear.
  3. Swift API of StoreKit 2 makes us check if the transaction was really verified.
  4. Transaction doesn’t provide us with the Product object that the user purchased. Instead, it gives us the product’s id and then we should get it separately.
  5. After we provide the content again, we must finish the transaction.

Now we need to start the listener. To do that, add a property that will store a detached task first.

Swift
var transacitonListener: Task<Void, Error>?

And then call the task creation method we’ve just written in the initializer of the Store object.

Swift
init() {
   transacitonListener = listenForTransactions()
   Task {
     await requestProducts()
 }
}

The warning won’t appear if you run the app again. However, after you purchased the product once, you won’t see the purchasing menu if you try to purchase it again. Instead, the counter of curling stones will just increase by one every time you tap on the product.

The reason for that is you can purchase a non-consumable in-app only once but with every tap purchase method executes the success case where we append the same product to the array. Let’s apply an easy fix by changing the array for non-consumables to a set of them.

Change 

Swift
@Published var purchasedNonConsumables = [Product]()

To

Swift
@Published var purchasedNonConsumables = Set<Product>()

And in 2 places change

Swift
purchasedNonConsumables.append(product)

To

Swift
purchasedNonConsumables.insert(product)

Try and test in-app purchases now and you’ll see that the counter updates only once.

The last problem we are going to solve with transaction listening is the in-app purchase persistence problem I mentioned at the end of the previous step.

First, let’s put the transaction processing in a separate method.

Swift
@MainActor
private func handle(transactionVerification result: VerificationResult <Transaction> ) async {
  switch result {
    case let.verified(transaction):
      guard
      let product = self.products.first(where: {
        $0.id == transaction.productID
      })
      else {
        return
      }
      self.purchasedNonConsumables.insert(product)
      await transaction.finish()
    default:
      return
  }
}

And call this method in `listenForTransactions`:

Swift
func listenForTransactions() -> Task<Void, Error> {
 return Task.detached {
  for await result in Transaction.updates {
   await self.handle(transactionVerification: result)
  }
 }
}

Now let’s add the current user entitlements processing.

Swift
private func updateCurrentEntitlements() async {
  for await result in Transaction.currentEntitlements {
    await self.handle(transactionVerification: result)
        }
    }

To provide the desired app behavior for each in-app entitlement status, the Transaction.currentEntitlements will retrieve the latest transactions, provided that there is an internet connection. In the absence of internet connectivity, it will fetch locally cached data. Additionally, transactions are automatically synced to the device whenever the internet connection is restored, allowing the app to maintain up-to-date transactions even when the user goes offline.

And add the call to this function in the initializer:

Swift
init() {
 transacitonListener = listenForTransactions()
  Task {
   await requestProducts()
   // Must be called after the products are already fetched
   await updateCurrentEntitlements()
  }
}

We must call the current entitlements to update after the products are fetched because the transaction doesn’t include the product.

If you run the app now, it will update the counter of curling stones right after the launch of the application. 

However, this way of understanding which of the products is already purchased doesn’t work for every type of in-app purchase. One of them is consumables which I’ll cover in the next step.

Step 7. Deliver Content for consumables

Let’s introduce a new entity into our in-game store. To do that open the Products.storekit, tap on the ‘+’ button, and select “Add Consumable In-App Purchase”. The reference name is “Heart”, the Product ID is “heart”, and the price is $2.99. After tapping on the available localization change the Display Name to “Life Heart”.

Extend the array productIDs with one more item “heart” in the “Store” class. This should fetch one more product for our product list.

Fhm2bgCB FyPyUk29p9LIQV 24D9WgHgLkHiEKMAWKTaJS3jj4yNpjmJN7wgcSGoXm8O0Ic8cvtKPAW3JufbhvG4FFRLeXN8zVQ

If you try to buy the new item now, it will be included in the curling stone category. We should fix that by creating a separate array for consumables. Note that we can not use a set this time, as consumables can be purchased multiple times.

Add this line to the “Store” class:

Swift
@Published var purchasedNonConsumables = Set<Product>()
@Published var purchasedConsumables = [Product]() // new line

Then also change the ProductView quantity parameter:

Swift
 ProductView(
  icon: "❤️",
  quantity: "(store.purchasedConsumables.count)"
)

Add a new function for adding a purchased product which will put the different types of products into separate collections.

Swift
private func addPurchased(_ product: Product) {
  switch product.type {
   case .consumable:
     purchasedConsumables.append(product)
   case .nonConsumable:
    purchasedNonConsumables.insert(product)
   default:
   return
  }
}

Add a call to this function in handle(transactionVerification:) function by replacing a line

Swift
self.purchasedNonConsumables.insert(product)

By

Swift
self.addPurchased(product)

If you try to purchase the “Life Heart” now, even multiple hearts, everything will work correctly. Right until you try to restart the app. You will see “0” Hearts again.

The reason for that is consumables are not stored in `Transaction.currentEntitlements`.

It’s extremely important to persist the consumable in-app purchases using other storage mechanisms. We will apt for UserDefaults.

Create a new file called Persistence.swift. Add the following implementation into it.

Swift
import Foundation
class Persistence {
 static let consumablesCountKey = "consumablesCount"
 private static let storage = UserDefaults()
    
 static func increaseConsumablesCount() {
    let currentValue = storage.integer(forKey: Persistence.consumablesCountKey)
        storage.set(currentValue + 1, forKey: Persistence.consumablesCountKey)
    }
}

The Persistence class has a simple function of storage of the current number of purchased consumables for key Persistence.consumablesCountKey. We can use the same key to get the number in the UI later.

Add a new property to the ContentView.

Swift
@AppStorage(Persistence.consumablesCountKey) var consumableCount: Int = 0

Make it the displayed number of hearts.

Swift
ProductView(
 icon: "❤️",
 quantity: "(consumableCount)"
)

Call the increaseConsumablesCount in addPurchased(_ product:) function:

Swift
purchasedConsumables.append(product)
  Persistence.increaseConsumablesCount() // new line

Now if you run the application again, buy some hearts and restart it, you will see that it works correctly.

62Y B24UlpNaOjavKBasjMybJoJBGWpHZ1PzmCrveK1ozsS xTM0CJVwq2pT4SThSMCdqRPfGOXHmmVIj1iqkBhW rGPaRZvuPCS f79 KF 1mpGygEVvc70uUo1uyB7i5FaKa0GoeGRteKKvwNVngM

We have 2 more types of in-app purchases to implement but first, let’s see how to prepare a published version of IAPs.

Step 8. Create an app in App Store Connect

First, we need to create the app in App Store Connect to make our in-app purchases available to real users. To do that you need to

  1. Log in to your Apple Developer account and navigate to the App Store Connect homepage.
  2. Accept all the agreements App Store Connect asks you to in the banner on the Home page of your App Store Connect account.
  3. On the App Store Connect homepage click on the ‘+’ icon in the top left corner of the screen and select ‘New App’ from the dropdown menu.
  4. Tick the iOS platform, and select the Bundle ID that you have chosen in Xcode before.
  5. Fill the rest of the fields to your liking and click “Create”.
dqFPZ3c4OsmfXP8hqrFQfYXJpXrQOkUQz3u1VGHcPl4QOE4m73K7qqm0u mtUCsv0OEoIe7GFO w dNwyF079jpcZR ZaguBncxweUXjiNgu7dEj6n0Bg4EBgG XWrKiv

The app should appear on the homepage dashboard.

Step 9. Register IAPs in App Store Connect

At the App Store Homepage click “My Apps” and then on the app, we have just created. On the sidebar, you can find a “Features” section. The in-app purchases are created using the “In-App Purchases” and “Subscriptions” pages. 

Register Consumable In-App Purchase

“In-app purchases” is used for consumable and non-consumable IAPs. Let’s start with them.

Go to “In-app purchases” and click the “Create” button in the middle of the page. You can also use a ‘+’ button near the header.

NJ6Yb9znotKIDFiflHjBWpygZhScBYlLBJ0oW31ZAYcsfEd 2XyJhyGx1UxsdLMHeovkzDYDr0rQLyuZc0NsCDTi

Select a “Consumable” type, “Heart” as Reference Name, “heart” as Product ID, and click “Create”.

mIL8OV4eIoLklUI1OkBwsQp426fIO D0IbwNGQN11h3 Fr5VfXGkspj1lc4nRR AC3Hfogt8pA5oIOtNF1qy1 mKH2Oqtv5xKgOTavKfOhBXiz25p7dEMlibuS72pGq4J4 DgHiH38pUyI60MbJybwg

Scroll the loaded page down to the Availability section and click on “Set up availability”. Here you can change the regions where this IAP is available in. We’ll leave everything as it is and click “Confirm”.

Lower on the product page you can find “Price Schedule”. Click on “Add Pricing”. Select currency and price and click “Next”. On the next page you can adapt the price for the different regions. We won’t change anything so just click “Next” and then “Confirm”.

On the product’s page scroll down to “App Store Localization” and click on “Add Localization”. Type “Life Heart” in “Display Name” and any suitable description. Then click “Create”.

All the data necessary for testing is added. Now you can click on “Save” at the top of the page.

Register Non-Consumable In-App Purchase

Now we should do the same for the “Indestructible Stone”. Click on plus button and fill out the data exactly as in Step 4. Fill in the “Availability”, “Price Schedule” and “App Store Localization” section and click “Save”.

Register Non-Renewing Subscription

To create the non-renewing subscription you should go to “Subscriptions” page, scroll down to the bottom and in a section called “Non-Renewing Subscriptions” click on “Manage” link.

Then click on the ‘+’ button next to the header, type in Reference Name “Tournament Streaming”, Product ID “tournament” and click create. Set up global availability, global price schedule of $9.99 and localization. Write “Tournament Streaming” for Display Name.

Click “Save” and the new IAP is ready.

Register Auto-Renewing Subscription

The auto-renewing type of subscription is created a bit differently. First, you need to create a subscription group and then create different tiers or types of subscriptions.

Users can buy only one subscription in the same subscription group. Using this you can easily handle subscription upgrades and we see how to do that later in the code.

To create the subscription group go to the subscriptions page and click on ‘+’ button next to the “Subscription Groups” title. Type in the Reference Name “Membership” and click “Create”.

1I sg6Xv oprslyGCujIorEU Y5slBe6Jj cD Lj3hKVgJJU yPOBLN28sT mGKevldxCM4QqRqVbpGysPlLOZO36A0ZWF181gFBJf5yB teUKy1c

On the loaded page add App Store Localization with the Subscription Group Display Name “Membership”.

TDwITMkqzVuJebAfms4WhsqROW201JhFTjnGQealQp10niX4 VZ7NDhAdd1xkROwZAlIrGiudrw7yw1bKKB G8ZPuMK9U29 SvBr2ez8u00b4

Now let’s create the first level of service subscription. On the Subscription Group page in the section “Subscriptions” click “Create”. Add the Reference Name “Elite” and Product ID membership.elite and click “Create”.

On the opened page fill in global availability, price of $9.99, and localization with the Display Name “Elite Membership”. In comparison to other in-apps, subscription has another required field which is “Subscription Duration”. Set it to 1 month and click “Save” above.

Add another tier the same way. Set the Product ID to membership.pro, priced at $4.99, 1-month duration, and Display Name “Pro Membership”. Click “Save”.

Note that the subscriptions in the list should always go from the highest level of service to the lowest in descending order.

Step 10. Synced .storekit file

Now let’s create another .storekit file that contains all the in-app purchases and subscriptions that we created in App Store Connect. To do that

  1. In Xcode, navigate to File > New > File or use the keyboard shortcut ⌘N.
  2. In the template selection dialog, choose “StoreKit Configuration File” under the “iOS” section.
  3. Type in the name “Products” and tick “Sync this file with an app in App Store Connect”.
  4. Select the team profile and an app we created before in App Store Connect, click “Next”, and then “Create”.
  5. Click on the target icon with your app name in the Xcode status bar. Then click “Edit Scheme”, and in the “Options” tab of the “Run” scheme select the SyncedProducts.storekit file we created

Now if you launch the app, you should see all the newly created products from App Store Connect. If you’ll need to re-sync the file then open it and tap the button in the bottom left corner that looks like the page reload in the browser.

Step 11. Track time for Non-Renewing subscriptions

Let’s add a tournament streaming in-app to the products list. First, let’s add a new product id.

Swift
private var productIDs = ["stone", "heart", "tournament"]

Then we need to add a new set for the subscription. A user can buy only one non-renewing subscription with the same id.

Swift
@Published var purchasedNonConsumables = Set<Product>()
@Published var purchasedNonRenewables = Set<Product>() // new line

For the tournament streaming 

Swift
var tournamentEndDate: Date = {
  var components = DateComponents()
  components.year = 2033
  components.month = 2
  components.day = 1
  return Calendar.current.date(from: components)!
}()

Update `addPurchased(product:)` by adding the following case.

Swift
case .nonRenewable:
 if Date() <= tournamentEndDate {
 purchasedNonRenewables.insert(product)
}

Non-renewable subscriptions do not contain an expiry date.

Step 12. Auto-renewing subscriptions

Auto-renewing subscriptions have become one of the most popular ways of monetization. Let’s implement the two levels of membership that we have registered.

First, let’s add a set for the subscription products. There can be only one subscription with the same product id at a time.

Swift
@Published var purchasedSubscriptions = Set<Product>()

Then we should add the insertion in the addProduct method for the autoRenewable case:

Swift
case .autoRenewable:
 purchasedSubscriptions.insert(product)

And finally let’s update the UI so that it shows the actual subscription information:

Swift
ProductView(
 icon: "⚽️",
 quantity: "(store.purchasedSubscriptions.count)"
)

Now launch the app and subscribe for the Pro Membership. Your subscription count should become “1” after that. You might need to wait for a couple of seconds for that. If you subscribe to Elite Membership now, you will see “2” in the subscription counter.

simulator screenshot 6F36D616 400D 4DBA 8F64 A6325F8F47D4

But something is wrong as we created a group of subscriptions and there can be only one subscription valid in the same group. Why does Transaction.currentEntitlements still return the old subscription?

That happens because Apple still provides you with the previously purchased subscription as a current entitlement. However, there is a mechanism for filtering out the upgraded subscriptions.

Let’s add another line to our handle(transactionVerification:) method.

Swift
guard !transaction.isUpgraded else { return transaction } // New line
self.addPurchased(product)

Now, whenever the user upgrades the subscription, we filter out the lower-level subscription and provide the higher access features. The subscription counter should show “1” now.

But what if our user wants to change the subscription again or cancel it? What should we do with it?

Step 13. Subscription management and special offers

Note: Everything in this step works only for auto-renewable subscriptions.

Subscription Management

You can provide a native interface for subscription management. In our current app subscription purchasing stays available but in your real app, you’ll probably hide the subscription view and show only upgrade offers.

It is still good to provide a user an option to update the subscription level or cancel it from the app. Maybe somewhere in the support section.

Let’s try to implement this part and go to our SupportView. First, we need to add a flag for the state of the subscription management screen showing:

Swift
@State var isManageSubscriptionsSheetPresented: Bool = false

Then let’s add a function that opens the subscription management screen.

Swift
func showManageSubscriptionSheet() {
 isManageSubscriptionsSheetPresented = true
}

And lastly we should add a SwiftUI modifier function call to the Form. You can do it this way:

Swift
Form {
  Button("Subscription management") {
    showManageSubscriptionSheet()
  }
  Button("Request a refund") {
  }
  Button("Redeem code") {
  }
}
.manageSubscriptionsSheet(isPresented: $isManageSubscriptionsSheetPresented)

Now you can run the app, go to the “Support” section and after tapping on “Subscription management” you should see the following:

gO0ht1xfq8F2lEYJ3Q7VyEx 2P ignkbGhxw iVAY TM8f14RiETjZ

Subscription Offer Codes

Another great thing you can do for your users is provide them with a promo code. Unfortunately, you can only do that after your app and IAP are approved by Apple. Before that, you can test the feature using our beloved .storekit file.

Its API is almost the same as subscription management. Try to implement it yourself in SupportView using this API:

Swift
.offerCodeRedemption(isPresented: $isOfferCodeRedepmtionPresented)

If you have trouble implementing it, you can download the final version of the project.

Step 14. Add the Restore Purchases option

StoreKit 2 automatically keeps track of in-app subscription status and transaction history through Transaction.currentEntitlements, making it unnecessary for users to manually restore purchases. 

However, it’s a best practice to have a “Restore Purchases” button, which satisfies the App Store Review Guidelines. Adding in the “Restore Purchases” functionality is easy thanks to AppStore.sync()

It’s rarely needed but nice to have, as users may wonder whether purchases are up to date or not. In cases where a user suspects that the app is not showing all transactions, calling AppStore.sync() will force the app to obtain transaction information and subscription status from the App Store.

You need to add one line to the restore method for the “Restore Purchases” button to start working in our app.

Swift
@MainActor
 func restore() async throws {
  try await AppStore.sync()
}

This should trigger the transaction update. However, in StoreKit 2 it’s mostly done because of Apple requirements as users feel safer this way.

Step 15. Handling Refunds and Customer Support

The app market gets more and more competitive and customer support is more important than ever, and it’s essential to have a solid strategy in place for handling service issues and refunds.

Handling Refunds

To handle the refunds we need the user to select transactions for which to handle the refund. Open the Store class and add the entitlements array.

Swift
@Published var entitlements = [Transaction]() 

Then we need to start appending the transactions provided by StoreKit to this array. Change the body of updateCurrentEntitlements in the following way.

Swift
for await result in Transaction.currentEntitlements {
  if let transaction = await self.handle(transactionVerification: result) {
    entitlements.append(transaction)
 }
}

Now we need to update the UI.

We will handle refunds in a separate screen as the user needs to choose which transaction to refund. Create a new file by pressing ⌘N. Select the file type SwiftUI View. And name it RefundView.swift.

Add

Swift
import StoreKit

Near the SwiftUI import statement.

Next, add the in the top of the RefundView struct.

Swift
@EnvironmentObject var store: Store
@Environment(.dismiss) private var dismiss
    
@State var selectedTransactionID: UInt64?
@State var isRefundRequestPresented: Bool = false

After the body property, write a function that will configure and show the refund screen.

Swift
func startRefund(transactionID: UInt64) {
 selectedTransactionID = transactionID
 isRefundRequestPresented = true
}

Replace the body contents by this:

Swift
Form {
 ForEach(store.entitlements, id: .id) {
  transaction in
   HStack {
    Text(transaction.purchaseDate.formatted())
      Spacer()
       Button("Refund") {
         startRefund(transactionID: transaction.id)
    }
   }
  }
 }

Then add to the body the special refund modifier.

Swift
 Form {
   
 }
.refundRequestSheet(
  for: selectedTransactionID ?? 0,
  isPresented: $isRefundRequestPresented
 ) { result in
	handleRefund(result: result)
}

After that the code won’t compile. Add the handleRefund(result:) function that we just used to resolve the issue.

Swift
func handleRefund(result: Result<StoreKit.Transaction.RefundRequestStatus, StoreKit.Transaction.RefundRequestError>) {
 switch result {
 case .success(.success):
    dismiss()
 default:
   return
 }
}

Main documentation of Refund API is made for UIKit. It is more descriptive.

Consumption API

For consumable in-app purchase refunds you need to share information about a customer’s in-app purchase with the App Store. When a customer requests a refund for a consumable in-app purchase, the App Store will send a ConsumptionRequest notification to the developer’s server, which can respond with the consumption data. This information can be used to inform the refund decision process.

Dealing With Customer Support Issues

In terms of handling renewals, cancellations, billing issues, and other subscription management tasks, Transaction.currentEntitlements proves to be a useful tool. It will fetch and return the latest transactions for each active auto-renewable subscription and non-renewing subscription every time the app is launched, allowing users to maintain their entitlements after renewals and lose them upon cancellation. However, this may not always result in the best user experience. 

For instance, while renewals and cancellations can go unnoticed, billing issues should be brought to the users’ attention so that they can be resolved in time. Although on-device subscription handling can keep the subscription status up-to-date, it cannot effectively inform the user of billing issues and grace periods. 

Therefore, server-side subscription handling is preferable, as it can promptly detect and notify the user of such events, providing a better user experience.

Receipt Validation

In StoreKit 1 validating receipts was a major part of properly integrating StoreKit. It required validating and parsing receipts to determine purchases and what to unlock for users. Apple has documentation on the best ways to validate receipts, which includes local, on-device receipt validation and server-side receipt validation with the App Store. 

StoreKit 2 improves the process by encapsulating all validation and parsing inside Transaction.currentEntitlements, which makes it easier for developers to use. 

As a result, there is no need to worry about validating receipts in StoreKit 2 unless you want to share the purchases with the backend and across multiple platforms. If you want to see how it works in action, use our receipt validator.

Check you Apple receipts for free
Easily debug your in-app subscriptions in a few clicks.
Check Here

IAPs best practices

When it comes to in-app purchases, there are several best practices that you should follow to create a seamless and user-friendly experience. One of the most important is to let users experience your app before making a purchase, as they may be more likely to invest in paid items or features after discovering their value. Additionally, if you offer auto-renewable subscriptions, consider supporting limited free access to your content.

Another key factor is designing an integrated shopping experience that does not make users feel like they’ve entered a different app. Your products should be presented in a way that mirrors the style of your app, with simple and succinct names and descriptions. It’s also important to display the total billing price for each in-app purchase you offer, regardless of the type, so users know the total amount they will be charged.

Lastly, it’s crucial to use the default confirmation sheet provided by the system to prevent accidental purchases, and not modify or replicate it. By following these guidelines, app developers can create a smooth and transparent in-app purchase experience for their users.

Hide the store when the user cannot make a payment

There are cases when a user is forbidden to make payments. For example, when parental control is on or the device has an MDM profile that doesn’t allow payments. MDM is mobile device management, you can read more here.

Let’s implement it in our app. Find the ForEach block that is used for the product display. Add a condition that the user is allowed to make payments and show the warning in case it is not possible to proceed with the payment instead of showing the products.

Swift
if AppStore.canMakePayments {
  ForEach(store.products) {
    product in
      HStack {
        Text(product.displayName)
        Spacer()
        Button("(product.displayPrice)") {
          Task {
            try await store.purchase(product)
        }
      }
    }
  }
} else {
  Text("Unfortunately, your Apple account doesn't allow payments")
}

Enable family sharing

Family sharing is allowed for non-consumable and auto-renewable in-app purchases. To test the family sharing feature, go to Products.storekit file. Select one of the subscriptions and select “On” in “Family sharing”.

Now you can check if a product is set to be shared with the family by calling product.isFamilyShareable.

To enable family sharing for an actual subscription, go to App Store Connect and select one of the subscription levels. In the top section of the subscription page find “Family Sharing” and click on the “Turn On” link below. Then click “Confirm”. It’s that easy!

Do I need server-side logic for in-app purchases?

I ask myself this question for every feature of the app. Think of the following to decide on the matter. While client-side logic may seem simpler to implement, it has its limitations, especially when it comes to analytics, handling refunds, and promotional offers. Server-side logic, on the other hand, provides more flexibility and control, but it also has its drawbacks.

One significant advantage of server-side logic is security. It can help prevent fraudulent purchases and ensure that only legitimate purchases are granted access to the app’s content. Additionally, server-side logic allows for cross-platform compatibility, making it possible to extend in-app purchases to other platforms such as Android or the web.

However, implementing server-side logic can come at a cost, both in terms of time and money. You’ll need to manage both the server and client side, which can be complex and time-consuming. Additionally, when your backend app grows beyond the free tier of infrastructure hosting, you may need to spend more money.

Ultimately, the decision to use server-side logic for in-app purchases depends on the specific needs of your app and your available resources. While it may be more complex and costly to implement, server-side logic provides more flexibility and control over managing in-app purchases, making it a worthwhile consideration for developers.

Improve Your In-App Purchases with Adapty

We at Adapty understand that not all developers can use the latest iOS 16+ APIs that are utilized in this tutorial, which is why we offer a solution that supports older iOS 9+ versions of iOS. Our aim is to simplify the integration of in-app purchases without the need for server coding. Our developer-friendly iOS SDK handles everything from free trials to refunds, so developers can focus on creating great apps while we take care of the backend.

In conclusion, we want to help developers succeed and provide them with the knowledge and tools to build their own solutions. Whether you choose to use StoreKit 2 or Adapty, our goal remains the same – to help you make more money through in-app purchases.

Maximize your app’s potential with Adapty! If you’re an app developer or product owner aiming to boost your conversion rates and in-app subscriptions, schedule a free demo call with us! We’ll walk you through our platform, detailing our myriad of benefits and features at absolutely no cost and with no obligation. Discover how Adapty’s solutions, compatible with iOS 9+ versions, can elevate your in-app purchase integration and backend management, allowing you to concentrate on crafting outstanding apps.

FAQ

Apple charges a 30% commission on most in-app purchases made through the App Store. The Small Business Program reduces the commission to 15% for small businesses that earn less than $1 million per year. If the user is paying you for a subscription for more than one year, then the fee from their subscription also reduces to 15%. 

According to documentation – up to 48 hours.

No, this file is only used for testing purposes. The only way to create the actual in-app purchase is through App Store Connect.

Receipt validation verifies the legitimacy of in-app purchases to prevent fraud and ensure feature access only for legitimate users.

Yes, use Universal Purchase for that.

Unlock 2024 subscription holiday secrets
Discover why apps thrive during Black Friday, Christmas & New Year and how you can do the same.
Get your free report
Unlock 2024 subscription holiday secrets

Recommended posts