How to Integrate in-app Purchases (IAP) in iOS Applications

Naveen Kothiyal Apr 12th, 2023

Image by Freepik

In recent years, the majority of applications have begun offering the ability to access additional features or content through in app purchases, also known as in app Purchases (IAP).

In fact, this method is a great way for iOS or MacOS application developers to earn money, and it’s also a key source of revenue for Apple. In fact, Apple takes a cut of 30% from each successful transaction made through an application.
So today, we’re going to delve into the topic of in app Purchases and explore the process of integrating them into iOS applications written in Swift 5.0.

Types of in-app Purchases:

When a user decides to purchase additional content or a subscription through an in-app Purchase (IAP), they can choose from the following options:

Consumable

Consumable IAP refers to a type of IAP where the user can purchase a product or service within the app and use it once. Examples of consumable IAP include game currency, health, and hints. After the user consumes the product, they need to purchase it again if they want to use it again.

Non-consumable

These items are known as Non-Consumable in-app Purchases (IAP) and are purchased only once by the user. Unlike consumable IAPs, non-consumable IAPs can be used in the future for free and do not need to be repurchased. In the event of reinstalling the app or switching devices, the user will not lose their non-consumable IAPs, and they can be restored for free through the “Restore In-App Purchases” feature. Examples of non-consumable IAPs include upgrading the app to a pro version, removing ads, and unlocking additional features.

Non-renewing subscriptions

Non-Renewing Subscriptions in Apple allows users to purchase access to a service or content within an app for a limited period of time. Unlike standard subscriptions, which get renewed automatically on a recurring basis. Non-renewing subscriptions do not automatically renew, and the user must manually purchase the subscription again if they wish to continue accessing the service or content.

Examples of non-renewing subscriptions include access to a service for a set period of time, such as a monthly or yearly subscription, or access to content that is updated regularly, such as a magazine subscription. After the subscription period expires, the user must manually renew the subscription if they wish to continue accessing the content or services.

Auto-renewable subscriptions

As the name suggests, Auto-Renewable Subscriptions are a type of in-app Purchases (IAP) in Apple that allow users to purchase access to a service or content within an app on a recurring basis. The subscription will automatically renew at the end of each billing cycle, and the user will be charged again unless they cancel the subscription.
For example: Ongoing services (Netflix, Hotstar, etc.), Magazine subscriptions etc.

We will cover everything necessary for incorporating in-app Purchases into an iOS application. The following subjects will be highlighted-

  • Configure In-App Purchase in developer account
  • Code in swift
  • Testing of In-App Purchase(with sandbox account)

Configure In-App Purchase in developer account:

This part includes 3 sections:

  • App creation in iTunes
  • InAppPurchase product creation
  • Sandbox user creation

App creation in iTunes- To build an app on iTunes, navigate to the “My Apps” section in your iTunes Connect account. If you already have an app, you can use it. To create a new app, you’ll need to generate an App ID from your developer account.

ott business ott business

InAppPurchase product creation- In order to create the product in developer account you just move to the InAppPurchase section in itunes connect app for which you are creating the products.
Here’s how you can do it:

ott software ott video on demand

NOTE: The product ids’ should be unique and understandable, we have to use the same to get the IAP product from apple.

Sandbox user creation- To create the sandbox user just move to user and access section in from developer portal. And then move to sandbox users and click on plus(+) icon.
When creating the user you need to keep in mind that the email id you are using to create the sandbox user is not already an apple id, sandbox user requires a fresh id.

top ott platform all ott platforms in one app

Bonus point: You can create multiple sandbox users with the same email id, just by adding +1,+2,+3 after your email.
Example : sandboxtest@gmail.com, sandboxtest+1@gmail.com, sandboxtest+2@gmail.com, sandboxtest+4@gmail.com,
For all the above email the verification mail will come in sandboxtest@gmail.com.

Code in Swift:

Here, I am using two classes, named IAPHelper and IAPService. You can change the class name as per your choice.

IAPService Class

import Foundation
import StoreKit

/// Purchase Delegate
protocol IAPServiceDelegate: class {

func didCompleteTransaction(_ transaction: SKPaymentTransaction)
func didFailTransaction(_ transaction: SKPaymentTransaction)
func getProducts(products: [SKProduct])
func getInvalisProductIdentifier(ids: [String])
}

