
Product-releases
April 6, 2022
3 min read
October 2, 2020
14 min read
We continue our series of articles dedicated to iOS in-app purchases. In the previous part, we have discussed the process of creating in-app purchases and their configuration. You can read all the tutorials following the links:
Now we are going to show you how to create a simple paywall (payment screen) as well as how to initialize and process purchases that we have configured.
Any app that uses in-app purchases has a paywall. There are some guidelines from Apple that determine must-have elements. At this step, we’ll implement just some of them to give you a sense of that to do next.
So, our screen will have the following:
I’ve used Interface Builder Storyboard for layout, but you can use just coding. I’ve also moved UIActivityIndicatorView to ViewController to present the payment process.
Let’s build a core of our ViewController. It doesn’t have any logic and we’ll add it later.
import StoreKit
import UIKit
class ViewController: UIViewController {
// 1:
@IBOutlet private weak var purchaseButtonA: UIButton!
@IBOutlet private weak var purchaseButtonB: UIButton!
@IBOutlet private weak var activityIndicator: UIActivityIndicatorView!
override func viewDidLoad() {
super.viewDidLoad()
activityIndicator.hidesWhenStopped = true
// 2:
showSpinner()
Purchases.default.initialize {
[weak self] result in
guard let self = self else {
return
}
self.hideSpinner()
switch result {
case let .success(products):
DispatchQueue.main.async {
self.updateInterface(products: products)
}
default:
break
}
}
}
// 3:
private func updateInterface(products: [SKProduct]) {
updateButton(purchaseButtonA, with: products[0])
updateButton(purchaseButtonB, with: products[1])
}
// 4:
@IBAction func purchaseAPressed(_ sender: UIButton) {
}
@IBAction func purchaseBPressed(_ sender: UIButton) {
}
@IBAction func restorePressed(_ sender: UIButton) {
}
}
Let’s see what’s inside:
Helpers:
extension ViewController {
// 1:
func updateButton(_ button: UIButton, with product: SKProduct) {
let title = “\(product.title ?? product.productIdentifier) for \(product.localizedPrice)”
button.setTitle(title, for: .normal)
}
func showSpinner() {
DispatchQueue.main.async {
self.activityIndicator.startAnimating()
self.activityIndicator.isHidden = false
}
}
func hideSpinner() {
DispatchQueue.main.async {
self.activityIndicator.stopAnimating()
}
}
}Spinner
Pay attention to the usage (1) of SKProduct objects. We make an extension to easily extract the information. This implementation is more flexible then using SKProduct props directly:
extension SKProduct {
var localizedPrice: String {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = priceLocale
return formatter.string(from: price)!
}
var title: String? {
switch productIdentifier {
case “barcode_month_subscription”:
return “Monthly Subscription”
case “barcode_year_subscription”:
return “Annual Subscription”
default:
return nil
}
}
}
I’ve slightly refined the Purchases class to make it possible to show the result of an asynchronous operation in the interface. I also cached SKProduct objects for further usage.
typealias RequestProductsResult = Result<[SKProduct], Error>
typealias PurchaseProductResult = Result
typealias RequestProductsCompletion = (RequestProductsResult) -> Void
typealias PurchaseProductCompletion = (PurchaseProductResult) -> Void
class Purchases: NSObject {
static let `default` = Purchases()
private let productIdentifiers = Set(
arrayLiteral: "barcode_month_subscription", "barcode_year_subscription"
)
private var products: [String: SKProduct]?
private var productRequest: SKProductsRequest?
func initialize(completion: @escaping RequestProductsCompletion) {
requestProducts(completion: completion)
}
private var productsRequestCallbacks = [RequestProductsCompletion]()
private func requestProducts(completion: @escaping RequestProductsCompletion) {
guard productsRequestCallbacks.isEmpty else {
productsRequestCallbacks.append(completion)
return
}
productsRequestCallbacks.append(completion)
let productRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
productRequest.delegate = self
productRequest.start()
self.productRequest = productRequest
}
}
Delegate:
extension Purchases: SKProductsRequestDelegate {
guard !response.products.isEmpty else {
print(“Found 0 products”)
productsRequestCallbacks.forEach { $0(.success(response.products)) }
productsRequestCallbacks.removeAll()
return
}
var products = [String: SKProduct]()
for skProduct in response.products {
print(“Found product: \(skProduct.productIdentifier)”)
products[skProduct.productIdentifier] = skProduct
}
self.products = products
productsRequestCallbacks.forEach { $0(.success(response.products)) }
productsRequestCallbacks.removeAll()
}
func request(_ request: SKRequest, didFailWithError error: Error) {
print(“Failed to load products with error:\n \(error)”)
productsRequestCallbacks.forEach { $0(.failure(error)) }
productsRequestCallbacks.removeAll()
}
}
Let’s add the Errors handling to our code. I created enum PurchaseError to implement Error protocol (or LocalizedError):
enum PurchasesError: Error {
case purchaseInProgress
case productNotFound
case unknown
}
All errors on StoreKit level will be caught as a single error, get more information about the error types in the documentation.
purchaseProduct() function launches the purchase process of the selected product, and restorePurchases() function requests the list of the users’ purchases from the system: auto renewable subscriptions or non-consumable purchases:
fileprivate var productPurchaseCallback: ((PurchaseProductResult) -> Void)?
func purchaseProduct(productId: String, completion: @escaping (PurchaseProductResult) -> Void) {
// 1:
guard productPurchaseCallback == nil else {
completion(.failure(PurchasesError.purchaseInProgress))
return
}
// 2:
guard let product = products?[productId] else {
completion(.failure(PurchasesError.productNotFound))
return
}
productPurchaseCallback = completion
// 3:
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
public func restorePurchases(completion: @escaping (PurchaseProductResult) -> Void) {
guard productPurchaseCallback == nil else {
completion(.failure(PurchasesError.purchaseInProgress))
return
}
productPurchaseCallback = completion
// 4:
SKPaymentQueue.default().restoreCompletedTransactions()
}
So:
To process the result of the purchase, we need to implementSKPaymentTransactionObserver() protocol:
extension Purchases: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
// 1:
for transaction in transactions {
switch transaction.transactionState {
// 2:
case .purchased, .restored:
if finishTransaction(transaction) {
SKPaymentQueue.default().finishTransaction(transaction)
productPurchaseCallback?(.success(true))
} else {
productPurchaseCallback?(.failure(PurchasesError.unknown))
}
// 3:
case .failed:
productPurchaseCallback?(.failure(transaction.error ?? PurchasesError.unknown))
SKPaymentQueue.default().finishTransaction(transaction)
default:
break
}
}
productPurchaseCallback = nil
}
}
extension Purchases {
// 4:
func finishTransaction(_ transaction: SKPaymentTransaction) -> Bool {
let productId = transaction.payment.productIdentifier
print(“Product \(productId) successfully purchased”)
return true
}
}
The core part is done, so we just need to call execute these functions in the ViewController.
@IBAction func purchaseAPressed(_ sender: UIButton) {
showSpinner()
Purchases.default.purchaseProduct(productId: “barcode_month_subscription”) {
[weak self] _ in
self?.hideSpinner()
// Handle result
}
}
@IBAction func purchaseBPressed(_ sender: Any) {
showSpinner()
Purchases.default.purchaseProduct(productId: “barcode_year_subscription”) {
[weak self] _ in
self?.hideSpinner()
// Handle result
}
}
@IBAction func restorePressed(_ sender: UIButton) {
showSpinner()
Purchases.default.restorePurchases {
[weak self] _ in
self?.hideSpinner()
// Handle result
}
}
This is it. In the next part, we will discuss the basic ways to test the purchase mechanism, and how to design a paywall to pass the App Store review.
Further reading
Product-releases
April 6, 2022
3 min read
Product-releases
October 11, 2022
3 min read