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.
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:
Next, fill out the details for the IAP as follows:
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.
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.
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.
Select the Capabilities tab. Scroll down to In-App Purchase and toggle the switch to ON.
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
}
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
}
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!)
}
}
Not to forget a method to have restore purchase.
func restoreTapped(_ sender: AnyObject) {
IAPProducts.store.restorePurchases()
}
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.
When submitting the app to the App Store, make sure you have add the created in-app purchase item at ‘feature’ panel before submission.
Resources:
RW Tutorial
Apple Guideline for IAP