extension IAPServiceDelegate {
func getInvalisProductIdentifier(ids: [String]) {}
}

class IAPService: NSObject {

static let shared = IAPService()
private override init() { }

weak var delegate: IAPServiceDelegate?

/// Get Product from ITunes
func getProduct(sku: Set) {
let request = SKProductsRequest(productIdentifiers: sku)
SKPaymentQueue.default().add(self)
request.delegate = self
request.start()
}

/// Purchase product
func purchase(product: SKProduct) {
Log.d(“Buying \(product.productIdentifier)…”)
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}

/// Restore Product
func restorePurchases() {
SKPaymentQueue.default().restoreCompletedTransactions()
}
}

// MARK: StoreKit Product Delegate
extension IAPService: SKProductsRequestDelegate {

func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {

if response.products.count != 0 {
delegate?.getProducts(products: response.products)
} else {
delegate?.getProducts(products: [])
}
if response.invalidProductIdentifiers.count != 0 {
// purchaseDelegate?.getInvalisProductIdentifier(ids: response.invalidProductIdentifiers)
}
}
}

// MARK: – SKPaymentTransactionObserver

extension IAPService: SKPaymentTransactionObserver {

public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchased:
complete(transaction: transaction)
break
case .failed:
fail(transaction: transaction)
break
case .restored:
restore(transaction: transaction)
break
case .deferred:
break
case .purchasing:
break
}
}
}

private func complete(transaction: SKPaymentTransaction) {
Log.d(“complete…”)
deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
delegate?.didCompleteTransaction(transaction)
}

private func restore(transaction: SKPaymentTransaction) {
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
Log.d(“restore… \(productIdentifier)”)
deliverPurchaseNotificationFor(identifier: productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
// purchaseDelegate?.didCompleteTransaction(transaction)
}

private func fail(transaction: SKPaymentTransaction) {
Log.d(“fail…”)
if let transactionError = transaction.error as NSError?,
let localizedDescription = transaction.error?.localizedDescription,
transactionError.code != SKError.paymentCancelled.rawValue {
Log.d(“Transaction Error: \(localizedDescription)”)
}
SKPaymentQueue.default().finishTransaction(transaction)
delegate?.didFailTransaction(transaction)

}

private func deliverPurchaseNotificationFor(identifier: String?) {
guard let identifier = identifier else { return }
// save identifiers to user defaults to avoid re-purchasing them

// purchasedProductIdentifiers.insert(identifier)
// UserDefaults.standard.set(true, forKey: identifier)
NotificationCenter.default.post(name: .IAPHelperPurchaseNotification, object: identifier)
}
}

IAPHelper Class

import StoreKit

public typealias ProductIdentifier = String
public typealias ProductsRequestCompletionHandler = (_ success: Bool, _ products: [SKProduct]?) -> Void

extension Notification.Name {
static let IAPHelperPurchaseNotification = Notification.Name(“IAPHelperPurchaseNotification”)
}

open class IAPHelper: NSObject {

private let productIdentifiers: Set private var purchasedProductIdentifiers: Set = []
private var productsRequest: SKProductsRequest?
private var productsRequestCompletionHandler: ProductsRequestCompletionHandler?

public init(productIds: Set) {
productIdentifiers = productIds
for productIdentifier in productIds {
let purchased = UserDefaults.standard.bool(forKey: productIdentifier)
if purchased {
purchasedProductIdentifiers.insert(productIdentifier)
Log.d(“Previously purchased: \(productIdentifier)”)
} else {
Log.d(“Not purchased: \(productIdentifier)”)
}
}
super.init()

SKPaymentQueue.default().add(self)
}
}

// MARK: – StoreKit API

extension IAPHelper {

public func requestProducts(_ completionHandler: @escaping ProductsRequestCompletionHandler) {
productsRequest?.cancel()
productsRequestCompletionHandler = completionHandler

productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
productsRequest!.delegate = self
productsRequest!.start()
}

public func buyProduct(_ product: SKProduct) {
Log.d(“Buying \(product.productIdentifier)…”)
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}

public func isProductPurchased(_ productIdentifier: ProductIdentifier) -> Bool {
return purchasedProductIdentifiers.contains(productIdentifier)
}

public class func canMakePayments() -> Bool {
return SKPaymentQueue.canMakePayments()
}

public func restorePurchases() {
SKPaymentQueue.default().restoreCompletedTransactions()
}
}

