# Superwall Source: https://superwall.com/docs/ios/sdk-reference/Superwall The shared instance of Superwall that provides access to all SDK features. You must call [`configure()`](/ios/sdk-reference/configure) before accessing `Superwall.shared`, otherwise your app will crash. ## Purpose Provides access to the configured Superwall instance after calling [`configure()`](/ios/sdk-reference/configure). ## Signature ```swift public static var shared: Superwall { get } ``` ## Parameters This is a computed property with no parameters. ## Returns / State Returns the shared `Superwall` instance that was configured via [`configure()`](/ios/sdk-reference/configure). ## Usage Configure first (typically in AppDelegate or SceneDelegate): ```swift Superwall.configure(apiKey: "pk_your_api_key") ``` Then access throughout your app: ```swift Superwall.shared.register(placement: "feature_access") { // Feature code here } ``` Set user identity and attributes: ```swift Superwall.shared.identify(userId: "user123") Superwall.shared.setUserAttributes([ "plan": "premium", "signUpDate": Date() ]) ``` Reset the user: ```swift Superwall.shared.reset() ``` Avoid calling `Superwall.shared.reset()` repeatedly. Resetting rotates the anonymous user ID, clears local paywall assignments, and requires the SDK to re-download configuration state. Only trigger a reset when a user explicitly logs out or you intentionally need to forget their identity. See [User Management](/ios/quickstart/user-management) for more guidance. Set delegate: ```swift Superwall.shared.delegate = self ``` Override products: ```swift Superwall.shared.overrideProductsByName = [ "primary": "produceID_to_replace_primary_product" ] ``` Access customer info: ```swift // Get current customer info let customerInfo = Superwall.shared.customerInfo // Get customer info asynchronously let customerInfo = await Superwall.shared.getCustomerInfo() // Observe customer info changes Superwall.shared.$customerInfo .sink { customerInfo in print("Customer has \(customerInfo.subscriptions.count) subscriptions") } .store(in: &cancellables) ``` Set integration attributes: ```swift Superwall.shared.setIntegrationAttributes([ .amplitudeUserId: "user123", .mixpanelDistinctId: "distinct456", .firebaseInstallationId: "abc123" ]) ``` Get device attributes: ```swift let deviceAttributes = await Superwall.shared.getDeviceAttributes() // Use in audience filters or for debugging print("Device attributes: \(deviceAttributes)") ``` Confirm all experiment assignments: ```swift // Get all experiment assignments let assignments = await Superwall.shared.confirmAllAssignments() print("Confirmed \(assignments.count) assignments") ``` Manually refresh configuration (development-only): ```swift // Useful when hot-reloading paywalls during development await Superwall.shared.refreshConfiguration() ``` --- # PurchaseController Source: https://superwall.com/docs/ios/sdk-reference/PurchaseController A protocol for handling Superwall's subscription-related logic with your own purchase implementation. **This protocol is not required.** By default, Superwall handles all subscription-related logic automatically. When implementing PurchaseController, you must manually update [`subscriptionStatus`](/ios/sdk-reference/subscriptionStatus) whenever the user's entitlements change. ## Purpose Use this protocol only if you want complete control over purchase handling, such as when using RevenueCat or other third-party purchase frameworks. ## Signature ```swift public protocol PurchaseController: AnyObject { @MainActor func purchase(product: StoreProduct) async -> PurchaseResult @MainActor func restorePurchases() async -> RestorationResult } ``` ## Parameters | Name | Type | Description | Required | | ---------------- | --------------------- | ------------------------------------------------------------------------------------------------------ | -------- | | purchase | product: StoreProduct | Called when user initiates purchasing. Implement your purchase logic here. Returns \`PurchaseResult\`. | yes | | restorePurchases | None | Called when user initiates restore. Implement your restore logic here. Returns \`RestorationResult\`. | yes | ## Returns / State * `purchase()` returns a `PurchaseResult` (`.purchased`, `.failed(Error)`, or `.cancelled`) * `restorePurchases()` returns a `RestorationResult` (`.restored` or `.failed(Error?)`) When using a PurchaseController, you must also manage [`subscriptionStatus`](/ios/sdk-reference/subscriptionStatus) yourself. ## Usage For implementation examples and detailed guidance, see [Using RevenueCat](/ios/guides/using-revenuecat). This is commonly used with RevenueCat, StoreKit 2, or other third-party purchase frameworks where you want to maintain your existing purchase logic. --- # SubscriptionTransaction Source: https://superwall.com/docs/ios/sdk-reference/SubscriptionTransaction Represents a subscription transaction in the customer's purchase history. Introduced in 4.10.0. `offerType`, `subscriptionGroupId`, and `store` were added in 4.11.0. ## Purpose Provides details about a single subscription transaction returned from [`CustomerInfo`](/ios/sdk-reference/customerInfo). Use this to understand renewal status, applied offers, and the store that fulfilled the purchase. ## Properties | Name | Type | Description | Required | | ---------------------- | ----------------------------- | ----------------------------------------------------- | -------- | | transactionId | String | Unique identifier for the transaction. | yes | | productId | String | The product identifier for the subscription. | yes | | purchaseDate | Date | When the App Store charged the account. | yes | | willRenew | Bool | Whether the subscription is set to auto-renew. | yes | | isRevoked | Bool | \`true\` if the transaction has been revoked. | yes | | isInGracePeriod | Bool | \`true\` if the subscription is in grace period. | yes | | isInBillingRetryPeriod | Bool | \`true\` if the subscription is in billing retry. | yes | | isActive | Bool | \`true\` when the subscription is currently active. | yes | | expirationDate | Date? | Expiration date, if applicable. | no | | offerType | LatestSubscription.OfferType? | Offer applied to this transaction (4.11.0+). | no | | subscriptionGroupId | String? | Subscription group identifier if available (4.11.0+). | no | | store | ProductStore | Store that fulfilled the purchase (4.11.0+). | yes | ### Offer types (4.11.0+) * `trial` — introductory offer. * `code` — offer redeemed with a promo code. * `promotional` — StoreKit promotional offer. * `winback` — win-back offer (iOS 17.2+ only). ### Store values (4.11.0+) `appStore`, `stripe`, `paddle`, `playStore`, `superwall`, `other`. ## Usage Inspect subscription transactions: ```swift let customerInfo = Superwall.shared.customerInfo for subscription in customerInfo.subscriptions { print("Product: \(subscription.productId)") print("Store: \(subscription.store)") print("Offer: \(subscription.offerType?.rawValue ?? "none")") print("Group: \(subscription.subscriptionGroupId ?? "unknown")") print("Expires: \(String(describing: subscription.expirationDate))") } ``` ## Related * [`CustomerInfo`](/ios/sdk-reference/customerInfo) - Source of subscription data * [`NonSubscriptionTransaction`](/ios/sdk-reference/NonSubscriptionTransaction) - Non-subscription transactions * [`getCustomerInfo()`](/ios/sdk-reference/getCustomerInfo) - Fetch customer info asynchronously --- # PaywallOptions Source: https://superwall.com/docs/ios/sdk-reference/PaywallOptions Configuration for paywall presentation and behavior in the Superwall iOS SDK. `PaywallOptions` is provided via the `paywalls` property on [`SuperwallOptions`](/ios/sdk-reference/SuperwallOptions) and is passed when calling [`configure`](/ios/sdk-reference/configure). ## Purpose Customize how paywalls look and behave, including preload behavior, alerts, dismissal, and haptics. ## Signature ```swift @objcMembers public final class PaywallOptions: NSObject { public var isHapticFeedbackEnabled: Bool = true public final class RestoreFailed: NSObject { public var title: String = "No Subscription Found" public var message: String = "We couldn't find an active subscription for your account." public var closeButtonTitle: String = "Okay" } public var restoreFailed: RestoreFailed = RestoreFailed() public var shouldShowWebRestorationAlert: Bool = true public final class NotificationPermissionsDenied: NSObject { public var title: String = "Notification Permissions Denied" public var message: String = "Please enable notification permissions from the Settings app so we can notify you when your free trial ends." public var actionButtonTitle: String = "Open Settings" public var closeButtonTitle: String = "Not now" } public var notificationPermissionsDenied: NotificationPermissionsDenied? public var shouldShowPurchaseFailureAlert: Bool = true public var shouldPreload: Bool = true public var automaticallyDismiss: Bool = true public var overrideProductsByName: [String: String]? = [:] public var shouldShowWebPurchaseConfirmationAlert: Bool = true public enum TransactionBackgroundView: Int { case spinner case none } public var transactionBackgroundView: TransactionBackgroundView = .spinner } ``` ## Parameters | Name | Type | Description | Default | Required | | ----------------------------------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | -------- | | isHapticFeedbackEnabled | Bool | Enables haptic feedback during key paywall interactions. | true | no | | restoreFailed | RestoreFailed | Messaging for the restore-failed alert. | | yes | | restoreFailed.title | String | Title for restore-failed alert. | No Subscription Found | no | | restoreFailed.message | String | Message for restore-failed alert. | We couldn't find an active subscription for your account. | no | | restoreFailed.closeButtonTitle | String | Close button title for restore-failed alert. | Okay | no | | shouldShowWebRestorationAlert | Bool | Shows an alert asking the user to try restoring on the web if web checkout is enabled. | true | no | | notificationPermissionsDenied | NotificationPermissionsDenied? | Customize the alert shown when notification permissions are denied. \`nil\` disables the alert. | | no | | notificationPermissionsDenied.title | String | Title for notification-permissions-denied alert. | Notification Permissions Denied | no | | notificationPermissionsDenied.message | String | Message for notification-permissions-denied alert. | | yes | | notificationPermissionsDenied.actionButtonTitle | String | Action button title for notification-permissions-denied alert. | Open Settings | no | | notificationPermissionsDenied.closeButtonTitle | String | Close button title for notification-permissions-denied alert. | Not now | no | | shouldShowPurchaseFailureAlert | Bool | Shows an alert after a purchase fails. Set to \`false\` if you handle failures via a \`PurchaseController\`. | true | no | | shouldPreload | Bool | Preloads and caches trigger paywalls and products during SDK initialization. | true | no | | automaticallyDismiss | Bool | Automatically dismisses the paywall on successful purchase or restore. | true | no | | overrideProductsByName | \[String: String]? | Overrides products on all paywalls using name→identifier mapping (e.g., \`"primary"\` → \`"com.example.premium\_monthly"\`). | | no | | shouldShowWebPurchaseConfirmationAlert | Bool | Shows a localized alert confirming a successful web checkout purchase. | true | no | | transactionBackgroundView | TransactionBackgroundView | View shown behind the system payment sheet during a transaction. \`.spinner\` by default; set to \`.none\` to remove. | | no | ## Usage ```swift let paywallOptions = PaywallOptions() paywallOptions.isHapticFeedbackEnabled = true paywallOptions.shouldShowPurchaseFailureAlert = false paywallOptions.shouldPreload = true paywallOptions.automaticallyDismiss = true paywallOptions.transactionBackgroundView = .spinner paywallOptions.overrideProductsByName = [ "primary": "com.example.premium_monthly", "tertiary": "com.example.premium_annual" ] paywallOptions.shouldShowWebRestorationAlert = true paywallOptions.notificationPermissionsDenied = { let n = PaywallOptions.NotificationPermissionsDenied() n.title = "Notification Permissions Denied" n.message = "Please enable notification permissions from the Settings app so we can notify you when your free trial ends." n.actionButtonTitle = "Open Settings" n.closeButtonTitle = "Not now" return n }() let options = SuperwallOptions() options.paywalls = paywallOptions Superwall.configure( apiKey: "pk_your_api_key", options: options ) ``` ## Related * [`SuperwallOptions`](/ios/sdk-reference/SuperwallOptions) --- # register() Source: https://superwall.com/docs/ios/sdk-reference/register A function that registers a placement that can be remotely configured to show a paywall and gate feature access. ## Purpose Registers a placement so that when it's added to a campaign on the Superwall Dashboard, it can trigger a paywall and optionally gate access to a feature. ## Signature ```swift public func register( placement: String, params: [String: Any]? = nil, handler: PaywallPresentationHandler? = nil, feature: @escaping () -> Void ) ``` ```swift public func register( placement: String, params: [String: Any]? = nil, handler: PaywallPresentationHandler? = nil ) ``` ## Parameters | Name | Type | Description | Default | Required | | --------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- | | placement | String | The name of the placement you wish to register. | | yes | | params | \[String: Any]? | Optional parameters to pass with your placement. These can be referenced within audience filters in your campaign. Keys beginning with \`$\` are reserved for Superwall and will be dropped. Arrays and dictionaries are currently unsupported and will be ignored. | nil | no | | handler | PaywallPresentationHandler? | A handler whose functions provide status updates for the paywall lifecycle. | nil | no | | feature | (() -> Void)? | An optional completion block representing the gated feature. It is executed based on the paywall's gating mode: called immediately for \*\*Non-Gated\*\*, called after the user subscribes or if already subscribed for \*\*Gated\*\*. | | no | ## Returns / State This function returns `Void`. If you supply a `feature` block, it will be executed according to the paywall's gating configuration, as described above. ## Usage ```swift Superwall.shared.register( placement: "premium_feature", params: ["source": "onboarding"] ) { // Code that unlocks the premium feature openPremiumScreen() } ``` ```swift Superwall.shared.register( placement: "onboarding_complete", params: ["source": "onboarding"], handler: self ) ``` --- # subscriptionStatus Source: https://superwall.com/docs/ios/sdk-reference/subscriptionStatus A published property that indicates the subscription status of the user. If you're using a custom [`PurchaseController`](/ios/sdk-reference/PurchaseController), you must update this property whenever the user's entitlements change. You can also observe changes via the [`SuperwallDelegate`](/ios/sdk-reference/SuperwallDelegate) method `subscriptionStatusDidChange(from:to:)`. ## Purpose Indicates the current subscription status of the user and can be observed for changes using Combine or SwiftUI. ## Signature ```swift @Published public var subscriptionStatus: SubscriptionStatus { get set } ``` ## Parameters This property accepts a `SubscriptionStatus` enum value: * `.unknown` - Status is not yet determined * `.active(Set)` - User has active entitlements (set of entitlement identifiers) * `.inactive` - User has no active entitlements ## Returns / State Returns the current `SubscriptionStatus`. When using a [`PurchaseController`](/ios/sdk-reference/PurchaseController), you must set this property yourself. Otherwise, Superwall manages it automatically. ## Usage Set subscription status (when using PurchaseController): ```swift Superwall.shared.subscriptionStatus = .active(["premium", "pro_features"]) Superwall.shared.subscriptionStatus = .inactive ``` Get current subscription status: ```swift let status = Superwall.shared.subscriptionStatus switch status { case .unknown: print("Subscription status unknown") case .active(let entitlements): print("User has active entitlements: \(entitlements)") case .inactive: print("User has no active subscription") } ``` Observe changes with Combine: ```swift import Combine class ViewController: UIViewController { private var cancellables = Set() override func viewDidLoad() { super.viewDidLoad() Superwall.shared.$subscriptionStatus .sink { [weak self] status in self?.updateUI(for: status) } .store(in: &cancellables) } func updateUI(for status: SubscriptionStatus) { switch status { case .active: showPremiumContent() case .inactive: showFreeContent() case .unknown: showLoadingState() } } } ``` SwiftUI observation: ```swift struct ContentView: View { @StateObject var superwall = Superwall.shared var body: some View { VStack { switch superwall.subscriptionStatus { case .active(let entitlements): Text("Premium user with: \(entitlements.joined(separator: ", "))") case .inactive: Text("Free user") case .unknown: Text("Loading...") } } } } ``` --- # getPaywall() Source: https://superwall.com/docs/ios/sdk-reference/advanced/getPaywall A function that retrieves a PaywallViewController for custom presentation. You're responsible for ensuring the returned `PaywallViewController` is no longer presented or embedded in a view after the user has moved past the paywall. The app may crash if you or the SDK attempts to present the same `PaywallViewController` instance again from elsewhere. Be especially careful when mixing `register()` and `getPaywall()`, which is not recommended. The remotely configured presentation style is ignored when using this method. You must handle presentation styling programmatically. ## Purpose Retrieves a PaywallViewController that you can present however you want, bypassing Superwall's automatic presentation logic. ## Signature ```swift // Async/await version @MainActor public func getPaywall( forPlacement placement: String, params: [String: Any]? = nil, paywallOverrides: PaywallOverrides? = nil, delegate: PaywallViewControllerDelegate ) async throws -> PaywallViewController // Completion handler version public func getPaywall( forPlacement placement: String, params: [String: Any]? = nil, paywallOverrides: PaywallOverrides? = nil, delegate: PaywallViewControllerDelegate, completion: @escaping (PaywallViewController?, PaywallSkippedReason?, Error?) -> Void ) ``` ## Parameters | Name | Type | Description | Default | Required | | ---------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- | | placement | String | The name of the placement as defined on the Superwall dashboard. | | yes | | params | \[String: Any]? | Optional parameters to pass with your placement for audience filters. Keys beginning with \`$\` are reserved and will be dropped. | nil | no | | paywallOverrides | PaywallOverrides? | Optional overrides for products and presentation style. | nil | no | | delegate | PaywallViewControllerDelegate | A delegate to handle user interactions with the retrieved PaywallViewController. | | yes | | completion | @escaping (PaywallViewController?, PaywallSkippedReason?, Error?) -> Void | Completion block for the callback version. | | yes | ## Returns / State Returns a `PaywallViewController` that you can present. If presentation should be skipped, throws a `PaywallSkippedReason` error. ## Usage Using async/await: ```swift Task { do { let paywallViewController = try await Superwall.shared.getPaywall( forPlacement: "premium_feature", params: ["source": "settings"], delegate: self ) present(paywallViewController, animated: true) } catch let reason as PaywallSkippedReason { print("Paywall skipped: \(reason)") } catch { print("Error getting paywall: \(error)") } } ``` Using completion handler: ```swift Superwall.shared.getPaywall( forPlacement: "premium_feature", delegate: self ) { paywall, skippedReason, error in if let paywall = paywall { present(paywall, animated: true) } else if let reason = skippedReason { print("Paywall skipped: \(reason)") } else if let error = error { print("Error: \(error)") } } ``` --- # NonSubscriptionTransaction Source: https://superwall.com/docs/ios/sdk-reference/NonSubscriptionTransaction Represents a non-subscription transaction (consumables and non-consumables). Introduced in 4.10.0. The `store` property was added in 4.11.0. ## Purpose Provides details about one-time purchases in [`CustomerInfo`](/ios/sdk-reference/customerInfo), including which store fulfilled the purchase. ## Properties | Name | Type | Description | Required | | ------------- | ------------ | -------------------------------------------------------- | -------- | | transactionId | String | Unique identifier for the transaction. | yes | | productId | String | Product identifier for the purchase. | yes | | purchaseDate | Date | When the charge occurred. | yes | | isConsumable | Bool | \`true\` for consumables, \`false\` for non-consumables. | yes | | isRevoked | Bool | \`true\` if the transaction has been revoked. | yes | | store | ProductStore | Store that fulfilled the purchase (4.11.0+). | yes | ### Store values (4.11.0+) `appStore`, `stripe`, `paddle`, `playStore`, `superwall`, `other`. ## Usage Inspect non-subscription purchases: ```swift let customerInfo = Superwall.shared.customerInfo for purchase in customerInfo.nonSubscriptions { print("Product: \(purchase.productId)") print("Store: \(purchase.store)") print("Consumable: \(purchase.isConsumable)") print("Revoked: \(purchase.isRevoked)") } ``` ## Related * [`CustomerInfo`](/ios/sdk-reference/customerInfo) - Source of transaction data * [`SubscriptionTransaction`](/ios/sdk-reference/SubscriptionTransaction) - Subscription transactions * [`getCustomerInfo()`](/ios/sdk-reference/getCustomerInfo) - Fetch customer info asynchronously --- # userId Source: https://superwall.com/docs/ios/sdk-reference/userId A property on Superwall.shared that returns the current user's ID. The anonymous user ID is automatically generated and persisted to disk, so it remains consistent across app launches until the user is identified. ## Purpose Returns the current user's unique identifier, either from a previous call to [`identify()`](/ios/sdk-reference/identify) or an anonymous ID if not identified. ## Signature ```swift // Accessed via Superwall.shared public var userId: String { get } ``` ## Parameters This is a read-only computed property on the [`Superwall.shared`](/ios/sdk-reference/Superwall) instance with no parameters. ## Returns / State Returns a `String` representing the user's ID. If [`identify()`](/ios/sdk-reference/identify) has been called, returns that user ID. Otherwise, returns an automatically generated anonymous user ID that is cached to disk. ## Usage Get the current user ID: ```swift let currentUserId = Superwall.shared.userId print("User ID: \(currentUserId)") ``` Check if user is identified: ```swift if Superwall.shared.isLoggedIn { print("User is identified with ID: \(Superwall.shared.userId)") } else { print("User is anonymous with ID: \(Superwall.shared.userId)") } ``` Example usage in analytics: ```swift func trackAnalyticsEvent() { let userId = Superwall.shared.userId Analytics.track("feature_used", properties: [ "user_id": userId, "timestamp": Date() ]) } ``` Example usage in custom logging: ```swift func logError(_ error: Error) { Logger.log("Error for user \(Superwall.shared.userId): \(error)") } ``` --- # integrationAttributes Source: https://superwall.com/docs/ios/sdk-reference/integrationAttributes Gets the current integration attributes that have been set. This property was introduced in version 4.8.1. It provides read-only access to integration attributes. ## Purpose Returns a dictionary of all integration attributes that have been set using [`setIntegrationAttributes(_:)`](/ios/sdk-reference/setIntegrationAttributes). ## Signature ```swift public var integrationAttributes: [String: String] { get } ``` ## Parameters This is a computed property with no parameters. ## Returns / State Returns a dictionary mapping attribute keys (as strings) to their values. ## Usage Get current integration attributes: ```swift let attributes = Superwall.shared.integrationAttributes // Access specific attributes if let amplitudeUserId = attributes["amplitudeUserId"] { print("Amplitude User ID: \(amplitudeUserId)") } if let firebaseInstallationId = attributes["firebaseInstallationId"] { print("Firebase installation ID: \(firebaseInstallationId)") } // Iterate over all attributes for (key, value) in attributes { print("\(key): \(value)") } ``` Check if an attribute exists: ```swift let attributes = Superwall.shared.integrationAttributes if attributes["mixpanelDistinctId"] != nil { print("Mixpanel distinct ID is set") } ``` ## Related * [`setIntegrationAttributes(_:)`](/ios/sdk-reference/setIntegrationAttributes) - Set integration attributes --- # confirmAllAssignments Source: https://superwall.com/docs/ios/sdk-reference/confirmAllAssignments Confirms all experiment assignments and returns them in an array. The assignments may be different when a placement is registered due to changes in user, placement, or device parameters used in audience filters. ## Purpose Confirms all experiment assignments for the current user and returns them. This is useful for debugging experiment assignments or tracking which experiments a user is enrolled in. ## Signature ```swift public func confirmAllAssignments() async -> [Assignment] public func confirmAllAssignments(completion: (([Assignment]) -> Void)? = nil) ``` ## Parameters The completion handler version takes an optional completion closure that receives an array of `Assignment` objects. ## Returns / State Returns an array of `Assignment` objects representing all confirmed experiment assignments. If config hasn't been retrieved yet, this will return an empty array. ## Usage Using async/await: ```swift let assignments = await Superwall.shared.confirmAllAssignments() print("User has \(assignments.count) experiment assignments") for assignment in assignments { print("Experiment: \(assignment.experimentId), Variant: \(assignment.variant)") } ``` Using completion handler: ```swift Superwall.shared.confirmAllAssignments { assignments in print("Confirmed \(assignments.count) assignments") // Process assignments for assignment in assignments { // Handle assignment } } ``` ## Related * This method tracks a `confirmAllAssignments` event that can be received via [`SuperwallDelegate.handleSuperwallEvent(withInfo:)`](/ios/sdk-reference/SuperwallDelegate) --- # identify() Source: https://superwall.com/docs/ios/sdk-reference/identify A function that creates an account with Superwall by linking a userId to the automatically generated alias. Call this as soon as you have a user ID, typically after login or when the user's identity becomes available. ## Purpose Links a user ID to Superwall's automatically generated alias, creating an account for analytics and personalization. ## Signature ```swift public func identify( userId: String, options: IdentityOptions? = nil ) ``` ## Parameters | Name | Type | Description | Default | Required | | ------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- | | userId | String | Your user's unique identifier, as defined by your backend system. | | yes | | options | IdentityOptions? | Optional configuration for identity behavior. Set \`restorePaywallAssignments\` to \`true\` to wait for paywall assignments from the server. Use only in advanced cases where users frequently switch accounts. | nil | no | `appAccountToken` must be a UUID to be accepted by StoreKit. If the `userId` you pass to `identify` is not a valid UUID string, StoreKit will not accept it for `appAccountToken` and the SDK will fall back to the anonymous alias UUID. This can cause the identifier in App Store Server Notifications to differ from the `userId` you passed. See Apple's docs: [appAccountToken](https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken). ## Returns / State This function returns `Void`. After calling, [`isLoggedIn`](/ios/sdk-reference/userId) will return `true` and [`userId`](/ios/sdk-reference/userId) will return the provided user ID. ## Usage Basic identification: ```swift Superwall.shared.identify(userId: "user_12345") ``` With options for account switching scenarios: ```swift let options = IdentityOptions() options.restorePaywallAssignments = true Superwall.shared.identify( userId: "returning_user_67890", options: options ) ``` Call as soon as you have a user ID: ```swift func userDidLogin(user: User) { Superwall.shared.identify(userId: user.id) // Set additional user attributes Superwall.shared.setUserAttributes([ "email": user.email, "plan": user.subscriptionPlan, "signUpDate": user.createdAt ]) } ``` --- # SuperwallEvent Source: https://superwall.com/docs/ios/sdk-reference/SuperwallEvent An enum representing analytical events that are automatically tracked by Superwall. These events provide comprehensive analytics about user behavior and paywall performance. Use them to track conversion funnels, user engagement, and revenue metrics in your analytics platform. Common events to track for conversion analysis include `triggerFire`, `paywallOpen`, `transactionStart`, and `transactionComplete`. ## Purpose Represents internal analytics events tracked by Superwall and sent to the [`SuperwallDelegate`](/ios/sdk-reference/SuperwallDelegate) for forwarding to your analytics platform. ## Signature ```swift public enum SuperwallEvent { // User lifecycle events case firstSeen case appOpen case appLaunch case appClose case sessionStart case identityAlias case appInstall // Deep linking case deepLink(url: URL) // Paywall events case triggerFire(placementName: String, result: TriggerResult) case paywallOpen(paywallInfo: PaywallInfo) case paywallClose(paywallInfo: PaywallInfo) case paywallDecline(paywallInfo: PaywallInfo) case paywallWebviewProcessTerminated(paywallInfo: PaywallInfo) case paywallPreloadStart(paywallCount: Int) case paywallPreloadComplete(paywallCount: Int) // Transaction events case transactionStart(product: StoreProduct, paywallInfo: PaywallInfo) case transactionComplete(transaction: StoreTransaction?, product: StoreProduct, type: TransactionType, paywallInfo: PaywallInfo) case transactionFail(error: TransactionError, paywallInfo: PaywallInfo) case transactionAbandon(product: StoreProduct, paywallInfo: PaywallInfo) case transactionRestore(restoreType: RestoreType, paywallInfo: PaywallInfo) case transactionTimeout(paywallInfo: PaywallInfo) // Subscription events case subscriptionStart(product: StoreProduct, paywallInfo: PaywallInfo) case freeTrialStart(product: StoreProduct, paywallInfo: PaywallInfo) case subscriptionStatusDidChange // System events case deviceAttributes(attributes: [String: Any]) case reviewRequested(count: Int) // Permission events (Request permission action) case permissionRequested(permissionName: String, paywallIdentifier: String) case permissionGranted(permissionName: String, paywallIdentifier: String) case permissionDenied(permissionName: String, paywallIdentifier: String) // And more... } ``` ## Parameters Each event case contains associated values with relevant information for that event type. Common parameters include: * `paywallInfo: PaywallInfo` - Information about the paywall * `product: StoreProduct` - The product involved in transactions * `url: URL` - Deep link URLs * `attributes: [String: Any]` - Device or user attributes * `count: Int` - For `reviewRequested`, the number of times a review has been requested (available in version 4.8.1+) * `permissionName: String` / `paywallIdentifier: String` - The permission requested from the paywall and the identifier of the paywall that triggered it (new in 4.12.0). * `paywallCount: Int` - Total number of paywalls being preloaded when `paywallPreloadStart`/`paywallPreloadComplete` fire (new in 4.12.0). ## Returns / State This is an enum that represents different event types. Events are received via [`SuperwallDelegate.handleSuperwallEvent(withInfo:)`](/ios/sdk-reference/SuperwallDelegate). ## Usage These events are received via [`SuperwallDelegate.handleSuperwallEvent(withInfo:)`](/ios/sdk-reference/SuperwallDelegate) for forwarding to your analytics platform. ## Permission events (4.12.0+) The **Request permission** action for the paywall editor is rolling out and isn't visible in the dashboard yet. Editor support is coming very soon, so you may not see the action in your workspace today. When you wire the **Request permission** action in the paywall editor, the SDK emits `permission_requested`, `permission_granted`, and `permission_denied` events. Use them to track opt-in funnels or adapt your UI: ```swift func handleSuperwallEvent(withInfo eventInfo: SuperwallEventInfo) { switch eventInfo.event { case .permissionRequested(let permission, let paywallId): logger.info("Prompting \(permission) from paywall \(paywallId)") case .permissionGranted(let permission, _): analytics.track("permission_granted", properties: eventInfo.params) case .permissionDenied(let permission, _): showSettingsNudge(for: permission) default: break } } ``` See the [Request permissions from paywalls guide](/ios/guides/advanced/request-permissions-from-paywalls) for setup details and Info.plist requirements. ## Paywall preloading events (4.12.0+) `paywallPreload_start` and `paywallPreload_complete` fire whenever the SDK preloads cached paywalls in the background. Both events include `paywall_count` inside `eventInfo.params`, and the enum cases expose the same value via `paywallCount`. This makes it easy to time or monitor cache warm-up: ```swift switch eventInfo.event { case .paywallPreloadStart(let count): Metrics.shared.begin("paywall_preload", metadata: ["count": count]) case .paywallPreloadComplete(let count): Metrics.shared.end("paywall_preload", metadata: ["count": count]) default: break } ``` Pair these events with the [`shouldPreload`](/ios/sdk-reference/PaywallOptions#properties) option if you want to compare “on-demand” versus background caching strategies. --- # getCustomerInfo Source: https://superwall.com/docs/ios/sdk-reference/getCustomerInfo Gets the latest CustomerInfo asynchronously. This method was introduced in version 4.10.0. It provides an async way to get the latest customer information. ## Purpose Retrieves the latest `CustomerInfo` asynchronously. If customer info is already available, it returns immediately. Otherwise, it waits for the first non-placeholder customer info to be loaded. ## Signature ```swift public func getCustomerInfo() async -> CustomerInfo public func getCustomerInfo(completion: @escaping (CustomerInfo) -> Void) ``` ## Parameters No parameters for the async version. The completion handler version takes a completion closure. ## Returns / State Returns a `CustomerInfo` object containing the latest customer purchase and subscription data. ## Usage Using async/await: ```swift let customerInfo = await Superwall.shared.getCustomerInfo() // Use the customer info print("User has \(customerInfo.subscriptions.count) subscriptions") print("Active product IDs: \(customerInfo.activeSubscriptionProductIds)") ``` Using completion handler: ```swift Superwall.shared.getCustomerInfo { customerInfo in // Handle customer info print("User has \(customerInfo.subscriptions.count) subscriptions") // Update UI on main thread DispatchQueue.main.async { self.updatePurchaseHistory(customerInfo) } } ``` In a Task: ```swift Task { let customerInfo = await Superwall.shared.getCustomerInfo() // Process customer info await processCustomerInfo(customerInfo) } ``` ## Related * [`customerInfo`](/ios/sdk-reference/customerInfo) - Published property for customer info * [`customerInfoStream`](/ios/sdk-reference/Superwall#customerinfostream) - AsyncStream of customer info changes * [`customerInfoDidChange(from:to:)`](/ios/sdk-reference/SuperwallDelegate#customerinfodidchange) - Delegate method for customer info changes --- # getPresentationResult() Source: https://superwall.com/docs/ios/sdk-reference/getPresentationResult Check the outcome of a placement without presenting a paywall. ## Purpose Retrieves the presentation result for a placement without presenting the paywall. Call this when you need to know whether a placement would show a paywall, send the user to a holdout, or fail due to missing configuration before you decide how to render UI. ## Signature ```swift public func getPresentationResult( forPlacement placement: String, params: [String: Any]? = nil ) async -> PresentationResult ``` ```swift public func getPresentationResult( forPlacement placement: String, params: [String: Any]? = nil, completion: @escaping (PresentationResult) -> Void ) ``` ## Parameters | Name | Type | Description | Default | Required | | --------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- | | placement | String | The placement you want to evaluate. | | yes | | params | \[String: Any]? | Optional parameters passed to audience filters. Keys starting with \`$\` are reserved by Superwall and removed. Nested dictionaries and arrays are ignored. | nil | no | ## Returns / State Returns a `PresentationResult`, which can be: * `.placementNotFound` – The placement name is not attached to any campaign. * `.noAudienceMatch` – No audience matched, so nothing would be shown. * `.paywall(experiment: Experiment)` – A paywall would be shown; the experiment contains assignment info. * `.holdout(experiment: Experiment)` – The user would be held out of the paywall for the experiment. * `.paywallNotAvailable` – The SDK could not show a paywall (for example, no window to present from). ## Usage ```swift let result = await Superwall.shared.getPresentationResult( forPlacement: "premium_feature", params: ["source": "settings"] ) switch result { case .paywall(let experiment): analytics.log("Paywall would show", metadata: [ "experimentId": experiment.id, "groupId": experiment.groupId ]) case .holdout(let experiment): showHoldoutBadge(for: experiment) case .noAudienceMatch: unlockFeature() case .placementNotFound: assertionFailure("Missing campaign setup") case .paywallNotAvailable: fallbackToDefaultFlow() } ``` ```swift Superwall.shared.getPresentationResult(forPlacement: "premium_feature") { result in guard case .paywall(let experiment) = result else { return } // Update UI with experiment metadata while keeping the user on the current screen self.paywallExperimentId = experiment.id } ``` ## Related * [`register()`](/ios/sdk-reference/register) – Registers a placement that may present a paywall. * [`PaywallPresentationHandler`](/ios/sdk-reference/PaywallPresentationHandler) – Observe the lifecycle when you do present a paywall. --- # setIntegrationAttributes Source: https://superwall.com/docs/ios/sdk-reference/setIntegrationAttributes Sets integration attributes for third-party analytics and attribution providers. This method was introduced in version 4.8.1. It allows you to set attributes for third-party integrations like Amplitude, Mixpanel, and other analytics platforms. ## Purpose Sets integration attributes that are sent to Superwall's servers and can be used for analytics and attribution tracking with third-party providers. ## Signature ```swift public func setIntegrationAttributes(_ props: [IntegrationAttribute: String?]) ``` ## Parameters | Name | Type | Description | Required | | ----- | -------------------------------- | ---------------------------------------------------------------------------------------------------- | -------- | | props | \[IntegrationAttribute: String?] | A dictionary mapping integration attribute keys to their values. Use \`nil\` to remove an attribute. | yes | ## Returns / State This method returns `Void`. The attributes are stored and sent to Superwall's servers. ## Usage Set integration attributes: ```swift Superwall.shared.setIntegrationAttributes([ .amplitudeUserId: "user123", .mixpanelDistinctId: "distinct456", .firebaseInstallationId: "abc123", .custom("myCustomKey"): "customValue" ]) ``` Remove an attribute by setting it to `nil`: ```swift Superwall.shared.setIntegrationAttributes([ .amplitudeUserId: nil // Removes the amplitudeUserId attribute ]) ``` Access current integration attributes: ```swift let attributes = Superwall.shared.integrationAttributes print("Current attributes: \(attributes)") ``` ## IntegrationAttribute Types Common integration attributes include: * `.amplitudeUserId` - Amplitude user ID * `.mixpanelDistinctId` - Mixpanel distinct ID * `.firebaseInstallationId` - Firebase installation ID (4.10.8+) * `.custom(String)` - Custom attribute key ## Related * [`integrationAttributes`](/ios/sdk-reference/integrationAttributes) - Get current integration attributes * [`Superwall.shared.integrationAttributes`](/ios/sdk-reference/Superwall#integrationattributes) - Published property for integration attributes --- # configure() Source: https://superwall.com/docs/ios/sdk-reference/configure A static function that configures a shared instance of Superwall for use throughout your app. This is a static method called on the `Superwall` class itself, not on the shared instance. ## Purpose Configures the shared instance of Superwall with your API key and optional configurations, making it ready for use throughout your app. ## Signature ```swift public static func configure( apiKey: String, purchaseController: PurchaseController? = nil, options: SuperwallOptions? = nil, completion: (() -> Void)? = nil ) -> Superwall ``` ## Parameters | Name | Type | Description | Default | Required | | ------------------ | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------- | -------- | | apiKey | String | Your Public API Key from the Superwall dashboard settings. | | yes | | purchaseController | PurchaseController? | Optional object for handling all subscription-related logic yourself. If \`nil\`, Superwall handles subscription logic. | nil | no | | options | SuperwallOptions? (see /ios/sdk-reference/SuperwallOptions) | Optional configuration object for customizing paywall appearance and behavior. | nil | no | | completion | (() -> Void)? | Optional completion handler called when Superwall finishes configuring. | nil | no | ## Returns / State Returns the configured `Superwall` instance. The instance is also accessible via [`Superwall.shared`](/ios/sdk-reference/Superwall). ## Usage Basic configuration: ```swift Superwall.configure(apiKey: "pk_your_api_key") ``` With custom options: ```swift let options = SuperwallOptions() options.paywalls.shouldShowPurchaseFailureAlert = false Superwall.configure( apiKey: "pk_your_api_key", options: options ) { print("Superwall configured successfully") } ``` With custom purchase controller: ```swift Superwall.configure( apiKey: "pk_your_api_key", purchaseController: MyPurchaseController() ) ``` --- # getDeviceAttributes Source: https://superwall.com/docs/ios/sdk-reference/getDeviceAttributes Gets properties stored about the device that are used in audience filters. This method returns all device attributes that can be used in audience filters in the Superwall dashboard. `isApplePayAvailable` is deprecated starting in 4.11.2 and now always returns `true`. It will be removed in a future release and should no longer be used in audience filters. ## Purpose Retrieves a dictionary of device properties that are used when evaluating audience filters. This is useful for debugging audience filter behavior or understanding what device attributes are available. ## Signature ```swift public func getDeviceAttributes() async -> [String: Any] ``` ## Parameters No parameters. ## Returns / State Returns a dictionary mapping attribute names to their values. Common attributes include: * `isApplePayAvailable` - Deprecated in 4.11.2 and always `true` (previously indicated Apple Pay availability) * `swiftVersion` - The Swift version (available in version 4.7.0+) * `compilerVersion` - The compiler version (available in version 4.7.0+) * `localeIdentifier` - The device locale * `osVersion` - The iOS version * `model` - The device model * And many more device-specific attributes ## Usage Get device attributes: ```swift let deviceAttributes = await Superwall.shared.getDeviceAttributes() // Check specific attributes // `isApplePayAvailable` is deprecated and always true in 4.11.2+ // Print all attributes for debugging print("Device attributes: \(deviceAttributes)") ``` Use in audience filter debugging: ```swift Task { let attributes = await Superwall.shared.getDeviceAttributes() // Check if user matches an audience filter condition if let swiftVersion = attributes["swiftVersion"] as? String { print("Swift version: \(swiftVersion)") } } ``` ## Related * Device attributes are automatically used in audience filters * `isApplePayAvailable` was added in 4.9.0, updated in 4.10.5, and deprecated in 4.11.2 (now always `true`) * `swiftVersion` and `compilerVersion` were added in version 4.7.0 --- # refreshConfiguration Source: https://superwall.com/docs/ios/sdk-reference/refreshConfiguration Manually refreshes the Superwall configuration. Intended for development and wrapper SDKs. This method is intended for development workflows (for example, wrapper SDK hot reloading) and should not be called during normal app runtime. It triggers extra network requests and re-caches paywalls. ## Purpose Fetches the latest configuration from the Superwall dashboard and refreshes cached paywalls. Useful when iterating on paywalls during development without restarting the app. ## Signatures ```swift public func refreshConfiguration() async public func refreshConfiguration(completion: (() -> Void)? = nil) ``` ## Parameters * `completion` (optional): Called after the refresh finishes when using the completion-based API. ## Returns / State * Refreshes configuration and reloads any paywalls that have changed or been removed. * Does not return a value. * Development-only; avoid calling in production code paths. ## Usage Refresh configuration after editing paywalls in development: ```swift Task { await Superwall.shared.refreshConfiguration() // Updated paywalls will be used on the next presentation } ``` Objective-C or completion-based refresh: ```swift Superwall.shared.refreshConfiguration { // Handle post-refresh work here } ``` ## Related * [`configure()`](/ios/sdk-reference/configure) - Initial configuration of the SDK * [`PaywallOptions/shouldPreload`](/ios/sdk-reference/PaywallOptions#properties) - Controls whether paywalls are preloaded * [`preloadAllPaywalls()`](/ios/sdk-reference/Superwall#usage) - Preload paywalls manually --- # PaywallPresentationHandler Source: https://superwall.com/docs/ios/sdk-reference/PaywallPresentationHandler A handler class that provides status updates for paywall presentation in register() calls. Use this handler when you need fine-grained control over paywall events for a specific [`register()`](/ios/sdk-reference/register) call, rather than global events via [`SuperwallDelegate`](/ios/sdk-reference/SuperwallDelegate). This handler is specific to the individual `register()` call. For global paywall events across your app, use [`SuperwallDelegate`](/ios/sdk-reference/SuperwallDelegate) instead. ## Purpose Provides callbacks for paywall lifecycle events when using [`register()`](/ios/sdk-reference/register) with a specific handler instance. ## Signature ```swift @objcMembers public final class PaywallPresentationHandler: NSObject { public func onPresent(_ handler: @escaping (PaywallInfo) -> Void) public func onWillDismiss(_ handler: @escaping (PaywallInfo, PaywallResult) -> Void) public func onDismiss(_ handler: @escaping (PaywallInfo, PaywallResult) -> Void) public func onSkip(_ handler: @escaping (PaywallSkippedReason) -> Void) public func onError(_ handler: @escaping (Error) -> Void) } ``` ## Parameters | Name | Type | Description | Required | | ------------- | --------------------------------------------- | -------------------------------------------------------------------------------------- | -------- | | onPresent | handler: (PaywallInfo) -> Void | Sets a handler called when the paywall is presented. | yes | | onWillDismiss | handler: (PaywallInfo, PaywallResult) -> Void | Sets a handler called when the paywall will be dismissed. Available in version 4.9.0+. | yes | | onDismiss | handler: (PaywallInfo, PaywallResult) -> Void | Sets a handler called when the paywall is dismissed. | yes | | onSkip | handler: (PaywallSkippedReason) -> Void | Sets a handler called when paywall presentation is skipped. | yes | | onError | handler: (Error) -> Void | Sets a handler called when an error occurs during presentation. | yes | ## Returns / State Each method returns `Void` and configures the handler for the specific paywall lifecycle event. ## Usage Basic handler setup: ```swift func registerFeatureWithHandler() { let handler = PaywallPresentationHandler() handler.onPresent { paywallInfo in print("Paywall presented: \(paywallInfo.id)") // Pause background tasks, analytics, etc. } handler.onWillDismiss { paywallInfo, result in print("Paywall will dismiss: \(paywallInfo.id)") // Prepare for dismissal, save state, etc. } handler.onDismiss { paywallInfo, result in print("Paywall dismissed with result: \(result)") switch result { case .purchased: showSuccessMessage() case .cancelled: showPromotionalOffer() case .restored: updateUIForActiveSubscription() } } Superwall.shared.register( placement: "premium_feature", params: ["source": "feature_screen"], handler: handler ) { unlockPremiumFeature() } } ``` Handle skip and error cases: ```swift let handler = PaywallPresentationHandler() handler.onSkip { reason in print("Paywall skipped: \(reason)") switch reason { case .userIsSubscribed: proceedToFeature() case .holdout: proceedToFeature() default: break } } handler.onError { error in print("Paywall error: \(error)") showErrorAlert(error) } ``` Method chaining style: ```swift func registerWithChaining() { let handler = PaywallPresentationHandler() .onPresent { _ in pauseVideo() } .onDismiss { _, result in resumeVideo() handlePurchaseResult(result) } .onError { error in showAlert(error) } Superwall.shared.register( placement: "remove_ads", handler: handler ) { hideAdsFromUI() } } ``` --- # handleDeepLink() Source: https://superwall.com/docs/ios/sdk-reference/handleDeepLink A function that handles deep links and triggers paywalls based on configured campaigns. Configure deep link campaigns on the Superwall dashboard by adding the `deepLink` event to a campaign trigger. Deep link events are also tracked via [`SuperwallEvent.deepLink`](/ios/sdk-reference/SuperwallEvent) and sent to your [`SuperwallDelegate`](/ios/sdk-reference/SuperwallDelegate). ## Purpose Processes a deep link URL and triggers any associated paywall campaigns configured on the Superwall dashboard. Returns whether Superwall will handle the URL so you can fall back to your own routing. ## Signature ```swift @discardableResult public static func handleDeepLink(_ url: URL) -> Bool ``` ## Parameters | Name | Type | Description | Required | | ---- | ---- | -------------------------------------------------- | -------- | | url | URL | The deep link URL to process for paywall triggers. | yes | ## Returns / State Returns `true` when Superwall will handle the URL. If called before `Superwall.configure(...)` completes, it only returns `true` for known Superwall URL formats or when cached config contains a `deepLink_open` trigger. Use the return value to continue your own deep-link handling when it is `false`. ## Usage In your SceneDelegate or AppDelegate: ```swift func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { guard let url = URLContexts.first?.url else { return } // Handle the deep link with Superwall let handled = Superwall.handleDeepLink(url) // Continue with your app's deep link handling if Superwall won't if !handled { handleAppDeepLink(url) } } ``` iOS 13+ SceneDelegate: ```swift func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Handle deep link on app launch if let url = connectionOptions.urlContexts.first?.url { let handled = Superwall.handleDeepLink(url) if !handled { handleAppDeepLink(url) } } } ``` Legacy AppDelegate: ```swift func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { // Handle the deep link with Superwall let handled = Superwall.handleDeepLink(url) // Continue with your app's deep link handling if handled { return true } return handleAppDeepLink(url) } ``` --- # customerInfo Source: https://superwall.com/docs/ios/sdk-reference/customerInfo Contains the latest information about all of the customer's purchase and subscription data. `CustomerInfo` was introduced in version 4.10.0. It provides comprehensive information about the customer's purchase history, subscriptions, and entitlements. `CustomerInfo` is a published property, so you can subscribe to it using Combine or SwiftUI. You can also use the delegate method [`customerInfoDidChange(from:to:)`](/ios/sdk-reference/SuperwallDelegate#customerinfodidchange) or the `AsyncStream` [`customerInfoStream`](/ios/sdk-reference/Superwall#customerinfostream). ## Purpose Provides access to the customer's complete purchase and subscription history, including all transactions and entitlements. This is useful for displaying purchase history, understanding subscription status, and implementing features that depend on transaction data. ## Signature ```swift @Published public var customerInfo: CustomerInfo { get } ``` ## Properties | Name | Type | Description | Required | | ---------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------- | -------- | | subscriptions | \[SubscriptionTransaction] | All subscription transactions, ordered by purchase date (ascending). | yes | | nonSubscriptions | \[NonSubscriptionTransaction] | All non-subscription transactions (consumables and non-consumables), ordered by purchase date (ascending). | yes | | entitlements | \[Entitlement] | All entitlements available to the user. | yes | | activeSubscriptionProductIds | Set\ | Product identifiers for active subscriptions. | yes | | userId | String | The ID of the user. Equivalent to Superwall.userId. | yes | Starting in 4.11.0, transactions include offer metadata (`offerType`), the `subscriptionGroupId`, and the `store` (`ProductStore`) that fulfilled the purchase to help you audit cross-store sales. ## Returns / State Returns a `CustomerInfo` object containing the latest customer purchase and subscription data. This object is immutable and does not update automatically—you must access the property again to get the latest data. ## Usage Access customer info: ```swift let customerInfo = Superwall.shared.customerInfo // Get all subscriptions let subscriptions = customerInfo.subscriptions // Get all non-subscription purchases let nonSubscriptions = customerInfo.nonSubscriptions // Get all entitlements let entitlements = customerInfo.entitlements // Get active subscription product IDs let activeProductIds = customerInfo.activeSubscriptionProductIds ``` Observe changes with Combine: ```swift import Combine class ViewController: UIViewController { private var cancellables = Set() override func viewDidLoad() { super.viewDidLoad() Superwall.shared.$customerInfo .sink { [weak self] customerInfo in self?.updatePurchaseHistory(customerInfo) } .store(in: &cancellables) } func updatePurchaseHistory(_ customerInfo: CustomerInfo) { // Update UI with purchase history print("User has \(customerInfo.subscriptions.count) subscriptions") print("User has \(customerInfo.nonSubscriptions.count) non-subscription purchases") } } ``` Use with AsyncStream (iOS 15+): ```swift Task { for await customerInfo in Superwall.shared.customerInfoStream { // Handle customer info updates print("Customer info updated: \(customerInfo.subscriptions.count) subscriptions") } } ``` Get customer info asynchronously: ```swift // Using async/await let customerInfo = await Superwall.shared.getCustomerInfo() // Using completion handler Superwall.shared.getCustomerInfo { customerInfo in // Handle customer info } ``` Display purchase history: ```swift func displayPurchaseHistory() { let customerInfo = Superwall.shared.customerInfo // Display subscriptions for subscription in customerInfo.subscriptions { print("Product: \(subscription.productId)") print("Purchase Date: \(subscription.purchaseDate)") print("Active: \(subscription.isActive)") if let expirationDate = subscription.expirationDate { print("Expires: \(expirationDate)") } } // Display non-subscription purchases for purchase in customerInfo.nonSubscriptions { print("Product: \(purchase.productId)") print("Purchase Date: \(purchase.purchaseDate)") print("Consumable: \(purchase.isConsumable)") } } ``` Check active subscriptions: ```swift func hasActiveSubscription() -> Bool { let customerInfo = Superwall.shared.customerInfo return !customerInfo.activeSubscriptionProductIds.isEmpty } func getActiveSubscriptionProductIds() -> Set { return Superwall.shared.customerInfo.activeSubscriptionProductIds } ``` ## Related * [`getCustomerInfo()`](/ios/sdk-reference/getCustomerInfo) - Get customer info asynchronously * [`customerInfoStream`](/ios/sdk-reference/Superwall#customerinfostream) - AsyncStream of customer info changes * [`customerInfoDidChange(from:to:)`](/ios/sdk-reference/SuperwallDelegate#customerinfodidchange) - Delegate method for customer info changes * [`SubscriptionTransaction`](/ios/sdk-reference/SubscriptionTransaction) - Represents a subscription transaction * [`NonSubscriptionTransaction`](/ios/sdk-reference/NonSubscriptionTransaction) - Represents a non-subscription transaction * [`Entitlement`](/ios/sdk-reference/Entitlement) - Represents an entitlement --- # entitlements Source: https://superwall.com/docs/ios/sdk-reference/entitlements The entitlements tied to the device, accessible via Superwall.shared.entitlements. The `entitlements` property provides access to all entitlements, both active and inactive, as well as methods to query entitlements by product IDs. ## Purpose Provides access to the user's entitlements and methods to query them. Entitlements represent subscription tiers or features that a user has access to. ## Signature ```swift public var entitlements: EntitlementsInfo { get } ``` ## Properties and Methods | Name | Type | Description | Required | | ----------------- | ----------------------------------- | ---------------------------------------------------------------------------------- | -------- | | active | Set\ | The active entitlements. | yes | | inactive | Set\ | The inactive entitlements. | yes | | all | Set\ | All entitlements, regardless of whether they're active or not. | yes | | web | Set\ | Active entitlements redeemed via the web. | yes | | byProductId(\_:) | (String) -> Set\ | Returns entitlements for a given product ID. | yes | | byProductIds(\_:) | (Set\) -> Set\ | Returns entitlements for a given set of product IDs. Available in version 4.10.0+. | yes | ## Returns / State Returns an `EntitlementsInfo` object that provides access to entitlements and methods to query them. ## Usage Get all active entitlements: ```swift let activeEntitlements = Superwall.shared.entitlements.active for entitlement in activeEntitlements { print("Active entitlement: \(entitlement.id)") } ``` Get entitlements by product ID: ```swift let productId = "com.example.premium_monthly" let entitlements = Superwall.shared.entitlements.byProductId(productId) print("Product \(productId) unlocks \(entitlements.count) entitlements") ``` Get entitlements by multiple product IDs (4.10.0+): ```swift let productIds: Set = [ "com.example.premium_monthly", "com.example.premium_annual" ] let entitlements = Superwall.shared.entitlements.byProductIds(productIds) print("Products unlock \(entitlements.count) entitlements") ``` Check if user has specific entitlement: ```swift let hasPremium = Superwall.shared.entitlements.active.contains { $0.id == "premium" } if hasPremium { showPremiumFeatures() } ``` Get web entitlements: ```swift let webEntitlements = Superwall.shared.entitlements.web if !webEntitlements.isEmpty { print("User has \(webEntitlements.count) web entitlements") } ``` ## Related * [`Entitlement`](/ios/sdk-reference/Entitlement) - Represents an individual entitlement * [`subscriptionStatus`](/ios/sdk-reference/subscriptionStatus) - Subscription status which includes active entitlements * [`customerInfo`](/ios/sdk-reference/customerInfo) - Customer info which includes all entitlements --- # SuperwallDelegate Source: https://superwall.com/docs/ios/sdk-reference/SuperwallDelegate A protocol that handles Superwall lifecycle events and analytics. Set the delegate using `Superwall.shared.delegate = self` to receive these callbacks. Use `handleSuperwallEvent(withInfo:)` to track Superwall analytics events in your own analytics platform for a complete view of user behavior. ## Purpose Provides callbacks for Superwall lifecycle events, analytics tracking, and custom paywall interactions. ## Signature ```swift public protocol SuperwallDelegate: AnyObject { @MainActor func subscriptionStatusDidChange( from oldValue: SubscriptionStatus, to newValue: SubscriptionStatus ) @MainActor func handleSuperwallEvent(withInfo eventInfo: SuperwallEventInfo) @MainActor func handleCustomPaywallAction(withName name: String) @MainActor func willDismissPaywall(withInfo paywallInfo: PaywallInfo) @MainActor func willPresentPaywall(withInfo paywallInfo: PaywallInfo) @MainActor func didDismissPaywall(withInfo paywallInfo: PaywallInfo) @MainActor func didPresentPaywall(withInfo paywallInfo: PaywallInfo) @MainActor func paywallWillOpenURL(url: URL) @MainActor func paywallWillOpenDeepLink(url: URL) @MainActor func handleLog( level: LogLevel, scope: LogScope, message: String, info: [String: Any]?, error: Error? ) @MainActor func customerInfoDidChange( from oldValue: CustomerInfo, to newValue: CustomerInfo ) @MainActor func userAttributesDidChange(newAttributes: [String: Any]) } ``` ## Parameters All methods are optional to implement. Key methods include: | Name | Type | Description | Required | | --------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | -------- | | subscriptionStatusDidChange | oldValue: SubscriptionStatus, newValue: SubscriptionStatus | Called when subscription status changes. | yes | | handleSuperwallEvent | eventInfo: SuperwallEventInfo | Called for all internal analytics events. Use for tracking in your own analytics. | yes | | handleCustomPaywallAction | name: String | Called when user taps elements with \`data-pw-custom\` tags. | yes | | willPresentPaywall | paywallInfo: PaywallInfo | Called before paywall presentation. | yes | | didPresentPaywall | paywallInfo: PaywallInfo | Called after paywall presentation. | yes | | willDismissPaywall | paywallInfo: PaywallInfo | Called before paywall dismissal. | yes | | didDismissPaywall | paywallInfo: PaywallInfo | Called after paywall dismissal. | yes | | customerInfoDidChange | oldValue: CustomerInfo, newValue: CustomerInfo | Called when customer info changes. Available in version 4.10.0+. | yes | | userAttributesDidChange | newAttributes: \[String: Any] | Called when user attributes change outside your app (for example via the “Set user attributes” paywall action). | yes | ## Returns / State All delegate methods return `Void`. They provide information about Superwall events and state changes. ## Usage Basic delegate setup: ```swift class ViewController: UIViewController, SuperwallDelegate { override func viewDidLoad() { super.viewDidLoad() Superwall.shared.delegate = self } } ``` Track subscription status changes: ```swift func subscriptionStatusDidChange( from oldValue: SubscriptionStatus, to newValue: SubscriptionStatus ) { print("Subscription changed from \(oldValue) to \(newValue)") updateUI(for: newValue) } ``` Forward analytics events: ```swift func handleSuperwallEvent(withInfo eventInfo: SuperwallEventInfo) { switch eventInfo.event { case .paywallOpen(let info): Analytics.track("paywall_opened", properties: [ "paywall_id": info.id, "placement": info.placement ]) case .transactionComplete(let transaction, let product, _, let info): Analytics.track("subscription_purchased", properties: [ "product_id": product.id, "paywall_id": info.id ]) case .permissionGranted(let permission, let paywallId): Analytics.track("permission_granted", properties: [ "permission": permission, "paywall_id": paywallId ]) case .permissionDenied(let permission, let paywallId): Analytics.track("permission_denied", properties: [ "permission": permission, "paywall_id": paywallId ]) default: break } } ``` Handle custom paywall actions: ```swift func handleCustomPaywallAction(withName name: String) { switch name { case "help": presentHelpScreen() case "contact": presentContactForm() default: print("Unknown custom action: \(name)") } } ``` Handle paywall lifecycle: ```swift func willPresentPaywall(withInfo paywallInfo: PaywallInfo) { // Pause video, hide UI, etc. pauseBackgroundTasks() } func didDismissPaywall(withInfo paywallInfo: PaywallInfo) { // Resume video, show UI, etc. resumeBackgroundTasks() } ``` Handle customer info changes: ```swift func customerInfoDidChange( from oldValue: CustomerInfo, to newValue: CustomerInfo ) { // Check if user gained or lost subscriptions let oldSubscriptionCount = oldValue.subscriptions.count let newSubscriptionCount = newValue.subscriptions.count if newSubscriptionCount > oldSubscriptionCount { print("User gained a new subscription") } // Update purchase history UI updatePurchaseHistoryUI(with: newValue) } func userAttributesDidChange(newAttributes: [String: Any]) { // React to server-driven or paywall-triggered updates refreshProfileUI(with: newAttributes) } ``` --- # setUserAttributes() Source: https://superwall.com/docs/ios/sdk-reference/setUserAttributes A function that sets user attributes for use in paywalls and analytics on the Superwall dashboard. These attributes should not be used as a source of truth for sensitive information. Keys beginning with `$` are reserved for Superwall internal use and will be ignored. Arrays and dictionaries are not supported as values. ## Purpose Sets custom user attributes that can be used in paywall personalization, audience filters, and analytics on the Superwall dashboard. ## Signature ```swift public func setUserAttributes(_ attributes: [String: Any?]) ``` ## Parameters | Name | Type | Description | Required | | ---------- | --------------- | ---------------------------------------------------------------------------------------------------------------- | -------- | | attributes | \[String: Any?] | A dictionary of custom attributes to store for the user. Values can be any JSON encodable value, URLs, or Dates. | yes | ## Returns / State This function returns `Void`. If an attribute already exists, its value will be overwritten while other attributes remain unchanged. ## Usage Set multiple user attributes: ```swift let attributes: [String: Any] = [ "name": "John Doe", "email": "john@example.com", "plan": "premium", "signUpDate": Date(), "profilePicUrl": URL(string: "https://example.com/pic.jpg")!, "isVip": true, "loginCount": 42 ] Superwall.shared.setUserAttributes(attributes) ``` Set individual attributes over time: ```swift Superwall.shared.setUserAttributes(["lastActiveDate": Date()]) Superwall.shared.setUserAttributes(["featureUsageCount": 15]) ``` Remove an attribute by setting it to nil: ```swift Superwall.shared.setUserAttributes(["temporaryFlag": nil]) ``` Real-world example after user updates profile: ```swift func updateUserProfile(user: User) { Superwall.shared.setUserAttributes([ "name": user.displayName, "avatar": user.avatarURL, "preferences": user.notificationPreferences, "lastUpdated": Date() ]) } ``` Use these attributes in campaign audience filters on the Superwall dashboard to show targeted paywalls to specific user segments. --- # SuperwallOptions Source: https://superwall.com/docs/ios/sdk-reference/SuperwallOptions A configuration class for customizing paywall appearance and behavior. Only modify `networkEnvironment` if explicitly instructed by the Superwall team. Use `.release` (default) for production apps. Use different `SuperwallOptions` configurations for debug and release builds to optimize logging and behavior for each environment. The SDK automatically chooses StoreKit 2 on iOS 15+ and falls back to StoreKit 1 on older versions, but you can override this with `storeKitVersion`. ## Purpose Configures various aspects of Superwall behavior including paywall presentation, networking, logging, and StoreKit version preferences. ## Signature ```swift @objcMembers public final class SuperwallOptions: NSObject { public var paywalls: PaywallOptions public var storeKitVersion: StoreKitVersion public var networkEnvironment: NetworkEnvironment public var logging: LoggingOptions public var localeIdentifier: String? public var shouldBypassAppTransactionCheck: Bool } ``` ## Parameters | Name | Type | Description | Default | Required | | ------------------------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | -------- | | paywalls | PaywallOptions (see /ios/sdk-reference/PaywallOptions) | Configuration for paywall appearance and behavior. | | yes | | storeKitVersion | StoreKitVersion | Preferred StoreKit version (\`.storeKit1\` or \`.storeKit2\`). | StoreKit 2 on iOS 15+ | no | | networkEnvironment | NetworkEnvironment | Network environment (\`.release\`, \`.releaseCandidate\`, \`.developer\`, \`.custom(String)\`). \*\*Use only if instructed by Superwall team.\*\* | | yes | | logging | LoggingOptions | Logging configuration including level and scopes. | | yes | | localeIdentifier | String? | Override locale for paywall localization (e.g., "en\_GB"). | | no | | shouldBypassAppTransactionCheck | Bool | Disables the app transaction check on SDK launch. Useful in testing environments to avoid triggering the Apple ID sign-in prompt. Available in version 4.9.0+. | false | no | ## Returns / State This is a configuration object used when calling [`configure()`](/ios/sdk-reference/configure). ## Usage Basic options setup: ```swift let options = SuperwallOptions() // Configure paywall behavior options.paywalls.shouldShowPurchaseFailureAlert = false options.paywalls.shouldAutoShowPurchaseLoadingIndicator = true options.paywalls.automaticallyDismiss = true // Set StoreKit version preference options.storeKitVersion = .storeKit2 // Configure logging options.logging.level = .warn options.logging.scopes = [.superwallCore, .paywallViewController] // Set locale for testing options.localeIdentifier = "en_GB" // Bypass app transaction check (useful for testing) options.shouldBypassAppTransactionCheck = true // Use with configure Superwall.configure( apiKey: "pk_your_api_key", options: options ) ``` PaywallOptions configuration: ```swift let paywallOptions = PaywallOptions() // Presentation behavior paywallOptions.shouldShowPurchaseFailureAlert = false paywallOptions.shouldAutoShowPurchaseLoadingIndicator = true paywallOptions.automaticallyDismiss = true // Transaction behavior paywallOptions.transactionTimeout = 30.0 // seconds paywallOptions.restoreFailedPurchaseAlert.title = "Restore Failed" paywallOptions.restoreFailedPurchaseAlert.message = "Please try again" // Product overrides paywallOptions.overrideProductsByName = [ "primary": "produceID_to_replace_primary_product" ] // Assign to main options options.paywalls = paywallOptions ``` Logging configuration: ```swift let loggingOptions = LoggingOptions() loggingOptions.level = .debug loggingOptions.scopes = [.all] // or specific scopes like [.superwallCore, .network] options.logging = loggingOptions ``` Real-world example for production: ```swift func configureSuperwallForProduction() { let options = SuperwallOptions() // Minimal logging for production options.logging.level = .error // Customize paywall behavior options.paywalls.shouldShowPurchaseFailureAlert = true options.paywalls.automaticallyDismiss = true // Use StoreKit 2 for better performance on iOS 15+ options.storeKitVersion = .storeKit2 Superwall.configure( apiKey: "pk_your_production_api_key", options: options ) } ``` Debug configuration for development: ```swift func configureSuperwallForDebug() { let options = SuperwallOptions() // Verbose logging for debugging options.logging.level = .debug options.logging.scopes = [.all] // Show detailed error alerts options.paywalls.shouldShowPurchaseFailureAlert = true // Test with specific locale options.localeIdentifier = "es_ES" Superwall.configure( apiKey: "pk_your_test_api_key", options: options ) } ``` ## Related * [`PaywallOptions`](/ios/sdk-reference/PaywallOptions) ## Runtime Interface Style Configuration While `SuperwallOptions` provides initial configuration, you can dynamically change the interface style (light/dark mode) for paywalls at runtime using: ```swift // Force dark mode for all paywalls Superwall.shared.setInterfaceStyle(to: .dark) // Force light mode for all paywalls Superwall.shared.setInterfaceStyle(to: .light) // Revert to system default Superwall.shared.setInterfaceStyle(to: nil) ``` Use this method if you have a themeing system that is different than the system. The change takes effect immediately and persists until changed again or the app restarts. --- # Overview Source: https://superwall.com/docs/ios/sdk-reference Reference documentation for the Superwall iOS SDK. ## Welcome to the Superwall iOS SDK Reference You can find the source code for the SDK [on GitHub](https://github.com/superwall/Superwall-iOS) along with our [example apps](https://github.com/superwall/Superwall-iOS/tree/develop/Examples). ## Feedback We are always improving our SDKs and documentation! If you have feedback on any of our docs, please leave a rating and message at the bottom of the page. If you have any issues with the SDK, please [open an issue on GitHub](https://github.com/superwall/superwall-ios/issues). --- # Tracking Subscription State Source: https://superwall.com/docs/ios/quickstart/tracking-subscription-state Monitor user subscription status in your iOS app Superwall tracks the subscription state of a user for you. So, you don't need to add in extra logic for this. However, there are times in your app where you simply want to know if a user is on a paid plan or not. In your app's models, you might wish to set a flag representing whether or not a user is on a paid subscription: ```swift @Observable class UserData { var isPaidUser: Bool = false } ``` ### Using subscription status You can do this by observing the `subscriptionStatus` property on `Superwall.shared`. This property is an enum that represents the user's subscription status: ```swift switch Superwall.shared.subscriptionStatus { case .active(let entitlements): logger.info("User has active entitlements: \(entitlements)") userData.isPaidUser = true case .inactive: logger.info("User is free plan.") userData.isPaidUser = false case .unknown: logger.info("User is inactive.") userData.isPaidUser = false } ``` One natural way to tie the logic of your model together with Superwall's subscription status is by having your own model conform to the [Superwall Delegate](/using-superwall-delegate): ```swift @Observable class UserData { var isPaidUser: Bool = false } extension UserData: SuperwallDelegate { // MARK: Superwall Delegate func subscriptionStatusDidChange(from oldValue: SubscriptionStatus, to newValue: SubscriptionStatus) { switch newValue { case .active(_): // If you're using more than one entitlement, you can check which one is active here. // This example just assumes one is being used. logger.info("User is pro plan.") self.isPaidUser = true case .inactive: logger.info("User is free plan.") self.isPaidUser = false case .unknown: logger.info("User is free plan.") self.isPaidUser = false } } } ``` Another shorthand way to check? The `isActive` flag, which returns true if any entitlement is active: ```swift if Superwall.shared.subscriptionStatus.isActive { userData.isPaidUser = true } ``` :::ios ### Listening for entitlement changes in SwiftUI For Swift based apps, you can also create a flexible custom modifier which would fire if any changes to a subscription state occur. Here's how: ```swift import Foundation import SuperwallKit import SwiftUI // MARK: - Notification Handling extension NSNotification.Name { static let entitlementDidChange = NSNotification.Name("entitlementDidChange") } extension NotificationCenter { func entitlementChangedPublisher() -> NotificationCenter.Publisher { return self.publisher(for: .entitlementDidChange) } } // MARK: View Modifier private struct EntitlementChangedModifier: ViewModifier { // Or, change the `Bool` to `Set` if you want to know which entitlements are active. // This example assumes you're only using one. let handler: (Bool) -> () func body(content: Content) -> some View { content .onReceive(NotificationCenter.default.entitlementChangedPublisher(), perform: { _ in switch Superwall.shared.subscriptionStatus { case .active(_): handler(true) case .inactive: handler(false) case .unknown: handler(false) } }) } } // MARK: View Extensions extension View { func onEntitlementChanged(_ handler: @escaping (Bool) -> ()) -> some View { self.modifier(EntitlementChangedModifier(handler: handler)) } } // Then, in any view, this modifier will fire when the subscription status changes struct SomeView: View { @State private var isPro: Bool = false var body: some View { VStack { Text("User is pro: \(isPro ? "Yes" : "No")") } .onEntitlementChanged { isPro in self.isPro = isPro } } } ``` ::: ### Superwall checks subscription status for you Remember that the Superwall SDK uses its [audience filters](/campaigns-audience#matching-to-entitlements) for a similar purpose. You generally don't need to wrap your calls registering placements around `if` statements checking if a user is on a paid plan, like this: ```swift // Unnecessary if !Superwall.shared.subscriptionStatus.isActive { Superwall.shared.register(placement: "campaign_trigger") } ``` In your audience filters, you can specify whether or not the subscription state should be considered... ![](/images/entitlementCheck.png) ...which eliminates the needs for code like the above. This keeps you code base cleaner, and the responsibility of "Should this paywall show" within the Superwall campaign platform as it was designed. --- # Setting User Attributes Source: https://superwall.com/docs/ios/quickstart/setting-user-properties Customize paywalls and target users by setting user attributes By setting user attributes, you can display information about the user on the paywall. You can also define [audiences](/campaigns-audience) in a campaign to determine which paywall to show to a user, based on their user attributes. If a paywall uses the **Set user attributes** action, the merged attributes are sent back to your app via `SuperwallDelegate.userAttributesDidChange(newAttributes:)`. You do this by passing a `[String: Any?]` dictionary of attributes to `Superwall.shared.setUserAttributes(_:)`: :::ios ```swift Swift let attributes: [String: Any] = [ "name": user.name, "apnsToken": user.apnsTokenString, "email": user.email, "username": user.username, "profilePic": user.profilePicUrl, "stripe_customer_id": user.stripeCustomerId // Optional: For Stripe checkout prefilling ] Superwall.shared.setUserAttributes(attributes) // (merges existing attributes) ``` ```swift Objective-C NSDictionary *attributes = @{ @"name": user.name, @"apnsToken": user.apnsTokenString, @"email": user.email, @"username": user.username, @"profilePic": user.profilePicUrl, @"stripe_customer_id": user.stripeCustomerId // Optional: For Stripe checkout prefilling }; [[Superwall sharedInstance] setUserAttributes:attributes]; // (merges existing attributes) ``` ::: ## Usage This is a merge operation, such that if the existing user attributes dictionary already has a value for a given property, the old value is overwritten. Other existing properties will not be affected. To unset/delete a value, you can pass `nil` for the value. You can reference user attributes in [audience filters](/campaigns-audience) to help decide when to display your paywall. When you configure your paywall, you can also reference the user attributes in its text variables. For more information on how to that, see [Configuring a Paywall](/paywall-editor-overview). --- # Presenting Paywalls Source: https://superwall.com/docs/ios/quickstart/feature-gating Control access to premium features with Superwall placements This allows you to register a [placement](/campaigns-placements) to access a feature that may or may not be paywalled later in time. It also allows you to choose whether the user can access the feature even if they don't make a purchase. Here's an example. #### With Superwall :::ios ```swift Swift func pressedWorkoutButton() { // remotely decide if a paywall is shown and if // navigation.startWorkout() is a paid-only feature Superwall.shared.register(placement: "StartWorkout") { navigation.startWorkout() } } ``` ```swift Objective-C - (void)pressedWorkoutButton { // remotely decide if a paywall is shown and if // navigation.startWorkout() is a paid-only feature [[Superwall sharedInstance] registerWithPlacement:@"StartWorkout" params:nil handler:nil feature:^{ [navigation startWorkout]; }]; } ``` ::: #### Without Superwall :::ios ```swift Swift func pressedWorkoutButton() { if (user.hasActiveSubscription) { navigation.startWorkout() } else { navigation.presentPaywall() { result in if (result) { navigation.startWorkout() } else { // user didn't pay, developer decides what to do } } } } ``` ```swift Objective-C - (void)pressedWorkoutButton { if (user.hasActiveSubscription) { [navigation startWorkout]; } else { [navigation presentPaywallWithCompletion:^(BOOL result) { if (result) { [navigation startWorkout]; } else { // user didn't pay, developer decides what to do } }]; } } ``` ::: ### How registering placements presents paywalls You can configure `"StartWorkout"` to present a paywall by [creating a campaign, adding the placement, and adding a paywall to an audience](/campaigns) in the dashboard. 1. The SDK retrieves your campaign settings from the dashboard on app launch. 2. When a placement is called that belongs to a campaign, audiences are evaluated ***on device*** and the user enters an experiment — this means there's no delay between registering a placement and presenting a paywall. 3. If it's the first time a user is entering an experiment, a paywall is decided for the user based on the percentages you set in the dashboard 4. Once a user is assigned a paywall for an audience, they will continue to see that paywall until you remove the paywall from the audience or reset assignments to the paywall. 5. After the paywall is closed, the Superwall SDK looks at the *Feature Gating* value associated with your paywall, configurable from the paywall editor under General > Feature Gating (more on this below) 1. If the paywall is set to ***Non Gated***, the `feature:` closure on `register(placement: ...)` gets called when the paywall is dismissed (whether they paid or not) 2. If the paywall is set to ***Gated***, the `feature:` closure on `register(placement: ...)` gets called only if the user is already paying or if they begin paying. 6. If no paywall is configured, the feature gets executed immediately without any additional network calls. Given the low cost nature of how register works, we strongly recommend registering **all core functionality** in order to remotely configure which features you want to gate – **without an app update**. :::ios ```swift Swift // on the welcome screen func pressedSignUp() { Superwall.shared.register(placement: "SignUp") { navigation.beginOnboarding() } } // in another view controller func pressedWorkoutButton() { Superwall.shared.register(placement: "StartWorkout") { navigation.startWorkout() } } ``` ```swift Objective-C // on the welcome screen - (void)pressedSignUp { [[Superwall sharedInstance] registerWithPlacement:@"SignUp" params:nil handler:nil feature:^{ [navigation beginOnboarding]; }]; } // In another view controller - (void)pressedWorkoutButton { [[Superwall sharedInstance] registerWithPlacement:@"StartWorkout" params:nil handler:nil feature:^{ [navigation startWorkout]; }]; } ``` ::: ### Automatically Registered Placements The SDK [automatically registers](/tracking-analytics) some internal placements which can be used to present paywalls: ### Register. Everything. To provide your team with ultimate flexibility, we recommend registering *all* of your analytics events, even if you don't pass feature blocks through. This way you can retroactively add a paywall almost anywhere – **without an app update**! If you're already set up with an analytics provider, you'll typically have an `Analytics.swift` singleton (or similar) to disperse all your events from. Here's how that file might look: :::ios ```swift Swift import SuperwallKit import Mixpanel import Firebase final class Analytics { static var shared = Analytics() func track( event: String, properties: [String: Any] ) { // Superwall Superwall.shared.register(placement: event, params: properties) // Firebase (just an example) Firebase.Analytics.logEvent(event, parameters: properties) // Mixpanel (just an example) Mixpanel.mainInstance().track(event: event, properties: properties) } } // And thus ... Analytics.shared.track( event: "workout_complete", properties: ["total_workouts": 17] ) // ... can now be turned into a paywall moment :) ``` ::: ### Getting a presentation result Use `getPresentationResult(forPlacement:params:)` when you need to ask the SDK what would happen when registering a placement — without actually showing a paywall. Superwall evaluates the placement and its audience filters then returns a `PresentationResult`. You can use this to adapt your app's behavior based on the outcome (such as showing a lock icon next to a pro feature if they aren't subscribed). In short, this lets you peek at the outcome first and decide how your app should respond: :::ios ```swift Task { let res = await Superwall.shared.getPresentationResult(forPlacement: "caffeineLogged") switch res { case .placementNotFound: // The placement name isn’t on any campaign in the dashboard. print("Superwall: Placement \"caffeineLogged\" not found ‑ double‑check spelling and dashboard setup.") case .noAudienceMatch: // The placement exists, but the user didn’t fall into any audience filters. print("Superwall: No matching audience for this user — paywall skipped.") case .paywall(let experiment): // User qualifies and will see the paywall for this experiment. print("Superwall: Showing paywall (experiment \(experiment.id)).") case .holdout(let experiment): // User is in the control/holdout group, so no paywall is shown. print("Superwall: User assigned to holdout group for experiment \(experiment.id) — paywall withheld.") case .paywallNotAvailable: // A paywall *would* have been shown, but some error likely occurred (e.g., no VC to present from, networking, etc). print("Superwall: Paywall not available — likely no internet, no presenting view controller, or another paywall is already visible.") } } ``` ::: --- # Configure the SDK Source: https://superwall.com/docs/ios/quickstart/configure undefined As soon as your app launches, you need to configure the SDK with your **Public API Key**. You'll retrieve this from the Superwall settings page. ### Sign Up & Grab Keys If you haven't already, [sign up for a free account](https://superwall.com/sign-up) on Superwall. Then, when you're through to the Dashboard, click **Settings** from the panel on the left, click **Keys** and copy your **Public API Key**: ![](/images/810eaba-small-Screenshot_2023-04-25_at_11.51.13.png) ### Initialize Superwall in your app Begin by editing your main Application entrypoint. Depending on the platform this could be `AppDelegate.swift` or `SceneDelegate.swift` for iOS, `MainApplication.kt` for Android, `main.dart` in Flutter, or `App.tsx` for React Native: :::ios ```swift Swift-UIKit // AppDelegate.swift import UIKit import SuperwallKit @main final class AppDelegate: UIResponder, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { Superwall.configure(apiKey: "MY_API_KEY") // Replace this with your API Key return true } } ``` ```swift SwiftUI // App.swift import SwiftUI import SuperwallKit @main struct MyApp: App { init() { let apiKey = "MY_API_KEY" // Replace this with your API Key Superwall.configure(apiKey: apiKey) } // etc... } ``` ```swift Objective-C // AppDelegate.m @import SuperwallKit; - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Initialize the Superwall service. [Superwall configureWithApiKey:@"MY_API_KEY"]; return YES; } ``` ::: This configures a shared instance of `Superwall`, the primary class for interacting with the SDK's API. Make sure to replace `MY_API_KEY` with your public API key that you just retrieved. By default, Superwall handles basic subscription-related logic for you. However, if you’d like greater control over this process (e.g. if you’re using RevenueCat), you’ll want to pass in a `PurchaseController` to your configuration call and manually set the `subscriptionStatus`. You can also pass in `SuperwallOptions` to customize the appearance and behavior of the SDK. See [Purchases and Subscription Status](/advanced-configuration) for more. You've now configured Superwall! :::ios For further help, check out our [iOS example apps](https://github.com/superwall/Superwall-iOS/tree/master/Examples) for working examples of implementing the Superwall SDK. ::: --- # User Management Source: https://superwall.com/docs/ios/quickstart/user-management Identifying users and managing their identity in your iOS app ### Anonymous Users Superwall automatically generates a random user ID that persists internally until the user deletes/reinstalls your app. You can call `Superwall.shared.reset()` to reset this ID and clear any paywall assignments. ### Identified Users If you use your own user management system, call `identify(userId:options:)` when you have a user's identity. This will alias your `userId` with the anonymous Superwall ID enabling us to load the user’s assigned paywalls. Calling `Superwall.shared.reset()` will reset the on-device userId to a random ID and clear the paywall assignments. :::ios ```swift Swift // After retrieving a user's ID, e.g. from logging in or creating an account Superwall.shared.identify(userId: user.id) // When the user signs out Superwall.shared.reset() ``` ```swift Objective-C // After retrieving a user's ID, e.g. from logging in or creating an account [[Superwall sharedInstance] identifyWithUserId:user.id]; // When the user signs out [[Superwall sharedInstance] resetWithCompletionHandler:completion]; ``` :::
**Advanced Use Case** You can supply an `IdentityOptions` object, whose property `restorePaywallAssignments` you can set to `true`. This tells the SDK to wait to restore paywall assignments from the server before presenting any paywalls. This should only be used in advanced use cases. If you expect users of your app to switch accounts or delete/reinstall a lot, you'd set this when users log in to an existing account. ### Best Practices for a Unique User ID * Do NOT make your User IDs guessable – they are public facing. * Do NOT set emails as User IDs – this isn't GDPR compliant. * Do NOT set IDFA or DeviceIds as User IDs – these are device specific / easily rotated by the operating system. * Do NOT hardcode strings as User IDs – this will cause every user to be treated as the same user by Superwall. ### Identifying users from App Store server events On iOS, Superwall always supplies an [`appAccountToken`](https://developer.apple.com/documentation/storekit/product/purchaseoption/3749440-appaccounttoken) with every StoreKit 2 transaction: | Scenario | Value used for `appAccountToken` | | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | You’ve called `Superwall.shared.identify(userId:)` | The exact `userId` you passed | | You *haven’t* called `identify` yet | The UUID automatically generated for the anonymous user (the **alias ID**), **without** the `$SuperwallAlias:` prefix | | You passed a non‑UUID `userId` to `identify` | StoreKit rejects it; Superwall falls back to the alias UUID | Because the SDK falls back to the alias UUID, purchase notifications sent to your server always include a stable, unique identifier—even before the user signs in. :::ios `appAccountToken` must be a UUID to be accepted by StoreKit. If the `userId` you pass to `identify` is not a valid UUID string, StoreKit will not accept it for `appAccountToken` and the SDK will fall back to the anonymous alias UUID. This can cause the identifier in App Store Server Notifications to differ from the `userId` you passed. See Apple's docs: [appAccountToken](https://developer.apple.com/documentation/appstoreserverapi/appaccounttoken). ::: ```swift // Generate and use a UUID user ID in Swift let userId = UUID().uuidString Superwall.shared.identify(userId: userId) ``` --- # Install the SDK Source: https://superwall.com/docs/ios/quickstart/install undefined Visual learner? Go watch our install video over on YouTube [here](https://youtu.be/geTHOGyL_60). ## Overview To see the latest release, [check out the repository](https://github.com/superwall/Superwall-iOS). You can install via [Swift Package Manager](#install-via-swift-package-manager) or [CocoaPods](#install-via-cocoapods). ## Install via Swift Package Manager [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the Swift compiler. In **Xcode**, select **File ▸ Add Packages...**: ![1174](/images/dd4b1e2-Screenshot_2022-03-21_at_11.26.05.png "Screenshot 2022-03-21 at 11.26.05.png"){" "} **Then, paste the GitHub repository URL:** ``` https://github.com/superwall/Superwall-iOS ``` in the search bar. With the **Superwall-iOS** source selected, set the **Dependency Rule** to **Up to Next Major Version** with the lower bound set to **4.0.0**. Make sure your project name is selected in **Add to Project**. Then click **Add Package**: ![](/images/somLatest.png) After the package has loaded, make sure **Add to Target** is set to your app's name and click **Add Package**: ![](/images/5ab25c4-Screenshot_2023-01-31_at_16.56.22.png)
**And you're done!** Now you're ready to configure the SDK 👇 ## Install via CocoaPods First, add the following to your Podfile: `pod 'SuperwallKit', '< 5.0.0' ` Next, run `pod repo update` to update your local spec repo. [Why?](https://stackoverflow.com/questions/43701352/what-exactly-does-pod-repo-update-do). Finally, run `pod install` from your terminal. Note that in your target's **Build Settings -> User Script Sandboxing**, this value should be set to **No**. ### Updating to a New Release To update to a new beta release, you'll need to update the version specified in the Podfile and then run `pod install` again. ### Import SuperwallKit You should now be able to `import SuperwallKit`: ```swift Swift import SuperwallKit ``` ```swift Objective-C @import SuperwallKit; ``` **And you're done!** Now you're ready to configure the SDK 👇 --- # Handling Deep Links Source: https://superwall.com/docs/ios/quickstart/in-app-paywall-previews undefined 1. Previewing paywalls on your device before going live. 2. Deep linking to specific [campaigns](/campaigns). 3. Web Checkout [Post-Checkout Redirecting](/web-checkout-post-checkout-redirecting) ## Setup :::ios There are two ways to deep link into your app: URL Schemes and Universal Links. ::: ### Adding a Custom URL Scheme :::ios Open **Xcode**. In your **info.plist**, add a row called **URL Types**. Expand the automatically created **Item 0**, and inside the **URL identifier** value field, type your **Bundle ID**, e.g., **com.superwall.Superwall-SwiftUI**. Add another row to **Item 0** called **URL Schemes** and set its **Item 0** to a URL scheme you'd like to use for your app, e.g., **exampleapp**. Your structure should look like this: ![](/images/1.png) With this example, the app will open in response to a deep link with the format **exampleapp\://**. You can [view Apple's documentation](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app) to learn more about custom URL schemes. ::: :::ios ### Adding a Universal Link Only required for [Web Checkout](/web-checkout), otherwise you can skip this step. Before configuring in your app, first [create](/web-checkout-creating-an-app) and [configure](/web-checkout-configuring-stripe-keys-and-settings) your Stripe app on the Superwall Dashboard. #### Add a new capability in Xcode Select your target in Xcode, then select the **Signing & Capabilities** tab. Click on the **+ Capability** button and select **Associated Domains**. This will add a new capability to your app. ![](/images/web-checkout-ul-add.png) #### Set the domain Next, enter in the domain using the format `applinks:[your-web-checkout-url]`. This is the domain that Superwall will use to handle universal links. Your `your-web-checkout-url` value should match what's under the "Web Paywall Domain" section. ![](/images/web-checkout-ul-domain.png) #### Testing If your Stripe app's iOS Configuration is incomplete or incorrect, universal links **will not work** You can verify that your universal links are working a few different ways. Keep in mind that it usually takes a few minutes for the associated domain file to propagate: 1. **Use Branch's online validator:** If you visit [branch.io's online validator](https://branch.io/resources/aasa-validator//) and enter in your web checkout URL, it'll run a similar check and provide the same output. 2. **Test opening a universal link:** If the validation passes from either of the two steps above, make sure visiting a universal link opens your app. Your link should be formatted as `https://[your web checkout link]/app-link/` — which is simply your web checkout link with `/app-link/` at the end. This is easiest to test on device, since you have to tap an actual link instead of visiting one directly in Safari or another browser. In the iOS simulator, adding the link in the Reminders app works too: ![](/images/web-checkout-test-link.jpg) ::: ### Handling Deep Links :::ios Depending on whether your app uses a SceneDelegate, AppDelegate, or is written in SwiftUI, there are different ways to tell Superwall that a deep link has been opened. Be sure to click the tab that corresponds to your architecture: ```swift AppDelegate.swift import SuperwallKit class AppDelegate: UIResponder, UIApplicationDelegate { // NOTE: if your app uses a SceneDelegate, this will NOT work! func application( _ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:] ) -> Bool { return Superwall.handleDeepLink(url) } func application( _ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void ) -> Bool { if userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL { return Superwall.handleDeepLink(url) } return false } } ``` ```swift SceneDelegate.swift import SuperwallKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { // for cold launches func scene( _ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions ) { if let url = connectionOptions.urlContexts.first?.url { Superwall.handleDeepLink(url) } else if let userActivity = connectionOptions.userActivities.first(where: { $0.activityType == NSUserActivityTypeBrowsingWeb }), let url = userActivity.webpageURL { Superwall.handleDeepLink(url) } } // for when your app is already running func scene( _ scene: UIScene, openURLContexts URLContexts: Set ) { if let url = URLContexts.first?.url { Superwall.handleDeepLink(url) } } func scene( _ scene: UIScene, continue userActivity: NSUserActivity ) { if userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL { Superwall.handleDeepLink(url) } } } ``` ```swift SwiftUI import SuperwallKit @main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() .onOpenURL { url in Superwall.handleDeepLink(url) } } } } ``` ```swift Objective-C // In your SceneDelegate.m #import "SceneDelegate.h" @import SuperwallKit; @interface SceneDelegate () @end @implementation SceneDelegate - (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions { [self handleURLContexts:connectionOptions.URLContexts]; [self handleUserActivity:connectionOptions.userActivities.allObjects.firstObject]; } - (void)scene:(UIScene *)scene openURLContexts:(NSSet *)URLContexts { [self handleURLContexts:URLContexts]; } - (void)scene:(UIScene *)scene continueUserActivity:(NSUserActivity *)userActivity { [self handleUserActivity:userActivity]; } #pragma mark - Deep linking - (void)handleURLContexts:(NSSet *)URLContexts { [URLContexts enumerateObjectsUsingBlock:^(UIOpenURLContext * _Nonnull context, BOOL * _Nonnull stop) { [[Superwall sharedInstance] handleDeepLink:context.URL]; }]; } - (void)handleUserActivity:(NSUserActivity *)userActivity { if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb] && userActivity.webpageURL) { [[Superwall sharedInstance] handleDeepLink:userActivity.webpageURL]; } } @end ``` ::: ## Previewing Paywalls Next, build and run your app on your phone. Then, head to the Superwall Dashboard. Click on **Settings** from the Dashboard panel on the left, then select **General**: ![](/images/c252198-image.png) With the **General** tab selected, type your custom URL scheme, without slashes, into the **Apple Custom URL Scheme** field: ![](/images/6b3f37e-image.png) Next, open your paywall from the dashboard and click **Preview**. You'll see a QR code appear in a pop-up: ![](/images/2.png)
![](/images/3.png) On your device, scan this QR code. You can do this via Apple's Camera app. This will take you to a paywall viewer within your app, where you can preview all your paywalls in different configurations. ## Using Deep Links to Present Paywalls Deep links can also be used as a placement in a campaign to present paywalls. Simply add `deepLink_open` as an placement, and the URL parameters of the deep link can be used as parameters! You can also use custom placements for this purpose. [Read this doc](/presenting-paywalls-from-one-another) for examples of both. --- # Post-Checkout Redirecting Source: https://superwall.com/docs/ios/guides/web-checkout/post-checkout-redirecting Learn how to handle users redirecting back to your app after a web purchase. After a user completes a web purchase, Superwall needs to redirect them back to your app. You can configure this behavior in two ways: ## Post-Purchase Behavior Modes You can configure how users are redirected after checkout in your [Application Settings](/web-checkout-configuring-stripe-keys-and-settings#post-purchase-behavior): ### Redeem Mode (Default) Superwall manages the entire redemption experience: * Users are automatically deep linked to your app with a redemption code * Fallback to App Store/Play Store if the app isn't installed * Redemption emails are sent automatically * The SDK handles redemption via delegate methods (detailed below) This is the recommended mode for most apps. ### Redirect Mode Redirect users to your own custom URL with purchase information: * **When to use**: You want to show a custom success page, perform additional actions before redemption, or have your own deep linking infrastructure * **What you receive**: Purchase data is passed as query parameters to your URL **Query Parameters Included**: * `app_user_id` - The user's identifier from your app * `email` - User's email address * `stripe_subscription_id` - The Stripe subscription ID * Any custom placement parameters you set **Example**: ``` https://yourapp.com/success? app_user_id=user_123& email=user@example.com& stripe_subscription_id=sub_1234567890& campaign_id=summer_sale ``` You'll need to implement your own logic to handle the redirect and deep link users into your app. *** ## Setting Up Deep Links Whether you're showing a checkout page in Safari or using the In-App Browser, the Superwall SDK relies on deep links to redirect back to your app. #### Prerequisites 1. [Configuring Stripe Keys and Settings](/web-checkout-configuring-stripe-keys-and-settings) 2. [Deep Links](/in-app-paywall-previews) If you're not using Superwall to handle purchases, then you'll need to follow extra steps to redeem the web purchase in your app. * [Using RevenueCat](/web-checkout-using-revenuecat) * [Using a PurchaseController](/web-checkout-linking-membership-to-iOS-app#using-a-purchasecontroller) *** ## Handling Redemption (Redeem Mode) When using Redeem mode (the default), handle the user experience when they're redirected back to your app using `SuperwallDelegate` methods: ### willRedeemLink When your app opens via the deep link, we will call the delegate method `willRedeemLink()` before making a network call to redeem the code. At this point, you might wish to display a loading indicator in your app so the user knows that the purchase is being redeemed. ```swift func willRedeemLink() { ToastView.show(message: "Activating...", showActivityIndicator: true) } ``` To present your own loading UI on top of the paywall, you can access the view controller of the paywall via `Superwall.shared.presentedViewController`. You can manually dismiss the paywall here, but note that the completion block of the original `register` call won't be triggered. The paywall will be dismissed automatically when the `didRedeemLink` method is called. ### didRedeemLink After receiving a response from the network, we will call `didRedeemLink(result:)` with the result of redeeming the code. This is an enum that has the following cases: * `success(code: String, redemptionInfo: RedemptionInfo)`: The redemption succeeded and `redemptionInfo` contains information about the redeemed code. * `error(code: String, error: ErrorInfo)`: An error occurred while redeeming. You can check the error message via the `error` parameter. * `expiredCode(code: String, expired: ExpiredCodeInfo)`: The code expired and `ExpiredCodeInfo` contains information about whether a redemption email has been resent and an optional obfuscated email address that the redemption email was sent to. * `invalidCode(code: String)`: The code that was redeemed was invalid. * `expiredSubscription(code: String, redemptionInfo: RedemptionInfo)`: The subscription that the code redeemed has expired. On network failure, the SDK will retry up to 6 times before returning an `error` `RedemptionResult` in `didRedeemLink(result:)`. Here, you should remove any loading UI you added in `willRedeemLink` and show a message to the user based on the result. If a paywall is presented, it will be dismissed automatically. ```swift func didRedeemLink(result: RedemptionResult) { switch result { case .expiredCode(let code, let expiredInfo): ToastView.show(message: "Expired Link", systemImageName: "exclamationmark.square.fill") print("[!] code expired", code, expiredInfo) break case .error(let code, let error): ToastView.show(message: error.message, systemImageName: "exclamationmark.square.fill") print("[!] error", code, error) break case .expiredSubscription(let code, let redemptionInfo): ToastView.show(message: "Expired Subscription", systemImageName: "exclamationmark.square.fill") print("[!] expired subscription", code, redemptionInfo) break case .invalidCode(let code): ToastView.show(message: "Invalid Link", systemImageName: "exclamationmark.square.fill") print("[!] invalid code", code) break case .success(_, let redemptionInfo): if let email = redemptionInfo.purchaserInfo.email { Superwall.shared.setUserAttributes(["email": email]) ToastView.show(message: email, systemImageName: "person.circle.fill") } else { ToastView.show(message: "Welcome!", systemImageName: "person.circle.fill") } break } } ``` --- # Redeeming In-App Source: https://superwall.com/docs/ios/guides/web-checkout/linking-membership-to-iOS-app Handle a deep link in your app and use the delegate methods. After purchasing from a web paywall, the user will be redirected to your app by a deep link to redeem their purchase on device. Please follow our [Post-Checkout Redirecting](/web-checkout-post-checkout-redirecting) guide to handle this user experience. If you're using Superwall to handle purchases, then you don't need to do anything here. If you're using your own `PurchaseController`, you will need to update the subscription status with the redeemed web entitlements. If you're using RevenueCat, you should follow our [Using RevenueCat](/web-checkout-using-revenuecat) guide. ### Using a PurchaseController If you're using StoreKit in your PurchaseController, you'll need to merge the web entitlements with the device entitlements before setting the subscription status. Here's an example of how you might do this: ```swift func syncSubscriptionStatus() async { var products: Set = [] // Get the device entitlements for await verificationResult in Transaction.currentEntitlements { switch verificationResult { case .verified(let transaction): products.insert(transaction.productID) case .unverified: break } } let storeProducts = await Superwall.shared.products(for: products) let deviceEntitlements = Set(storeProducts.flatMap { $0.entitlements }) // Get the web entitlements from Superwall let webEntitlements = Superwall.shared.entitlements.web // Merge the two sets of entitlements let allEntitlements = deviceEntitlements.union(webEntitlements) await MainActor.run { Superwall.shared.subscriptionStatus = .active(allEntitlements) } } ``` In addition to syncing the subscription status when purchasing and restoring, you'll need to sync it whenever `didRedeemLink(result:)` is called: ```swift final class Delegate: SuperwallDelegate { func didRedeemLink(result: RedemptionResult) { Task { await syncSubscriptionStatus() } } } ``` ### Refreshing of web entitlements If you aren't using a Purchase Controller, the SDK will refresh the web entitlements every 24 hours. ### Redeeming while a paywall is open If a redeem event occurs when a paywall is open, the SDK will track that as a restore event and the paywall will close. --- # Using RevenueCat Source: https://superwall.com/docs/ios/guides/web-checkout/using-revenuecat Handle a deep link in your app and use the delegate methods to link web checkouts with RevenueCat. After purchasing from a web paywall, the user will be redirected to your app by a deep link to redeem their purchase on device. Please follow our [Post-Checkout Redirecting](/web-checkout-post-checkout-redirecting) guide to handle this user experience. If you're using Superwall to handle purchases, then you don't need to do anything here. You only need to use a `PurchaseController` if you want end-to-end control of the purchasing pipeline. The recommended way to use RevenueCat with Superwall is by putting it in observer mode. If you're using your own `PurchaseController`, you should follow our [Redeeming In-App](/web-checkout-linking-membership-to-iOS-app) guide. ### Using a PurchaseController with RevenueCat If you're using RevenueCat, you'll need to follow [steps 1 to 4 in their guide](https://www.revenuecat.com/docs/web/integrations/stripe) to set up Stripe with RevenueCat. Then, you'll need to associate the RevenueCat customer with the Stripe subscription IDs returned from redeeming the code. You can do this by extracting the ids from the `RedemptionResult` and sending them to RevenueCat's API by using the `didRedeemLink(result:)` delegate method: ```swift import Foundation import RevenueCat final class Delegate: SuperwallDelegate { // The user tapped on a deep link to redeem a code func willRedeemLink() { print("[!] willRedeemLink") // Optionally show a loading indicator here } // Superwall received a redemption result and validated the purchase with Stripe. func didRedeemLink(result: RedemptionResult) { print("[!] didRedeemLink", result) // Send Stripe IDs to RevenueCat to link purchases to the customer // Get a list of subscription ids tied to the customer. guard let stripeSubscriptionIds = result.stripeSubscriptionIds else { return } guard let url = URL(string: "https://api.revenuecat.com/v1/receipts") else { return } let revenueCatStripePublicAPIKey = "strp....." // replace with your RevenueCat Stripe Public API Key let appUserId = Purchases.shared.appUserID // In the background... Task.detached { await withTaskGroup(of: Void.self) { group in // For each subscription id, link it to the user in RevenueCat for stripeSubscriptionId in stripeSubscriptionIds { group.addTask { var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("stripe", forHTTPHeaderField: "X-Platform") request.setValue("Bearer \(revenueCatStripePublicAPIKey)", forHTTPHeaderField: "Authorization") do { request.httpBody = try JSONEncoder().encode([ "app_user_id": appUserId, "fetch_token": stripeSubscriptionId ]) let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { print("[!] Error: Received an invalid response for \(stripeSubscriptionId)") return } guard (200..<300).contains(httpResponse.statusCode) else { let body = String(data: data, encoding: .utf8) ?? "" print("[!] Error: RevenueCat responded with \(httpResponse.statusCode) for \(stripeSubscriptionId). Body: \(body)") return } let json = try JSONSerialization.jsonObject(with: data, options: []) print("[!] Success: linked \(stripeSubscriptionId) to user \(appUserId)", json) } catch { // Surface network errors so you can retry or notify the user. print("[!] Error: unable to link \(stripeSubscriptionId) to user \(appUserId)", error) } } } } /// After all network calls complete, invalidate the cache without switching to the main thread. Purchases.shared.getCustomerInfo(fetchPolicy: .fetchCurrent) { customerInfo, error in /// If you're using `Purchases.shared.customerInfoStream`, or keeping Superwall Entitlements in sync /// via RevenueCat's `PurchasesDelegate` methods, you don't need to do anything here. Those methods will be /// called automatically when this call fetches the most up to customer info, ignoring any local caches. /// Otherwise, if you're manually calling `Purchases.shared.getCustomerInfo` to keep Superwall's entitlements /// in sync, you should use the newly updated customer info here to do so. } /// You could always access web entitlements here as well /// `let webEntitlements = Superwall.shared.entitlements.web` // After all network calls complete... await MainActor.run { // Perform UI updates on the main thread, like letting the user know their subscription was redeemed } } } } ``` The snippet logs HTTP failures and propagates network errors so you can build retries, show UI, or report the issue. Be sure to adapt the error handling to match your monitoring and UX needs. If you call `logIn` from RevenueCat's SDK, then you need to call the logic you've implemented inside `didRedeemLink(result:)` again. For example, that means if `logIn` was invoked from RevenueCat, you'd either abstract out this logic above into a function to call again, or simply call this function directly. The web entitlements will be returned along with other existing entitlements in the `CustomerInfo` object accessible via RevenueCat's SDK. If you’re logging in and out of RevenueCat, make sure to resend the Stripe subscription IDs to RevenueCat’s endpoint after logging in. --- # Web Checkout Source: https://superwall.com/docs/ios/guides/web-checkout Integrate Superwall web checkout with your iOS app for seamless cross-platform subscriptions ## Dashboard Setup 1. [Set up Web Checkout in the dashboard](/web-checkout/web-checkout-overview) 2. [Add web products to your paywall](/web-checkout/web-checkout-direct-stripe-checkout) ## SDK Setup 1. [Set up deep links](/sdk/quickstart/in-app-paywall-previews) 2. [Handle Post-Checkout redirecting](/sdk/guides/web-checkout/post-checkout-redirecting) 3. **Only if you're using RevenueCat:** [Using RevenueCat](/sdk/guides/web-checkout/using-revenuecat) 4. **Only if you're using your own PurchaseController:** [Redeeming In-App](/sdk/guides/web-checkout/linking-membership-to-iOS-app) ## Testing 1. [Testing Purchases](/web-checkout/web-checkout-testing-purchases) 2. [Managing Memberships](/web-checkout/web-checkout-managing-memberships) ## Troubleshooting If a user has issues accessing their subscription in your app after paying via web checkout, direct them to your plan management page to retrieve their subscription link or manage billing: For example: `http://yourapp.superwall.app/manage` ## FAQ [Web Checkout FAQ](/web-checkout/web-checkout-faq) --- # Migrating from v2 to v3 - iOS Source: https://superwall.com/docs/ios/guides/migrations/migrating-to-v3 SuperwallKit 3.0 is a major release of Superwall's iOS SDK, previously known as `Paywall`. This introduces breaking changes. Note that the minimum deployment target has changed for v3 from iOS 11 to iOS 13 This is so that we can use newer APIs internally and externally. ## Migration steps ### 1. Update Swift Package Manager dependency (if needed) Our GitHub URL has changed. Although you can keep using the old one, its best if you replace it with the newer one. If you're using Swift Package Manager to handle dependencies: * Select your project from the **Project Navigator**, select your project under **Project** and click **Package Dependencies**. * Remove the old dependency for `paywall-ios`. * Click **+** and search for our new url [https://github.com/superwall/Superwall-iOS](https://github.com/superwall/Superwall-iOS) in the search bar. * Set the **Dependency Rule** to **Up to Next Major Version** with the lower bound set to **3.0.0**. * Make sure your project name is selected in **Add to Project**. * Then, **Add Package**. Sometimes Xcode keeps the old framework reference around by accident, so select your target in Xcode, then go to Build Phases, and ensure that your target’s Link Binary with Libraries section references SuperwallKit, and remove the reference to Paywall if it was still there. ![](/images/02c9746-Screenshot_2022-11-10_at_18.21.27.png) If you have any Xcode issues during building you might need to clean the build folder by going to **Product** > **Clean Build Folder** and then restart Xcode. ### 1.1 Update CocoaPods dependency (if needed) If instead you're using CocoaPods to manage dependencies, in your Podfile update the reference to the Pod from `Paywall` to `SuperwallKit` then run `pod install`: | Before | After | | ------------------------- | ------------------------------ | | pod 'Paywall', '\< 3.0.0' | pod 'SuperwallKit', '\< 4.0.0' | ### 1.2 Update Framework References Since our framework is now called `SuperwallKit`, you'll now need to explicitly import `SuperwallKit` instead of `Paywall` throughout your code: #### Swift | Before | After | | -------------- | ------------------- | | import Paywall | import SuperwallKit | #### Objective-C | Before | After | | ---------------- | --------------------- | | @import Paywall; | @import SuperwallKit; | ## 2. Update code references In some cases, you should be able to update references using the automatic renaming suggestions that Xcode provides. For other cases where this hasn't been possible, you'll need to run through this list to manually update your code. ### 2.1 Update references to `Paywall.foo` to `Superwall.shared.foo` You'll see errors saying `Cannot find 'Paywall' in scope`. This is because the main class for interacting with our API is now called `Superwall`. All variables and functions (apart from configure) are now instance functions. This means you'll need to use the shared instance `Superwall.shared`. ### 2.2 Triggering is now registering Previously you'd use `Paywall.track(...)` to implicitly trigger a paywall, and `Paywall.trigger(...)` to explicitly trigger a paywall. This was confusing as they essentially did the same thing. `Paywall.track` provided completion blocks for what happened on the paywall when really you needed to know what to do next. We wanted to make this simpler so at the heart of this release is `Superwall.shared.register(event:params:handler:feature:)`. This allows you to register an event to access a feature that may or may not be paywalled later in time. It also allows you to choose whether the user can access the feature even if they don't make a purchase. You can read our docs on [how register works](/docs/feature-gating) to learn more. Given the low cost nature of how register works, we strongly recommend wrapping all core functionality in a register `feature` block in order to remotely configure which features you want to gate – without an app update. For SwiftUI apps, we have removed the `.triggerPaywall` view modifier in favor of this register function. ### **2.3 Rename `PaywallDelegate` to `SuperwallDelegate`** The following method has changed: | Before | After | | ----------------------------------------------------------------------- | --------------------------------------------------------------------- | | func trackAnalyticsEvent(withName name: String, params: \[String: Any]) | func handleSuperwallEventInfo(withInfo eventInfo: SuperwallEventInfo) | This has a `SuperwallEventInfo` parameter. This has a `params` dictionary and an `event` enum whose cases contain associated values. Note that the methods for handling subscription-related logic no longer exist inside `SuperwallDelegate`, as discussed in the next section. ### 2.4 Handling subscription-related logic SuperwallKit now handles all subscription-related logic by default making integration super easy. We track the user's subscription status for you and expose the published property `Superwall.shared.subscriptionStatus`. This means that if you were previously using StoreKit you can simply delete that code and let SuperwallKit handle it. However, if you're using RevenueCat or still want to keep control over subscription-related logic, you'll need to conform to the `PurchaseController` protocol. This is a protocol that handles purchasing and restoring, much like the `PaywallDelegate` did in v2.x of the SDK. You set the purchase controller when you configure the SDK. You can read more about that in our [Purchases and Subscription Status](/docs/advanced-configuration) guide. The following methods were previously in the `PaywallDelegate` but are now in the `PurchaseController` and have changed slightly: #### Purchasing | Before | After | | --------------------------------- | --------------------------------------------------------- | | func purchase(product: SKProduct) | func purchase(product: SKProduct) async -> PurchaseResult | Here, you purchase the product but then return the result of the purchase as a `PurchaseResult` enum case. Make sure you handle all cases of `PurchaseResult`. #### Restoring | Before | After | | ----------------------------------------------------------- | -------------------------------------------------- | | func restorePurchases(completion: @escaping (Bool) -> Void) | func restorePurchases() async -> RestorationResult | This has changed to an async function that returns the result of restoring a purchase. If you need help converting between completion blocks and async, [check out this article](https://wwdcbysundell.com/2021/wrapping-completion-handlers-into-async-apis/). #### Subscription Status | Before | After | | ------------------------------- | ------------------------------------------------------------ | | func isUserSubscribed() -> Bool | Superwall.shared.subscriptionStatus = .active (or .inactive) | `isUserSubscribed()` has been removed in favor of a `subscriptionStatus` variable which you **must** set every time the user's subscription status changes. On first app install this starts off as `.unknown` until you determine the user's subscription status and set it to `.active` when they have an active subscription, or `.inactive` when they don't. Paywalls will not show until the user's subscription status is set. You can [check out our docs](/docs/advanced-configuration) for detailed info about implementing the `PurchaseController`. ### 2.5 Rename `PaywallOptions` to `SuperwallOptions` This now clearly defines which of the options are explicit to paywalls vs other configuration options within the SDK. ### 2.6 Configuring and Identity management When configuring the API, you now no longer provide a userId or delegate. | Before | After | | --------------------------------------------- | ----------------------------------------------------------- | | configure(apiKey\:userId\:delegate\:options:) | configure(apiKey\:purchaseController\:options\:completion:) | To use the optional delegate, set `Superwall.shared.delegate`. To identify a user, use `Superwall.shared.identify(userId:options:)`. You can [read more](/docs/identity-management) about identity management in our docs. ## 3. Check out the full change log You can view this on [our GitHub page](https://github.com/superwall/Superwall-iOS/blob/master/CHANGELOG.md). ## 4. Check out our updated example apps All of our example apps have been updated to use the latest SDK. We have created a dedicated app that shows you how to integrate Superwall with RevenueCat. In addition, we have added an Objective-C app. ## 5. Read our docs and view the updated iOS SDK documentation Visit the links in the sidebar or [click here to go to the iOS SDK docs](https://sdk.superwall.me/documentation/superwallkit/). --- # Migrating from v3 to v4 - iOS Source: https://superwall.com/docs/ios/guides/migrations/migrating-to-v4 SuperwallKit 4.0 is a major release of Superwall's iOS SDK. This introduces breaking changes. ## Migration steps ## 1. Update code references ### 1.1 Rename references from `event` to `placement` In some cases, you should be able to update references using the automatic renaming suggestions that Xcode provides. For other cases where this hasn't been possible, you'll need to run through this list to manually update your code. | Before | After | | ------------------------------------- | ----------------------------------------- | | func register(event:) | func register(placement:) | | func preloadPaywalls(forEvents:) | func preloadPaywalls(forPlacements:) | | func getPaywall(forEvent:) | func getPaywall(forPlacement:) | | func getPresentationResult(forEvent:) | func getPresentationResult(forPlacement:) | | TriggerResult.eventNotFound | TriggerResult.placementNotFound | ### 1.2 Update PurchaseController method The following has been changed in the `PurchaseController`: | Before | After | | --------------------------------------------------------- | ------------------------------------------------------------ | | func purchase(product: SKProduct) async -> PurchaseResult | func purchase(product: StoreProduct) async -> PurchaseResult | This provides a `StoreProduct` object, which contains information about the product to be purchased. ## 2. StoreKit 2 The SDK defaults to using StoreKit 2 for users who are on iOS 15+. However, you can choose to stay on StoreKit 1 by setting the `SuperwallOption` `storeKitVersion` to `.storeKit1`. There are a few caveats to this however. In the following scenarios, the SDK will choose StoreKit 1 automatically: 1. If you're using Objective-C and using a `PurchaseController`. 2. If you're using Objective-C and observing purchases by setting the `SuperwallOption` `shouldObservePurchases` to `true`. 3. If you have set the key `SKIncludeConsumableInAppPurchaseHistory` to `true` in your info.plist, the SDK will use StoreKit 1 for everyone who isn't on iOS 18+. If you're using Objective-C and using `purchase(_:)` you must manually set the `SuperwallOption` `storeKitVersion` to `.storeKit1`. If you're using a `PurchaseController`, you access the StoreKit 2 product to purchase using `product.sk2Product` and the StoreKit 1 product `product.sk1Product` if you're using StoreKit 1. You should take the above scenarios into account when choosing which product to purchase. ### 3. Getting the purchased product The `onDismiss` block of the `PaywallPresentationHandler` now accepts both a `PaywallInfo` object and a `PaywallResult` object. This allows you to easily access the purchased product from the result when the paywall dismisses. ### 4. Entitlements The `subscriptionStatus` has been changed to accept a set of `Entitlement` objects. This allows you to give access to entitlements based on products purchased. For example, in your app you might have Bronze, Silver, and Gold subscription tiers, i.e. entitlements, which entitle a user to access a certain set of features within your app. Every subscription product must be associated with one or more entitlements, which is controlled via the dashboard. Superwall will already have associated all your products with a default entitlement. If you don't use more than one entitlement tier within your app and you only use subscription products, you don't need to do anything extra. However, if you use one-time purchases or multiple entitlements, you should review your products and their entitlements. In general, consumables should not be associated with an entitlement, whereas non-consumables should be. Check your products [here](https://superwall.com/applications/\:app/products/v2). If you're using a `PurchaseController`, you'll need to set the `entitlements.status` instead of the `subscriptionStatus`: | Before | After | | --------------------------------------------- | ---------------------------------------------------------------- | | Superwall.shared.subscriptionStatus = .active | Superwall.shared.subscriptionStatus = .active(Set(entitlements)) | You can get the `StoreProducts` and their associated entitlements from Superwall by calling the method `products(for:)`. Here is an example of how you'd sync your subscription status with Superwall using these methods: ```swift Swift func syncSubscriptionStatus() async { var products: Set = [] for await verificationResult in Transaction.currentEntitlements { switch verificationResult { case .verified(let transaction): products.insert(transaction.productID) case .unverified: break } } let storeProducts = await Superwall.shared.products(for: products) let entitlements = Set(storeProducts.flatMap { $0.entitlements }) await MainActor.run { Superwall.shared.subscriptionStatus = .active(entitlements) } } ``` ```swift RevenueCat func syncSubscriptionStatus() { assert(Purchases.isConfigured, "You must configure RevenueCat before calling this method.") Task { for await customerInfo in Purchases.shared.customerInfoStream { // Gets called whenever new CustomerInfo is available let superwallEntitlements = customerInfo.entitlements.activeInCurrentEnvironment.keys.map { Entitlement(id: $0) } await MainActor.run { [superwallEntitlements] in if superwallEntitlements.isEmpty { Superwall.shared.subscriptionStatus = .inactive } else { Superwall.shared.subscriptionStatus = .active(Set(superwallEntitlements)) } } } } } ``` You can listen to the published property `Superwall.shared.subscriptionStatus` to be notified when the subscriptionStatus changes. Or you can use the `SuperwallDelegate` method `subscriptionStatusDidChange(from:to:)`, which replaces `subscriptionStatusDidChange(to:)`. ### 5. Paywall Presentation Condition In the Paywall Editor you can choose whether to always present a paywall or ask the SDK to check the user subscription before presenting a paywall. For users on v4 of the SDK, this is replaced with a check on the entitlements within the audience filter. As you migrate your users from v3 to v4 of the SDK, you'll need to make sure you set both the entitlements check and the paywall presentation condition in the paywall editor. ![](/images/camp-presentation-conditions.png) ## 6. Check out the full change log You can view this on [our GitHub page](https://github.com/superwall/Superwall-iOS/blob/master/CHANGELOG.md). ## 7. Check out our updated example apps All of our example apps have been updated to use the latest SDK. We now only have two apps: Basic and Advanced. Basic shows you the basic integration of Superwall without needing a purchase controller or multiple entitlements. Advanced shows you how to use entitlements within your app as well as optionally using a purchase controller with StoreKit or RevenueCat. ## 8. Read our docs and view the updated iOS SDK documentation Visit the links in the sidebar or [click here to go to the iOS SDK docs](https://sdk.superwall.me/documentation/superwallkit/). --- # Using the Presentation Handler Source: https://superwall.com/docs/ios/guides/advanced/using-the-presentation-handler undefined You can provide a `PaywallPresentationHandler` to `register`, whose functions provide status updates for a paywall: * `onDismiss`: Called when the paywall is dismissed. Accepts a `PaywallInfo` object containing info about the dismissed paywall, and there is a `PaywallResult` informing you of any transaction. * `onPresent`: Called when the paywall did present. Accepts a `PaywallInfo` object containing info about the presented paywall. * `onError`: Called when an error occurred when trying to present a paywall. Accepts an `Error` indicating why the paywall could not present. * `onSkip`: Called when a paywall is skipped. Accepts a `PaywallSkippedReason` enum indicating why the paywall was skipped. ```swift Swift let handler = PaywallPresentationHandler() handler.onDismiss { paywallInfo, result in print("The paywall dismissed. PaywallInfo: \(paywallInfo). Result: \(result)") } handler.onPresent { paywallInfo in print("The paywall presented. PaywallInfo:", paywallInfo) } handler.onError { error in print("The paywall presentation failed with error \(error)") } handler.onSkip { reason in switch reason { case .holdout(let experiment): print("Paywall not shown because user is in a holdout group in Experiment: \(experiment.id)") case .noAudienceMatch: print("Paywall not shown because user doesn't match any audiences.") case .placementNotFound: print("Paywall not shown because this placement isn't part of a campaign.") } } Superwall.shared.register(placement: "campaign_trigger", handler: handler) { // Feature launched } ``` ```swift Objective-C SWKPaywallPresentationHandler *handler = [[SWKPaywallPresentationHandler alloc] init]; [handler onDismiss:^(SWKPaywallInfo * _Nonnull paywallInfo, enum SWKPaywallResult result, SWKStoreProduct * _Nullable product) { NSLog(@"The paywall presented. PaywallInfo: %@ - result: %ld", paywallInfo, (long)result); }]; [handler onPresent:^(SWKPaywallInfo * _Nonnull paywallInfo) { NSLog(@"The paywall presented. PaywallInfo: %@", paywallInfo); }]; [handler onError:^(NSError * _Nonnull error) { NSLog(@"The paywall presentation failed with error %@", error); }]; [handler onSkip:^(enum SWKPaywallSkippedReason reason) { switch (reason) { case SWKPaywallSkippedReasonUserIsSubscribed: NSLog(@"Paywall not shown because user is subscribed."); break; case SWKPaywallSkippedReasonHoldout: NSLog(@"Paywall not shown because user is in a holdout group."); break; case SWKPaywallSkippedReasonNoAudienceMatch: NSLog(@"Paywall not shown because user doesn't match any audiences."); break; case SWKPaywallSkippedReasonPlacementNotFound: NSLog(@"Paywall not shown because this placement isn't part of a campaign."); break; case SWKPaywallSkippedReasonNone: // The paywall wasn't skipped. break; } }]; [[Superwall sharedInstance] registerWithPlacement:@"campaign_trigger" params:nil handler:handler feature:^{ // Feature launched. }]; ``` ```kotlin Kotlin val handler = PaywallPresentationHandler() handler.onDismiss { paywallInfo, result -> println("The paywall dismissed. PaywallInfo: ${it}") } handler.onPresent { println("The paywall presented. PaywallInfo: ${it}") } handler.onError { println("The paywall errored. Error: ${it}") } handler.onSkip { when (it) { is PaywallSkippedReason.PlacementNotFound -> { println("The paywall was skipped because the placement was not found.") } is PaywallSkippedReason.Holdout -> { println("The paywall was skipped because the user is in a holdout group.") } is PaywallSkippedReason.NoAudienceMatch -> { println("The paywall was skipped because no audience matched.") } } } Superwall.instance.register(placement = "campaign_trigger", handler = handler) { // Feature launched } ``` ```dart Flutter PaywallPresentationHandler handler = PaywallPresentationHandler(); handler.onPresent((paywallInfo) async { String name = await paywallInfo.name; print("Handler (onPresent): $name"); }); handler.onDismiss((paywallInfo, paywallResult) async { String name = await paywallInfo.name; print("Handler (onDismiss): $name"); }); handler.onError((error) { print("Handler (onError): ${error}"); }); handler.onSkip((skipReason) async { String description = await skipReason.description; if (skipReason is PaywallSkippedReasonHoldout) { print("Handler (onSkip): $description"); final experiment = await skipReason.experiment; final experimentId = await experiment.id; print("Holdout with experiment: ${experimentId}"); } else if (skipReason is PaywallSkippedReasonNoAudienceMatch) { print("Handler (onSkip): $description"); } else if (skipReason is PaywallSkippedReasonPlacementNotFound) { print("Handler (onSkip): $description"); } else { print("Handler (onSkip): Unknown skip reason"); } }); Superwall.shared.registerPlacement("campaign_trigger", handler: handler, feature: () { // Feature launched }); ``` ```typescript React Native const handler = new PaywallPresentationHandler() handler.onPresent((paywallInfo) => { const name = paywallInfo.name console.log(`Handler (onPresent): ${name}`) }) handler.onDismiss((paywallInfo, paywallResult) => { const name = paywallInfo.name console.log(`Handler (onDismiss): ${name}`) }) handler.onError((error) => { console.log(`Handler (onError): ${error}`) }) handler.onSkip((skipReason) => { const description = skipReason.description if (skipReason instanceof PaywallSkippedReasonHoldout) { console.log(`Handler (onSkip): ${description}`) const experiment = skipReason.experiment const experimentId = experiment.id console.log(`Holdout with experiment: ${experimentId}`) } else if (skipReason instanceof PaywallSkippedReasonNoAudienceMatch) { console.log(`Handler (onSkip): ${description}`) } else if (skipReason instanceof PaywallSkippedReasonPlacementNotFound) { console.log(`Handler (onSkip): ${description}`) } else { console.log(`Handler (onSkip): Unknown skip reason`) } }) Superwall.shared.register({ placement: 'campaign_trigger', handler: handler, feature: () => { // Feature launched } }); ``` Wanting to see which product was just purchased from a paywall? Use `onDismiss` and the `result` parameter. Or, you can use the [SuperwallDelegate](/3rd-party-analytics#using-events-to-see-purchased-products). --- # Observer Mode Source: https://superwall.com/docs/ios/guides/advanced/observer-mode undefined If you wish to make purchases outside of Superwall's SDK and paywalls, you can use **observer mode** to report purchases that will appear in the Superwall dashboard, such as transactions: ![](/images/om_transactions.png) This is useful if you are using Superwall solely for revenue tracking, and you're making purchases using frameworks like StoreKit or Google Play Billing Library directly. Observer mode will also properly link user identifiers to transactions. To enable observer mode, set it using `SuperwallOptions` when configuring the SDK: :::ios ```swift iOS let options = SuperwallOptions() options.shouldObservePurchases = true Superwall.configure(apiKey: "your_api_key", options: options) ``` ::: There are a few things to keep in mind when using observer mode: 1. On iOS, if you're using StoreKit 2, then Superwall solely reports transaction completions. If you're using StoreKit 1, then Superwall will report transaction starts, abandons, and completions. 2. When using observer mode, you can't make purchases using our SDK — such as `Superwall.shared.purchase(aProduct)`. For more on setting up revenue tracking, check out this [doc](/overview-settings-revenue-tracking). --- # Viewing Purchased Products Source: https://superwall.com/docs/ios/guides/advanced/viewing-purchased-products undefined When a paywall is presenting and a user converts, you can view the purchased products in several different ways. ### Use the `PaywallPresentationHandler` Arguably the easiest of the options — simply pass in a presentation handler and check out the product within the `onDismiss` block. ```swift Swift let handler = PaywallPresentationHandler() handler.onDismiss { _, result in switch result { case .declined: print("No purchased occurred.") case .purchased(let product): print("Purchased \(product.productIdentifier)") case .restored: print("Restored purchases.") } } Superwall.shared.register(placement: "caffeineLogged", handler: handler) { logCaffeine() } ``` ```swift Objective-C SWKPaywallPresentationHandler *handler = [SWKPaywallPresentationHandler new]; [handler onDismiss:^(SWKPaywallInfo * _Nonnull info, enum SWKPaywallResult result, SWKStoreProduct * _Nullable product) { switch (result) { case SWKPaywallResultPurchased: NSLog(@"Purchased %@", product.productIdentifier); default: NSLog(@"Unhandled event."); } }]; [[Superwall sharedInstance] registerWithPlacement:@"caffeineLogged" params:@{} handler:handler feature:^{ [self logCaffeine]; }]; ``` ```kotlin Android val handler = PaywallPresentationHandler() handler.onDismiss { _, paywallResult -> when (paywallResult) { is PaywallResult.Purchased -> { // The user made a purchase! val purchasedProductId = paywallResult.productId println("User purchased product: $purchasedProductId") // ... do something with the purchased product ID ... } is PaywallResult.Declined -> { // The user declined to make a purchase. println("User declined to make a purchase.") // ... handle the declined case ... } is PaywallResult.Restored -> { // The user restored a purchase. println("User restored a purchase.") // ... handle the restored case ... } } } Superwall.instance.register(placement = "caffeineLogged", handler = handler) { logCaffeine() } ``` ```dart Flutter PaywallPresentationHandler handler = PaywallPresentationHandler(); handler.onDismiss((paywallInfo, paywallResult) async { String name = await paywallInfo.name; print("Handler (onDismiss): $name"); switch (paywallResult) { case PurchasedPaywallResult(productId: var id): // The user made a purchase! print('User purchased product: $id'); // ... do something with the purchased product ID ... break; case DeclinedPaywallResult(): // The user declined to make a purchase. print('User declined the paywall.'); // ... handle the declined case ... break; case RestoredPaywallResult(): // The user restored a purchase. print('User restored a previous purchase.'); // ... handle the restored case ... break; } }); Superwall.shared.registerPlacement( "caffeineLogged", handler: handler, feature: () { logCaffeine(); }); ``` ```typescript React Native import * as React from "react" import Superwall from "../../src" import { PaywallPresentationHandler, PaywallInfo } from "../../src" import type { PaywallResult } from "../../src/public/PaywallResult" const Home = () => { const navigation = useNavigation() const presentationHandler: PaywallPresentationHandler = { onDismiss: (handler: (info: PaywallInfo, result: PaywallResult) => void) => { handler = (info, result) => { console.log("Paywall dismissed with info:", info, "and result:", result) if (result.type === "purchased") { console.log("Product purchased with ID:", result.productId) } } }, onPresent: (handler: (info: PaywallInfo) => void) => { handler = (info) => { console.log("Paywall presented with info:", info) // Add logic for when the paywall is presented } }, onError: (handler: (error: string) => void) => { handler = (error) => { console.error("Error presenting paywall:", error) // Handle any errors that occur during presentation } }, onSkip: () => { console.log("Paywall presentation skipped") // Handle the case where the paywall presentation is skipped }, } const nonGated = () => { Superwall.shared.register({ placement: "non_gated", handler: presentationHandler, feature: () => { navigation.navigate("caffeineLogged", { value: "Go for caffeine logging", }) }); } return // Your view code here } ``` ### Use `SuperwallDelegate` Next, the [SuperwallDelegate](/using-superwall-delegate) offers up much more information, and can inform you of virtually any Superwall event that occurred: ```swift Swift class SWDelegate: SuperwallDelegate { func handleSuperwallEvent(withInfo eventInfo: SuperwallEventInfo) { switch eventInfo.event { case .transactionComplete(_, let product, _, _): print("Transaction complete: product: \(product.productIdentifier)") case .subscriptionStart(let product, _): print("Subscription start: product: \(product.productIdentifier)") case .freeTrialStart(let product, _): print("Free trial start: product: \(product.productIdentifier)") case .transactionRestore(_, _): print("Transaction restored") case .nonRecurringProductPurchase(let product, _): print("Consumable product purchased: \(product.id)") default: print("Unhandled event.") } } } @main struct Caffeine_PalApp: App { @State private var swDelegate: SWDelegate = .init() init() { Superwall.configure(apiKey: "my_api_key") Superwall.shared.delegate = swDelegate } var body: some Scene { WindowGroup { ContentView() } } } ``` ```swift Objective-C // SWDelegate.h... #import @import SuperwallKit; NS_ASSUME_NONNULL_BEGIN @interface SWDelegate : NSObject @end NS_ASSUME_NONNULL_END // SWDelegate.m... @implementation SWDelegate - (void)handleSuperwallEventWithInfo:(SWKSuperwallEventInfo *)eventInfo { switch(eventInfo.event) { case SWKSuperwallEventTransactionComplete: NSLog(@"Transaction complete: %@", eventInfo.params[@"primary_product_id"]); } } // In AppDelegate.m... #import "AppDelegate.h" #import "SWDelegate.h" @import SuperwallKit; @interface AppDelegate () @property (strong, nonatomic) SWDelegate *delegate; @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. self.delegate = [SWDelegate new]; [Superwall configureWithApiKey:@"my_api_key"]; [Superwall sharedInstance].delegate = self.delegate; return YES; } ``` ```kotlin Android class SWDelegate : SuperwallDelegate { override fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) { when (eventInfo.event) { is SuperwallPlacement.TransactionComplete -> { val transaction = (eventInfo.event as SuperwallPlacement.TransactionComplete).transaction val product = (eventInfo.event as SuperwallPlacement.TransactionComplete).product val paywallInfo = (eventInfo.event as SuperwallPlacement.TransactionComplete).paywallInfo println("Transaction Complete: $transaction, Product: $product, Paywall Info: $paywallInfo") } else -> { // Handle other cases } } } } class MyApplication : Application() { override fun onCreate() { super.onCreate() Superwall.configure(this, "my_api_key") Superwall.instance.delegate = SWDelegate() } } ``` ```dart Flutter import 'dart:io'; import 'package:flutter/material.dart'; import 'package:superwallkit_flutter/superwallkit_flutter.dart'; class _MyAppState extends State implements SuperwallDelegate { final logging = Logging(); @override void initState() { super.initState(); configureSuperwall(useRevenueCat); } Future configureSuperwall(bool useRevenueCat) async { try { final apiKey = Platform.isIOS ? 'ios_api_project_key' : 'android_api_project_key'; final logging = Logging(); logging.level = LogLevel.warn; logging.scopes = {LogScope.all}; final options = SuperwallOptions(); options.paywalls.shouldPreload = false; options.logging = logging; Superwall.configure(apiKey, purchaseController: null, options: options, completion: () { logging.info('Executing Superwall configure completion block'); }); Superwall.shared.setDelegate(this); } catch (e) { // Handle any errors that occur during configuration logging.error('Failed to configure Superwall:', e); } } @override Future handleSuperwallEvent(SuperwallEventInfo eventInfo) async { switch (eventInfo.event.type) { case PlacementType.transactionComplete: final product = eventInfo.params?['product']; logging.info('Transaction complete event received with product: $product'); // Add any additional logic you need to handle the transaction complete event break; // Handle other events if necessary default: logging.info('Unhandled event type: ${eventInfo.event.type}'); break; } } } ``` ```typescript React Native import { PaywallInfo, SubscriptionStatus, SuperwallDelegate, SuperwallPlacementInfo, PlacementType, } from '../../src'; export class MySuperwallDelegate extends SuperwallDelegate { handleSuperwallPlacement(placementInfo: SuperwallPlacementInfo) { console.log('Handling Superwall placement:', placementInfo); switch (placementInfo.placement.type) { case PlacementType.transactionComplete: const product = placementInfo.params?.["product"]; if (product) { console.log(`Product: ${product}`); } else { console.log("Product not found in params."); } break; default: break; } } } export default function App() { const delegate = new MySuperwallDelegate(); React.useEffect(() => { const setupSuperwall = async () => { const apiKey = Platform.OS === 'ios' ? 'ios_api_project_key' : 'android_api_project_key'; Superwall.configure({ apiKey: apiKey, }); Superwall.shared.setDelegate(delegate); }; } } ``` ### Use a purchase controller If you are controlling the purchasing pipeline yourself via a [purchase controller](/advanced-configuration), then naturally the purchased product is available: ```swift Swift final class MyPurchaseController: PurchaseController { func purchase(product: StoreProduct) async -> PurchaseResult { print("Kicking off purchase of \(product.productIdentifier)") do { let result = try await MyPurchaseLogic.purchase(product: product) return .purchased // .cancelled, .pending, .failed(Error) } catch { return .failed(error) } } // 2 func restorePurchases() async -> RestorationResult { print("Restoring purchases") return .restored // false } } @main struct Caffeine_PalApp: App { private let pc: MyPurchaseController = .init() init() { Superwall.configure(apiKey: "my_api_key", purchaseController: pc) } var body: some Scene { WindowGroup { ContentView() } } } ``` ```swift Objective-C // In MyPurchaseController.h... #import @import SuperwallKit; @import StoreKit; NS_ASSUME_NONNULL_BEGIN @interface MyPurchaseController : NSObject + (instancetype)sharedInstance; @end NS_ASSUME_NONNULL_END // In MyPurchaseController.m... #import "MyPurchaseController.h" @implementation MyPurchaseController + (instancetype)sharedInstance { static MyPurchaseController *sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [MyPurchaseController new]; }); return sharedInstance; } - (void)purchaseWithProduct:(SWKStoreProduct * _Nonnull)product completion:(void (^ _Nonnull)(enum SWKPurchaseResult, NSError * _Nullable))completion { NSLog(@"Kicking off purchase of %@", product.productIdentifier); // Do purchase logic here completion(SWKPurchaseResultPurchased, nil); } - (void)restorePurchasesWithCompletion:(void (^ _Nonnull)(enum SWKRestorationResult, NSError * _Nullable))completion { // Do restore logic here completion(SWKRestorationResultRestored, nil); } @end // In AppDelegate.m... #import "AppDelegate.h" #import "MyPurchaseController.h" @import SuperwallKit; @interface AppDelegate () @end @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:^{ }]; return YES; } ``` ```kotlin Android class MyPurchaseController(val context: Context): PurchaseController { override suspend fun purchase( activity: Activity, productDetails: ProductDetails, basePlanId: String?, offerId: String? ): PurchaseResult { println("Kicking off purchase of $basePlanId") return PurchaseResult.Purchased() } override suspend fun restorePurchases(): RestorationResult { TODO("Not yet implemented") } } class MyApplication : Application() { override fun onCreate() { super.onCreate() Superwall.configure(this, "my_api_key", purchaseController = MyPurchaseController(this)) } } ``` ```dart Flutter class MyPurchaseController extends PurchaseController { // 1 @override Future purchaseFromAppStore(String productId) async { print('Attempting to purchase product with ID: $productId'); // Do purchase logic return PurchaseResult.purchased; } @override Future purchaseFromGooglePlay( String productId, String? basePlanId, String? offerId ) async { print('Attempting to purchase product with ID: $productId and basePlanId: $basePlanId'); // Do purchase logic return PurchaseResult.purchased; } @override Future restorePurchases() async { // Do resture logic } } ``` ```typescript React Native export class MyPurchaseController extends PurchaseController { // 1 async purchaseFromAppStore(productId: string): Promise { console.log("Kicking off purchase of ", productId) // Purchase logic return await this._purchaseStoreProduct(storeProduct) } async purchaseFromGooglePlay( productId: string, basePlanId?: string, offerId?: string ): Promise { console.log("Kicking off purchase of ", productId, " base plan ID", basePlanId) // Purchase logic return await this._purchaseStoreProduct(storeProduct) } // 2 async restorePurchases(): Promise { // TODO // ---- // Restore purchases and return true if successful. } } ``` ### SwiftUI - Use `PaywallView` The `PaywallView` allows you to show a paywall by sending it a placement. It also has a dismiss handler where the purchased product will be vended: ```swift @main struct Caffeine_PalApp: App { @State private var presentPaywall: Bool = false init() { Superwall.configure(apiKey: "my_api_key") } var body: some Scene { WindowGroup { Button("Log") { presentPaywall.toggle() } .sheet(isPresented: $presentPaywall) { PaywallView(placement: "caffeineLogged", params: nil, paywallOverrides: nil) { info, result in switch result { case .declined: print("No purchased occurred.") case .purchased(let product): print("Purchased \(product.productIdentifier)") case .restored: print("Restored purchases.") } } feature: { print("Converted") presentPaywall.toggle() } } } } } ``` --- # Request permissions from paywalls Source: https://superwall.com/docs/ios/guides/advanced/request-permissions-from-paywalls Trigger the iOS system permission dialog directly from a Superwall paywall action. ## Overview Use the **Request permission** action in the paywall editor when you want to gate features behind iOS permissions without sending users into your app settings flow. When the user taps the element, SuperwallKit presents the native prompt, reports the result back to the paywall so you can update the design, and emits analytics events you can forward through `SuperwallDelegate`. The **Request permission** action is rolling out to the paywall editor and is not visible in the dashboard just yet. We're shipping it very soon, so keep an eye on the changelog if you don't see it in your editor today. ## Add the action in the editor 1. Open your paywall in the editor and select the button or element you want to wire up. 2. Set its action to **Request permission**. 3. Choose the permission to request. You can add multiple buttons if you need to prime more than one permission (for example, notification + camera). 4. Republish the paywall. No code changes are required beyond making sure the necessary Info.plist strings exist in your app. ## Supported permissions and Info.plist keys | Editor option | `permission_type` sent from the paywall | Required Info.plist keys | Notes | | ---------------------- | --------------------------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | | Notifications | `notification` | *None* | Uses `UNUserNotificationCenter` with alert, badge, and sound options. | | Location (When In Use) | `location` | `NSLocationWhenInUseUsageDescription` | Prompts for foreground access only. | | Location (Always) | `background_location` | `NSLocationWhenInUseUsageDescription`, `NSLocationAlwaysAndWhenInUseUsageDescription` | The SDK first ensures When-In-Use is granted, then escalates to Always. | | Photos | `read_images` | `NSPhotoLibraryUsageDescription` | Requests `.readWrite` access on iOS 14+. | | Contacts | `contacts` | `NSContactsUsageDescription` | Uses `CNContactStore.requestAccess`. | | Camera | `camera` | `NSCameraUsageDescription` | Uses `AVCaptureDevice.requestAccess`. | If a required Info.plist key is missing—or the platform does not support the permission, such as background location on visionOS—the action finishes with an `unsupported` status, and the delegate receives a `permissionDenied` event so you can log the misconfiguration. ## What the SDK tracks Each button tap generates three analytics events that flow through `handleSuperwallEvent(withInfo:)`: * `permission_requested` when the native dialog is about to appear. * `permission_granted` if the user allows access. * `permission_denied` if the user declines or the permission is unsupported on the current device. All three events include: ```json { "permission_name": "", "paywall_identifier": "" } ``` Use the associated `SuperwallEvent.permissionRequested`, `.permissionGranted`, and `.permissionDenied` cases to branch on outcomes: ```swift func handleSuperwallEvent(withInfo eventInfo: SuperwallEventInfo) { switch eventInfo.event { case .permissionRequested(let permission, let paywallId): Analytics.track("permission_requested", with: [ "permission": permission, "paywall_id": paywallId ]) case .permissionGranted(let permission, _): FeatureFlags.unlock(permission: permission) case .permissionDenied(let permission, _): Alerts.presentPermissionDeclinedCopy(for: permission) default: break } } ``` ## Status values returned to the paywall The paywall receives a `permission_result` event with one of the following statuses so you can branch in your paywall logic (for example, swapping a button for a checklist item): * `granted` – The system reported success. * `denied` – The user denied the request or an earlier session already denied it. * `unsupported` – The permission is not available on the current device or the Info.plist copy block is missing. Because the permissions are requested from real user interaction, you can safely stack actions—for example, ask for notifications first and, on success, show a camera prompt that immediately appears inside the same paywall session. ## Troubleshooting * See `unsupported`? Double-check the Info.plist keys in the table above and confirm the permission exists on the target OS (background location is not available on visionOS). * Nothing happens when you tap the button? Make sure the action is published as **Request permission** and that the app has been updated with the new paywall revision. * Want to show fallback copy after a denial? Configure `PaywallOptions.notificationPermissionsDenied` or handle the `permissionDenied` event in your delegate to display a Settings deep link. --- # Custom Paywall Actions Source: https://superwall.com/docs/ios/guides/advanced/custom-paywall-actions undefined For example, adding a custom action called `help_center` to a button in your paywall gives you the opportunity to present a help center whenever that button is pressed. To set this up, implement `handleCustomPaywallAction(withName:)` in your `SuperwallDelegate`: :::ios ```swift Swift func handleCustomPaywallAction(withName name: String) { if name == "help_center" { HelpCenterManager.present() } } ``` ```swift Objective-C - (void)handleCustomPaywallActionWithName:(NSString *)name { if ([name isEqualToString:"help_center"]) { [HelpCenterManager present]; } } ``` :::
Remember to set `Superwall.shared.delegate`! For implementation details, see the [Superwall Delegate](/using-superwall-delegate) guide. --- # Purchasing Products Outside of a Paywall Source: https://superwall.com/docs/ios/guides/advanced/direct-purchasing undefined If you're using Superwall for revenue tracking, but want a hand with making purchases in your implementation, you can use our `purchase` methods: :::ios ```swift iOS // For StoreKit 1 private func purchase(_ product: SKProduct) async throws -> PurchaseResult { return await Superwall.shared.purchase(product) } // For StoreKit 2 private func purchase(_ product: StoreKit.Product) async throws -> PurchaseResult { return await Superwall.shared.purchase(product) } // Superwall's `StoreProduct` private func purchase(_ product: StoreProduct) async throws -> PurchaseResult { return await Superwall.shared.purchase(product) } ``` ::: For iOS, the `purchase()` method supports StoreKit 1, 2 and Superwall's abstraction over a product, `StoreProduct`. You can fetch the products you've added to Superwall via the `products(for:)` method. Similarly, in Android, you can fetch a product using a product identifier — and the first base plan will be selected: :::ios ```swift iOS private func fetchProducts(for identifiers: Set) async -> Set { return await Superwall.shared.products(for: identifiers) } ``` ::: If you already have your own product fetching code, simply pass the product representation to these methods. For example, in StoreKit 1 — an `SKProduct` instance, in StoreKit 2, `Product`, etc. Each `purchase()` implementation returns a `PurchaseResult`, which informs you of the transaction's resolution: * `.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. --- # Retrieving and Presenting a Paywall Yourself Source: https://superwall.com/docs/ios/guides/advanced/presenting-paywalls undefined If you want complete control over the paywall presentation process, you can use `getPaywall(forPlacement:params:paywallOverrides:delegate:)`. This returns the `UIViewController` subclass `PaywallViewController`, which you can then present however you like. Or, you can use a SwiftUI `View` via `PaywallView`. The following is code is how you'd mimic [register](/docs/feature-gating): ```swift Swift final class MyViewController: UIViewController { private func presentPaywall() async { do { // 1 let paywallVc = try await Superwall.shared.getPaywall( forPlacement: "campaign_trigger", delegate: self ) self.present(paywallVc, animated: true) } catch let skippedReason as PaywallSkippedReason { // 2 switch skippedReason { case .holdout, .noAudienceMatch, .placementNotFound: break } } catch { // 3 print(error) } } private func launchFeature() { // Insert code to launch a feature that's behind your paywall. } } // 4 extension MyViewController: PaywallViewControllerDelegate { func paywall( _ paywall: PaywallViewController, didFinishWith result: PaywallResult, shouldDismiss: Bool ) { if shouldDismiss { paywall.dismiss(animated: true) } switch result { case .purchased, .restored: launchFeature() case .declined: let closeReason = paywall.info.closeReason let featureGating = paywall.info.featureGatingBehavior if closeReason != .forNextPaywall && featureGating == .nonGated { launchFeature() } } } } ``` ```swift Objective-C @interface MyViewController : UIViewController - (void)presentPaywall; @end @interface MyViewController () @end @implementation MyViewController - (void)presentPaywall { // 1 [[Superwall sharedInstance] getPaywallForEvent:@"campaign_trigger" params:nil paywallOverrides:nil delegate:self completion:^(SWKGetPaywallResult * _Nonnull result) { if (result.paywall != nil) { [self presentViewController:result.paywall animated:YES completion:nil]; } else if (result.skippedReason != SWKPaywallSkippedReasonNone) { switch (result.skippedReason) { // 2 case SWKPaywallSkippedReasonHoldout: case SWKPaywallSkippedReasonUserIsSubscribed: case SWKPaywallSkippedReasonEventNotFound: case SWKPaywallSkippedReasonNoRuleMatch: case SWKPaywallSkippedReasonNone: break; }; } else if (result.error) { // 3 NSLog(@"%@", result.error); } }]; } -(void)launchFeature { // Insert code to launch a feature that's behind your paywall. } // 4 - (void)paywall:(SWKPaywallViewController *)paywall didFinishWithResult:(enum SWKPaywallResult)result shouldDismiss:(BOOL)shouldDismiss { if (shouldDismiss) { [paywall dismissViewControllerAnimated:true completion:nil]; } SWKPaywallCloseReason closeReason; SWKFeatureGatingBehavior featureGating; switch (result) { case SWKPaywallResultPurchased: case SWKPaywallResultRestored: [self launchFeature]; break; case SWKPaywallResultDeclined: closeReason = paywall.info.closeReason; featureGating = paywall.info.featureGatingBehavior; if (closeReason != SWKPaywallCloseReasonForNextPaywall && featureGating == SWKFeatureGatingBehaviorNonGated) { [self launchFeature]; } break; } } @end ``` ```swift SwiftUI import SuperwallKit struct MyAwesomeApp: App { @State var store: AppStore = .init() init() { Superwall.configure(apiKey: "MyAPIKey") } var body: some Scene { WindowGroup { ContentView() .fullScreenCover(isPresented: $store.showPaywall) { // You can just use 'placement' at a minimum. The 'feature' // Closure fires if they convert PaywallView(placement: "a_placement", onSkippedView: { skip in switch skip { case .userIsSubscribed, .holdout(_), .noRuleMatch, .eventNotFound: MySkipView() } }, onErrorView: { error in MyErrorView() }, feature: { // User is subscribed as a result of the paywall purchase // Or they already were (which would happen in `onSkippedView`) }) } } } } ``` ```kotlin Kotlin // This is an example of how to use `getPaywall` to use a composable` import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.AndroidView import com.superwall.sdk.Superwall import com.superwall.sdk.paywall.presentation.get_paywall.getPaywall import com.superwall.sdk.paywall.presentation.internal.request.PaywallOverrides import com.superwall.sdk.paywall.vc.PaywallView import com.superwall.sdk.paywall.vc.delegate.PaywallViewCallback @Composable fun PaywallComposable( event: String, params: Map? = null, paywallOverrides: PaywallOverrides? = null, callback: PaywallViewCallback, errorComposable: @Composable ((Throwable) -> Unit) = { error: Throwable -> // Default error composable Text(text = "No paywall to display") }, loadingComposable: @Composable (() -> Unit) = { // Default loading composable Box(modifier = Modifier.fillMaxSize()) { Column( modifier = Modifier.align(Alignment.Center), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { CircularProgressIndicator() } } } ) { val viewState = remember { mutableStateOf(null) } val errorState = remember { mutableStateOf(null) } val context = LocalContext.current LaunchedEffect(Unit) { PaywallBuilder(event) .params(params) .overrides(paywallOverrides) .delegate(delegate) .activity(context as Activity) .build() .fold(onSuccess = { viewState.value = it }, onFailure = { errorState.value = it }) } when { viewState.value != null -> { viewState.value?.let { viewToRender -> DisposableEffect(viewToRender) { viewToRender.onViewCreated() onDispose { viewToRender.beforeOnDestroy() viewToRender.encapsulatingActivity = null CoroutineScope(Dispatchers.Main).launch { viewToRender.destroyed() } } } AndroidView( factory = { context -> viewToRender } ) } } errorState.value != null -> { errorComposable(errorState.value!!) } else -> { loadingComposable() } } } ``` This does the following: 1. Gets the paywall view controller. 2. Handles the cases where the paywall was skipped. 3. Catches any presentation errors. 4. Implements the delegate. This is called when the user is finished with the paywall. First, it checks `shouldDismiss`. If this is true then is dismissed the paywall from view before launching any features. This may depend on the `result` depending on how you first presented your view. Then, it switches over the `result`. If the result is `purchased` or `restored` the feature can be launched. However, if the result is `declined`, it checks that the the `featureGating` property of `paywall.info` is `nonGated` and that the `closeReason` isn't `.forNextPaywall`. ### Best practices 1. **Make sure to prevent a paywall from being accessed after a purchase has occurred**. If a user purchases from a paywall, it is your responsibility to make sure that the user can't access that paywall again. For example, if after successful purchase you decide to push a new view on to the navigation stack, you should make sure that the user can't go back to access the paywall. 2. **Make sure the paywall view controller deallocates before presenting it elsewhere**. If you have a paywall view controller presented somewhere and you try to present the same view controller elsewhere, you will get a crash. For example, you may have a paywall in a tab bar controller, and then you also try to present it modally. We plan on improving this, but currently it's your responsibility to ensure this doesn't happen. :::ios 3. **Listening for Loading State Changes**. If you have logic that depends on the progress of the paywall's loading state, you can use the delegate function `paywall(_:loadingStateDidChange:)`. Or, if you have an instance of a `PaywallViewController`, you can use the published property: ```swift let stateSub = paywall.$loadingState.sink { state in print(state) } ``` ::: --- # Game Controller Support Source: https://superwall.com/docs/ios/guides/advanced/game-controller-support undefined :::ios First, set the `SuperwallOption` `isGameControllerEnabled` to `true`: ```swift let options = SuperwallOptions() options.isGameControllerEnabled = true Superwall.configure(apiKey: "MY_API_KEY", options: options); ``` Forward events to your paywall by calling `gamepadValueChanged(gamepad:element:)` from your own gamepad's `valueChanged` handler: ```swift controller.extendedGamepad?.valueChangedHandler = { gamepad, element in // send values to Superwall Superwall.shared.gamepadValueChanged(gamepad: gamepad, element: element) // ... rest of your code } ``` ::: --- # Article-Style Paywalls: Inline with Additional Plans Source: https://superwall.com/docs/ios/guides/embedded-paywalls-in-scrollviews Embed an inline paywall in a scrollable article and optionally present a second full-screen paywall for additional plans. Article-style paywalls let you keep readers in the flow of a long-form page while still prompting for upgrade options. You can place an inline paywall inside a scroll view, then present a second, full-screen paywall when users tap “see more plans.” This pattern is common in paid media and magazine apps: a portion of the article is readable, the rest is blurred or gated, and a footer paywall offers an inline purchase with a “see more plans” option that opens a full-screen paywall. Check out this working example:
![](/images/article-vid-example.gif)
This guide will show you how to build this example by explaining the APIs involved, and then a full code sample. There’s also a live working example in [CaffeinePal](https://github.com/superwall/CaffeinePal/tree/using-superwall-sdk). Look at the `RecipesView` to see it in action. ## Key APIs Use `getPaywall()` to fetch a paywall you can embed inline, and configure it with a placement so you can control which paywall variant shows from the dashboard. For more on presenting paywalls in custom presentations, check out our [blog post](https://superwall.com/blog/custom-paywall-presentation-in-ios-with-the-superwall-sdk/). For the second paywall, trigger a custom action from the inline paywall and call `getPaywall()` again to present the full-screen option. You're responsible for removing embedded paywall views when users move on. Reusing the same `PaywallViewController` or `PaywallView` instance elsewhere can cause a crash. For UIKit, avoid mixing `register()` and `getPaywall()` when you embed paywalls. ## Presenting a second paywall To get the inline paywall to trigger a second, full-screen paywall, create a custom action in the paywall editor in the embedded paywall. In this example, a custom action called "showFromLine" is triggered from the "or, view all plans" button: ![](/images/showCustomAction.jpeg) Then, respond to that action in your [`SuperwallDelegate`](/ios/sdk-reference/SuperwallDelegate) to retrieve the second paywall and present it. In the code below, our second paywall is normally triggered via the `showAllPlansPaywall` placement that was setup in the Superwall dashboard within a campaign: ```swift extension MyAppLogic: SuperwallDelegate, PaywallViewControllerDelegate { // Custom action comes in func handleCustomPaywallAction(withName name: String) { if name == "showFromInline" { Task { await presentAllPlansPaywall() } } } // MARK: PaywallViewControllerDelegate func paywall( _ paywall: PaywallViewController, didFinishWith result: PaywallResult, shouldDismiss: Bool ) { if shouldDismiss { paywall.dismiss(animated: true) } } func paywall( _ paywall: PaywallViewController, loadingStateDidChange loadingState: PaywallLoadingState ) { // Handle loading state changes if needed } // MARK: Custom Paywall Presentation private func presentAllPlansPaywall() async { do { let paywallViewController = try await Superwall.shared.getPaywall( forPlacement: "showAllPlansPaywall", delegate: self ) guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let rootViewController = windowScene.windows.first?.rootViewController else { return } var topController = rootViewController while let presented = topController.presentedViewController { topController = presented } topController.present(paywallViewController, animated: true) } catch let reason as PaywallSkippedReason { print("Paywall skipped: \(reason)") } catch { print("Error presenting paywall: \(error)") } } } ``` This keeps the inline paywall embedded while you intentionally present the next paywall. The entire flow looks like this: **In your dashboard** 1. Have a paywall setup for your "footer" or bottom paywall. 2. Add a custom action to it to present a second paywall over it. 3. Make sure both paywalls are active in a campaign, and remember the placements used to trigger them **In your code** 1. Use `getPaywall` and `PaywallView` to embed the first paywall in your scrollview. 2. Users can purchase from there, or tap another button to present a second paywall. 3. Handle a custom action fired from a "View all plans" or similar button in a `SuperwallDelegate`. 4. Use `PaywallViewControllerDelegate` to manage presentation of the second one. Here's some code to model your approach, showing the first paywall as either an overlay at the bottom or inline with scrolled content: ```swift enum PaywallEmbedMode { case overlay case inline } struct ArticlePaywallDemoView: View { let mode: PaywallEmbedMode let placement: String = "getPaywallTest" var body: some View { ScrollView { VStack(alignment: .leading) { Text("How to embed a Superwall paywall alongside your own content") .font(.title) Text("By Superwall").font(.caption) Text("...article content...") .padding(.vertical, 16) if mode == .inline { paywallContent } } .padding() .overlay(alignment: .bottom) { if mode == .overlay { paywallContent } } } } private var paywallContent: some View { PaywallView(placement: placement) .frame(maxWidth: .infinity) .frame(height: 300) } } ``` If you need to remove the paywall, remove the `PaywallView` from the view hierarchy and recreate it when you need to show it again. --- # Overriding Introductory Offer Eligibility Source: https://superwall.com/docs/ios/guides/intro-offer-eligibility-override Control when users see free trials and intro offers on your paywalls by overriding the default eligibility logic. ## Overview Starting with iOS SDK 4.11.0, you can override the default introductory offer eligibility logic to control when users see free trials and intro offers on your paywalls. This allows you to show intro offers to returning users (as "promo offers") or to prevent them from appearing entirely. This feature is configured entirely through the Paywall Editor in the Superwall Dashboard. No code changes are required in your app. ## Requirements * **iOS SDK:** Version 4.11.0 or later * **Platform:** iOS 18.2+ only (App Store products) * **Xcode Version:** 16.3+ * You must have set up the [App Store Connect API](/overview-settings-revenue-tracking#app-store-connect-api) and the [In App Purchase Configuration](/overview-settings-revenue-tracking#in-app-purchase-configuration). ## If you're using a `PurchaseController` `PurchaseController` support for intro offer eligibility override was added in SDK version 4.12.8. You'll need to add the JWS token that we generate for the product in a `PurchaseOption` that you pass to StoreKit when you purchase: ```swift Swift func purchase(product: StoreProduct) async -> PurchaseResult { guard let sk2Product = product.sk2Product else { return .cancelled } var options: Set = [] // Grab the introOfferToken if let introOfferToken = product.introOfferToken { // Add it as a PurchaseOption options.insert(.introductoryOfferEligibility(compactJWS: introOfferToken.token)) } // Pass it in to the StoreKit purchase function let result = try await sk2Product.purchase(options: options) // etc... } ``` ## How It Works By default, Superwall uses Apple's StoreKit to determine if a user is eligible for an introductory offer. Apple's rules state that users can only claim an introductory offer once per subscription group. With this feature, you can override this behavior to: * **Show intro offers to returning users** who have already used a trial (useful for win-back campaigns) * **Hide intro offers entirely** even if users are eligible * **Use the default behavior** (let StoreKit decide) ## Configuration 1. Open your paywall in the Paywall Editor 2. Go to the **Products** menu in the left sidebar 3. Select an option from the **"Introductory Offer Eligibility"** dropdown 4. Publish your paywall ### Options **Automatic (Default)** Uses Apple's default eligibility rules **Always Eligible** Allows users to see and claim intro offers, even if they've used one before **Always Ineligible** Prevents users from seeing intro offers --- # Experimental Flags Source: https://superwall.com/docs/ios/guides/experimental-flags undefined Experimental flags in Superwall's SDK allow you to opt into features that are safe for production but are still being refined. These features may undergo naming changes or internal restructuring in future SDK versions. We expose them behind flags to give you early access while preserving flexibility for ongoing development. These flags are configured via the `SuperwallOptions` struct: ```swift let options = SuperwallOptions() options.enableExperimentalDeviceVariables = true Superwall.configure(apiKey: "my_api_key", options: options) ``` ## Available experimental flags When these flags are enabled and the user runs your app, these values become available in campaign filters. Currently, these include: **Latest Subscription Period Type (String)**: Represents whether the user is in a trial, promotional, or a similar phase. Possible values include: * `trial` * `code` * `subscription` * `promotional` * `winback` * `revoked` Represented as `latestSubscriptionPeriodType` in campaign filters. **Latest Subscription State (String)**: Represents what *state* the actual subscription is in. Possible values include: * `inGracePeriod` * `subscribed` * `expired` * `inBillingRetryPeriod` * `revoked` Represented as `latestSubscriptionState` in campaign filters. **Latest Subscription Will Auto Renew (Bool)**: If the user is set to renew or not. Either `true` or `false` Represented as `latestSubscriptionWillAutoRenew` in campaign filters. ### Detecting users who've cancelled an active trial One common use case for these flags is detecting users who've cancelled an active trial. In that case, the filter in the campaign would check for `latestSubscriptionWillAutoRenew` to be `false` and `latestSubscriptionPeriodType` to be `trial`. --- # Advanced Purchasing Source: https://superwall.com/docs/ios/guides/advanced-configuration 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: 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. :::ios ```swift Swift // 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) } } ``` ```swift Objective-C @import SuperwallKit; @import StoreKit; // MyPurchaseController @interface MyPurchaseController: NSObject + (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); } @end ```
```swift import StoreKit import SuperwallKit final class SWPurchaseController: PurchaseController { // MARK: Sync Subscription Status /// Makes sure that Superwall knows the customer's subscription status by /// changing `Superwall.shared.subscriptionStatus` func syncSubscriptionStatus() async { /// Every time the customer info changes, the subscription status should be updated. for await _ in Superwall.shared.customerInfoStream { var products: Set = [] for await verificationResult in Transaction.currentEntitlements { switch verificationResult { case .verified(let transaction): products.insert(transaction.productID) case .unverified: break } } let activeDeviceEntitlements = Superwall.shared.entitlements.byProductIds(products) let activeWebEntitlements = Superwall.shared.entitlements.web let allActiveEntitlements = activeDeviceEntitlements.union(activeWebEntitlements) await MainActor.run { Superwall.shared.subscriptionStatus = .active(allActiveEntitlements) } } } // MARK: Handle Purchases /// Makes a purchase with Superwall and returns its result after syncing subscription status. This gets called when /// someone tries to purchase a product on one of your paywalls. func purchase(product: StoreProduct) async -> PurchaseResult { let result = await Superwall.shared.purchase(product) return result } // MARK: Handle Restores /// Makes a restore with Superwall and returns its result after syncing subscription status. /// This gets called when someone tries to restore purchases on one of your paywalls. func restorePurchases() async -> RestorationResult { let result = await Superwall.shared.restorePurchases() return result } } ``` ::: 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: :::ios ```swift UIKit // 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 } } ``` ```swift SwiftUI @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() } } } ``` ```swift Objective-C // 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: 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: :::ios ```swift Swift import SuperwallKit func syncSubscriptionStatus() async { var purchasedProductIds: Set = [] // 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) } } ``` ```swift Objective-C @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: :::ios ```swift iOS 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](/3rd-party-analytics#using-events-to-see-purchased-products). ### Product Overrides Product overrides allow you to dynamically substitute products on paywalls without modifying the paywall design in the Superwall dashboard. 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 :::ios ```swift Swift // 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 ) ``` ```swift Objective-C // 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:** 1. Product names (e.g., "primary", "secondary") must match exactly as defined in the Superwall dashboard's Paywall Editor 2. The SDK substitutes the original product IDs with your override IDs before fetching from the App Store 3. The paywall maintains its visual design while showing the substituted products 4. Your `PurchaseController` will receive the overridden products when `purchase(product:)` is called Product overrides only affect the products shown on paywalls. They don't change your subscription logic or entitlement validation. --- # Cohorting in 3rd Party Tools Source: https://superwall.com/docs/ios/guides/3rd-party-analytics/cohorting-in-3rd-party-tools To easily view Superwall cohorts in 3rd party tools, we recommend you set user attributes based on the experiments that users are included in. You can also use custom placements for creating analytics events for actions such as interacting with an element on a paywall. :::ios ```swift Swift extension SuperwallService: SuperwallDelegate { func handleSuperwallEvent(withInfo eventInfo: SuperwallEventInfo) { if eventInfo.event.description == "trigger_fire" { MyAnalyticsService.shared.setUserAttributes([ "sw_experiment_\(eventInfo.event.params["experiment_id"])": true, "sw_variant_\(eventInfo.event.params["variant_id"])": true ]) } } } ``` ::: Once you've set this up, you can easily ask for all users who have an attribute `sw_experiment_1234` and breakdown by both variants to see how users in a Superwall experiment behave in other areas of your app. --- # Custom Paywall Analytics Source: https://superwall.com/docs/ios/guides/3rd-party-analytics/custom-paywall-analytics Learn how to log events from paywalls, such as a button tap or product change, to forward to your analytics service. You can create customized analytics tracking for any paywall event by using custom placements. With them, you can get callbacks for actions such as interacting with an element on a paywall sent to your [Superwall delegate](/using-superwall-delegate). This can be useful for tracking how users interact with your paywall and how that affects their behavior in other areas of your app. For example, in the paywall below, perhaps you're interested in tracking when people switch the plan from "Standard" and "Pro": ![](/images/3pa_cp_2.jpeg) You could create a custom placement [tap behavior](/paywall-editor-styling-elements#tap-behaviors) which fires when a segment is tapped: ![](/images/3pa_cp_1.jpeg) Then, you can listen for this placement and forward it to your analytics service: ```swift Swift extension SuperwallService: SuperwallDelegate { func handleSuperwallEvent(withInfo eventInfo: SuperwallEventInfo) { switch eventInfo.event { case let .customPlacement(name, params, paywallInfo): // Prints out didTapPro or didTapStandard print("\(name) - \(params) - \(paywallInfo)") MyAnalyticsService.shared.send(event: name, params: params) default: print("Default event: \(eventInfo.event.description)") } } } ``` For a walkthrough example, check out this [video on YouTube](https://youtu.be/4rM1rGRqDL0). --- # Superwall Events Source: https://superwall.com/docs/ios/guides/3rd-party-analytics/tracking-analytics The SDK automatically tracks some events, which power the charts in the dashboard. We encourage you to track them in your own analytics as described in [3rd Party Analytics](/3rd-party-analytics). The following Superwall events can be used as placements to present paywalls: * `app_install` * `app_launch` * `deepLink_open` * `session_start` * `paywall_decline` * `transaction_fail` * `transaction_abandon` * `survey_response` For more info about how to use these, check out [how to add them using a Placement](/campaigns-placements#adding-a-placement). The full list of events is as follows: | **Event Name** | **Action** | **Parameters** | | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `adServicesTokenRequestComplete` | When the AdServices token request finishes. | `["token": String]` | | `adServicesTokenRequestFail` | When the AdServices token request fails. | `["error": Error]` | | `adServicesTokenRequestStart` | When the AdServices token request starts. | None | | `app_close` | Anytime the app leaves the foreground. | Same as `app_install` | | `app_install` | When the SDK is configured for the first time. | `["is_superwall": true, "app_session_id": String, "using_purchase_controller": Bool]` | | `app_launch` | When the app is launched from a cold start. | Same as `app_install` | | `app_open` | Anytime the app enters the foreground. | Same as `app_install` | | `configAttributes` | When the attributes affecting Superwall's configuration are set or changed. | None | | `configFail` | When the Superwall configuration fails to be retrieved. | None | | `configRefresh` | When the Superwall configuration is refreshed. | None | | `confirmAllAssignments` | When all experiment assignments are confirmed. | None | | `customPlacement` | When the user taps on an element in the paywall that has a `custom_placement` action. | `["name": String, "params": [String: Any], "paywallInfo": PaywallInfo]` | | [`deepLink_open`](/campaigns-standard-placements#using-the-deeplink-open-event) | When a user opens the app via a deep link. | `["url": String, "path": String", "pathExtension": String, "lastPathComponent": String, "host": String, "query": String, "fragment": String]` + any query parameters in the deep link URL | | `device_attributes` | When device attributes are sent to the backend every session. | Includes `app_session_id`, `app_version`, `os_version`, `device_model`, `device_locale`, and various hardware/software details. | | `first_seen` | When the user is first seen in the app, regardless of login status. | Same as `app_install` | | `freeTrial_start` | When a user completes a transaction for a subscription product with an introductory offer. | Same as `subscription_start` | | `identityAlias` | When the user's identity aliases after calling `identify`. | None | | `nonRecurringProduct_purchase` | When the user purchases a non-recurring product. | Same as `subscription_start` | | `paywall_close` | When a paywall is closed (either manually or after a transaction succeeds). | \[“paywall\_webview\_load\_complete\_time”: String?, “paywall\_url”: String, “paywall\_response\_load\_start\_time”: String?, “paywall\_products\_load\_fail\_time”: String?, “secondary\_product\_id”: String, “feature\_gating”: Int, “paywall\_response\_load\_complete\_time”: String?, “is\_free\_trial\_available”: Bool, “is\_superwall”: true, “presented\_by”: String, “paywall\_name”: String, “paywall\_response\_load\_duration”: String?, “paywall\_identifier”: String, “paywall\_webview\_load\_start\_time”: String?, “paywall\_products\_load\_complete\_time”: String?, “paywall\_product\_ids”: String, “tertiary\_product\_id”: String, “paywall\_id”: String, “app\_session\_id”: String, “paywall\_products\_load\_start\_time”: String?, “primary\_product\_id”: String, “survey\_attached”: Bool, “survey\_presentation”: String?] | | [`paywall_decline`](/campaigns-standard-placements#using-the-paywall-decline-event) | When a user manually dismisses a paywall. | Same as `paywall_close` | | `paywall_open` | When a paywall is opened. | Same as `paywall_close` | | `paywallPresentationRequest` | When something happened during the paywall presentation, whether a success or failure. | `[“source_event_name”: String, “status”: String, “is_superwall”: true, “app_session_id”: String, “pipeline_type”: String, “status_reason”: String]` | | `paywallProductsLoad_complete` | When the request to load a paywall's products completes. | Same as `paywallResponseLoad_start` | | `paywallProductsLoad_fail` | When the request to load a paywall's products fails. | Same as `paywallResponseLoad_start` | | `paywallProductsLoad_retry` | When the request to load a paywall's products fails and is being retried. | `["triggeredPlacementName": String?, "paywallInfo": PaywallInfo, "attempt": Int]` | | `paywallProductsLoad_start` | When the request to load a paywall's products starts. | Same as `paywallResponseLoad_start` | | `paywallResponseLoad_complete` | When a paywall request to Superwall's servers completes. | Same as `paywallResponseLoad_start` | | `paywallResponseLoad_fail` | When a paywall request to Superwall's servers fails. | Same as `paywallResponseLoad_start` | | `paywallResponseLoad_notFound` | When a paywall request returns a 404 error. | Same as `paywallResponseLoad_start` | | `paywallResponseLoad_start` | When a paywall request to Superwall's servers has started. | Same as `app_install` + `["is_triggered_from_event": Bool]` | | `paywallWebviewLoad_complete` | When a paywall's webpage completes loading. | Same as `paywall_close` | | `paywallWebviewLoad_fail` | When a paywall's webpage fails to load. | Same as `paywall_close` | | `paywallWebviewLoad_fallback` | When a paywall's webpage fails and loads a fallback version. | Same as `paywall_close` | | `paywallWebviewLoad_start` | When a paywall's webpage begins to load. | Same as `paywall_close` | | `paywallWebviewLoad_processTerminated` | When the paywall's web view content process terminates. | Same as `paywall_close` | | `reset` | When `Superwall.reset()` is called. | None | | `restoreComplete` | When a restore completes successfully. | None | | `restoreFail` | When a restore fails. | `["message": String]` | | `restoreStart` | When a restore is initiated. | None | | `session_start` | When the app is opened after at least 60 minutes since last `app_close`. | Same as `app_install` | | `shimmerViewComplete` | When the shimmer view stops showing. | None | | `shimmerViewStart` | When the shimmer view starts showing. | None | | `subscription_start` | When a user completes a transaction for a subscription product without an introductory offer. | \[“product\_period\_days”: String, “product\_price”: String, “presentation\_source\_type”: String?, “paywall\_response\_load\_complete\_time”: String?, “product\_language\_code”: String, “product\_trial\_period\_monthly\_price”: String, “paywall\_products\_load\_duration”: String?, “product\_currency\_symbol”: String, “is\_superwall”: true, “app\_session\_id”: String, “product\_period\_months”: String, “presented\_by\_event\_id”: String?, “product\_id”: String, “trigger\_session\_id”: String, “paywall\_webview\_load\_complete\_time”: String?, “paywall\_response\_load\_start\_time”: String?, “product\_raw\_trial\_period\_price”: String, “feature\_gating”: Int, “paywall\_id”: String, “product\_trial\_period\_daily\_price”: String, “product\_period\_years”: String, “presented\_by”: String, “product\_period”: String, “paywall\_url”: String, “paywall\_name”: String, “paywall\_identifier”: String, “paywall\_products\_load\_start\_time”: String?, “product\_trial\_period\_months”: String, “product\_currency\_code”: String, “product\_period\_weeks”: String, “product\_periodly”: String, “product\_trial\_period\_text”: String, “paywall\_webview\_load\_start\_time”: String?, “paywall\_products\_load\_complete\_time”: String?, “primary\_product\_id”: String, “product\_trial\_period\_yearly\_price”: String, “paywalljs\_version”: String?, “product\_trial\_period\_years”: String, “tertiary\_product\_id”: String, “paywall\_products\_load\_fail\_time”: String?, “product\_trial\_period\_end\_date”: String, “product\_weekly\_price”: String, “variant\_id”: String, “presented\_by\_event\_timestamp”: String?, “paywall\_response\_load\_duration”: String?, “secondary\_product\_id”: String, “product\_trial\_period\_days”: String, “product\_monthly\_price”: String, “paywall\_product\_ids”: String, “product\_locale”: String, “product\_daily\_price”: String, “product\_raw\_price”: String, “product\_yearly\_price”: String, “product\_trial\_period\_price”: String, “product\_localized\_period”: String, “product\_identifier”: String, “experiment\_id”: String, “is\_free\_trial\_available”: Bool, “product\_trial\_period\_weeks”: String, “paywall\_webview\_load\_duration”: String?, “product\_period\_alt”: String, “product\_trial\_period\_weekly\_price”: String, “presented\_by\_event\_name”: String?] | | `subscriptionStatus_didChange` | When a user's subscription status changes. | `["is_superwall": true, "app_session_id": String, "subscription_status": String]` | | `surveyClose` | When the user chooses to close a survey instead of responding. | None | | [`survey_response`](/campaigns-standard-placements#using-the-survey-response-event) | When a user responds to a paywall survey. | `["survey_selected_option_title": String, "survey_custom_response": String, "survey_id": String, "survey_assignment_key": String, "survey_selected_option_id": String]` | | `touches_began` | When the user touches the app's UIWindow for the first time (if tracked by a campaign). | Same as `app_install` | | `transaction_abandon` | When the user cancels a transaction. | Same as `subscription_start` | | `transaction_complete` | When the user completes checkout and any product is purchased. | Same as subscription\_start + \[“web\_order\_line\_item\_id”: String, “app\_bundle\_id”: String, “config\_request\_id”: String, “state”: String, “subscription\_group\_id”: String, “is\_upgraded”: String, “expiration\_date”: String, “trigger\_session\_id”: String, “original\_transaction\_identifier”: String, “id”: String, “transaction\_date”: String, “is\_superwall”: true, “store\_transaction\_id”: String, “original\_transaction\_date”: String, “app\_session\_id”: String] | | `transaction_fail` | When the payment sheet fails to complete a transaction (ignores user cancellation). | Same as `subscription_start` + `["message": String]` | | `transaction_restore` | When the user successfully restores their purchases. | Same as `subscription_start` | | `transaction_start` | When the payment sheet is displayed to the user. | Same as `subscription_start` | | `transaction_timeout` | When the transaction takes longer than 5 seconds to display the payment sheet. | `["paywallInfo": PaywallInfo]` | | `trigger_fire` | When a registered placement triggers a paywall. | `[“trigger_name”: String, “trigger_session_id”: String, “variant_id”: String?, “experiment_id”: String?, “paywall_identifier”: String?, “result”: String, “unmatched_rule_”: “”]. unmatched_rule_ indicates why a rule (with a specfiic experiment id) didn’t match. It will only exist if the result is no_rule_match. Its outcome will either be OCCURRENCE, referring to the limit applied to a rule, or EXPRESSION.` | | `user_attributes` | When the user attributes are set. | `[“aliasId”: String, “seed”: Int, “app_session_id”: String, “applicationInstalledAt”: String, “is_superwall”: true, “application_installed_at”: String] + provided attributes` | --- # 3rd Party Analytics Source: https://superwall.com/docs/ios/guides/3rd-party-analytics undefined ### Hooking up Superwall events to 3rd party tools SuperwallKit automatically tracks some internal events. You can [view the list of events here](/tracking-analytics). We encourage you to also track them in your own analytics by implementing the [Superwall delegate](/using-superwall-delegate). Using the `handleSuperwallEvent(withInfo:)` function, you can forward events to your analytics service: :::ios ```swift Swift extension SuperwallService: SuperwallDelegate { func handleSuperwallEvent(withInfo eventInfo: SuperwallEventInfo) { print("analytics event called", eventInfo.event.description) MyAnalyticsService.shared.track( event: eventInfo.event.description, params: eventInfo.params ) } } ``` ```swift Objective-C - (void)handleSuperwallEventWithInfo:(SWKSuperwallEventInfo *)info { NSLog(@"Analytics event called %@", info.event.description)); [[MyAnalyticsService shared] trackEvent:info.event.description params:info.params]; } ``` :::
You might also want to set user attribute to allow for [Cohorting in 3rd Party Tools](/cohorting-in-3rd-party-tools) Alternatively, if you want typed versions of all these events with associated values, you can access them via `eventInfo.event`: :::ios ```swift func handleSuperwallEvent(withInfo eventInfo: SuperwallEventInfo) { switch eventInfo.event { case .firstSeen: break case .appOpen: break case .appLaunch: break case .identityAlias: break case .appInstall: break case .sessionStart: break case .deviceAttributes(let attributes): break case .subscriptionStatusDidChange: break case .appClose: break case .deepLink(let url): break case .triggerFire(let placementName, let result): break case .paywallOpen(let paywallInfo): break case .paywallClose(let paywallInfo): break case .paywallDecline(let paywallInfo): break case .transactionStart(let product, let paywallInfo): break case .transactionFail(let error, let paywallInfo): break case .transactionAbandon(let product, let paywallInfo): break case .transactionComplete(let transaction, let product, let type, let paywallInfo): break case .subscriptionStart(let product, let paywallInfo): break case .freeTrialStart(let product, let paywallInfo): break case .transactionRestore(let restoreType, let paywallInfo): break case .transactionTimeout(let paywallInfo): break case .userAttributes(let atts): break case .nonRecurringProductPurchase(let product, let paywallInfo): break case .paywallResponseLoadStart(let triggeredPlacementName): break case .paywallResponseLoadNotFound(let triggeredPlacementName): break case .paywallResponseLoadFail(let triggeredPlacementName): break case .paywallResponseLoadComplete(let triggeredPlacementName, let paywallInfo): break case .paywallWebviewLoadStart(let paywallInfo): break case .paywallWebviewLoadFail(let paywallInfo): break case .paywallWebviewLoadComplete(let paywallInfo): break case .paywallWebviewLoadTimeout(let paywallInfo): break case .paywallWebviewLoadFallback(let paywallInfo): break case .paywallWebviewProcessTerminated(let paywallInfo): break case .paywallProductsLoadStart(let triggeredPlacementName, let paywallInfo): break case .paywallProductsLoadFail(let triggeredPlacementName, let paywallInfo): break case .paywallProductsLoadComplete(let triggeredPlacementName): break case .paywallProductsLoadRetry(let triggeredPlacementName, let paywallInfo, let attempt): break case .surveyResponse(let survey, let selectedOption, let customResponse, let paywallInfo): break case .paywallPresentationRequest(let status, let reason): break case .touchesBegan: break case .surveyClose: break case .reset: break case .restoreStart: break case .restoreFail(let message): break case .restoreComplete: break case .configRefresh: break case .customPlacement(let name, let params, let paywallInfo): break case .configAttributes: break case .confirmAllAssignments: break case .configFail: break case .adServicesTokenRequestStart: break case .adServicesTokenRequestFail(let error): break case .adServicesTokenRequestComplete(let token): break case .shimmerViewStart: break case .shimmerViewComplete: break } } ``` ::: Wanting to use events to see which product was purchased on a paywall? Check out this [doc](/viewing-purchased-products). --- # Using Superwall Deep Links Source: https://superwall.com/docs/ios/guides/superwall-deep-links How to use Superwall Deep Links to trigger paywalls or custom in-app behavior. A Superwall Deep Link is a URL hosted at `https://.superwall.app/app-link/...` that opens your app to trigger a paywall as configured on the Superwall dashboard, or custom in-app behavior via the Superwall delegate. ## Prerequisites :::ios 1. Set up [deep link handling](/ios/quickstart/in-app-paywall-previews) ::: 2. Create a [Web Checkout app](/web-checkout/web-checkout-creating-an-app), even if you do not plan to charge through Web Checkout, this provisions the `*.superwall.app` domain that powers Superwall Deep Links. ## Handling incoming links * Always call `handleDeepLink` first. It returns `true` when the SDK recognizes the URL and plans to take over presentation, or `false` when you should continue routing inside your own app. * When a recognized link arrives before `Superwall.configure(...)` finishes, the SDK caches it and replays it immediately after configuration completes, so it is safe to forward links during cold launch. * If the return value is `false`, continue with your normal router—those links are not associated with any Superwall experience. :::ios ```swift func application( _ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:] ) -> Bool { let handled = Superwall.handleDeepLink(url) if handled { return true } return routeInternally(url) } ``` ::: ## Link formats and campaigns Deep link URLs are hosted at `https://.superwall.app/app-link/...`, you can have anything after the `/app-link/` path, including query parameters. These values will be availble to you in audience filters on the Superwall dashboard, or in the `handleSuperwallDeepLink` delegate method. :::ios ```swift final class PaywallDelegate: SuperwallDelegate { func handleSuperwallDeepLink( _ url: URL, pathComponents: [String], queryParameters: [String: String] ) { guard let head = pathComponents.first else { return } switch head { case "campaign": if pathComponents.count > 1 { routeToCampaignDetail(id: pathComponents[1]) } default: break } } } ``` ::: Keep your own routing logic in place for non-Superwall URLs and for any additional behaviors you want to stack on top of Superwall’s default presentation flow. --- # Making Purchases Source: https://superwall.com/docs/ios/guides/direct-purchasing Purchase any StoreKit product easily, with or without a paywall. Making purchases of a consumable, non-consumable or subscription product in Superwall takes only one line. You can use this whether or not you are using Superwall's paywalls: ```swift let result = await Superwall.shared.purchase(product) ``` This method takes a `StoreProduct` and returns a `PurchaseResult` so you can take action on the result. Here's an example from our demo app, [Caffeine Pal](https://github.com/superwall/CaffeinePal/blob/using-superwall-sdk/Caffeine%20Pal/Store%20and%20Models/CaffeineStore.swift#L121): ```swift func purchase(_ product: StoreProduct) async throws { let result = await Superwall.shared.purchase(product) switch result { case .cancelled: throw CaffeinePalStoreFrontError.cancelled case .purchased: // In `handleSuperwallEvent` delegate method, we'll check if an espresso recipe was // Purchased and if it was, we'll add it to the purchased drinks set. print("Purchased product \(product.productIdentifier)") case .pending: throw CaffeinePalStoreFrontError.pending case .failed(let error): throw error } } ``` For the SDK reference, check out this [page](/ios/guides/advanced/direct-purchasing). The flow looks like this: 1. Fetch your products. 2. Call `purchase` on any of them. 3. Respond to the result. Here's an example: ### Fetch products A `StoreProduct` can be fetched using its corresponding identifier from App Store Connect or a [StoreKit Configuration File](/ios/guides/testing-purchases). For example, `subscription.caffeinePalPro.monthly` here: ![](/images/dp_product_id.jpeg) That product could be fetched like so: ```swift let caffeineSub = await Superwall.shared.products(for: Set(["subscription.caffeinePalPro.monthly"])) ``` ### Call purchase Now, simply call `purchase`: ```swift let result = await Superwall.shared.purchase(caffeineSub) ``` ### Respond to result Finally, respond to the result: ```swift switch result { case .cancelled: // user cancelled the purchase flow case .purchased: // Purchase completed case .pending: // Purchase in flight case .failed(let error): // Couldn't purchase, check out the error } ``` There are a number of additional ways to respond to a purchase outside of this `result`, depending on how the product was purchased (for example, within a paywall). For examples, see this [doc](/ios/guides/advanced/viewing-purchased-products). --- # Advanced Configuration Source: https://superwall.com/docs/ios/guides/configuring When configuring the SDK you can pass in options that configure Superwall, the paywall presentation, and its appearance. ### Logging Logging is enabled by default in the SDK and is controlled by two properties: `level` and `scopes`. `level` determines the minimum log level to print to the console. There are five types of log level: 1. **debug**: Prints all logs from the SDK to the console. Useful for debugging your app if something isn't working as expected. 2. **info**: Prints errors, warnings, and useful information from the SDK to the console. 3. **warn**: Prints errors and warnings from the SDK to the console. 4. **error**: Only prints errors from the SDK to the console. 5. **none**: Turns off all logs. The SDK defaults to `info`. `scopes` defines the scope of logs to print to the console. For example, you might only care about logs relating to `paywallPresentation` and `paywallTransactions`. This defaults to `.all`. Check out [LogScope](https://sdk.superwall.me/documentation/superwallkit/logscope) for all possible cases. You set these properties like this: :::ios ```swift Swift let options = SuperwallOptions() options.logging.level = .warn options.logging.scopes = [.paywallPresentation, .paywallTransactions] Superwall.configure(apiKey:"MY_API_KEY", options: options); // Or you can set: Superwall.shared.logLevel = .warn ``` ```swift Objective-C SWKSuperwallOptions *options = [[SWKSuperwallOptions alloc] init]; options.logging.level = SWKLogLevelWarn; [Superwall configureWithApiKey:@"pk_e6bd9bd73182afb33e95ffdf997b9df74a45e1b5b46ed9c9" purchaseController:nil options:options completion:nil ]; [Superwall sharedInstance].logLevel = SWKLogLevelWarn; ``` ::: ### Preloading Paywalls Paywalls are preloaded by default when the app is launched from a cold start. The paywalls that are preloaded are determined by the list of placements that result in a paywall for the user when [registered](/docs/feature-gating). Preloading is smart, only preloading paywalls that belong to audiences that could be matched. Paywalls are cached by default, which means after they load once, they don't need to be reloaded from the network unless you make a change to them on the dashboard. However, if you have a lot of paywalls, preloading may increase network usage of your app on first load of the paywalls and result in slower loading times overall. You can turn off preloading by setting `shouldPreload` to `false`: :::ios ```swift Swift let options = SuperwallOptions() options.paywalls.shouldPreload = false Superwall.configure(apiKey: "MY_API_KEY", options: options) ``` ```swift Objective-C SWKSuperwallOptions *options = [[SWKSuperwallOptions alloc] init]; options.paywalls.shouldPreload = false; [Superwall configureWithApiKey:@"MY_API_KEY" purchaseController:nil options:options completion:nil ]; ``` ::: Then, if you'd like to preload paywalls for specific placements you can use `preloadPaywalls(forPlacements:)`: :::ios ```swift Swift Superwall.shared.preloadPaywalls(forPlacements: ["campaign_trigger"]); ``` ```swift Objective-C NSMutableSet *eventNames = [NSMutableSet set]; [eventNames addObject:@"campaign_trigger"]; [[Superwall sharedInstance] preloadPaywallsForPlacements:placementNames]; ``` ::: If you'd like to preload all paywalls you can use `preloadAllPaywalls()`: :::ios ```swift Swift Superwall.shared.preloadAllPaywalls() ``` ```swift Objective-C [[Superwall sharedInstance] preloadAllPaywalls]; ``` ::: Note: These methods will not reload any paywalls that have already been preloaded. ### External Data Collection By default, Superwall sends all registered events and properties back to the Superwall servers. However, if you have privacy concerns, you can stop this by setting `isExternalDataCollectionEnabled` to `false`: :::ios ```swift Swift let options = SuperwallOptions() options.isExternalDataCollectionEnabled = false Superwall.configure(apiKey: "MY_API_KEY", options: options) ``` ```swift Objective-C SWKSuperwallOptions *options = [[SWKSuperwallOptions alloc] init]; options.isExternalDataCollectionEnabled = false; [Superwall configureWithApiKey:@"MY_API_KEY" purchaseController:nil options:options completion:nil]; ``` ::: Disabling this will not affect your ability to create triggers based on properties. ### Automatically Dismissing the Paywall By default, Superwall automatically dismisses the paywall when a product is purchased or restored. You can disable this by setting `automaticallyDismiss` to `false`: :::ios ```swift Swift let options = SuperwallOptions() options.paywalls.automaticallyDismiss = false Superwall.configure(apiKey: "MY_API_KEY", options: options) ``` ```swift Objective-C SWKSuperwallOptions *options = [[SWKSuperwallOptions alloc] init]; options.automaticallyDismiss = false; [Superwall configureWithApiKey:@"MY_API_KEY" purchaseController:nil options:options completion:^{}]; ``` ::: To manually dismiss the paywall , call `Superwall.shared.dismiss()`. ### Custom Restore Failure Message You can set the title, message and close button title for the alert that appears after a restoration failure: :::ios ```swift Swift let options = SuperwallOptions() options.paywalls.restoreFailed.title = "My Title" options.paywalls.restoreFailed.message = "My message" options.paywalls.restoreFailed.closeButtonTitle = "Close" Superwall.configure(apiKey: "MY_API_KEY", options: options) ``` ```swift Objective-C SWKSuperwallOptions *options = [[SWKSuperwallOptions alloc] init]; options.paywalls.restoreFailed.title = @"My Title"; options.paywalls.restoreFailed.message = @"My message"; options.paywalls.restoreFailed.closeButtonTitle = @"Close"; [Superwall configureWithApiKey:@"MY_API_KEY" purchaseController:nil options:options completion:nil]; ``` ::: ### Haptic Feedback On iOS, the paywall uses haptic feedback by default after a user purchases or restores a product, opens a URL from the paywall, or closes the paywall. To disable this, set the `isHapticFeedbackEnabled` `PaywallOption` to false: :::ios ```swift Swift let options = SuperwallOptions() options.paywalls.isHapticFeedbackEnabled = false Superwall.configure(apiKey: "MY_API_KEY", options: options) ``` ```swift Objective-C SWKSuperwallOptions *options = [[SWKSuperwallOptions alloc] init]; options.isHapticFeedbackEnabled = false; [Superwall configureWithApiKey:@"MY_API_KEY" purchaseController:nil options:options completion:^{}]; ``` ::: Note: Android does not use haptic feedback. ### Transaction Background View During a transaction, we add a `UIActivityIndicator` behind the view to indicate a loading status. However, you can remove this by setting the `transactionBackgroundView` to `nil`: :::ios ```swift Swift let options = SuperwallOptions() options.paywalls.transactionBackgroundView = nil Superwall.configure(apiKey: "MY_API_KEY", options: options) ``` ```swift Objective-C SWKSuperwallOptions *options = [[SWKSuperwallOptions alloc] init]; options.paywalls.transactionBackgroundView = SWKTransactionBackgroundViewNone; [Superwall configureWithApiKey:@"MY_API_KEY" purchaseController:nil options:options completion:nil ]; ``` ::: ### Purchase Failure Alert When a purchase fails, we automatically present an alert with the error message. If you'd like to show your own alert after failure, set the `shouldShowPurchaseFailureAlert` `PaywallOption` to `false`: :::ios ```swift Swift let options = SuperwallOptions() options.paywalls.shouldShowPurchaseFailureAlert = false Superwall.configure(apiKey: "MY_API_KEY", options: options) ``` ```swift Objective-C SWKSuperwallOptions *options = [[SWKSuperwallOptions alloc] init]; options.paywalls.shouldShowPurchaseFailureAlert = false; [Superwall configureWithApiKey:@"MY_API_KEY" purchaseController:nil options:options completion:nil ]; ``` ::: ### Web Purchase Confirmation Alert When a user completes a purchase via web checkout (app2web flow), you can control whether to show a confirmation alert. By default, this is set to `false` to prevent duplicate alerts. Set `shouldShowWebPurchaseConfirmationAlert` to `true` if you want to show the native confirmation alert: :::ios ```swift Swift let options = SuperwallOptions() options.paywalls.shouldShowWebPurchaseConfirmationAlert = true Superwall.configure(apiKey: "MY_API_KEY", options: options) ``` ```swift Objective-C SWKSuperwallOptions *options = [[SWKSuperwallOptions alloc] init]; options.paywalls.shouldShowWebPurchaseConfirmationAlert = true; [Superwall configureWithApiKey:@"MY_API_KEY" purchaseController:nil options:options completion:nil ]; ``` ::: ### Locale Identifier When evaluating rules, the device locale identifier is set to `autoupdatingCurrent`. However, you can override this if you want to test a specific locale: :::ios ```swift Swift let options = SuperwallOptions() options.localeIdentifier = "en_GB" Superwall.configure(apiKey: "MY_API_KEY", options: options) // Or you can set: Superwall.shared.localeIdentifier = "en_GB" // To revert to default: Superwall.shared.localeIdentifier = nil ``` ```swift Objective-C SWKSuperwallOptions *options = [[SWKSuperwallOptions alloc] init]; options.localeIdentifier = @"en_GB"; [Superwall configureWithApiKey:@"MY_API_KEY" purchaseController:nil options:options completion:^{}]; // Or you can set: [Superwall sharedInstance].localeIdentifier = "en_GB" // To revert to default: [Superwall sharedInstance].localeIdentifier = nil ``` ::: For a list of locales that are available on iOS, take a look at [this list](https://gist.github.com/jacobbubu/1836273). You can also preview your paywall in different locales using [In-App Previews](/docs/in-app-paywall-previews). ### Game Controller If you're using a game controller, you can enable this in `SuperwallOptions` too. Check out our [Game Controller Support](/docs/game-controller-support) article. Take a look at [SuperwallOptions](https://sdk.superwall.me/documentation/superwallkit/superwalloptions) in our SDK reference for more info. --- # Setting up StoreKit testing Source: https://superwall.com/docs/ios/guides/testing-purchases undefined StoreKit testing in Xcode is a local test environment for testing in-app purchases without requiring a connection to App Store servers. Set up in-app purchases in a local StoreKit configuration file in your Xcode project, or create a synced StoreKit configuration file in Xcode from your in-app purchase settings in App Store Connect. After you enable the configuration file, the test environment uses this local data on your paywalls when your app calls StoreKit APIs. ### Add a StoreKit Configuration File Go to **File ▸ New ▸ File...** in the menu bar , select **StoreKit Configuration File** and hit **Next**: ![](/images/3dbedb3-Screenshot_2023-03-02_at_11.35.16.png) Give it the name **Products**. For a configuration file synced with an app on App Store Connect, select the checkbox, specify your team and app in the drop-down menus that appear, then click **Next**. For a local configuration, leave the checkbox unselected, then click **Next**. Save the file in the top-level folder of your project. You don't need to add it to your target. ### Create a New Scheme for StoreKit Testing It's best practice to create a new scheme in Xcode to be used for StoreKit testing. This allows you to separate out staging and production environments. Click the scheme in the scheme menu and click **Manage Schemes...**: ![](/images/e0e2c89-Screenshot_2023-03-02_at_11.44.02.png) If you haven't already got a Staging scheme, select your current scheme and click **Duplicate**: ![](/images/d3c7e96-Screenshot_2023-03-02_at_11.44.47.png) In the scheme editor, add the StoreKit Configuration file to your scheme by clicking on **Run** in the side bar, selecting the **Options** tab and choosing your configuration file in **StoreKit Configuration**. Then, click **Close**: ![](/images/444719d-Screenshot_2023-03-02_at_11.46.34.png) You can rename your scheme to **MyAppName (Staging)**. ### Setting up the StoreKit Configuration File If you've chosen to sync your configuration file with the App Store, your apps will automatically be loaded into your StoreKit Configuration file. When you add new products, just sync again. If you're using a local configuration, open **Products.storekit**, click the **+** button at the bottom and create a new product. In this tutorial, we'll create an auto-renewable subscription: ![](/images/6d89a21-Screenshot_2023-03-02_at_12.07.50.png) Enter a name for a new subscription group and click **Done**. The subscription group name should match one that is set up for your app in App Store Connect, but it's not a requirement. That means you can test your subscription groups and products in the simulator and then create the products in App Store Connect later: ![](/images/717d912-Screenshot_2023-03-02_at_12.09.07.png) Configure the subscription as needed by filling in the **Reference Name**, **Product ID**, **Price**, **Subscription Duration**, and optionally an **Introductory Offer**. Again, this product doesn't have to exist in App Store Connect for you to test purchasing in the simulator. Here is a sample configuration: ![](/images/d6a9b7f-Screenshot_2023-03-02_at_12.10.27.png) Repeat this for all of your products. When configuring a paywall, the product ID you enter here must match the product ID on the paywall. You're now all set! ## Testing purchases with Transaction Manager Once you've set up your StoreKit configuration file, you can leverage Xcode's Transaction Manager. Find it under **Debug -> StoreKit -> Manage Transactions...**: ![](/images/transactionManager.png) Use this to quickly test purchasing your products. Once you make a purchase, you can open Transaction Manager to delete it, refund it, request parental approval and much more. Most commonly, you'll probably delete the transaction to reset your subscription state: ![](/images/transactionRemove.png) This makes everything a little faster, saving you the trouble of having to delete and reinstall your app to test these states. If you'd like to see a video over how to use it, check this one out: