Advanced Purchasing
If you need fine-grain control over the purchasing pipeline, use a purchase controller to manually handle purchases and subscription status.
Using a PurchaseController is only recommended for advanced use cases. By default, Superwall handles all
subscription-related logic and purchasing operations for you out of the box.
By default, Superwall handles basic subscription-related logic for you:
- Purchasing: When the user initiates a checkout on a paywall.
- Restoring: When the user restores previously purchased products.
- 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.
// MyPurchaseController.swift
import SuperwallKit
import StoreKit
final class MyPurchaseController: PurchaseController {
static let shared = MyPurchaseController()
// 1
func purchase(product: StoreProduct) async -> PurchaseResult {
// Use StoreKit or some other SDK to purchase...
// Send Superwall the result.
return .purchased // .cancelled, .pending, .failed(Error)
}
func restorePurchases() async -> RestorationResult {
// Use StoreKit or some other SDK to restore...
// Send Superwall the result.
return .restored // Or failed(error)
}
}@import SuperwallKit;
@import StoreKit;
// MyPurchaseController
@interface MyPurchaseController: NSObject <SWKPurchaseController>
+ (instancetype)sharedInstance;
@end
@implementation MyPurchaseController
+ (instancetype)sharedInstance
{
static MyPurchaseController *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [MyPurchaseController new];
});
return sharedInstance;
}
// 1
- (void)purchaseWithProduct:(SWKStoreProduct * _Nonnull)product completion:(void (^ _Nonnull)(enum SWKPurchaseResult, NSError * _Nullable))completion {
// TODO
// ----
// Purchase via StoreKit, RevenueCat, Qonversion or however
// you like and return a valid SWKPurchaseResult
completion(SWKPurchaseResultPurchased, nil);
}
// 2
- (void)restorePurchasesWithCompletion:(void (^ _Nonnull)(enum SWKRestorationResult, NSError * _Nullable))completion {
// TODO
// ----
// Restore purchases and return `SWKRestorationResultRestored` if successful.
// Return an `NSError` if not.
completion(SWKRestorationResultRestored, nil);
}
@endHere’s what each method is responsible for:
- 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:.cancelled: The purchase was cancelled..purchased: The product was purchased..pending: The purchase is pending/deferred and requires action from the developer..failed(Error): The purchase failed for a reason other than the user cancelling or the payment pending.
- Restoring purchases. Here, you restore purchases and return a
RestorationResultindicating whether the restoration was successful or not. If it was, return.restore, orfailedalong 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
}
}@main
struct MyApp: App {
init() {
Superwall.configure(
apiKey: "MY_API_KEY",
purchaseController: MyPurchaseController.shared // <- Handle purchases on your own
)
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}// AppDelegate.m
@import SuperwallKit;
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[Superwall configureWithApiKey:@"MY_API_KEY" purchaseController:[MyPurchaseController sharedInstance] options:nil completion:nil];
return YES;
}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:
.unknown: This is the default value. In this state, paywalls will not show and their presentation will be automatically delayed untilsubscriptionStatuschanges to a different value..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..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 entitlements from purchased product ids from Superwall
let entitlements = Superwall.shared.entitlements.byProductIds(products)
// set subscription status
await MainActor.run {
Superwall.shared.subscriptionStatus = .active(entitlements)
}
}@import SuperwallKit;
// when a subscription is purchased, restored, validated, expired, etc...
[myService setSubscriptionStatusDidChange:^{
if (user.hasActiveSubscription) {
[Superwall sharedInstance] setActiveSubscriptionStatusWith:[NSSet setWithArray:@[myEntitlements]]];
} else {
[[Superwall sharedInstance] setInactiveSubscriptionStatus];
}
}];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.
Product Overrides
When using a PurchaseController, you may want to override specific products shown on your paywalls. This is useful for:
- A/B testing different subscription tiers
- Showing region-specific products
- Dynamically changing products based on user segments
- Testing promotional pricing without modifying paywalls
// Configure product overrides when setting up Superwall
let paywallOptions = PaywallOptions()
paywallOptions.overrideProductsByName = [
"primary": "com.example.premium_monthly_promo",
"secondary": "com.example.premium_annual_promo",
"tertiary": "com.example.premium_lifetime"
]
Superwall.configure(
apiKey: "MY_API_KEY",
purchaseController: MyPurchaseController.shared,
options: paywallOptions
)// Configure product overrides when setting up Superwall
SWKPaywallOptions *paywallOptions = [[SWKPaywallOptions alloc] init];
paywallOptions.overrideProductsByName = @{
@"primary": @"com.example.premium_monthly_promo",
@"secondary": @"com.example.premium_annual_promo",
@"tertiary": @"com.example.premium_lifetime"
};
[Superwall configureWithApiKey:@"MY_API_KEY"
purchaseController:[MyPurchaseController sharedInstance]
options:paywallOptions
completion:nil];How Product Overrides Work:
- Product names (e.g., "primary", "secondary") must match exactly as defined in the Superwall dashboard's Paywall Editor
- The SDK substitutes the original product IDs with your override IDs before fetching from the App Store
- The paywall maintains its visual design while showing the substituted products
- Your
PurchaseControllerwill receive the overridden products whenpurchase(product:)is called
Product overrides only affect the products shown on paywalls. They don't change your subscription logic or entitlement validation.
How is this guide?
Edit on GitHub