Superwall
Android

Superwall Android 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

  1. Install the Superwall SDK
  2. Configure it with my API key: {{ PUBLIC_API_KEY }}
  3. Present a paywall
  4. Set up deep link handling
  5. 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 Gradle

Gradle is the preferred way to install Superwall for Android.

In your build.gradle or build.gradle.kts add the latest Superwall SDK. You can find the latest release here.

implementation "com.superwall.sdk:superwall-android:2.6.5"
implementation("com.superwall.sdk:superwall-android:2.6.5")
[libraries]
superwall-android = { group = "com.superwall.sdk", name = "superwall-android", version = "2.6.5" }

// And in your build.gradle.kts
dependencies {
    implementation(libs.superwall.android)
}

Make sure to run Sync Now to force Android Studio to update.


Go to your AndroidManifest.xml and add the following permissions:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="com.android.vending.BILLING" />

Then add our Activity to your AndroidManifest.xml:

<activity
  android:name="com.superwall.sdk.paywall.view.SuperwallPaywallActivity"
  android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar"
  android:configChanges="orientation|screenSize|keyboardHidden">
</activity>
<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" />

Set your app's theme in the android:theme section.

When choosing a device or emulator to run on make sure that it has the Play Store app and that you are signed in to your Google account on the Play Store.

And you're done! Now you're ready to configure the SDK 👇

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:

// MainApplication.kt

class MainApplication : android.app.Application(), SuperwallDelegate {
    override fun onCreate() {
        super.onCreate()

        // Setup
        Superwall.configure(this, "MY_API_KEY")

        // OR using the DSL
        configureSuperwall("MY_API_KEY") {
            purchaseController = MyPurchaseController(this@MainApplication)
        }
    }
}

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 Android 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.

Google Play receives the identifier you pass to BillingFlowParams.Builder.setObfuscatedAccountId as both the obfuscatedExternalAccountId and the externalAccountId we forward to your Superwall backend. By default we SHA-256 hash your userId before sending it. If you want the raw appUserId to appear in Play Console and downstream server events, set SuperwallOptions().passIdentifiersToPlayStore = true before configuring the SDK. Make sure the value complies with Google's policies—it must not contain personally identifiable information.

// After retrieving a user's ID, e.g. from logging in or creating an account
Superwall.instance.identify(user.id)

// When the user signs out
Superwall.instance.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:

ScenarioValue used for appAccountToken
You’ve called Superwall.shared.identify(userId:)The exact userId you passed
You haven’t called identify yetThe UUID automatically generated for the anonymous user (the alias ID), without the $SuperwallAlias: prefix
You passed a non‑UUID userId to identifyStoreKit 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.

// 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

fun pressedWorkoutButton() {
  // remotely decide if a paywall is shown and if
  // navigation.startWorkout() is a paid-only feature
  Superwall.instance.register("StartWorkout") {
    navigation.startWorkout()
  }
}

Without Superwall

fun pressedWorkoutButton() {
  if (user.hasActiveSubscription) {
    navigation.startWorkout()
  } else {
    navigation.presentPaywall { 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.

  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.

// on the welcome screen
fun pressedSignUp() {
  Superwall.instance.register("SignUp") {
    navigation.beginOnboarding()
  }
}

// in another view controller
fun pressedWorkoutButton() {
  Superwall.instance.register("StartWorkout") {
    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

The easiest way to track subscription status in Android is by accessing the subscriptionStatus StateFlow:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // Get current status
        val status = Superwall.instance.subscriptionStatus.value
        when (status) {
            is SubscriptionStatus.Active -> {
                Log.d("Superwall", "User has active entitlements: ${status.entitlements}")
                showPremiumContent()
            }
            is SubscriptionStatus.Inactive -> {
                Log.d("Superwall", "User is on free plan")
                showFreeContent()
            }
            is SubscriptionStatus.Unknown -> {
                Log.d("Superwall", "Subscription status unknown")
                showLoadingState()
            }
        }
    }
}

The SubscriptionStatus sealed class has three possible states:

  • SubscriptionStatus.Unknown - Status is not yet determined
  • SubscriptionStatus.Active(Set<String>) - User has active entitlements (set of entitlement identifiers)
  • SubscriptionStatus.Inactive - User has no active entitlements

Observing subscription status changes

You can observe real-time subscription status changes using Kotlin's StateFlow:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        lifecycleScope.launch {
            Superwall.instance.subscriptionStatus.collect { status ->
                when (status) {
                    is SubscriptionStatus.Active -> {
                        Log.d("Superwall", "User upgraded to pro!")
                        updateUiForPremiumUser()
                    }
                    is SubscriptionStatus.Inactive -> {
                        Log.d("Superwall", "User is on free plan")
                        updateUiForFreeUser()
                    }
                    is SubscriptionStatus.Unknown -> {
                        Log.d("Superwall", "Loading subscription status...")
                        showLoadingState()
                    }
                }
            }
        }
    }
}

