iOS In-App Purchase

11 minutes, 3 seconds

In-app purchase is a way to give more value to the customer from basic feature that being provided from our app.

We have to take care of the process deeply to make sure the transaction work smoothly, with taking consideration of many conditions. Such interupted purchase, cancelled and of course to restore purchase.

We will go step by step, on the iTunes Connect and also on the Xcode project itself.

iTC Setup

1. Creating In-App Purchase Products

When offering IAP within an an app, you must first add an entry for each individual purchase within iTunes Connect.

When the user makes a purchase, the App Store handles the complex process of charging the user’s iTunes account.

IAP types:

  • Consumable: These can be bought more than once and can be used up. These are things such as extra lives, in-game currency, temporary power-ups, and the like.
  • Non-Consumable: Something that you buy once, and expect to have permanently such as extra levels and unlockable content.
  • Non-Renewing Subscription: Content that’s available for a fixed period of time.
  • Auto-Renewing Subscription: A repeating subscription.

Screen Shot 2016-11-02 at 5.28.29 PM.png

Screen Shot 2016-11-02 at 5.31.23 PM.png

2. Creating Non-Consumable Product

Next, fill out the details for the IAP as follows:

  • Reference Name: A nickname identifying the IAP within iTunes Connect. This name does not appear anywhere in the app.

  • Product ID: This is a unique string identifying the IAP. Usually it’s best to start with the Bundle ID and then append a unique name specific to this purchasable item. So, for example: com.companyname.app.item.

  • Cleared for Sale: Enables or disables the sale of the IAP.

  • Price Tier: The cost of the IAP.

Now scroll down to the Localizations section and note that there is a default entry for English (U.S.). Enter Display Name and the Description. Click Save.

Don’t forget to add screenshot for the in app purchase product.

3. Creating a Sandbox User

In iTunes Connect, click iTunes Connect in the top left corner of the window to get back to the main menu. Select Users and Roles, then click the Sandbox Testers tab. Click + next to the “Tester” title.

Project Configuration

Select the Capabilities tab. Scroll down to In-App Purchase and toggle the switch to ON.

Listing IAP

Create a file called IAPHelper.swift.


import StoreKit

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

open class IAPHelper : NSObject  {
  
