Using a PurchaseController is only recommended for advanced use cases. Superwall handles subscription-related logic for you out of the box.

By default, Superwall handles basic subscription-related logic for you:

  1. Purchasing: When the user initiates a checkout on a paywall.
  2. Restoring: When the user restores previously purchased products.
  3. Subscription Status: When the user’s subscription status changes to active or expired (by checking the local receipt).

However, if you want more control, you can pass in a PurchaseController when configuring the SDK via configure(apiKey:purchaseController:options:) and manually set Superwall.shared.subscriptionStatus to take over this responsibility.

Step 1: Creating a PurchaseController

A PurchaseController handles purchasing and restoring via protocol methods that you implement. You pass in your purchase controller when configuring the SDK:

// MyPurchaseController.swift

import SuperwallKit
import StoreKit

final class MyPurchaseController: PurchaseController {
  static let shared = MyPurchaseController()

  // 1
  func purchase(product: StoreProduct) async -> PurchaseResult {

    // TODO:
    // ----
   	// Purchase via StoreKit, RevenueCat, Qonversion or however
    // you like and return a valid PurchaseResult

    return .purchased // .cancelled,  .pending, .failed(Error)
  }

  // 2
  func restorePurchases() async -> RestorationResult {

    // TODO
    // ----
    // Restore purchases and return true if successful.

    return true // false
  }
}

Here’s what each method is responsible for:

  1. Purchasing a given product. In here, enter your code that you use to purchase a product. Then, return the result of the purchase as a PurchaseResult. For Flutter, this is separated into purchasing from the App Store and Google Play. This is an enum that contains the following cases, all of which must be handled:
    1. .cancelled: The purchase was cancelled.
    2. .purchased: The product was purchased.
    3. .pending: The purchase is pending/deferred and requires action from the developer.
    4. .failed(Error): The purchase failed for a reason other than the user cancelling or the payment pending.
  2. Restoring purchases. Here, you restore purchases and return a RestorationResult indicating whether the restoration was successful or not. If it was, return .restore, or failed along with the error reason.

Step 2: Configuring the SDK With Your PurchaseController

Pass your purchase controller to the configure(apiKey:purchaseController:options:) method:

// AppDelegate.swift

import UIKit
import SuperwallKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    Superwall.configure(
      apiKey: "MY_API_KEY",
      purchaseController: MyPurchaseController.shared // <- Handle purchases on your own
    )

    return true
  }
}

Step 3: Keeping subscriptionStatus Up-To-Date

You must set Superwall.shared.subscriptionStatus every time the user’s subscription status changes, otherwise the SDK won’t know who to show a paywall to. This is an enum that has three possible cases:

  1. .unknown: This is the default value. In this state, paywalls will not show and their presentation will be automatically delayed until subscriptionStatus changes to a different value.
  2. .active(let entitlements): Indicates that the user has an active entitlement. Paywalls will not show in this state unless you remotely set the paywall to ignore subscription status. A user can have one or more active entitlement.
  3. .inactive: Indicates that the user doesn’t have an active entitlement. Paywalls can show in this state.

Here’s how you might do this:

import SuperwallKit

func syncSubscriptionStatus() async {
  var purchasedProductIds: Set<String> = []

  // get all purchased product ids
  for await verificationResult in Transaction.currentEntitlements {
    switch verificationResult {
    case .verified(let transaction):
      purchasedProductIds.insert(transaction.productID)
    case .unverified:
      break
    }
  }

  // get store products for purchased product ids from Superwall
  let storeProducts = await Superwall.shared.products(for: purchasedProductIds)

  // get entitlements from purchased store products
  let entitlements = Set(storeProducts.flatMap { $0.entitlements })

  // set subscription status
  await MainActor.run {
    Superwall.shared.subscriptionStatus = .active(entitlements)
  }
}

subscriptionStatus is cached between app launches

Listening for subscription status changes

If you need a simple way to observe when a user’s subscription status changes, on iOS you can use the Publisher for it. Here’s an example:

subscribedCancellable = Superwall.shared.$subscriptionStatus
  .receive(on: DispatchQueue.main)
  .sink { [weak self] status in
    switch status {
    case .unknown:
      self?.subscriptionLabel.text = "Loading subscription status."
    case .active(let entitlements):
      self?.subscriptionLabel.text = "You currently have an active subscription: \(entitlements.map { $0.id }). Therefore, the paywall will not show unless feature gating is disabled."
    case .inactive:
      self?.subscriptionLabel.text = "You do not have an active subscription so the paywall will show when clicking the button."
    }
  }

You can do similar tasks with the SuperwallDelegate, such as viewing which product was purchased from a paywall.