Using with Jetpack Compose

If you're using Jetpack Compose, you can observe subscription status reactively:

@Composable
fun ContentScreen() {
    val subscriptionStatus by Superwall.instance.subscriptionStatus
        .collectAsState()
    
    Column {
        when (subscriptionStatus) {
            is SubscriptionStatus.Active -> {
                val entitlements = (subscriptionStatus as SubscriptionStatus.Active).entitlements
                Text("Premium user with: ${entitlements.joinToString()}")
                PremiumContent()
            }
            is SubscriptionStatus.Inactive -> {
                Text("Free user")
                FreeContent()
            }
            is SubscriptionStatus.Unknown -> {
                Text("Loading...")
                LoadingIndicator()
            }
        }
    }
}

Checking for specific entitlements

If your app has multiple subscription tiers (e.g., Bronze, Silver, Gold), you can check for specific entitlements:

val status = Superwall.instance.subscriptionStatus.value
when (status) {
    is SubscriptionStatus.Active -> {
        if (status.entitlements.contains("gold")) {
            // Show gold-tier features
            showGoldFeatures()
        } else if (status.entitlements.contains("silver")) {
            // Show silver-tier features
            showSilverFeatures()
        }
    }
    else -> showFreeFeatures()
}

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 RevenueCatPurchaseController : PurchaseController {
    
    override suspend fun purchase(
        activity: Activity,
        product: StoreProduct
    ): PurchaseResult {
        return try {
            val result = Purchases.sharedInstance.purchase(activity, product.sku)
            
            // Update Superwall subscription status based on RevenueCat result
            if (result.isSuccessful) {
                val entitlements = result.customerInfo.entitlements.active.keys
                Superwall.instance.setSubscriptionStatus(
                    SubscriptionStatus.Active(entitlements)
                )
                PurchaseResult.Purchased
            } else {
                PurchaseResult.Failed(Exception("Purchase failed"))
            }
        } catch (e: Exception) {
            PurchaseResult.Failed(e)
        }
    }
    
    override suspend fun restorePurchases(): RestorationResult {
        return try {
            val customerInfo = Purchases.sharedInstance.restorePurchases()
            val activeEntitlements = customerInfo.entitlements.active.keys
            
            if (activeEntitlements.isNotEmpty()) {
                Superwall.instance.setSubscriptionStatus(
                    SubscriptionStatus.Active(activeEntitlements)
                )
            } else {
                Superwall.instance.setSubscriptionStatus(SubscriptionStatus.Inactive)
            }
            
            RestorationResult.Restored
        } catch (e: Exception) {
            RestorationResult.Failed(e)
        }
    }
}

You can also listen for subscription changes from your billing service:

class SubscriptionManager {
    
    fun syncSubscriptionStatus() {
        Purchases.sharedInstance.getCustomerInfoWith { customerInfo ->
            val activeEntitlements = customerInfo.entitlements.active.keys
            
            if (activeEntitlements.isNotEmpty()) {
                Superwall.instance.setSubscriptionStatus(
                    SubscriptionStatus.Active(activeEntitlements)
                )
            } else {
                Superwall.instance.setSubscriptionStatus(SubscriptionStatus.Inactive)
            }
        }
    }
}

Using SuperwallDelegate

You can also listen for subscription status changes using the SuperwallDelegate:

class MyApplication : Application() {
    
    override fun onCreate() {
        super.onCreate()
        
        Superwall.configure(
            applicationContext = this,
            apiKey = "YOUR_API_KEY",
            options = SuperwallOptions().apply {
                delegate = object : SuperwallDelegate() {
                    override fun subscriptionStatusDidChange(
                        from: SubscriptionStatus,
                        to: SubscriptionStatus
                    ) {
                        when (to) {
                            is SubscriptionStatus.Active -> {
                                Log.d("Superwall", "User is now premium")
                            }
                            is SubscriptionStatus.Inactive -> {
                                Log.d("Superwall", "User is now free")
                            }
                            is SubscriptionStatus.Unknown -> {
                                Log.d("Superwall", "Status unknown")
                            }
                        }
                    }
                }
            }
        )
    }
}

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
if (Superwall.instance.subscriptionStatus.value !is SubscriptionStatus.Active) {
    Superwall.instance.register("campaign_trigger")
}