  static let IAPHelperPurchaseNotification = "IAPHelperPurchaseNotification"
  fileprivate let productIdentifiers: Set
  fileprivate var purchasedProductIdentifiers = Set()
  fileprivate var productsRequest: SKProductsRequest?
  fileprivate 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)
        print("Previously purchased: \(productIdentifier)")
      } else {
        print("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) {
    print("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) {
    let products = response.products
    print("Loaded list of products...")
    productsRequestCompletionHandler?(true, products)
    clearRequestAndHandler()

    for p in products {
      print("Found product: \(p.productIdentifier) \(p.localizedTitle) \(p.price.floatValue)")
    }
  }
  
  public func request(_ request: SKRequest, didFailWithError error: Error) {
    print("Failed to load list of products.")
    print("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) {
    print("complete...")
    deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier)
    SKPaymentQueue.default().finishTransaction(transaction)
  }
 
  private func restore(transaction: SKPaymentTransaction) {
    guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
 
    print("restore... \(productIdentifier)")
    deliverPurchaseNotificationFor(identifier: productIdentifier)
    SKPaymentQueue.default().finishTransaction(transaction)
  }
 
  private func fail(transaction: SKPaymentTransaction) {
    print("fail...")
    if let transactionError = transaction.error as? NSError {
      if transactionError.code != SKError.paymentCancelled.rawValue {
        print("Transaction Error: \(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)
    UserDefaults.standard.synchronize()
    NotificationCenter.default.post(name: NSNotification.Name(rawValue: IAPHelper.IAPHelperPurchaseNotification), object: identifier)
  }
}

Create a IAPProducts.swift class.


import Foundation

public struct IAPProducts {
  
  public static let IAPItem = "com.companyname.app.item"
  
  fileprivate static let productIdentifiers: Set = [IAPProducts.IAPItem]

  public static let store = IAPHelper(productIds: IAPProducts.productIdentifiers)
  
}

func resourceNameForProductIdentifier(_ productIdentifier: String) -> String? {
  return productIdentifier.components(separatedBy: ".").last
}

At the Store Page Table View Controller

We use table view controller to show list of our products. We will use custome cell to handle the purchase.


var products = [SKProduct]()

We will add a refresh control, so that user can refresh the list.


// add inside viewDidLoad()
refreshControl = UIRefreshControl()
refreshControl?.addTarget(self, action: #selector(self.reload), for: .valueChanged)

NotificationCenter.default.addObserver(self, selector: #selector(self.handlePurchaseNotification(_:)),
                                                           name: NSNotification.Name(rawValue: IAPHelper.IAPHelperPurchaseNotification),
                                                         object: nil)
                                                         

And also we will add observer for IAP notification and its handler.


// this only called when the transaction is successfully verified by AppStore
func handlePurchaseNotification(_ notification: Notification) {
  guard let productID = notification.object as? String else { return }
  
  for (index, product) in products.enumerated() {
    guard product.productIdentifier == productID else { continue }
    
    tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .fade)
  }
}

At the very begining we will call reload() when the view controller is appeared.


// add this at viewDidAppear
reload()

By using the IAPProducts products class we will request the products on the app store.


func reload() {
  products = []
  
  tableView.reloadData()
  
  IAPProducts.store.requestProducts{success, products in
    if success {
      self.products = products!
      
      self.tableView.reloadData()
    }
    
    self.refreshControl?.endRefreshing()
  }
}

Setup the table view’s data source. Make sure at storyboard, you have to set the table view’s cell with “Cell” identifier because we will refer it later.


// MARK: - UITableViewDataSource
override func numberOfSections(in tableView: UITableView) -> Int {
  return 1
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  return products.count
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ProductCell
  
  let product = products[(indexPath as NSIndexPath).row]
  
  cell.product = product
  cell.buyButtonHandler = { product in
    IAPProducts.store.buyProduct(product)
  }
  
  return cell
}

ProductCell Table View Cell

The cell have a responsibility to request purchase and show the state of the purchase.

ProductCell.swift will look like this.


import UIKit
import StoreKit

class ProductCell: UITableViewCell {
  static let priceFormatter: NumberFormatter = {
    let formatter = NumberFormatter()
    
    formatter.formatterBehavior = .behavior10_4
    formatter.numberStyle = .currency
    
    return formatter
  }()
  
  var buyButtonHandler: ((_ product: SKProduct) -> ())?
  
  var product: SKProduct? {
    didSet {
      guard let product = product else { return }
     
      textLabel?.text = product.localizedTitle
     
      if IAPProducts.store.isProductPurchased(product.productIdentifier) {
        accessoryType = .checkmark
        accessoryView = nil
        detailTextLabel?.text = ""
      } else if IAPHelper.canMakePayments() {
        ProductCell.priceFormatter.locale = product.priceLocale
        detailTextLabel?.text = ProductCell.priceFormatter.string(from: product.price)
     
        accessoryType = .none
        accessoryView = self.newBuyButton()
      } else {
        detailTextLabel?.text = "Not available"
      }
    }
  }
  
  override func prepareForReuse() {
    super.prepareForReuse()
    
    textLabel?.text = ""
    detailTextLabel?.text = ""
    accessoryView = nil
  }
  
  func newBuyButton() -> UIButton {
    let button = UIButton(type: .system)
    button.setTitleColor(tintColor, for: UIControlState())
    button.setTitle("Buy", for: UIControlState())
    button.addTarget(self, action: #selector(ProductCell.buyButtonTapped(_:)), for: .touchUpInside)
    button.sizeToFit()
    
    return button
  }
  
  func buyButtonTapped(_ sender: AnyObject) {
    buyButtonHandler?(product!)
  }
}

Restore Purchase

Not to forget a method to have restore purchase.


func restoreTapped(_ sender: AnyObject) {
  IAPProducts.store.restorePurchases()
}

Covering Purchasing Circumstance

We probably need want to check if the user is about to purchasing by listening to StoreKit notificaiton at early as AppDelegate’s didFinishLaunching method get called.

But asking the credential of App Store is not appropriate however. We need to know either customer has tapped on purchase button, then make sure the deliverables is can be consumed by user.

Submission to The App Store

When submitting the app to the App Store, make sure you have add the created in-app purchase item at ‘feature’ panel before submission.

A


Resources:
RW Tutorial
Apple Guideline for IAP

  • Ikmal Harun

    A very good explanation for iOS In-App Purchase, thumbs up!