Superwall iOS Vibe Coding Guide
This is an all-in-one prompt to get the Superwall SDK set up in your app.
Prerequisites
We recommend setting up the Superwall Docs MCP for any further setup/questions, but it is not required. If you want it, ask me to add it and refer me to the docs: https://superwall.com/docs/sdk/guides/vibe-coding
Integration Steps
- Install the Superwall SDK
- Configure it with my API key:
{{ PUBLIC_API_KEY }} - Present a paywall
- Set up deep link handling
- Ask me about how I want to handle these two items:
- Identifying the user
- Setting user attributes
How to Implement a Step
Implement each step one at a time. For each step, do the following:
- If the Superwall Docs MCP is available, get context using it before starting. Use it as the primary source of truth for Superwall knowledge.
- If the MCP is not available, use the SDK quickstart reference below as the source of truth.
- Then, help implement the step as described in the docs.
- If needed, ask for project-specific clarification.
- Finally, test the step or instruct on how to test it.
- Explain what you did, what step is complete, and what step is next.
Install the SDK
Visual learner? Go watch our install video over on YouTube here.
Overview
To see the latest release, check out the repository.
You can install via Swift Package Manager or CocoaPods.
Install via Swift Package Manager
Swift 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...:
Then, paste the GitHub repository URL:
https://github.com/superwall/Superwall-iOSin 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:
After the package has loaded, make sure Add to Target is set to your app's name and click Add Package:
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?.
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:
import SuperwallKit@import SuperwallKit;Configure the SDK
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 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:

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:
// 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
}
}// 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...
}// 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 for more.
You've now configured Superwall!
For further help, check out our iOS example apps for working examples of implementing the Superwall SDK.
User Management
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.
// 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()// 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 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.
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.
// Generate and use a UUID user ID in Swift
let userId = UUID().uuidString
Superwall.shared.identify(userId: userId)Presenting Paywalls
This allows you to register a placement 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
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()
}
}- (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
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
}
}
}
}- (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 in the dashboard.
- The SDK retrieves your campaign settings from the dashboard on app launch.
- 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.
- 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
- 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.
- 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)
- If the paywall is set to Non Gated, the
feature:closure onregister(placement: ...)gets called when the paywall is dismissed (whether they paid or not) - If the paywall is set to Gated, the
feature:closure onregister(placement: ...)gets called only if the user is already paying or if they begin paying.
- If the paywall is set to Non Gated, the
- 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.
// 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()
}
}// 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 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:
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:
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.")
}
}Tracking Subscription State
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:
@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:
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:
@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:
if Superwall.shared.subscriptionStatus.isActive {
userData.isPaidUser = true
}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:
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<Entitlement>` 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 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:
// 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...
...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
By setting user attributes, you can display information about the user on the paywall. You can also define audiences 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(_:):
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)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 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.
Handling Deep Links
- Previewing paywalls on your device before going live.
- Deep linking to specific campaigns.
- Web Checkout Post-Checkout Redirecting
Setup
There are two ways to deep link into your app: URL Schemes and Universal Links.
Adding a Custom URL Scheme
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:

With this example, the app will open in response to a deep link with the format exampleapp://. You can view Apple's documentation to learn more about custom URL schemes.
Adding a Universal Link
Only required for Web Checkout, otherwise you can skip this step.
Before configuring in your app, first create and configure 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.

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.

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:
-
Use Branch's online validator: If you visit branch.io's online validator and enter in your web checkout URL, it'll run a similar check and provide the same output.
-
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:

Handling Deep Links
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:
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
}
}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<UIOpenURLContext>
) {
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)
}
}
}import SuperwallKit
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
Superwall.handleDeepLink(url)
}
}
}
}// 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<UIOpenURLContext *> *)URLContexts {
[self handleURLContexts:URLContexts];
}
- (void)scene:(UIScene *)scene continueUserActivity:(NSUserActivity *)userActivity {
[self handleUserActivity:userActivity];
}
#pragma mark - Deep linking
- (void)handleURLContexts:(NSSet<UIOpenURLContext *> *)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];
}
}
@endPreviewing 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:
With the General tab selected, type your custom URL scheme, without slashes, into the Apple Custom URL Scheme field:
Next, open your paywall from the dashboard and click Preview. You'll see a QR code appear in a pop-up:
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 for examples of both.
How is this guide?
Edit on GitHub