// ✅ Just register the placement
Superwall.instance.register("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(_:):

val attributes = mapOf(
    "name" to user.name,
    "apnsToken" to user.apnsTokenString,
    "email" to user.email,
    "username" to user.username,
    "profilePic" to user.profilePicUrl,
    "stripe_customer_id" to user.stripeCustomerId // Optional: For Stripe checkout prefilling
)

Superwall.instance.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

  1. Previewing paywalls on your device before going live.
  2. Deep linking to specific campaigns.
  3. Web Checkout Post-Checkout Redirecting

Setup

The way to deep link into your app is URL Schemes.

Adding a Custom URL Scheme

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.

In your MainActivity (or the activity specified in your intent-filter), add the following Kotlin code to handle deep links:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Respond to deep links
        respondToDeepLinks()
    }

    private fun respondToDeepLinks() {
        intent?.data?.let { uri ->
            Superwall.instance.handleDeepLink(uri)
        }
    }
}

Android App links enable seamless integration between the Web checkout and your app, enabling users to redeem the purchase automatically.

To allow Android app links to open your app, you will need to follow these steps:

1. Add your app's fingerprint and schema to Stripe settings

To verify that the request to open the app is legitimate, Android requires your app's keystore SHA256 fingerprint, with at least one for your development keystore and one for your release keystore. You can obtain these fingerprints in the following way:

Development fingerprints

If you're using Android studio or have Android components installed, you can obtain your debug key by running the following command in your terminal:

keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android

And then copying the outputted SHA256 fingerprint.

Release fingerprints

To obtain the release fingerprints, you'll need your own keystore file (the one you use to sign the final application package before publishing).

You can do this by running the following command in your terminal:

keytool -list -v -keystore <PATH_TO_YOUR_KEYSTORE> -alias <YOUR_KEY_ALIAS>

And then copying the outputted SHA256 fingerprint.

Adding the fingerprints to the project

To add the fingerprints to Superwall, open the Settings tab of your Superwall Stripe application. Under the Android Configuration title, you should see three fields:

  • Package schema - this allows us to know which schema your app uses to open and parse deep links
  • Package name - your app's package name, i.e. com.mydomain.myapp
  • App fingerprints - One or more of your app's fingerprints, comma separated

Once added, click the Update Configuration button which will ensure the application asset links are properly generated for Google to verify.

2. Add the schema to your app's Android Manifest

For this, you'll need to copy the domain from your Superwall Stripe settings. Then, open your AndroidManifest.xml and inside the <activity ...> tag declaring your deep link handling activity, add the following, replacing the domain with the one from the settings:

<intent-filter android:autoVerify="true">  
    <action android:name="android.intent.action.VIEW" />  
  
    <category android:name="android.intent.category.DEFAULT" />  
    <category android:name="android.intent.category.BROWSABLE" />  
  
    <data android:scheme="https" />  
	<data android:host="yourappdomain.superwall.app"
		  android:pathPrefix="/redeem" />  
</intent-filter>

In the same activity as in step #2, you'll need to pass deeplinks along to Superwall SDK. You can do this by overriding your Activity's onCreate and onNewIntent methods and passing along the intent data to Superwall using Superwall.handleDeepLinks() method. The method returns a kotlin.Result indicating if the deep link will be handled by Superwall SDK.

override fun onCreate(savedInstanceState: Bundle?) {  
    super.onCreate(savedInstanceState)
	// your onCreate code
	intent?.data?.let { uri ->  
	    Superwall.handleDeepLink(uri)  
	}
}

override fun onNewIntent(intent: Intent, caller: ComponentCaller) {  
    super.onNewIntent(intent, caller)  
    // ... Your onNewIntent code
    intent?.data?.let { uri ->  
	    Superwall.handleDeepLink(uri)  
	}
}

If you are running your app using Android Studio, you need to be aware that it won't automatically allow verified links to be opened using the app, but you will need to enable it in settings yourself. This is not the case when installing from Play Store, and all the links will be handled automatically.

To do that, once you install the app on your device, open: Settings > Apps > Your Application Name > Open By Default

Under there, the Open supported links should be enabled. Tap Add Links button and selected the available links.

Testing & more details

For details regarding testing and setup, you can refer to Android's guide for verifying app links. Note - Superwall generates the assetlinks.json for you. To check the file, you can use the subdomain from your Superwall stripe configuation:

https://my-app.superwall.app/.well-known/assetlinks.json

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.

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