Superwall Flutter 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
Overview
To see the latest release, check out the repository.
Install via pubspec.yaml
To use Superwall in your Flutter project, add superwallkit_flutter as a dependency in your pubspec.yaml file:
dependencies:
superwallkit_flutter: ^2.0.5After adding the dependency, run dart pub get in your terminal to fetch the package.
Install via Command Line (Alternative)
You can also add the dependency directly from your terminal using the following command:
$ flutter pub add superwallkit_flutteriOS Deployment Target
Superwall requires iOS 14.0 or higher. Ensure your Flutter project's iOS deployment target is 14.0 or higher by updating ios/Podfile.
platform :ios, '14.0'Android Configuration
First, add our SuperwallActivity to your AndroidManifest.xml:
<!-- ... inside your <application> tag -->
<activity
android:name="com.superwall.sdk.paywall.view.SuperwallPaywallActivity"
android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar"
android:configChanges="orientation|screenSize|keyboardHidden">
</activity>
<!-- Optional -->
<activity android:name="com.superwall.sdk.debug.DebugViewActivity" />
<activity android:name="com.superwall.sdk.debug.localizations.SWLocalizationActivity" />
<activity android:name="com.superwall.sdk.debug.SWConsoleActivity" />Superwall requires a minimum SDK version of 26 or higher and a minimum compile SDK target of 34. Ensure your Flutter project's Android minimal SDK target is set to 26 or higher and that your compilation SDK target is 34 by updating android/app/build.gradle.
android {
...
compileSdkVersion 34
...
defaultConfig {
...
minSdkVersion 26
...
}
}To use the compile target SDK 34, you'll also need to ensure your Gradle version is 8.6 or higher and your Android Gradle plugin version is 8.4 or higher.
You can do that by checking your gradle/wrapepr/gradle-wrapper.properties file and ensuring it is updated to use the latest Gradle version:
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zipAnd your android/build.gradle file is updated to use the latest Android Gradle plugin version:
plugins {
id 'com.android.application' version '8.4.1' apply false
}To find the latest compatible versions, you can always check the Gradle Plugin Release Notes.
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:
// main.dart
void initState() {
// Determine Superwall API Key for platform
String apiKey = Platform.isIOS ? "MY_IOS_API_KEY" : "MY_ANDROID_API_KEY";
Superwall.configure(apiKey);
}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 Flutter 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.
When you ship the Flutter SDK on Android, you must explicitly set options.passIdentifiersToPlayStore = true (for example, final options = SuperwallOptions()..passIdentifiersToPlayStore = true;) if you need the un-hashed appUserId to flow through Google Play's obfuscatedExternalAccountId field. This flag has no effect on iOS builds. Follow Google's rules so the value is not rejected.
// After retrieving a user's ID, e.g. from logging in or creating an account
Superwall.shared.identify(user.id);
// When the user signs out
Superwall.shared.reset();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.
On 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.
// 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
void pressedWorkoutButton() {
// remotely decide if a paywall is shown and if
// navigation.startWorkout() is a paid-only feature
Superwall.shared.registerPlacement('StartWorkout', feature: () {
navigation.startWorkout();
});
}Without Superwall
void pressedWorkoutButton() {
if (user.hasActiveSubscription) {
navigation.startWorkout();
} else {
navigation.presentPaywall().then((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
void pressedSignUp() {
Superwall.shared.registerPlacement("SignUp", feature: () {
navigation.beginOnboarding();
});
}
// In another view controller
void pressedWorkoutButton() {
Superwall.shared.registerPlacement("StartWorkout", 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:
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:
Tracking Subscription State
Superwall tracks the subscription state of a user for you. However, there are times in your app where you need to know if a user is on a paid plan or not. For example, you might want to conditionally show certain UI elements or enable premium features based on their subscription status.
Using subscriptionStatus stream
The easiest way to track subscription status in Flutter is by listening to the subscriptionStatus stream:
class _MyAppState extends State<MyApp> {
StreamSubscription<SubscriptionStatus>? _subscription;
SubscriptionStatus _currentStatus = SubscriptionStatus.unknown;
@override
void initState() {
super.initState();
_subscription = Superwall.shared.subscriptionStatus.listen((status) {
setState(() {
_currentStatus = status;
});
switch (status) {
case SubscriptionStatus.active:
print('User has active subscription');
_showPremiumContent();
break;
case SubscriptionStatus.inactive:
print('User is on free plan');
_showFreeContent();
break;
case SubscriptionStatus.unknown:
print('Subscription status unknown');
_showLoadingState();
break;
}
});
}
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}
}The SubscriptionStatus enum has three possible values:
SubscriptionStatus.unknown- Status is not yet determinedSubscriptionStatus.active- User has an active subscriptionSubscriptionStatus.inactive- User has no active subscription
Use the isActive convenience property when you only need to know if the user is subscribed:
Superwall.shared.subscriptionStatus.listen((status) {
if (status.isActive) {
_showPremiumContent();
} else {
_showFreeContent();
}
});Using SuperwallBuilder widget
For reactive UI updates based on subscription status, use the SuperwallBuilder widget:
SuperwallBuilder(
builder: (context, subscriptionStatus) {
switch (subscriptionStatus) {
case SubscriptionStatus.active:
return PremiumContent();
case SubscriptionStatus.inactive:
return FreeContent();
default:
return LoadingIndicator();
}
},
)This widget automatically rebuilds whenever the subscription status changes, making it perfect for conditionally rendering UI:
class SubscriptionStatusDisplay extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SuperwallBuilder(
builder: (context, status) => Center(
child: Text('Subscription Status: $status'),
),
);
}
}Using StreamBuilder
You can also use Flutter's StreamBuilder for more control over the stream subscription:
class PremiumFeatureButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StreamBuilder<SubscriptionStatus>(
stream: Superwall.shared.subscriptionStatus,
builder: (context, snapshot) {
final status = snapshot.data ?? SubscriptionStatus.unknown;
final isActive = status.isActive;
return ElevatedButton(
onPressed: isActive
? _accessPremiumFeature
: _showPaywall,
child: Text(
isActive
? 'Access Premium Feature'
: 'Upgrade to Premium',
),
);
},
);
}
void _accessPremiumFeature() {
// Access premium feature
}
void _showPaywall() {
Superwall.shared.registerPlacement('premium_feature');
}
}Checking subscription status programmatically
If you need to check the subscription status at a specific moment without listening to the stream:
Future<void> checkSubscription() async {
// Note: You'll need to get the current value from the stream
final subscription = Superwall.shared.subscriptionStatus.listen((status) {
if (status.isActive) {
// User is subscribed
enablePremiumFeatures();
} else {
// User is not subscribed
showUpgradePrompt();
}
});
// Remember to cancel when done
subscription.cancel();
}Setting subscription status
When using Superwall with a custom purchase controller or third-party billing service, you need to manually update the subscription status. Here's how to sync with RevenueCat:
class RCPurchaseController extends PurchaseController {
Future<void> syncSubscriptionStatus() async {
try {
final customerInfo = await Purchases.getCustomerInfo();
final hasActiveSubscription = customerInfo.entitlements.active.isNotEmpty;
if (hasActiveSubscription) {
final entitlements = customerInfo.entitlements.active.keys
.map((id) => Entitlement(id: id))
.toSet();
await Superwall.shared.setSubscriptionStatus(
SubscriptionStatusActive(entitlements: entitlements)
);
} else {
await Superwall.shared.setSubscriptionStatus(
SubscriptionStatusInactive()
);
}
} catch (e) {
print('Failed to sync subscription status: $e');
}
}
@override
Future<PurchaseResult> purchaseFromAppStore(String productId) async {
try {
final result = await Purchases.purchaseProduct(productId);
if (result.isSuccess) {
// Sync status after successful purchase
await syncSubscriptionStatus();
return PurchaseResult.purchased;
}
return PurchaseResult.failed;
} catch (e) {
return PurchaseResult.failed;
}
}
}You can also listen for subscription changes from your payment service:
void setupSubscriptionListener() {
myPaymentService.addSubscriptionStatusListener((subscriptionInfo) {
final entitlements = subscriptionInfo.entitlements.active.keys
.map((id) => Entitlement(id: id))
.toSet();
final hasActiveSubscription = subscriptionInfo.isActive;
if (hasActiveSubscription) {
Superwall.shared.setSubscriptionStatus(
SubscriptionStatusActive(entitlements: entitlements)
);
} else {
Superwall.shared.setSubscriptionStatus(
SubscriptionStatusInactive()
);
}
});
}Using SuperwallDelegate
You can also listen for subscription status changes using the SuperwallDelegate:
class _MyAppState extends State<MyApp> implements SuperwallDelegate {
@override
void initState() {
super.initState();
// Set delegate
Superwall.shared.setDelegate(this);
}
@override
void subscriptionStatusDidChange(SubscriptionStatus newValue) {
print('Subscription status changed to: $newValue');
switch (newValue) {
case SubscriptionStatus.active:
print('User is now premium');
_handlePremiumUser();
break;
case SubscriptionStatus.inactive:
print('User is now free');
_handleFreeUser();
break;
case SubscriptionStatus.unknown:
print('Status unknown');
break;
}
}
void _handlePremiumUser() {
// Update UI or app state for premium user
}
void _handleFreeUser() {
// Update UI or app state for free user
}
}Handling subscription expiry
If you need to check for subscription expiry manually:
Future<void> checkSubscriptionExpiry() async {
final expiryDate = await MyPaymentService.getSubscriptionExpiry();
if (expiryDate.isBefore(DateTime.now())) {
// Subscription has expired
await Superwall.shared.setSubscriptionStatus(
SubscriptionStatusInactive()
);
// Show renewal prompt
_showRenewalPrompt();
}
}Superwall checks subscription status for you
Remember that the Superwall SDK uses its audience filters for determining when to show paywalls. You generally don't need to wrap your calls to register placements with subscription status checks:
// ❌ Unnecessary
final subscription = Superwall.shared.subscriptionStatus.listen((status) {
if (status != SubscriptionStatus.active) {
Superwall.shared.registerPlacement('campaign_trigger');
}
});
// ✅ Just register the placement
Superwall.shared.registerPlacement('campaign_trigger');In your audience filters, you can specify whether the subscription state should be considered, which keeps your codebase cleaner and puts the "Should this paywall show?" logic where it belongs—in the Superwall dashboard.
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(_:):
Map<String, dynamic> 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.shared.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 (iOS only).
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:

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.
Android
Add the following to your AndroidManifest.xml file:
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="exampleapp" />
</intent-filter>
</activity>This configuration allows your app to open in response to a deep link with the format exampleapp:// from your MainActivity class.
Adding a Universal Link (iOS only)
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
In your pubspec.yaml file, add the uni_links package to your dependencies:
dependencies:
uni_links: ^0.5.1Then, run flutter pub get to install the package. Next, in your Flutter app, use the Superwall SDK to handle the deep link via Superwall.shared.handleDeepLink(theLink);. Here's a complete example:
import 'package:flutter/material.dart';
import 'package:superwallkit_flutter/superwallkit_flutter.dart';
import 'package:uni_links/uni_links.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
MyApp();
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
Superwall.configure('YOUR_SUPERWALL_API_KEY');
_handleIncomingLinks();
}
void _handleIncomingLinks() {
uriLinkStream.listen((Uri? uri) {
if (uri != null) {
Superwall.shared.handleDeepLink(uri);
}
}, onError: (Object err) {
print('Error receiving incoming link: $err');
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Text(
'Deep Link Preview Example',
style: TextStyle(fontSize: 24),
),
),
),
);
}
}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:
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