// MARK: – SKProductsRequestDelegate

extension IAPHelper: SKProductsRequestDelegate {

public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
Log.d(“Loaded list of products…”)
let products = response.products
productsRequestCompletionHandler?(true, products)
clearRequestAndHandler()

for p in products {
Log.d(“Found product: \(p.productIdentifier) \(p.localizedTitle) \(p.price.floatValue)”)
}
}

public func request(_ request: SKRequest, didFailWithError error: Error) {
Log.d(“Failed to load list of products.”)
Log.d(“Error: \(error.localizedDescription)”)
productsRequestCompletionHandler?(false, nil)
clearRequestAndHandler()
}

private func clearRequestAndHandler() {
productsRequest = nil
productsRequestCompletionHandler = nil
}
}

// MARK: – SKPaymentTransactionObserver

extension IAPHelper: SKPaymentTransactionObserver {

public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch (transaction.transactionState) {
case .purchased:
complete(transaction: transaction)
break
case .failed:
fail(transaction: transaction)
break
case .restored:
restore(transaction: transaction)
break
case .deferred:
break
case .purchasing:
break
}
}
}

private func complete(transaction: SKPaymentTransaction) {
Log.d(“complete…”)
deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}

private func restore(transaction: SKPaymentTransaction) {
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }

Log.d(“restore… \(productIdentifier)”)
deliverPurchaseNotificationFor(identifier: productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}

private func fail(transaction: SKPaymentTransaction) {
Log.d(“fail…”)
if let transactionError = transaction.error as NSError?,
let localizedDescription = transaction.error?.localizedDescription,
transactionError.code != SKError.paymentCancelled.rawValue {
Log.d(“Transaction Error: \(localizedDescription)”)
}

SKPaymentQueue.default().finishTransaction(transaction)
}

private func deliverPurchaseNotificationFor(identifier: String?) {
guard let identifier = identifier else { return }

purchasedProductIdentifiers.insert(identifier)
UserDefaults.standard.set(true, forKey: identifier)
NotificationCenter.default.post(name: .IAPHelperPurchaseNotification, object: identifier)
}
}

To use the above code you have to follow the following steps:

  • Get the plans from apple:

var serverProductIds = Set()
serverProductIds.insert(“svod_one_year_subscription_test”)
serverProductIds.insert(“svod_one_month_subscription_test”)
IAPService.shared.getProduct(sku: serverProductIds)

  • To get the response from apple we need to check the below delegate:

func getProducts(products: [SKProduct]) {
Log.d(“Products ->>>>> \(products)”)
}

  • Purchase the product:

IAPService.shared.purchase(product: buyingProduct) // here buying product type is SKProduct

  • Here is the delegate which will provide you purchase success/fail

func didCompleteTransaction(_ transaction: SKPaymentTransaction) {
DispatchQueue.main.async {
self.hideLoading()
}
}

func didFailTransaction(_ transaction: SKPaymentTransaction) {
DispatchQueue.main.async {
self.hideLoading()
}
}

Testing of In-App Purchase(with sandbox account):

First login with a sandbox user on your iPhone, move to setting->App Store-> scroll down.
If you are not able to see the sandbox login there, then open your app and try to purchase the subscription from the app, once you try it then you can see it in iPhone settings as well.

Now, you can purchase the subscription with your sandbox account. Also, do not worry about the amount displayed on the app, because it is dummy data your amount will not be debited.

Offering in-app purchases to iOS apps is not a challenging process, but it involves a lot of steps which need to be implemented successfully. This detailed guide with the screenshots will surely help you to get the better idea of using in-app purchases in iOS applications. Also, it is important to note that developers need to be in align with Apple’s recommendations and best practices regarding in-app purchases. If you have any queries regarding IAP, contact us.

Add a Comment

Your email address will not be published. Required fields are marked *

Looking for Streaming Solutions?

  • Go-Live on 12+ platforms
  • Zero Revenue Share
  • Multiple Monetization Models
  • 50+ Integrations

get a demo today

Take control of your digital media strategy.
Contact us for a no-obligation demo of the Experience
Cloud, tailor-made for you!